操作系统只要记录好并按照合理的次序推进(分配资源、进行调试)。
进程 = 资源 + 指令执行序列
将资源和指令分开,便得到了线程。线程保留了并发的优点、避免了进程切换的代价。
用户级线程(User Threads)
操作系统无法感知用户级线程的存在,一旦其中一个线程阻塞就会导致进程Schedule。
线程切换
假设两个执行序列在一个栈中切换:
void Yield() { jmp xxx; }
为了可以在两个执行序列之间进行切换,在执行第一个yield时应jmp到300,第二个yield中jmp到204。接下来执行到B函数的结尾
}
,即ret
指令,404出栈,跳到404执行,执行顺序出错。因此,两个执行序列应该在两个栈之间进行切换。
void Yield() { TCB2.esp = esp; esp = TCB1.esp; jmp 204; //应删除这一行 }
以第二个yield为例,执行时先切换esp,之后执行到B函数的
}
时已经是TCB1对应的esp,出栈的是204,到204执行,重复执行了204。因为yield中包含了jmp到204的指令,所以yield的
}
不会被执行,因此去掉jmp即可。线程创建
void ThreadCreate(A) { TCB *tcb = malloc(); *stack = malloc(); *stack = A;//100 程序入口,线程切换时通过弹出A找到要执行的地址 tcb.esp = stack; }
内核线程(Kernel Threads)
核心级线程的创建等操作是系统调用,会进行内核中执行,其中一个线程阻塞不会导致进程切换,并发性更好。
多核处理器中只包含一个MMU(内存管理单元),即一个映射表。只有在内核中才能把多个线程分配在不同的核上。
线程切换
用户级线程切换时是TCB切换,然后根据TCB切换用户栈。内核级线程包含用户栈和内核栈,因此线程切换时应该在两套栈之间进行切换。
当执行到中断时开始使用内核栈,自动压入多个寄存器,使内核栈通过指针与用户栈相连。
然后开始内核中的切换:switch_to
sys_read() { ... // 启动磁盘读并将自己阻塞 ... // 找到nextTCB switch_to(cur, next); }
switch_to通过TCB找到内核栈指针,然后通过
ret
(switch_to的}
)从内核栈中弹出一段包含iret
的指令,切换到某个内核程序,内核程序执行完后再用iret
指令通过内核栈中的CS:IP切换回用户程序、通过SS:IP还原用户栈。其中
??
是线程T的用户态代码;????
是一段包含iret
的完成第二级返回的代码;总结:
- 中断入口(进入切换,用户栈→内核栈):初始化内核栈,关联用户栈,调用中断处理;
- 中断处理(引发切换):启动了磁盘读或时钟中断,不应该继续执行,对其进行schedule;
- schedule:找到下一个线程TCB,调用switch_to;
- switch_to(内核栈切换):调取TCB中的esp切换内核栈,执行ret指令;
- 中断出口(第二级切换,内核栈→用户栈):通过iret指令切换回用户栈和执行地址
线程创建
void ThreadCreate(...) { TCB tcb = get_free_page(); *krlstack = ...; *userstack = ...; ... // 填写两个stack tcb.esp = krlstack; ... // tcb置为就绪态 ... // tcb入队 }
内核级线程实现
线程切换
从某个中断开始,以fork为例:
main() { A(); B(); } A() { fork(); }
执行到A函数,用户栈中压入A的返回地址(也是B的初始地址),然后进入A函数执行。
执行到
INT 0x80
时,CPU找到内核栈,并在内核栈中压入当前SS:SP
和CS:IP
。接下来,执行
INT 0x80
的中断处理函数,即system_call
。在内核栈中保存用户态的信息:
然后执行中断对应的系统调用,如
sys_fork
,此时有可能产生磁盘读写引起切换。sys_fork
中对当前进程的状态进行判断,非0时进行阻塞或时间片用尽,重新调度:reschedule
将完成切换的中间三步,然后由ret_from_sys_call
进行中断返回,完成内核栈到用户栈的切换。reschedule
中schedule是C函数,执行完后弹栈执行ret_from_sys_call
在linux-0.11中,
switch_to
切换算法采用TSS(Task state segment)实现:将cpu中所有寄存器放在当前TR指向的段中,然后将新的TSS中的内容全部复制回CPU。
线程创建
从
sys_fork
开始CreateThread
:(这里是进程创建,使用TSS创建)其中,esp0是内核栈,esp是用户栈;0x10是内核数据段;内核栈与PCB共用一页内存,用户栈与父进程共用。
if(!fork()) {exec(cmd);}
的实现:在执行exec系统调用时,在内核栈中修改EIP的值,中断结束
iret
后会自动跳转到修改后的位置执行。这里压栈的eax
是_do_execve
的参数。