如何组织PID命名空间的各种ID?PID命名空间基本概念简析

linux 支持以下命名空间类型:
mount (clone_newns;2.4.19,2002)uts (clone_newuts; 2.6.19,2006)ipc (clone_newipc; 2.6.19,2006)pid (clone_newpid; 2.6.24,2008)network(clone_newnet;2.6.29,2009)user (clone_newuser;3.8,2013)cgroup(clone_newcgroup;4.6,2016)命名空间 api 由三个系统调用(clone()、unshare()和setns())以及许多/proc文件组成。clone_new* 常量包括:
clone_newipc,clone_newns , clone_newnet , clone_newpid ,clone_newuser和 clone_newuts 。
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);有二十多个不同的clone_*标志 控制clone()操作的各个方面,包括父进程和子进程是否共享资源,例如虚拟内存、打开的文件描述符和信号配置。
如果在调用中指定了clone_new* 之一,则会创建相应类型的 新命名空间 ,并且新进程将成为该****命名空间的成员;可以在flags中指定多个 clone_new* 。
在本文中,我们将研究 clone系统调用的 pid 命名空间部分,以及内核如何组织 pid 命名空间的各种id。本文分析基于内核版本 linux-5.15.60。
一、pid命名空间基本概念
pid命名空间隔离的全局资源是“进程id编号”空间。这意味着“不同pid命名空间”中的进程可以具有“相同的进程id”。pid命名空间用于“在主机系统之间迁移的容器”,同时保持容器内部进程的相同进程id。
与传统linux(或unix)系统上的进程一样,在pid命名空间中的进程id是唯一的,并且从 pid 1开始按顺序分配。同样地,与传统linux系统一样,pid 1——init进程是特殊的:它是在命名空间内创建的第一个进程,并且在命名空间内执行某些管理任务。
通过调用带有 clone_newpid 标志的clone()函数可以“创建一个新的pid命名空间”。我们将展示一个简单的示例程序,使用clone()函数创建一个新的pid命名空间,并使用该程序来解释pid命名空间的一些基本概念。
主程序使用clone()函数创建一个新的pid命名空间,并显示生成子进程的pid:
child_pid = clone(childfunc, child_stack + stack_size, /* points to start of downwardly growing stack */ clone_newpid | sigchld, argv[1]);printf(pid returned by clone(): %ldn, (long) child_pid);新创建的子进程在childfunc()中开始执行,该函数接收clone()调用的最后一个参数(argv[1])作为它的参数。这个参数后面再解释。childfunc()函数显示由clone()创建的子进程的进程id和父进程id,并最后执行标准的sleep程序:
printf(childfunc(): pid = %ldn, (long) getpid());printf(childfunc(): ppid = %ldn, (long) getppid()); ...execlp(sleep, sleep, 1000, (char *) null);当我们运行这个程序时,输出的前几行如下:
[root@haha demo]# ./pidns_init_sleep /proc30pid returned by clone(): 25070childfunc(): pid = 1childfunc(): ppid = 0mountingprocfs at /proc30前两行输出显示了从两个不同pid命名空间的角度来看子进程的pid:调用clone()的“调用者的命名空间”和“子进程所在的命名空间”。
换句话说,子进程有两个pid:在父命名空间中为 25070,在clone()调用创建的新pid命名空间中为1。下一行输出显示了子进程在所在pid命名空间中的父进程id(即getppid()返回的值)。
父进程pid为0,展示了pid命名空间操作的一个小特殊情况。
正如我们后面详细介绍的那样,pid命名空间形成了一个层次结构:一个进程只能看到“自己所在的pid命名空间”和 嵌套在该pid命名空间下的“子命名空间中”的进程。
由于由clone()“创建的子进程的父进程”处于不同的命名空间中,子进程无法“看到”父进程;因此,getppid()将父进程pid报告为零。
要解释pidns_init_sleep的最后一行输出,我们需要回到一个我们在讨论childfunc()函数实现时跳过的代码片段。
在linux系统上,每个进程都有一个特殊的目录路径/proc/pid,其中pid表示进程的id。这个目录包含了描述该进程的虚拟文件。
这个机制被称为pid命名空间模型。在一个pid命名空间中,只有属于该命名空间或其子命名空间的进程的信息会显示在对应的/proc/pid目录中。
[root@haha linux-5.15.60]# mount |grep proc on /procproc on /proc type proc (rw,nosuid,nodev,noexec,relatime)proc on /proc2 type proc (rw,relatime)proc on /proc2 type proc (rw,relatime)proc on /proc10 type proc (rw,relatime)proc on /proc20 type proc (rw,relatime)proc on /proc30 type proc (rw,relatime)[root@haha linux-5.15.60]#但是,要使与pid命名空间对应的/proc/pid目录可见,需要将proc文件系统挂载到该pid命名空间。我们可以在一个pid命名空间内的shell中,运行 mount命令来实现:
mount -t proc proc /mount_point另外,也可以使用mount()系统调用来挂载procfs,我们程序的childfunc()函数就是这样的:
char *mount_point = arg; if (mount_point != null) { mkdir(mount_point, 0555); /* create directory for mount point */ if (mount(proc, mount_point, proc, 0,null) == -1) errexit(mount); printf(mounting procfs at %sn, mount_point); }在我们的shell会话中,在/proc上挂载的procfs将显示父pid命名空间中可见的进程的pid子目录,而在/proc30 上挂载的procfs将显示驻留在子pid命名空间中的进程的pid子目录。
让我们回到运行pidns_init_sleep的shell会话。我们停止程序并使用ps命令在父命名空间的上下文中检查父进程和子进程的一些细节。
上述输出的最后一行中的ppid值(25069)显示“执行sleep的进程”的父进程是执行pidns_init_sleep的进程。
通过使用readlink命令来显示/proc/pid/ns/pid符号链接,我们可以看到这两个进程位于不同的pid命名空间中:
[root@haha demo]# readlink /proc/25069/ns/pidpid:[4026531836][root@haha demo]# readlink /proc/25070/ns/pidpid:[4026537948][root@haha demo]#此时,我们还可以使用新挂载的procfs来获取有关新pid命名空间中进程的信息,从该命名空间的角度来看。首先,我们可以使用以下命令获取该命名空间中的pid列表:
[root@haha demo]# ls -d /proc30/[1-9]*/proc30/1如上所示,pid命名空间只包含一个进程,其pid(在该命名空间内)为1。我们还可以使用/proc/pid/status文件作为另一种方法,获取关于该进程的一些相同信息,就像我们之前在shell会话中看到的那样:
[root@haha demo]# cat /proc30/1/status | egrep '^(name|pp*id)'name: sleeppid: 1ppid: 0[root@hahademo]#文件中的ppid字段为0,与getppid()报告子进程的父进程id为0的事实相匹配。(子命名空间看不到父命名空间的进程)
二、嵌套的pid命名空间
如前所述,pid(进程标识符)命名空间以父子关系的层级嵌套方式存在。在一个pid命名空间内,可以看到同一命名空间中的所有其他进程,以及属于后代命名空间的所有进程。
在这里,“看到”意味着能够进行基于特定pid的系统调用(例如,使用kill()向进程发送信号)。子pid命名空间中的进程无法看到仅存在于父pid命名空间(或更远的祖先命名空间)中的进程。
一个进程在pid命名空间层级中的每一层都会有一个pid,从其所在的pid命名空间一直到根pid命名空间。调用getpid()始终报告与进程所在命名空间相关联的pid。
我们可以使用这里显示的程序(multi_pidns.c)来展示进程在每个可见的命名空间中具有不同的pid。为简洁起见,我们将简单地解释程序的功能,而不是逐行解析其代码。
该程序以嵌套pid命名空间中的子进程递归方式创建一系列子进程。在调用程序时指定的命令行参数确定要创建多少个子进程和pid命名空间:
./multi_pidns 5除了创建一个新的子进程,每个递归步骤还在一个唯一命名的挂载点上挂载procfs文件系统。在递归的最后,最后一个子进程执行了sleep程序。上述命令行输出如下:
[root@haha demo]# ls -d /proc4/[1-9]* /proc4/1 /proc4/2 /proc4/3 /proc4/4 /proc4/5[root@haha demo]# ls -d /proc3/[1-9]* /proc3/1 /proc3/2 /proc3/3 /proc3/4[root@haha demo]# ls -d /proc2/[1-9]* /proc2/1 /proc2/2 /proc2/3[root@haha demo]# ls -d /proc1/[1-9]* /proc1/1 /proc1/2[root@haha demo]# ls -d /proc0/[1-9]* /proc0/1查看每个procfs中的pid,我们可以看到每个连续的procfs 级别包含的pid越来越少,这也表示了每个pid命名空间只显示属于该pid命名空间或其后代命名空间的进程。
让我们看下在所有可见的命名空间中,递归结束时的pid:
[root@haha demo]# grep -h 'name:.*sleep'/proc?/[1-9]*/status/proc0/1/status:name: sleep/proc1/2/status:name: sleep/proc2/3/status:name: sleep/proc3/4/status:name: sleep/proc4/5/status:name: sleep[root@haha demo]#换句话说,在最深层嵌套的 pid 命名空间 ( /proc0 ) 中,执行sleep的进程的 pid 为 1,而在创建的最顶层 pid 命名空间 ( /proc4 ) 中,该进程的 pid 为 5。
三、内核实现pid命名空间
要了解内核如何组织和管理进程id,首先要知道进程id 的类型:
内核中进程id 的类型用 pid_type 来描述,它定义在 includelinuxpid.h 中
enum pid_type { pidtype_pid, pidtype_tgid, pidtype_pgid, pidtype_sid, pidtype_max,};pid 是内核唯一区分每个进程的id。使用 fork 或 clone 系统调用时生成的进程将被内核分配一个新的唯一 pid 值。tgid 是线程组id。在一个进程中,如果使用 clone_thread 标志来调用 clone创建的进程,那么它就是该进程的一个线程(即轻量级进程,linux没有严格的进程概念),它们在一个线程组中。同一线程组中所有进程都有相同的tgid,但由于是不同的进程,所以它们的pid不同;线程的领导者(也称为主线程)的tgid 与其 pid 相同。pgid 独立进程可以组成进程组(使用 setpgrp 系统调用),进程组可以简化向组内所有进程发送信号的操作。例如,通过管道连接的连接属于同一个进程组。进程组id 称为 pgid。进程组中所有的进程都有相同的 pgid,等于组长的 pid。sid 可以将多个进程组组成一个会话组(使用 setsid 系统调用),可用于终端编程。会话组中所有进程都有相同的sid,该sid 存储在 task_struct 的 session 成员中。pid命名空间的层级关系如下:有 4 个命名空间。父命名空间派生两个子命名空间,其中一个子命名空间派生另一个子命名空间。
由于每个命名空间是相互隔离的,所以每个命名空间可以有一个 pid 为1的进程。由于命名空间的层次性,父命名空间是知道子命名空间的存在的,所以子命名空间需要映射到父命名空间,
因此上图中 第 1 级 的两个两个子命名空间中的 6 个进程 都映射到 其父命名空间的 pid 号 5~ 10.
系统使用 struct task_struct 表示一个进程,进程中存储了全局id 和 本地id。
全局id ---- 内核本身和初始命名空间中的唯一id。 系统启动时 init 进程属于初始命名空间。全局id 包括 pid_t pid 和 pid_t tgid 。默认情况下 pid_t 用 int 表示。
本地id ---- 对于一个特定的命名空间来说,它在其命名空间中分配的id就是本地id。本地id 用 struct pid * thread_pid 表示。
pid 数据结构
成员 tasks 是一个数组,每个数组项是一个哈希表头,对应一个id 类型,因此一个id 可用于多个进程(比如多个进程的进程组相同)。
struct upid { int nr;// id 的具体值 struct pid_namespace* ns;};struct pid { refcount_t count;// 引用数, 一个pid 可能用于多个进程 unsigned int level; spinlock_t lock; /* lists of tasks that use this pid */ struct hlist_head tasks[pidtype_max]; struct hlist_head inodes; /* wait queue for pidfd notifications */ wait_queue_head_twait_pidfd; struct rcu_head rcu; struct upid numbers[1]; // 柔性数组,特定命名空间可见的信息, 数组大小为level};pid 命名空间结构
struct pid_namespace { struct idr idr; struct rcu_head rcu; unsigned int pid_allocated; // 已分配多少个pid struct task_struct* child_reaper; // 指向当前命名空间的 init 进程,每个命名空间都有一个相当于全局init进程的进程 struct kmem_cache* pid_cachep; // 指向分配pid 的slab地址 unsigned int level;// 当前命名空间的级别。初始命名空间的级别为0,其子命名空间级别为1,依次递增。 struct pid_namespace* parent; // 指向父命名空间#ifdefconfig_bsd_process_acct struct fs_pin* bacct;#endif struct user_namespace* user_ns; struct ucounts* ucounts; int reboot;/* group exit code if this pidns was rebooted */ struct ns_common ns;} __randomize_layout;假设一个进程组中有a、b 两个进程,且进程组组长为a,进程a 是在 2 级命名空间中创建的,它的pid为45 ,映射到1级命名空间,分配给它的pid为123;然后它被映射到级别 0 的命名空间,分配给它的 pid 是 27760。
进程a 创建了一个线程 a1, 那么 a, a1, b 的命名空间和进程的关系如下图所示:
进程 a 的成员 struct pid* thread_pid 是内核对进程标识符的内部表示方式。struct pid 以哈希链表的方式存储,可以通过数字pid值快速找到它和它所引用的进程。struct pid 保存了 嵌套的多个命名空间的指针 和 进程在此命名空间的进程标识符 nr。命名空间使用基数树保存当前命名空间的 所有 struct pid,基数树的索引就是 进程在此命名空间的进程标识符。
最后有个问题:如何通过pid 快速找到 task_struct?
内核代码通过 find_task_by_vpid 来实现这个功能,其实通过上面这张图就可以得出结论,简单的步骤如下:
首先,通过 pid 和 命名空间nr,在基数树上找到对应的 struct pid;
然后,通过 pid_type 在 struct pid 找到对应的节点struct hlist_node;
最后,根据内核的 container_of 机制 和 struct hlist_node 可以找到 struct task_struct 结构体。
struct task_struct* find_task_by_vpid(pid_t vnr) { return find_task_by_pid_ns(vnr,task_active_pid_ns(current));}struct task_struct* find_task_by_pid_ns(pid_t nr, struct pid_namespace* ns) { rcu_lockdep_warn(!rcu_read_lock_held(), find_task_by_pid_ns() needs rcu_read_lock() protection); return pid_task(find_pid_ns(nr, ns),pidtype_pid);}struct pid* find_pid_ns(int nr, struct pid_namespace* ns) { return idr_find(&ns- >idr, nr);}struct task_struct* pid_task(struct pid* pid, enum pid_type type) { struct task_struct* result = null; if (pid){ structhlist_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;}#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

2022年中国物联网产业大会暨品牌盛会召开 达实品牌荣获2项大奖
什么是5G直放站的带外杂散指标?
UCA认证和DGT V16成品整套认证服务方案
lcd1602在proteus中怎么找_lcd1602proteus仿真
2018未来趋势发布大会亮相未来生活节
如何组织PID命名空间的各种ID?PID命名空间基本概念简析
首例!科创板上会取消审议!这家国内知名芯片企业到底怎么了?
智能监控系统:视频监控行业的一次新的革命
放电管的特点及应用
针对嵌入式设备中网络威胁的入侵检测和防御系统的算法
威迈斯IPO上市:新能源汽车销量持续增长零部件需求旺盛
TPT19新特性之最坏情况执行时间的指示
英众科技携多款PC产品亮相CITE2022
云迹机器人助力酒店营收 帮助酒店渡过难关
联发科、高通对掐:新处理器谁更强?
厉害了我的哥:特斯拉用太阳能为整座小岛600多户居民供电
智能服务机器人在未来会不会成为我们生活的必需品
最灵巧机器人Dex-Net 物品分拣远超人类水平
主机PC四合一 国外一公司将四个主机端整合
Oclean极客版智能牙刷评测 让刷牙变得不再是单一的机械运动