Linux 学习笔记——第二章 进程管理和调度(3)

Linux 学习笔记——第二章 进程管理和调度(3)

《深入 Linux 内核架构》阅读笔记。书籍参考的内核版本较老,文章参考的 Linux 内核版本为 5.4.103,并根据新版内核调整了一些代码片段

进程 ID

UNIX 进程总是会分配一个号码用于在其命名空间中唯一地标识它们。该号码被称作进程 ID 号,简称 PID。用 forkclone 产生的每个进程都由内核自动地分配了一个新的唯一的 PID 值。每个进程除了 PID 这个特征值之外,还有其他的 ID。

  • 处于某个线程组中的所有进程都有统一的线程组 ID(TGID)。如果进程没有使用线程,则其 PID 和 TGID 相同。线程组中的主进程被称作组长(group leader)。通过 clone 创建的所有线程的 task_struct 的 group_leader 成员,会指向组长的 task_struct 实例。
  • 独立进程可以合并成进程组(使用 setpgrp 系统调用)。进程组简化了向组的所有成员发送信号的操作,用管道连接的进程包含在同一个进程组中。
  • 几个进程组可以合并成一个会话。SID 可以使用 setsid 系统调用设置。

PID 命名空间按层次组织,某些进程具有多个 PID,凡可以看到该进程的命名空间,都会为其分配一个PID,必须区分局部 ID 和全局 ID。

  • 全局 ID 是在内核本身和初始命名空间中的唯一 ID 号,在系统启动期间开始的 init 进程即属于初始命名空间。对每个 ID 类型,都有一个给定的全局 ID,保证在整个系统中是唯一的。
  • 局部 ID 属于某个特定的命名空间,不具备全局有效性。对每个 ID 类型,它们在所属的命名空间内部有效,但类型相同、值也相同的 ID 可能出现在不同的命名空间中。

管理 PID

相关数据结构

PID 分配器(pid allocator)用于加速新 ID 的分配,它需要依赖 struct pid_namespace 的一些数据。

// include/linux/pid_namespace.h
struct pid_namespace {
	// ...
    struct idr idr;
	struct rcu_head rcu;
	unsigned int pid_allocated;
	struct task_struct *child_reaper;
	unsigned int level;
	struct pid_namespace *parent;
	// ...
} __randomize_layout;
  • 每个 PID 命名空间都具有一个进程,其发挥的作用相当于全局的 init 进程。init 的一个目的是对孤儿进程调用 wait4,命名空间局部的 init 变体也必须完成该工作。child_reaper 保存了指向该进程的 task_struct 的指针。
  • parent 是指向父命名空间的指针。
  • level 表示当前命名空间在命名空间层次结构中的深度。初始命名空间的 level 为 0,该命名空间的子空间 level 为 1,下一层的子空间 level 为 2,依次递推。level 较高的命名空间中的 ID,对 level 较低的命名空间来说是可见的。

PID 的管理围绕两个数据结构展开:struct pid 是内核对 PID 的内部表示,而 struct upid 则表示特定的命名空间中可见的信息。

// include/linux/pid.h
struct upid {
	int nr; // ID 的数值
	struct pid_namespace *ns; // 指向该 ID 所属的命名空间的指针
};

struct pid
{
	refcount_t count; // 引用计数器
	unsigned int level; // 包含该进程的命名空间在命名空间层次结构中的深度
	/* lists of tasks that use this pid */
	struct hlist_head tasks[PIDTYPE_MAX]; // 每个数组项都是一个散列表头,对应于一个 ID 类型。因为一个 ID 可能用于几个进程。所有共享同一给定 ID 的 task_struct 实例,都通过该列表连接起来
	/* wait queue for pidfd notifications */
	wait_queue_head_t wait_pidfd;
	struct rcu_head rcu;
	struct upid numbers[1]; // 一个 upid 实例的数组,每个数组项都对应于一个命名空间。注意该数组形式上只有一个数组项,如果一个进程只包含在全局命名空间中,那么确实如此。由于该数组位于结构的末尾,因此只要分配更多的内存空间,即可向数组添加附加的项。
};

enum pid_type
{
	PIDTYPE_PID,
	PIDTYPE_TGID,
	PIDTYPE_PGID,
	PIDTYPE_SID,
	PIDTYPE_MAX,
};

在这里插入图片描述

由于所有共享同一 ID 的 task_struct 实例都按进程存储在一个散列表(pit->tasks)中,因此需要在 struct task_struct 中增加一个散列表元素:

// include/linux/sched.h
struct task_struct {
    // ...
    /* PID 与 PID 散列表的联系。 */
	struct hlist_node		pid_links[PIDTYPE_MAX];
    // ...
};

假如已经分配了 struct pid 的一个新实例,并设置用于给定的 ID 类型。它会如下附加到 task_struct:

// kernel/pid.c
void attach_pid(struct task_struct *task, enum pid_type type)
{
	struct pid *pid = *task_pid_ptr(task, type);
    // 将 task->pid_links[type] 链接在 pid->tasks[type] 的后面
	hlist_add_head_rcu(&task->pid_links[type], &pid->tasks[type]);
}
相关函数

