当进程被新建时,内核会给进程分配一个8K的空间作为进程的内核堆栈。同时我们知道task_struct结构体也会被创建,但有意思的是,内核不会给task_struct单独分别空间,而是直接将其扔到8k的内核堆栈中,与内核堆栈共享这8K内存。 假设我们用如下结构表示进程描述符和内核堆栈: struct task_struct task; unsigned long stack [INIT_TASK_SIZE/sizeof(long)]; 那么我们很容易将其做成一个联合体: union task_union { struct task_struct task; unsigned long stack[INIT_TASK_SIZE/sizeof(long)]; }; 实际上,进程创建时内核会分配2个连续的页面(8192byte),将其底部大约1k(960byte)的空间拿来存放进程描述符(task_struct),上部大约7k(7232byte)的空间拿来作为内核堆栈。(更多,参考这里和这里) 内核不会让内核堆栈超出7232byte,否则会覆盖task_struct而导致错误。 在内核空间(kernel space)中,有一个专门的全局数组来保存所有的进程描述符(指针),数组条目就是上面的task_union的指针, 这个数组称为Task Array。 很明显的一个问题:pid的最大值是32767,而系统允许的最大进程数是512,如果用pid作为Task Array的索引而将数组大小设为32768的话,数组空间上存在明显的浪费。实际上Task Array的实际大小是系统允许的最大进程数,而内核采用了一些其他的算法(???)将进程描述符分布到数组中去。 现在,如果我们想遍历系统中所有的进程,由于一般情况下Task数组中的大多数元素都是空的,所以下面的代码是低效的: for ( i=0; i<NR_TASKS; i++ ) { if (task[i]) //do something } 为此,task_struct结构体中加了如下字段:struct task_struct *next_task, *prev_task; 分别指向上一个进程和下一个进程,那么所有进程在连接在一起便形成了一个双向循环链表(double circular linked list),这样的遍历就变得高效了。该链表的头和尾都始终是init进程,因为init进程是长期存在的,这能保证链表一直有效。 随着进程的创建,我们需要在Task数组中找到空闲位置而将新进程插入进去。遍历Task数组的方式是可行却低效的,一个很简单的方式是准备一个堆栈(数据结构上的stack),让栈持有Task数组空闲位置的指针。当你需要插入新元素Task数组时,弹出栈顶元素供你使用便可;当你从Task数组中删除元素后,将该空闲位置压栈以便下次使用。而这个栈,就是linux中的tarray_freelist (自由时间列表) 试想如下case,进程p1要kill进程p2: p1: kill -9 pidOfP2 此时系统如何通过p2的PID找到p2的进程描述符?遍历task数组或遍历task_struct形成的双向循环链表都是低下的。 快速查找的一个简单方案是散列表(hash table),这个散列表称为pidhash,该表以pid为键(key),以指向pid对应的进程描述符的指针为值(value)。散列算法为: #define pid_hashfn(x) ((((x)>>8) ^ (x)) & (PIDHASH_SZ-1)) 其中PIDHASH_SZ为系统定义的此散列表的大小。 pidhash解决碰撞(冲突,colliding)的算法是“链接表法” (扩展, 其实效率最高的做法是直接通过pid做索引而用普通数组,只所以用散列表,道理和Task数组一样:节约空间)