Bootstrap

进程模型1-PID 管理

内核版本架构作者GitHubCSDN
Linux-3.0.1armv7-A

  Linux 中其实没有线程的概念,线程也是进程的一种都是通过 vfork、fork、clone创建,只是线程会和线程组内的线程leader共享mm、fs、file等资源,已下介绍时“进程”仅指代进程,“线程”仅指代线程,“任务”指代线程和进程。

PID设计

在pid设计前内核的设计者要考虑如下几个问题:

  1. 如何实现pid在不同命名空间的管理;
      每个进程拥有一个pid实体,每个pid实体下拥有成员 numbers[i],其中每个numbers[]对应了相同层级的pid命名空间中upid,使得每个进程在命名空间下都具有唯一的 pid 值。
  2. 如何在不同命名空间中分配唯一的pid值;
      每个pid命名空间描述符下都具有pidmap[i]成员,这个成员中的位图页面使用位图映射的方式标记所有在当前命名空间中已经使用的pid,从而保证当命名空间中pid的唯一性,同时由于在32位体系下一个page的大小为 4K,最多能表现 410248个 pid,所以pidmap[]需要使用数组来使用更多的位图页来表示更多的pid值。
  3. 如何通过进程描述符 task_struct 快速找到 pid 值;
      进程描述符下拥有 pids[type] 成员,通过次成员可以获取不同了类型的 pid 实体,再通过pid实体下的 numbers[i] 获取对应 pid 命名空间层级下的upid 描述符,最终在 upid 下获取 nr 值。
task->pids[type].pid->numbers[level].nr
  1. 如何通过pid的值快速找到对应的进程描述符 task_struct;
    首先通过pid 的值 nr 和所在层级的命名空间实体地址 ns 计算出所在的pid哈希散列表索引 i, 然后在 pid_hash[i] 下的链表中找到对应的 upid 描述符,通过upid 的地址找到所在的 pid 描述符实体地址,最后通过 pid 实体下的pid->tasks[type] 链表找到对应的 进程描述符 task_struct,具体代码件《PID寻找进程描述符》。

PID 种类

  在 Linux 内核中存在着多种 id,如 (pid、tgid、pgid、ppid、sid ),他们或多或少都和最原始的pid 有关,但是又表示了不同的意思,通过这些 id 的学习,了解一些基本概念。
  如下命令行下执行如下指令查看相关id的情况:

> ps -eL -o pid,tid,pgid,ppid,sid,comm

  PID   TID  PGID  PPID   SID COMMAND
    1     1     0     0     0 init
    1     8     0     0     0 init
 1091  1091  1091     1  1091 init
 1092  1092  1091  1091  1091 init
 1093  1093  1093  1092  1093 sh
 1094  1094  1093  1093  1093 sh
 1099  1099  1093  1094  1093 sh
 1103  1103  1093  1099  1093 node
 3150  3150  3150  1994  1994 getpid

PID(Process ID)

  • 定义:任务ID,是通过vfork、fork、clone 创建新任务时在当前 pid 命名空间下分配的唯一标识一个任务的整数。
  • 注解:
    • 不论是线程还是进程这里获取的都是在当前 pid命名空间标识他们的唯一id。
    • 在线程中使用 gettid 获取的线程 id 是pid命名空间下唯一的,区别于 pthread 库中的 pthread_self 函数获取的线程id,pthread_self 获取的id 是子进程内唯一的。
  • 系统调用:
SYSCALL_DEFINE0(gettid)
{
	return task_pid_vnr(current);
}

TGID(Thread Group ID)

  • 定义:线程组 id,表示线程所在线程组的 id,这个值是表示的一定是进程的 id。
  • 注解:
    • 对于没有使用线程的情况,进程的 tgid 等于自身的 pid。
    • 在多线程程序中,线程组中所有线程的 tgid 相同,等于父进程的 tgid 也就是父进程的 pid。
  • 系统调用:
SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current);
}

