进程描述符
为了管理进程,内核必须清楚地了解每个进程在做什么。例如,它必须知道进程的优先级、它是在 CPU 上运行还是在某个事件上被阻塞、分配给它的地址空间、允许它寻址的文件等等。这就是进程描述符的作用——一个task_struct
类型的结构,其字段包含与单个进程相关的所有信息。作为如此多信息的存储库,进程描述符相当复杂。除了包含进程属性的大量字段外,进程描述符还包含几个指向其他数据结构的指针,这些数据结构又包含指向其他结构的指针。下图示意性地描述了 Linux 进程描述符。
图中右侧的六个数据结构指的是进程所拥有的具体资源。这些资源中的大部分将在以后的章节中介绍。本章重点介绍两种类型的字段,它们是进程状态和进程父/子关系。
进程状态
- TASK_RUNNING
- TASK_INTERRUPTIBLE
- TASK_UNINTERRUPTIBLE
- TASK_STOPPED
- TASK_TRACED
识别进程
PID 通常是上一个创建进程 PID + 1。可以从头利用。
内核维护一个 bitmappidmap_array
用于记录哪些 PID 可用,哪些 PID 已被使用,用一个页装载(32位体系结构)。这些页从不释放。
进程描述符处理
内核在每个进程的内存中安排了两个数据结构:thread_info
和内核栈,这两个数据结构均保存在用户地址空间中,由一或两个页连续存储,属于第二章提到过的内核数据段。
C 语言允许thread_info
结构和进程的内核栈通过以下联合体结构方便地表示:
union thread_union {
struct thread_info thread_info;
unsigned long stack[2048];
};
内核使用alloc_thread_info
和free_thread_info
宏来分配和释放存储thread_info
结构和内核栈的内存区域。
识别当前进程
刚刚描述的thread_info
结构和内核栈之间的紧密关联在效率方面提供了一个关键优势:内核可以轻松地从 esp 寄存器的值中获取当前在 CPU 上运行的进程的thread_info
结构的地址。实际上,如果thread_union
结构的长度为8 KB,内核会屏蔽掉 esp 的13个最低有效位,以获得thread_info
结构的基地址;另一方面,如果thread_union
结构的长度为 4 KB,则内核会屏蔽掉 esp 的 12 个最低有效位。这是由current_thread_info()
函数完成的,该函数生成如下汇编语言指令:
movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */
andl %esp,%ecx
movl %ecx,p
执行完这三个指令后,p 包含了执行该指令的 CPU 上运行的进程的thread_info
结构指针。
大多数情况下,内核需要进程描述符的地址而不是thread_info
结构的地址。为了获取当前在 CPU 上运行的进程的进程描述符指针,内核使用current
宏,它本质上等同于current_thread_info()->task
并产生如下汇编语言指令:
movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */
andl %esp,%ecx
movl (%ecx),p
因为task
在thread_info
结构中的偏移量为0,所以在执行这三个指令后,p 包含了 CPU 上运行的进程的进程描述符指针。
宏current
通常作为进程描述符字段的前缀出现在内核代码中。例如,current->pid
返回当前在 CPU 上运行的进程的 ID。
与栈一起存储进程描述符的另一个优点出现在多处理器系统上:每个硬件处理器的当前进程可以通过检查栈来获得,如前所示。早期版本的 Linux 没有将内核栈和进程描述符存储在一起。相反,他们被迫引入一个名为current
的全局静态变量来识别正在运行的进程的描述符。在多处理器系统上,有必要将current
定义为一个数组——每个元素对应一个可用的 CPU。
双向链表
在继续描述内核如何跟踪系统中的各种进程之前,我们想强调实现双向链表的特殊数据结构的作用。对于每个列表,必须实现一组原始操作:初始化列表、插入和删除元素、遍历链表等。为每个不同的链表复制原始操作既浪费程序员的精力,也浪费内存。因此,Linux 内核定义了list_head
数据结构,其唯一字段 next
和prev
分别表示通用双向链表元素的前向和后向指针。然而,重要的是要注意,list_head
字段中的指针存储其他list_head
字段的地址,而不是包含 list_head
结构的整个数据结构的地址。
Linux 内核 2.6 还有另一种双向链表,主要区别于list_head
,因为它不是循环的;它主要用于哈希表,其中空间很重要,在常数时间内找到最后一个元素并不重要。列表头存储在hlist_head
数据结构中,它只是指向列表中第一个元素的指针(如果列表为空,则为 NULL)。每个元素由一个hlist_node
数据结构表示,它包括一个指向下一个元素的指针next
,以及一个指向前一个元素的next
字段的指针 pprev。因为列表不是循环的,所以将第一个元素的 pprev 字段和最后一个元素的 next 字段设置为 NULL。
进程链表
我们将查看的双向链表的第一个示例是进程链表,一个将所有的现有进程描述符链接在一起的链表。每个task_struct
结构都包含一个list_head
类型的tasks
成员,其prev
和next
字段分别指向前一个和下一个task_struct
元素。进程链表的头部是init_task
进程描述符;它是所谓的进程 0 或交换器的进程描述符。 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; )
该宏是内核程序员提供循环之后的循环控制语句。注意init_task
进程描述符是如何扮演列表头的角色的。宏首先从init_task
移动到下一个任务,然后继续直到它再次到达 init_task(由于列表的循环性)。在每次迭代中,作为宏参数传递的变量包含当前扫描的进程描述符的地址,由list_entry
宏返回。
TASK_RUNNING 进程列表
当寻找一个新的进程在 CPU 上运行时,内核必须只考虑可运行的进程(即处于 TASK_RUNNING 状态的进程)。早期的 Linux 版本将所有可运行的进程放在同一个列表中,称为runqueue
。因为根据进程优先级的维护列表成本太高,早期的调度程序被迫扫描整个列表以选择“最佳”可运行进程。Linux 2.6 以不同的方式实现运行队列。目的是允许调度程序在常数时间内选择最佳可运行进程,而与可运行进程的数量无关。
用于实现调度程序加速的技巧包括将runqueue
拆分为许多可运行进程列表,每个进程优先级一个列表。每个进程描述符都包含一个list_head
类型的run_list
字段。如果进程优先级等于 k(范围在 0 到 139 之间的值),则run_list
字段将进程描述符链接到具有优先级 k 的可运行进程列表。此外,在多处理器系统上,每个 CPU 都有自己的运行队列,即自己的一组进程列表。这是一个使数据结构更复杂以提高性能的经典示例:为了使调度程序操作更高效,runqueue
列表被拆分为 140 个不同的列表!
正如我们将看到的,内核必须为系统中的每个runqueue
保留大量数据;然而,runqueue
的主要数据结构是属于runqueue
的进程描述符列表;所有列表都由单个prio_array_t
数据结构实现。
类型 | 属性 | 说明 |
---|---|---|
int | nr_active | 链接到列表中的进程描述符的数量 |
unsigned long [5] | bitmap | 优先级位图:当且仅当相应的优先级列表不为空时,才会设置每个标志 |
struct list_head [140] | queue | 140个优先级列表头 |
enqueue_task(p,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
是指向其当前运行队列的prio_array_t
数据结构的指针。类似地,dequeue_task(p,array)
函数从运行队列列表中删除进程描述符。
进程之间的关系
程序创建的进程具有父/子关系。当一个进程创建多个子进程时,这些子进程具有兄弟关系。必须在进程描述符中引入几个字段来表示这些关系,由下表列出;进程 0 和 1 由内核创建;正如我们将在本章后面看到的,进程 1(init)是所有其他进程的祖先。
字段名 | 说明 |
---|---|
real_parent | 如果父进程不再存在,则指向创建 P 的进程的进程描述符或进程 1 (init) 的描述符。 (因此,当用户启动后台进程并退出 shell 时,后台进程将成为 init 的子进程。) |
parent | 指向 P 的当前父进程(这是子进程终止时必须发出信号的进程);它的值通常与 real_parent 的值一致。它可能偶尔会有所不同,例如当另一个进程发出ptrace() 系统调用请求允许它监视 P 时) |
children | 包含由 P 创建的所有孩子的列表的头部。 |
sibling | 指向兄弟进程列表中下一个和前一个元素的指针,这些元素与 P 具有相同的父进程。 |