内核把进程存放在叫做任务队列的双向循环列表中。链表中的每一项都是类型称为task_struct,称为进程描述符。
Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。
在进程的内核栈的栈低或栈顶(尾端),创建一个新的结构体struct_thread_ifo。此结构体中有一个指向进程描述符的指针。
内核通过一个唯一的进程标识值或PID来标识每个进程。
进程描述符中的state域描述了进程的当前状态。五种进程状态标识之一。
TASK_RUNNING(运行)——或者正在执行,或者在运行队列中等待执行。
TASK_INTERRUPTIBLE(可中断)——进程正在睡眠,等待某种条件的达成。一旦条件达成,则设置为运行态。处于此状态也会因为接受到的信号而提前被唤醒并投入运行。
TASK_UNINTERRUPTIBLE(不可中断)——除了不会接受到信号而被唤醒并投入运行外,与可中断状态相同。不受干扰的等待事件或等待的事件很快就会发生。使用的较少。
TASK_ZOMBIE(僵死)——该进程已经结束了,但其父进程还没有调用wait4()系统调用。
TASK_STOPPED(停止)——进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接受到SIGSTOP、SIGTSIP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接受到任何信号,都会使进程进入这种状态。
set_task_state(task,state)
必要的时候,它会设置内存屏蔽来强制其他处理器作重新排序,否则它等同于task->state = state;
所有进程都是PID为1的init进程的后代。
系统中的每个进程必有一个父进程。相应的,每个进程也可以拥有零个或多个子进程。进程间的关系存放在进程描述符中。
可以通过这种继承体系从系统任何一个进程出发查找到任意指定的其他进程。
进程创建,首先在新的地址空间创建进程,读入可执行文件,最后开始执行。Unix采用了与众不同的实现方式,分解到两个单独的函数中区执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资源和统计量(例如挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
vfork()函数,与fork()的功能相同,除了不拷贝父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。
调用exit()函数,进程相关联的所有资源释放掉了。进程不可运行并处于TASK_ZOMBIE状态。现在,它占用的所有资源就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向父进程提供信息。
wait()函数,wait4()函数。
release_task()会释放掉所有的资源。
孤儿进程,如果父进程在子进程返回前退出,必须有机制来保证子进程能找到一个新的父亲,否则的话这些孤儿进程就会在退出时永远处于僵死状态,白白的消费内存。
可执行队列,是给定处理器上的可执行进程链表,每个处理器上一个。每个可投入运行的进程都唯一的归属于一个可执行队列。
优先级数组,每个运行队列都有两个优先级数组,一个活跃的一个过期的。活动数组内的可执行队列上的进程都还有时间片剩余,而过期数组内的可执行队列上的进程都耗尽了时间片。当一个进程的时间片耗尽时,它会被移至过期数组,但在此之前,时间片已经给它重新计算好了。
Schedule()函数只切换优先级最高的进程。
进程拥有一个初始的优先级,叫做nice值。该数值变化范围为-20到+19,默认值为0。进程task_struct的static_prio域存放着这个值。它是由用户指定后,就不能改变。
effective_prio()函数可以返回一个进程的动态优先级。这个函数以nice值为基数,再加上-5到+5之间的进程交互式的奖励或惩罚。奖励和惩罚的标准是根据进程是I/O耗尽型还是处理器耗尽型。而I/O耗尽型和处理器耗尽型的判断又是根据进程的运行时间和休眠时间来判断的。
重新计算时间片只要以静态优先级为基础就可以了。在一个进程创建的时候,新建的子进程和父进程均分剩余的进程时间片。进程的优先级越高,它每次执行得到的时间片就越长。
此外,调度程序还提供了另外一种机制以支持交互进程:如果一个进程的交互性非常强,那么当它时间片用完后,它会被再放置到活动数组而不是过期数组中。回忆一下,重新计算时间片是通过活动数组与过期数组之间的切换来进行的。一般进程在用尽它们的时间片后,都会被移到过期数组,当活动数组中没有剩余时间时,这个两个数组被交换,活动数组变为过期数组,过期数组变为活动数组。
Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR。而普通的、非实时的调度策略是SCHED_NORMAL。SCHED_FIFO是一种简单的,先入先出的调度算法,它不使用时间片。SCHED_FIFO级的进程会比SCHED_NORMAL级的进程都先得到调度。一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行,直到它自己受阻塞或显示地释放处理器为止。它不基于时间片可以一直执行下去。只有较高优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。
SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再接着执行下去了。
这两种实时算法实现的都是静态优先级。内核不为实时进程计算动态优先级。
Linux提供了一簇系统调用,用于管理与调度程序相关的参数。具体使用时可查书参阅。