PGID(Process Group ID)

  • 定义:进程组 ID,标识一组进程所在组的 id,等于该组组长进程的 pid。
  • 注解:
    • 进程组可以用来管理一组进程,例如发送信号到整个进程组。
  • 系统调用:
SYSCALL_DEFINE1(getpgid, pid_t, pid)
{
    struct task_struct *p;
    struct pid *grp;
    int retval;

    rcu_read_lock();
    if (!pid)
        grp = task_pgrp(current);
    else {
        retval = -ESRCH;
        p = find_task_by_vpid(pid);
        if (!p)
            goto out;
        grp = task_pgrp(p);
        if (!grp)
            goto out;

        retval = security_task_getpgid(p);
        if (retval)
            goto out;
    }
    retval = pid_vnr(grp);
out:
    rcu_read_unlock();
    return retval;
}

PPID(Parent Process ID)

  • 定义:父进程 ID,一般情况下是创建当前进程的进程 id,一定是一个进程 id。
  • 注解:
    • 如果进程中创建的是线程,则新线程的父进程与线程创建者的父进程相同。
    • 如果线程中创建线程,则新线程的父节进程 id 等于线程组中ledaer进程的父进程 id(共享父节进程)。
    • 如果线程中创建进程,则新进程的父节进程 id 等于线程组中leader进程的id。
    • 如果进程中创建进程时设置了CLONE_PARENT 标志同那么也是与创建者的父进程相同。
  • 系统调用如
SYSCALL_DEFINE0(getppid)
{
    int pid;

    rcu_read_lock();
    pid = task_tgid_vnr(rcu_dereference(current->real_parent));
    rcu_read_unlock();

    return pid;
}

SID(Session ID)

  • 定义:会话 ID,几个进程组可以合并成一个会话组(使用setsid系统调用)。
  • 注解:
    • 会话组中所有进程都有相同的SID。
    • 进程的会话 id 等于当前进程组的会话id。
    • 线程的会话 id 就是线程的进程id。
  • 系统调用如
SYSCALL_DEFINE1(getsid, pid_t, pid)
{
    struct task_struct *p;
    struct pid *sid;
    int retval;

    rcu_read_lock();
    if (!pid)
        sid = task_session(current);
    else {
        retval = -ESRCH;
        p = find_task_by_vpid(pid);
        if (!p)
            goto out;
        sid = task_session(p);
        if (!sid)
            goto out;

        retval = security_task_getsid(p);
        if (retval)
            goto out;
    }
    retval = pid_vnr(sid);
out:
    rcu_read_unlock();
    return retval;
}

PID 命名空间

命名空间

  Namespace 是对全局系统资源的隔离和封装,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。
  目前 3.0.1 内核中支持的命名空间有如下:

struct nsproxy {
	atomic_t count;
	struct uts_namespace *uts_ns; /* UTS(UNIX Time Sharing) 命名空间。 */
	struct ipc_namespace *ipc_ns; /* IPC 命名空间 */
	struct mnt_namespace *mnt_ns; /* MNT 命名空间 */
	struct pid_namespace *pid_ns; /* PID 命名空间 */	
	struct net 	     *net_ns;	  /* NET 命名空间 */
};

PID命名空间

  PID 命名空间允许为一组进程创建独立的pid命名空间,使得在不同 pid 命名空间中可以使用相同的 pid 而不会产生冲突,同时pid 命名空间支持嵌套行为,父级pid命名空间可以看到子pid命名空间,但是子pid空间对父级pid命名空间不感知。从进程的角度来来看一个创建在命名空间为2的进程,会在他的每一个父级命名空间中映射出唯一pid。
在这里插入图片描述

PID 数据结构

以下相关数据结构仅关注主要成员。

pidmap

