您的位置:首页 > 理论基础 > 数据结构算法

《深入理解Linux内核》笔记2:进程的数据结构与其生命周期

2011-05-12 00:54 246 查看
(1)底层数据结构:双向链表

在进程管理中,双向链表是一个基础性的数据结构(后面涉及到的运行队列和等待队列等都使用了这个数据结构)。它的声明如下(虽然名称中含有head,但实际上每个结点都是相同的):

struct list_head {

struct list_head *next, *prev;

};

其中含有指向前一节点和后一节点的指针。而作为双向链表,提供的主要操作就是添加/删除元素、遍历链表(特别是list_for_each()函数很重要,可以对每个元素采取相同的操作)。

(2)进程描述符

进程描述符是一个名为task_struct的C结构(进程也就是任务,所以叫task),其中包含了进程所有的信息。其中有几个成员变量是我们下面将要用到的(图中用小黑框标出):run_list,tasks。(点击这里看大图

(3)双向链表与宿主的结合

我们回忆一下,如果我们需要实现二叉树数据结构,那么往往需要先实现其树节点的数据结构(含有左右子节点的指针),而且一般是包含在二叉树内部;高级的数据结构需要先实现底层的数据结构。我们将二叉树等这一类高级的数据结构称为“宿主”,其中包含有底层数据结构的节点。Linux内核中也不会直接应用双向链表这种数据结构,但是确实在很多地方都需要链表的接口,于是也采用了这种宿主与节点的模式。这实际上是面向对象设计中的组合,实现了代码复用以及降低了耦合性。

具体代码实现如下:
我们现在知道list成员是可以添加/删除元素、遍历的,但是如何才能遍历所有的foo对象或者foo对象中的data成员呢?内核采用了一个技巧,即知道宿主对象的首地址以及某成员相对首地址的偏移,就能知道某成员的地址了,然后就能取出相应的值。表示成公式就是:首地址+某成员偏移=某成员地址

struct foo {

int data;

struct list_head list;

};

这样的简单代码用过C语言的也应该都写过:

#include<stdio.h>

typedef struct _test

{ char i;

int j;

char k;

}Test;

int main()

{ Test *p = 0; printf("%p\n", &(p->k)); }

这里就可以打印出成员k相对于首地址的偏移。当然,上面的公式移项就可以换一种使用方法,已知某成员地址及其偏移量,即可求出宿主的首地址。

内核中实际上使用的是几个宏来具体计算:offsetof()/container_of()/typeof()。

其中typeof()宏就是获得其参数的类型,是由GCC编译器提供的。

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

#define container_of(ptr, type, member) ({ \

const typeof( ((type *)0)->member ) *__mptr = (ptr); \

(type *)( (char *)__mptr - offsetof(type,member) );})

这里唯一需要解释的就是offsetof宏中的(size_t) &((TYPE *)0)->MEMBER语句:(TYPE*)0其实就是相当于上面例子代码中的Test *p=0语句,将0看做一个地址,然后通过转型为TYPE型的指针,就创建了一个起始地址为0的TYPE型的对象,然后用&取出其MEMBER成员的地址,转化成以size_t为单位的偏移字节量。

最终我们拥有了已知某成员即可得到宿主及其成员变量的方法,这样也就是说最终宿主也拥有了添加/删除成员、遍历的接口。

(4)双向链表与进程的结合实例:进程链表,运行队列与等待队列

这里所说的进程链表(见《深入》p.93)是指把系统中所有的进程都串起来的链表。使用了进程描述符中的tasks字段,这个字段是list_head型。

运行队列则是将所有状态为TASK_RUNNING的进程(可运行进程,即可被调度马上执行的进程)串接起来的链表。由于2.6版的内核采用了新的调度系统,所有的可运行进程按照优先级被串在了140个不同的队列中(即共有140个高低不同的优先级)。使用的是进程描述符中的run_list字段。

等待队列是指状态为TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程的组织方式。它的底层数据结构也是双向链表。其头结点和普通节点的声明如下(重要字段用粗体标出):

struct _ _wait_queue_head {

spinlock_t lock;

struct list_head task_list;

};

struct __wait_queue {

unsigned int flags;

struct task_struct * task;

wait_queue_func_t func;

struct list_head task_list;

};

我们可以看到无论是头结点还是普通节点仍然通过包含一个list_head来实现串接,但是明显的与上面谈到的两个队列不同的是:(a)队头节点中含有自旋锁(具体原因请自行查阅);(b)等待队列不是包含在进程描述符中,而是在队列节点中包含了进程描述符task字段。

(5)进程哈希表pidhash及其中的链表

上面我们说到的数据结构都是链表,但是我们在进程管理中也用到一个hash表。为什么要使用hash表呢?因为我们在linux中经常会用到此类操作:给出进程号pid,要求得到进程描述符(例如kill()系统调用,参数为pid,但是函数要去改变进程描述符中的state字段)。如果我们在进程队列中遍历然后看其pid是否为所需的pid,这样做效率是很低的。而hash就是一个以空间换时间的方法。具体的hash函数就不写了(见《深入》p.97)。但是这里仍然使用了一个链表,是因为凡是hash表,就会发生冲突(因为我们一般不会使用一一对应的hash表,这里所用的hash表一般是2048项,但系统中的进程往往可以最大到32767项)。冲突的解决方法采用了分离链接法:即将所有冲突的项都串联到一个表项上,于是形成了链表。理论上会形成2048个链表,但是基本上冲突的概率比较小,所以链表都不会很长。

(6)进程的生命周期

进程的生命周期本章里面主要包括:创建、切换、撤销(调度将在第七章)。而这些功能主要都是由一些wrapper模式的内核函数实现的。

创建:clone()调用do_fork(),do_fork()再调用copy_process()。

切换:switch_to()宏

撤销:exit_group()调用do_group_exit()终止整个线程组;exit()调用do_exit()终止单个的线程。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: