Linux进程

进程描述符

为了管理进程,内核必须清楚地了解每个进程在做什么。例如,它必须知道进程的优先级、它是在 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_infofree_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

因为taskthread_info结构中的偏移量为0,所以在执行这三个指令后,p 包含了 CPU 上运行的进程的进程描述符指针。

current通常作为进程描述符字段的前缀出现在内核代码中。例如,current->pid返回当前在 CPU 上运行的进程的 ID。

与栈一起存储进程描述符的另一个优点出现在多处理器系统上:每个硬件处理器的当前进程可以通过检查栈来获得,如前所示。早期版本的 Linux 没有将内核栈和进程描述符存储在一起。相反,他们被迫引入一个名为current的全局静态变量来识别正在运行的进程的描述符。在多处理器系统上,有必要将current定义为一个数组——每个元素对应一个可用的 CPU。

双向链表

在继续描述内核如何跟踪系统中的各种进程之前,我们想强调实现双向链表的特殊数据结构的作用。对于每个列表,必须实现一组原始操作:初始化列表、插入和删除元素、遍历链表等。为每个不同的链表复制原始操作既浪费程序员的精力,也浪费内存。因此,Linux 内核定义了list_head数据结构,其唯一字段 nextprev分别表示通用双向链表元素的前向和后向指针。然而,重要的是要注意,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成员,其prevnext字段分别指向前一个和下一个task_struct元素。进程链表的头部是init_task进程描述符;它是所谓的进程 0 或交换器的进程描述符。 init_tasktasks->prev字段指向最后插入到列表中的进程描述符的tasks字段。

SET_LINKSREMOVE_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数据结构实现。

类型属性说明
intnr_active链接到列表中的进程描述符的数量
unsigned long [5]bitmap优先级位图:当且仅当相应的优先级列表不为空时,才会设置每个标志
struct list_head [140]queue140个优先级列表头

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 具有相同的父进程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值