struct pidmap {
       atomic_t nr_free;/* 位图中剩余可用个数 */
       void *page;      /* 位图页框指针,用作存储进程号的使用信息,32位下一页含有的位数32768 = 4*1024*8 */
};

  任务创建时内核会为其分配当前 pid 命名空间下唯一的 pid 值,在任务释放时会进行回收,如何使用最少的资源标对已经使用的 pid 值资源进行标识,是pid设计最先要解决的问题,如果简单用数组来存储已经使用的 pid 针对 32768 个 pid 时则需要 32768x4Bytes 来表示。为了解决以上问题, pid 使用了位图方式来进行管理,每个bit代表对应的位置代表了 pid 的值,如果已经使用则只需要将此bit置1即可,如此 32768 个pid只需要 4K 就能表示。

pid_namespace

struct pid_namespace {
    struct kref kref;
    struct pidmap pidmap[PIDMAP_ENTRIES];/* pid位图数组,数组只能管理4*1024*8个进程,可能不够所以需要用数组表示 */
    int last_pid;                        /* 命名空间中上一个进程所使用的pid */
    struct task_struct *child_reaper;    /* 指向当前命名空间1号进程,负责子进程收尸 */
    struct kmem_cache *pid_cachep;       /* 用于当前命名空间下申请pid实体,大小与level有关的,因为pid实体下的number[]是根据level有变化的,create_pid_namespace->create_pid_cachep */
    unsigned int level;                  /* 命名空间的层级 */
    struct pid_namespace *parent;        /* 父pid命名空间 */
#ifdef CONFIG_PROC_FS
    struct vfsmount *proc_mnt;
#endif
#ifdef CONFIG_BSD_PROCESS_ACCT
    struct bsd_acct_struct *bacct;
#endif
};

  pid 命名空间为 pid 提供了隔离特性,命名空间本身具有父子关系,在同一个level下可以有多个命名空间,使得进程在当前 pid 命名空间和他的父级命名空间中都具有唯一的pid值。pid 命名空间在进程描述符 task_struct 中可以找到,一个进程是否使用新的 pid 命名空间是由创建线程时是否带有标志位 CLONE_NEWPID 来决定的,如果CLONE_NEWPID标志设置则会申请新的pid命名空间,否则使用任务的创建者的命名空间。

task_struct

enum pid_type
{
	PIDTYPE_PID,	/* PID类型 */
	PIDTYPE_PGID,	/* Parent PID类型 */
	PIDTYPE_SID,	/* Session PID类型 */
	PIDTYPE_MAX
};

struct task_struct {
    ...
	struct pid_link pids[PIDTYPE_MAX];/* 带有链表节点的PID实体,对应三种不同的PID */

    pid_t pid;	/* 0层命名空间下当前进程或线程的id */
	pid_t tgid;	/* 0层命名空间下当前线程组id,线程组的所有线程使用和该线程组的领头线程相同的PID */
    
    struct nsproxy *nsproxy;/* 命名空间,其中包括 pid_ns */
    ...
};

  进程描述符中包括的 pids 成员具有三种类型,分别表示用来表示当前任务持有的三种 pid 描述符,对于 PIDTYPE_PID 类型除了 0号 init_task 之外,每个新任务在创建时都会申请一个 pid描述符。对于 PIDTYPE_PGID 类型如果新进程是进程组的组长,那么他的 PIDTYPE_PGID pid 描述符会指向 新进程创建者组长的 PIDTYPE_PGID pid 描述符(可能多个进程创建者在同一个组,具有相同组长,那么就会出现一个pid描述符被多个进程共享);如果创建的是线程那么他的 PIDTYPE_PGID pid 描述符是在进程描述符拷贝时继承于他的父任务,PIDTYPE_SID 类型的pid描述符和 PIDTYPE_PGID 的处理逻辑一致。
  对于 pid_t pidpid_t tgid 表示的是 pid 命名空间 0层下所看到的当前进程/线程的 pid 值和 tgid值,也称为全局 pid 值和全局 tgid 值,因为 0层是 0号进程 init_task 所在的固定层级。

pid_link

