1、进程的基本信息
1.1 标识一个进程——PID
每个进程都必须拥有它自己的进程描述符;
因此,即使共享内核大部分数据结构的轻量级进程(后面会提到),
也有它们自己的task_struct结构。
进程和进程描述符之间有非常严格的一一对应关系,所以我们可以方便地使用32位进程描述符地址标识进程。
进程描述符指针(task_struct*)指向这些地址。内核对进程的大部份引用都是通过进程描述符指针进行的。
另一方面,类Unix橾作系统允许用户使用一个叫做进程标识符processID (PID)的数来标识进程,PID存放在task_struct的pid字段中。
PID被顺序编号,新创建进程的PID通常是前一个进程的PID加1。
过,PID的值有一个上限,当内核使用的PID达到这个峰值的时候,就必须开始循环使用已闲置的小PID号。在缺省情况下,最大的PID号是32767
系统管理员可以通过往/proc/sys/kernel/pid_max 这个文件中写入一个更小的值来减小PID的上限值,使PID的上限小于32767。在64位体系结构中,系统管理员可以把PID的上限扩大到4194304。
由于循环使用PID编号,内核必须通过管理一个pidmap_array位图来表示当前已分配的PID和闲置的PID号。
因为一个页框包含32768个位(410248),所以在32位体系结构中pidmap_array位图正好存放在一个单独的页中。
系统会一直保存这些页而不释放的。
Linux只支持轻量级进程,不支持线程,但为了弥补这样的缺陷,Linux引入线程组的概念。
一个线程组中的所有线程使用和该线程组的领头线程相同的PID,
也就是该组中第一个轻量级进程的PID,它被存入进程描述符的tgid字段中。
getpid()系统调用返回当前进程的tgid值而不是pid值,
因此,一个多线程应用的所有线程共享相同的PID。
绝大多数进程都属于一个线程组;
而线程组的领头线程其tgid与pid的值相同,
因而getpid()系统调用对这类进程所起的作用和一般进程是一样的。
Linux虽不支持线程,但是它有具备支持线程的操作系统的所有特性,后面讲解轻量级进程的概念中还会详细讨论。
1.2 进程描述符定位
我们需要在3G之上线性地址的内存区为每个进程设计一个块——thread_union。
对每个进程来说,我们需要给其分配两个页面,即8192个字节的块。
Linux把两个不同数据结构紧凑地存放在一个单独为进程分配的存储区域内:
一个是内核态的进程堆栈
另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
unsigned long flags; /* low level flags */
unsigned long status; /* thread-synchronous flags */
__u32 cpu; /* current CPU */
__s32 preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space:
0-0xBFFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread
*/
struct restart_block restart_block;
unsigned long previous_esp; /* ESP of the previous stack in case
of nested (IRQ) stacks
*/
__u8 supervisor_stack[0];
};
esp为CPU栈指针寄存器,用来存放栈顶单元的地址。
在80x86系统中,栈起始于末端,并朝这个内存区的起始方向增长。
从用户态切换到内核态以后,进程的内核栈总是空的,因此,esp寄存器指向这个栈的顶端。
一旦数据写入堆栈,esp的值就递减。
特别要注意,这里的数据是指内核数据,其实用得很少,所以大多数时候这个内核栈是空的。
因为thread_info结构是52个字节的长度,所以内核栈能扩展到8140个字节。
C语言使用下列联合结构,方便地表示一个进程的线程描述符和内核栈:
union thread_union {
struct thread_info thread_info;
unsigned long stack[2048]; /* 1024 for 4KB stacks */
};
内核使用alloc_thread_info
和free_thread_info
宏分配和释放存储thread_info
结构和内核栈的内存区。
1.3 标识当前进程
我们再从效率的观点来看,刚才所讲的thread_info结构与内核态堆栈之间的紧密结合提供的主要好处还在
内核很容易从esp寄存器的值获得当前在CPU上正在运行进程的thread_info结构的地址。
事实上,如果thread_union的长度是8K(213字节),则内核屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址;
而如果thread_union的长度是4K,内核需要蔽掉esp的低12位有效位。
这项工作由current_thread_info()函数来完成,它产生如下一些汇编指令:
movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */
andl %esp,%ecx
movl %ecx,p
因为task字段在thread_info结构中的偏移量为0,所以执行完这三条指令之后,p就是CPU上运行进程的描述符指针。
current宏经常作为进程描述符字段的前缀出现在内核代码中,
例如,current->pid返回在CPU上正在执行CPU的进程的PID。
2、进程状态
2.1 进程链表
Linux内核把进程链表把所有进程的描述符链接起来。
每个task_struct结构都包含一个list_head类型的tasks字段,
这个类型的prev和next字段分别指向前面和后面的的task_struct元素。
进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。
init_task的tasks.prev字段指向链表中最后插入的进程描述符的tasks字段。
SET_LINKS 和 REMOVE_LINKS 宏分别用于从进程链表中插入和删除一个进程描述符。
这些宏考虑了进程间的父子关系。
另外,还有一个很有用的宏就是for_each_process,它的功能是扫描整个进程链表,其定义如下:
#define for_each_process(p) /
for (p=&init_task; (p=list_entry((p)->tasks.next, /
struct task_struct, tasks) /
) != &init_task; )
2.2 state字段
进程描述符task_struct结构的state字段描述了进程当前所处的状态。
它由一组标志组成,其中每个标志描述一种可能的进程状态。
在当前的Linux版本中,这些状态是互斥的,因此,严格意义上来说,只能设置一种状态,其余的标志位将被清除。
可运行状态(TASK_RUNNING)
进程要么在CPU上执行,要么准备执行。
可中断的等待状态(TASK_INTERRUPTIBLE)
进程被挂起(睡眠),直到某个条件变为真。产生一个硬件中断、释放进程正在等待的系统资源、或传递一个信号都是可以唤醒进程的条件(把进程状态放回到TASK_RUNNING)。
不可中断的等待状态(TASK_UNINTERRUPTIBLE)
与可中断的等待状态类似,但有一个例外,把信号传递到该睡眠进程时,不能改变它的状态。这种状态很少用到,但在一些特定条件下(进程必须等待,直到一个不能被中断的时事件发生),这种状态是很有用的。例如,当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备时会用到这种状态。探测完成以前,设备驱动程序不能被中断,否则,硬件设备会处于不可预知的状态。
暂停状态(TASK_STOPPED)
进程的执行被暂停。当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号后,进人暂停状态。
跟踪状态(TASK_TRACED)
进程的执行已由debugger程序暂停。当一个进程被另一个进程监控时(例如debugger执行ptrace()系统调用监控一个测试程序)任何信号都可以把这个进程置于TASK_TRACED状态。
还有两个进程状态既可以存放在进程描述符的state字段啊中,也可以存放在exit_state中字段中。从这两个字段的名称可以看出,只有当进程的执行被终止时,进程的状态才会变成此两种中的一种:
僵死状态(EXIT_ZOMBIE)
进程的执行被终止,但是父进程还没发布wait4()或waitpid()系统调用来返回有关死亡进程的信息。发布wait()类系统调用前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能还需要它。
僵死撤销状态(EXIT_DEAD)
最终状态:由于父进程刚发出wait4()或waitpid()系统调用,因而进程由系统删除。为了防止其他执行线程在同一个进程上也执行wait()类系统调用(这也是一种竞争条件),而把进程的状态由僵死(EXIT_ZOMBIE)状态改为僵死撤销状态(EXIT_DEAD)
state字段的值通常用一个简单的赋值语句设置,例如:
p->state = TASK_RUNNING;
内核也使用set_task_state
和set_current_state
宏:
它们分别设置指定进程的状态和当前执行进程的状态。
此外,这些宏确保编译程序或CPU控制单元不把赋值操作和其他指令混合。
混合指令的顺序有时会导致灾难性的后果。
2.3 TASK_RUNNING状态的进程链表
当内核寻找到一个新进程在CPU上运行时,必须只考虑可运行进程
(即处在TASK_RUNNING状态的进程)。
早先的Linux版本把所有的可运行进程都放在同一个叫做运行队列(runqueue)的链表中,由于维持链表中的进程优先级排序的开销过大,因此,早期的调度程序不得不为选择“最佳”可运行进程而扫描整个队列。
Linux 2.6实现的运行队列有所不同。其目的是让调度程序能在固定的时间内选出“最佳”可运行队列,与进程中可运行的进程数无关。
我们仅在此提供一些基本信息,《进程调度》博文中会详细描述这种新的运行队列。
提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先级对应一个不同的链表。
每个task_struct描述符包含一个list_head类型的字段run_list。
如果进程的优先权等于k(其取值范围从0到139),run_list字段就把该进程的优先级链入优先级为k的可运行进程的链表中。
此外,在多处理器系统中,每个CPU都有它自己的运行队列,即它自己的进程链表集。
这是一个通过使数据结构更复杂来改善性能的典型例子:调度程序的操作效率的确更高了,但运行队列的链表却为此被拆分成140个不同的队列!
内核必须为系统中每个运行队列保存大量的数据,不过运行队列的主要数据结构还是组成运行队列的进程描述符链表,
所有这些链表都由一个单独的prio_array_t数据结构来实现。
enqueue_task(p,array)函数把进程描述符(p参数)插入到某个运行队列的链表(基于prio_array_t结构的array参数)
代码本质上等同于如下代码:
list_add_tail(&p->run_list, &array->queue[p->prio]);
__set_bit(p->prio, array->bitmap);
array->nr_active++;
p->array = array;
进程描述符的prio字段存放进程的动态优先权
而array字段是一个指针,指向当前运行队列的proo_array_t
数据结构。
类似地,dequeue_task(p,array)
函数从运行队列的链表中删除一个进程的描述符。