内核提供了若干辅助函数,用于操作和扫描上面描述的数据结构。本质上内核必须完成下面两个不同的任务:

  • 给出 task_struct、ID 类型、命名空间,取得命名空间局部的数字 ID。这个过程包含下面两个步骤。

    • get_task_pid 函数获得与 task_struct 关联的 pid 实例,type 为 ID 的类型。task_pid_ptr 函数负责具体的查找操作,PID 直接存储在 task_struct 中,TGID、PGID 和 SID 都存储在 task_struct 的 signal 成员中。

      // kernel/pid.c
      static struct pid **task_pid_ptr(struct task_struct *task, enum pid_type type)
      {
      	return (type == PIDTYPE_PID) ?
      		&task->thread_pid :
      		&task->signal->pids[type];
      }
      
      struct pid *get_task_pid(struct task_struct *task, enum pid_type type)
      {
      	struct pid *pid;
      	rcu_read_lock();
      	pid = get_pid(rcu_dereference(*task_pid_ptr(task, type)));
      	rcu_read_unlock();
      	return pid;
      }
      
    • 在获得 pid 实例之后,从 struct pid 的 numbers 数组中的 upid 信息,即可获得数字 ID:

      // kernel/pid.c
      pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
      {
      	struct upid *upid;
      	pid_t nr = 0;
      
      	if (pid && ns->level <= pid->level) {
      		upid = &pid->numbers[ns->level];
      		if (upid->ns == ns)
      			nr = upid->nr;
      	}
      	return nr;
      }
      
      // 返回该 PID 所属的命名空间所看到的局部 ID
      pid_t pid_vnr(struct pid *pid)
      {
      	return pid_nr_ns(pid, task_active_pid_ns(current));
      }
      

      因为父命名空间可以看到子命名空间中的 PID,反过来却不行,内核必须确保当前命名空间的 level 小于或等于产生局部 PID 的命名空间的 level。

  • 给出局部数字 ID 和对应的命名空间,查找此二元组描述的 task_struct。同样需要下面两个步骤。

    • 给出进程的局部数字 ID 和关联的命名空间,来获取 pid 实例。

      // kernel/pid.c
      struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
      {
      	return idr_find(&ns->idr, nr);
      }
      
      struct pid *find_vpid(int nr)
      {
      	return find_pid_ns(nr, task_active_pid_ns(current));
      }
      
    • pid_task 取出 pid->tasks[type] 散列表中的第一个 task_struct 实例。

      // kernel/pid.c
      struct task_struct *pid_task(struct pid *pid, enum pid_type type)
      {
      	struct task_struct *result = NULL;
      	if (pid) {
      		struct hlist_node *first;
      		first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
      					      lockdep_tasklist_lock_is_held());
      		if (first)
      			result = hlist_entry(first, struct task_struct, pid_links[(type)]);
      	}
      	return result;
      }
      

生成 PID

除了管理 PID 之外,内核还负责提供机制来生成唯一的 PID(尚未分配)。在这种情况下,可以忽略各种不同类型的 PID 之间的差别,因为按一般的 UNIX 观念,只需要为 PID 生成唯一的数值即可。所有其他的 ID 都可以派生自 PID。

早期 Linux 内核使用一个大的位图生成唯一 ID,而新的内核采用 IDR 机制来生成。IDR 用于生成一些小的序号,例如 PID、文件描述符 ID 和网络协议中的 package ID 等,它可以将一个 ID 映射到一个指针。相关内容可参考 ID Allocation — The Linux Kernel documentation

// kernel/pid.c
struct pid *alloc_pid(struct pid_namespace *ns)
{
	struct pid *pid;
	enum pid_type type;
	int i, nr;
	struct pid_namespace *tmp;
	struct upid *upid;
	// ...
	tmp = ns;
	pid->level = ns->level;
	// 从当前命名空间向根命名空间进行遍历,对每个这样的命名空间,都需要生成一个局部 ID。
	for (i = ns->level; i >= 0; i--) {
		int pid_min = 1;
		// ...
        // 生成 ID
		nr = idr_alloc_cyclic(&tmp->idr, NULL, pid_min,
				      pid_max, GFP_ATOMIC);
		// ...
        // 将生成的 ID 放入 numbers 数组中
		pid->numbers[i].nr = nr;
		pid->numbers[i].ns = tmp;
		tmp = tmp->parent;
	}
    
    // 计数器加 1
	refcount_set(&pid->count, 1);
    // 初始化 tasks 数组
	for (type = 0; type < PIDTYPE_MAX; ++type)
		INIT_HLIST_HEAD(&pid->tasks[type]);

	// ...
    // numbers 数组的最后一个元素,即当前命名空间
	upid = pid->numbers + ns->level;
	for ( ; upid >= pid->numbers; --upid) {
		/* Make the PID visible to find_pid_ns. */
		idr_replace(&upid->ns->idr, pid, upid->nr);
		upid->ns->pid_allocated++;
	}
	// ...
	return pid;
}

进程关系

除了源于 ID 连接的关系之外,内核还负责管理建立在 UNIX 进程创建模型之上“家族关系”。

  • 如果进程 A A A 分支形成进程 B B B,进程 A A A 称之为父进程而进程 B B B 则是子进程
  • 如果进程 A A A 分支若干次形成几个子进程 B 1 , B 2 , … , B n B_1,B_2,\ldots,B_n B1B2Bn,各个 B i B_i Bi 进程之间的关系称之为兄弟关系

task_struct 数据结构提供了两个链表表头,用于实现这些关系:

// include/linux/sched.h
struct task_struct {
    // ...
    struct list_head children;	/* 子进程链表 */
    struct list_head sibling;	/* 连接到父进程的 children 链表 */
    // ...
}

新的子进程置于 sibling 链表的起始位置,这意味着可以重建进程分支的时间顺序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值