struct pid_link
{
    struct hlist_node node; /* 用于将进程描述加入pid.tasks[type]链表的节点 */
    struct pid *pid;        /* struct task_struct 中指向当前进程的pid实体 */
};

  pid_link 描述符包含在进程描述 task_struct 中,通过 node节点可以将进程加入到所有引用同一个pid描述符的 pid.tasks 链表中,方便从pid描述符找到引用它的所有进程描述符,这样使得同一个 pid 可以被不同的进程描述符使用成为可能,实际上 PIDTYPE_PGID 和 PIDTYPE_SID 在面对创建的进程是进程 leader 的时候就会出现这种情况。

pid

struct pid
{
    atomic_t count;         /* 当前pid引用计数 */
    unsigned int level;     /* pid所处的pid命名空间level */
    /* lists of tasks that use this pid */
    struct hlist_head tasks[PIDTYPE_MAX];/* 用于保存进程的3种pid的链表,不同的进程可以使用pid_link.node挂载到这个tasks对应类型的链表上 */
    struct rcu_head rcu;
    struct upid numbers[1]; /* upid数组,number[i]对应第i层pid命名空间中的pid值,大小与命名空间中的pid_cachep有关 */
};

  pid 描述符在任务创建时会申请对应的内存,用来描述pid 的相关属性,但是并不直接包含pid 值,真正的 pid 值存在于 number[i].nr 中,numbers[i].nr 对应了进程在 pid命名空间第 i 层使用的 pid 值,这种设计方案解决了 pid命名空间分层的问题。
  其中的struct upid numbers[1] 类似于一个零长数组,长度与 level(等于命名空间的leave) 有关,pid 描述符是从 pid_namespace.pid_cachep 中申请,而在命名空间创建时已经根据level 确定了 pid_cachep 中能申请的 numbers[] 长度,所以在通过 pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL) 申请时能够正确的申请到对应长度的 number[]。
  之前我们提到一个pid描述符符可以被多个进程共享,实际的做法就是将每个进程下的 pid_link.node 节点加入到对应类型的tasks[type] 链表上。

upid

struct upid {
    /* Try to keep pid_chain in the same cacheline as nr for find_vpid */
    int nr; 					/* nr表示ID值(这里ID的类型有三种,定义在pid_type枚举中) */
    struct pid_namespace *ns;   /* ns是指向该ID所属命名空间的指针 */
    struct hlist_node pid_chain;/* 用于挂载到pid_hash[i]哈希链上的节点 */
};

  upid 中才包含了真正的 pid 值,同时也包含了进程在每个 pid 命名空间的 ns 指针,其中的 pid_chain 是用于挂载到全局的 pid 哈希散列表 pid_hash[i] 上,方便我们通过 nr 和 ns 值能够快速在 pid_hash[i] 下的链表上找到所需要的 upid,然后一路往上找到 pid 描述符地址,最后找到 task_struct 进程描述符地址。

pid_hash

static struct hlist_head *pid_hash;/* 全局pid哈希散列表,用于把所有的 upid 集中管理起来 */

  pid_hash 是一个全局的 pid 哈希散列表,在 pidhash_init 初始化时会根据内存的大小确定pid_hash[] 数组的最大值,pid_hash[i] 下是一个双向链表头,pid_hash[i] 的索引是通过 upid 下的 nr + ns 作为参数计算得到,这种计算不可避免的会产生冲突,于是将这些计算冲突的 upid 组织在相同的索引下,形成双向链表,再通过nr 和 ns 反向查找 upid 需要再进行 nr 和 ns 判断来确认正确性。如下图所示 3个 nr=32 的upid 他们可能是一个任务下不同pid 命名空间层中的,也可能多个任务不同pid命名空间中的,pid_hash[] 在构成链表的时候并不关心属于哪个任务或那层pid命名空间,只是将他们按照规则连接成链表。
在这里插入图片描述

PID 代码分析

pidhash初始化

start_kernel
    └── pidhash_init
