Linux C编程--进程调度schedule

301 阅读4分钟
原文链接: zhuanlan.zhihu.com

进程在创建后,就可以被调度器选择、执行----这就是进程调度。

具体选择哪个进程去执行,是调度器的核心算法。

调度算法需要时间复杂度低、对用户操作的响应速度快。

Linux的调度算法,也是不断改进的。曾经2.6内核的O(1)调度器非常经典,但到了“深入Linux内核架构”那本书出版时,内核调度器就改成了以红黑树为基本数据结构的算法了。

调度算法,是个复杂的问题,但其功能非常明确,就是选择“合适的进程”去执行。

选择完进程之后,当然就是执行了,即:进程上下文的切换。

不管怎么选择,最终还是要执行的。今天,单说怎么去执行--进程上下文--切换


进程在用户态运行时,有三个寄存器非常关键:ESP、EIP、EFLAGS。

ESP存储当前的堆栈位置,EIP存储当前(下一条)指令的位置,EFLAGS存储当前的标志(状态)。

C中的if else代码,就会使用到EFLAGS中的标志位,并根据标志位的不同选择不同的执行分支。而任何的比较运算符--例如,>、<、==、!=,等等--都会改变EFLAGS中的对应标志位。


在内核态时,还有几个寄存器比较关键,即,控制寄存器:

cr0,控制实模式、保护模式转换,分页机制等,

cr3,页目录基地址寄存器,存放进程的页表指针

cr2,存放页错误发生时的内存地址(线性地址),缺页中断处理程序里使用

和进程切换有关的,就是cr3寄存器。

页表,关系到进程的虚拟地址到物理地址的映射。在总共4G物理内存下,每个进程有4G的虚拟地址空间,就是因为CPU的内存映射机制。该机制的关键,就是不同进程有不同的页表。当然,页表也可以相同,那时候进程之间就共享内存了,实际就是线程了。

进程切换,就是切换CPU的ESP、EIP、CR3,而EFLAGS保存在内核栈上,在切换后从内核栈弹出。

简易的实现进程切换的代码,大概原理如下:

asm volatile(

"pushl %ebp\r\n" #保存EBP,EBP关系到函数调用的栈帧基地址,

"pushfl\r\n" #保存EFLAGS,关系到标志位,EBP和EFLAGS都是保存在进程的内核栈上

"movl %esp, %0\r\n" #保存ESP到进程task_struct中的对应项current->esp

"movl 1f, %1\r\n" #保存标志1的地址到task_struct的current->eip,作为下一次进程被执行时的地址,任何被调度过的进程,下次执行的地址都是这里。

首次被调度执行的进程不是,而是sys_fork函数执行前的地址,对应着子进程中fork函数的返回。

"movl %2, %cr3\r\n" #切换cr3为下一个进程的页表

"movl %3, %esp\r\n" #切换ESP为下一个进程的内核栈指针

"pushl %4\r\n" #因为EIP不能直接赋值,又因为首次执行的进程EIP不是标志1,所以先把地址压栈,再弹出,而不是直接跳转到1执行

"ret\r\n" #弹出返回地址,这行之后就完成了切换

"1:\r\n" #标志1,

任何被调度过一次的,被调度出去后,再次被调度执行时,从这里开始。接下来两行汇编,恢复被调度出去前的EBP、FLAGS,保证调度器不会干扰进程的正常运行。也就是在进程看来,其运行是连续的,而不是被人“拦腰截断,再接上”。

首次被调度执行的进程,不会执行到这里。

"popfl\r\n" #弹出EFLAGS

"popl %ebp\r\n" #弹出EBP

:

:"m"(current->esp),"m"(current->eip),"r"(next->mm),"r"(next->esp),"r"(next->eip)

);


在用户态编程中,主动放弃CPU,执行进程调度的函数为sched_yield()。

在用户态的自旋锁中,在获取锁失败后需要主动放弃CPU,以让持有锁的进程更有机会及时的释放锁。而不是死等,直到被内核剥夺执行权。

Nginx中的spinlock,就是这么实现的。自旋等待一定次数后,如果还没获取到锁则执行sched_yield放弃CPU。


最近随着Go语言而火起来的协程(co-routine),其实就是在用户态模仿了内核的进程管理机制,从而简化了用户态的异步编程。