void __init pidhash_init(void)
{
    int i, pidhash_size;

    /* 1. 根据系统内核页面数申请pid_hash[],并返回 pidhash_size */
    pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18,
                       HASH_EARLY | HASH_SMALL,
                       &pidhash_shift, NULL, 4096);
    pidhash_size = 1 << pidhash_shift;
    
      /* 2. 初始化每个pid_hash[i]下的链表 */
    for (i = 0; i < pidhash_size; i++)
        INIT_HLIST_HEAD(&pid_hash[i]);
}

pid命名空间创建

do_fork
└── copy_process
    └── copy_namespaces
        └── create_new_namespaces
            └── copy_pid_ns
                └── create_pid_namespace
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{
     /* 1. 获取父进程的命名空间(tsk虽然是新进程但是此时的nsproxy还是从父进程复制过来的) */
	struct nsproxy *old_ns = tsk->nsproxy;
	struct nsproxy *new_ns;
	int err = 0;

	if (!old_ns)
		return 0;
    
	get_nsproxy(old_ns);
     /* 2. 如果不需要新的命名空间直接返回,共享父进程的命名空间 */
	if (!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
				CLONE_NEWPID | CLONE_NEWNET)))
		return 0;

...
    /* 3. 创建新进程的命名空间 */
	new_ns = create_new_namespaces(flags, tsk, tsk->fs);
	if (IS_ERR(new_ns)) {
		err = PTR_ERR(new_ns);
		goto out;
	}
    /* 4. 新的命名空间赋值给新进程 */
	tsk->nsproxy = new_ns;

out:
	put_nsproxy(old_ns);
	return err;
}

static struct nsproxy *create_new_namespaces(unsigned long flags,
			struct task_struct *tsk, struct fs_struct *new_fs)
{
	struct nsproxy *new_nsp;
	int err;

    /* 1. 申请新的命名空间 */
	new_nsp = create_nsproxy();
	if (!new_nsp)
		return ERR_PTR(-ENOMEM);
...
    /* 2. pid命名空间创建或使用父进程命名空间 */
	new_nsp->pid_ns = copy_pid_ns(flags, task_active_pid_ns(tsk));
	if (IS_ERR(new_nsp->pid_ns)) {
		err = PTR_ERR(new_nsp->pid_ns);
		goto out_pid;
	}

...
	return new_nsp;
...
	return ERR_PTR(err);
}

struct pid_namespace *copy_pid_ns(unsigned long flags, struct pid_namespace *old_ns)
{
    /* 1. 不需要创建新PID命名空间时直接返回之前继承的父进程pid命名空间 */
	if (!(flags & CLONE_NEWPID))
		return get_pid_ns(old_ns);
	if (flags & (CLONE_THREAD|CLONE_PARENT))
		return ERR_PTR(-EINVAL);
    /* 2. 创建新的pid命名空间 */
	return create_pid_namespace(old_ns);
}

static struct pid_namespace *create_pid_namespace(struct pid_namespace *parent_pid_ns)
{
	struct pid_namespace *ns;
	unsigned int level = parent_pid_ns->level + 1;
	int i, err = -ENOMEM;

    /* 1. 申请PID命名空间 */
	ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);
	if (ns == NULL)
		goto out;

    /* 2. 申请一个PID位图页框 */
	ns->pidmap[0].page = kzalloc(PAGE_SIZE, GFP_KERNEL);
	if (!ns->pidmap[0].page)
		goto out_free;
    
    /* 3. 初始化PID描述符申请使用的内存,因为PID描述符下的number[]是和ns level有关的,所以这里将level+1传递进去 */
    ns->pid_cachep = create_pid_cachep(level + 1);
    if (ns->pid_cachep == NULL)
        goto out_free_map;

    /* 4. pid命名空间level和父级命名空间设置 */
	kref_init(&ns->kref);
	ns->level = level;
    /* 命名空间可以存在父子关系,所以同一个level可以存在多个pid命名空间 */
	ns->parent = get_pid_ns(parent_pid_ns);

    /* 5. 除了0层命名空间,其他命名空间不使用0号作为进程号,第0层pid命名空间的位图初始化化由pidmap_init单独设置 */
    set_bit(0, ns->pidmap[0].page);/* 0 bit位置1 */
    atomic_set(&ns->pidmap[0].nr_free, BITS_PER_PAGE - 1);/* 设置pid可用个数-1 */

    /* 6.设置剩余位图页的可用个数,这里i=1也是由于第0层pid命名空间的位图初始化化由pidmap_init单独设置 */
	for (i = 1; i < PIDMAP_ENTRIES; i++)
		atomic_set(&ns->pidmap[i].nr_free, BITS_PER_PAGE);

	err = pid_ns_prepare_proc(ns);
	if (err)
		goto out_put_parent_pid_ns;

	return ns;

...
    
out:
	return ERR_PTR(err);
}

pid描述符申请和附着

do_fork
└── copy_process
    ├── alloc_pid
    └── attach_pid
static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					struct pt_regs *regs,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace)
{
...

    /* 1. 非0号进程时内核需要为新进程申请pid,除此之外pid=NULL */
	if (pid != &init_struct_pid) {
		retval = -ENOMEM;
        /* 2. 为进程分配pid描述符 */
		pid = alloc_pid(p->nsproxy->pid_ns);
		if (!pid)
			goto bad_fork_cleanup_io;
	}

    /* 2. 顶层pid 和 tgid 赋值 */
	p->pid = pid_nr(pid); 	/* 获取进程在第0层pid命名空间的pid值,放在进程描述符下,需要时可以直接找到 */
	p->tgid = p->pid;		/* 默认情况进程组id等于当前进程id */
	if (clone_flags & CLONE_THREAD)/* 如果是加入父进程的进程组(线程),则进程组id等于父进程组id */
		p->tgid = current->tgid;
...
    
    if (likely(p->pid)) {
		tracehook_finish_clone(p, clone_flags, trace);

		/* 如果本进程是线程组的group leader,则本进程的PGID和SID的pid实体共享父进程group leader的PGID和SID的pid实体,并把进程pid_link[type].node加入tasks[1]和tasks[2]链表中
         * 如果本进程是线程组中一个普通线程,它的PGID和SID从group leader复制时继承
         */
        /* 3. pid描述符关联到进程描述符上,并将pid_link.node 加入到对应的 pid.tasks[type]链表上 */
		if (thread_group_leader(p)) {
			if (is_child_reaper(pid))				 /* 判断进程是不是当前pid命名空间中的1号进程 */
				p->nsproxy->pid_ns->child_reaper = p;/* 1号进程负责给子进程收尸 */

			p->signal->leader_pid = pid;
			p->signal->tty = tty_kref_get(current->signal->tty);
			attach_pid(p, PIDTYPE_PGID, task_pgrp(current)); 		/* 将本进程加入到父进程的group leader 所在的pid.tasks[PIDTYPE_PGID]链表中,实现pid描述符共享 */
			attach_pid(p, PIDTYPE_SID, task_session(current));		/* 将本进程加入到父进程的group leader 所在的pid.tasks[PIDTYPE_SID]链表中,实现pid描述符共享 */
			list_add_tail(&p->sibling, &p->real_parent->children);	/* 本进程加入创建者的子进程链表,普通线程不用加入 */
			list_add_tail_rcu(&p->tasks, &init_task.tasks);			/* 本进程加init进程的进程链表,普通线程不用加入 */
			__this_cpu_inc(process_counts);
		}
		attach_pid(p, PIDTYPE_PID, pid);/* 将新进程和自己的pid实体关联起来,并将pid_link[PIDTYPE_PID].node 加入到 pid.tasks[PIDTYPE_PID]链表中 */
		nr_threads++;					/* 系统进程/线程数增加 */
	}

...
    return p;
...
    return ERR_PTR(retval);
}

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;

    /* 1. 申请pid实体,pid实体大小在pid命名空间创建时已经通过level初始化pid_cachep确定了,所以这里直接申请就可以了 */
    pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
    if (!pid)
        goto out;

    tmp = ns;
    /* 2. pid的命名空间存在多层的情况,第i层与numbers[i]对应,完成pid值的获取和ns赋值 */
    for (i = ns->level; i >= 0; i--) {
        nr = alloc_pidmap(tmp); /* 从当前层级的pid命名空间下的位图中找到pid 值 */
        if (nr < 0)
            goto out_free;

        pid->numbers[i].nr = nr;/* 设置当前层命名空间下进程的pid值 */
        pid->numbers[i].ns = tmp;/* 当前命名空间的父级pid命名空间 */
        tmp = tmp->parent;
    }

     /* 3. pid实体初始化 */
    get_pid_ns(ns);
    pid->level = ns->level;                     /* pid的level等于进程所处pid命名空间的level */
    atomic_set(&pid->count, 1);                 /* 当前pid引用+1 */
    for (type = 0; type < PIDTYPE_MAX; ++type)  /* 初始化三种pid链表头 */
        INIT_HLIST_HEAD(&pid->tasks[type]);

    upid = pid->numbers + ns->level;            /* 找到当前pid命名level下的uip */
    spin_lock_irq(&pidmap_lock);

    /* 4. 从当前level pid开始向顶层pid遍历,通过头插法将每层PID 命名空间的的upid插入pid_hash[]下的链表 */
    for ( ; upid >= pid->numbers; --upid)
        hlist_add_head_rcu(&upid->pid_chain,
                &pid_hash[pid_hashfn(upid->nr, upid->ns)]);
    spin_unlock_irq(&pidmap_lock);

out:
    return pid;
...
}

void attach_pid(struct task_struct *task, enum pid_type type,
		struct pid *pid)
{
	struct pid_link *link;

    /* 1. pid描述符挂载到进程描述的pid_link上 */
	link = &task->pids[type];
	link->pid = pid;
    /* 2. 将进程描述成下的pid_link通过node成员挂载到pid下的tasks[type]链表上 */
	hlist_add_head_rcu(&link->node, &pid->tasks[type]);
}

PID寻找进程描述符

find_task_by_pid_ns
├── find_pid_ns
└── pid_task
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns)
{
    rcu_lockdep_assert(rcu_read_lock_held());
    return pid_task(find_pid_ns(nr, ns), PIDTYPE_PID);
}

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
	struct hlist_node *elem;
	struct upid *pnr;

	hlist_for_each_entry_rcu(pnr, elem,
			&pid_hash[pid_hashfn(nr, ns)], pid_chain)/* 根据nr和ns计算出upid在pid_hash[i]的链表上,并查找可能得 pnr */
		if (pnr->nr == nr && pnr->ns == ns)			 /* pid_hash[i]链表上保存了多个冲突实体,所以还需进一步判断nr和ns */
			return container_of(pnr, struct pid,	 /* 根据number[level]的地址pnr 找到number[level]所处的pid实体的地址 */
					numbers[ns->level]);

	return NULL;
}
struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
	struct task_struct *result = NULL;
	if (pid) {
		struct hlist_node *first;
		/* DOUBT:如果是进程是进程组leader时,当前进程的pgid、sid会分别加入父进程的进程组中的pid tasks[type]链表中,
		意味着一个pid的tasks[1]或tasks[2]上会有多个进程描述符,这里直接取链表上的第一个不会有问题么 */
		first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
					      rcu_read_lock_held() ||
					      lockdep_tasklist_lock_is_held());
		if (first)
			result = hlist_entry(first, struct task_struct, pids[(type)].node);
	}
	return result;
}

  以上代码会将 pid 命名空间,进程描述符,pid实体,pid_hash散列表串联起来形成整个 pid 的管理体系。
在这里插入图片描述

🌀路西法 的个人博客拥有更多美文等你来读。

;