Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1027001
  • 博文数量: 136
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1800
  • 用 户 组: 普通用户
  • 注册时间: 2016-12-21 22:26
个人简介

90后空巢老码农

文章分类

全部博文(136)

文章存档

2020年(34)

2019年(54)

2018年(47)

2017年(1)

我的朋友

分类: LINUX

2020-07-27 18:06:23

大家在linux当中使用容器的时候,都会或多或少的听说过命名空间这个东西,今天咱们就来过一下linux当中的pid命名空间。

在linux当中,pid_namespace是按照层级结构来组织的,在建立一个新的命名空间时,该命名空间中的所有pid对父命名空间都是可见的,但是子命名空间却无法看见父命名空间的pid;这样就导致了存在某些进程会有多个pid的情况,凡是可以看到该进程的命名空间,都会为其分配一个pid。这样的话就需要linux内核通过特定的数据结构来管理所谓的pid了。
首先,需要明确的是,每个进程除了pid这个特征值之外,还有其他的id,如下代码:

点击(此处)折叠或打开

  1. enum pid_type
  2. {
  3.     PIDTYPE_PID,// 进程id
  4.     PIDTYPE_TGID,// 线程组长id
  5.     PIDTYPE_PGID,// 进程组长id
  6.     PIDTYPE_SID,// 会话组长id
  7.     PIDTYPE_MAX,
  8. };
其次,我们必须区分全局id和局部id:
全局id:内核本身和初始命名空间中的唯一id号,对于每一种id类型,都有一个给定的全局id,保证在整个系统中是唯一的。
局部id:属于某个特定的命名空间,不具备全局有效性。对每个id类型,它们在所属的命名空间内部有效,但类型相同、值也相同的id可能出现在不同的命名空间当中。

接着,就需要介绍以下内核管理进程id的相关数据结构了:

点击(此处)折叠或打开

  1. struct pid_namespace {
  2.     struct kref kref;
  3.     ...
  4.     struct task_struct *child_reaper;// 相当于init进程,为孤儿进程调用wait系列函数
  5.     ...
  6.     unsigned int level;// 当前命名空间在全局当中的层次深度,初始命名空间的level为0,其余递增
  7.     struct pid_namespace *parent;// 指向父命名空间的指针
  8.         ...
  9. };
有了level变量之后,就可以根据level的值来判断该命名空间管理的进程会关联到多少个id(层级关系,子命名空间的id在父命名空间可见)

PID的管理,在内核当中围绕两个数据结构展开:

点击(此处)折叠或打开

  1. struct upid {
  2.     int nr;// 在指定命名空间中的id数值
  3.     struct pid_namespace *ns;// 指向nr所属的命名空间
  4. };

  5. struct pid
  6. {
  7.     refcount_t count;
  8.     unsigned int level;// 包含该进程的命名空间在命名空间层次结构当中的深度
  9.     /* lists of tasks that use this pid 所有用这个pid的task都位于下面的散列表*/
  10.     struct hlist_head tasks[PIDTYPE_MAX];// 每个数组元素都是一个散列表头,对应于一个id类型的task_struct散列表
  11.     /* wait queue for pidfd notifications */
  12.     wait_queue_head_t wait_pidfd;
  13.     struct rcu_head rcu;
  14.     struct upid numbers[1];// 别看定义只有一个,由于其在结构体最后,可以多分配,直接插入就可以实现数组功能
  15. };
内核当中pid相关的变量如下:

点击(此处)折叠或打开

  1. struct task_struct{
  2. ...
  3.     struct pid *thread_pid;
  4.     struct hlist_node pid_links[PIDTYPE_MAX];
  5. ...
  6. };

这样的话,内核当中的数据结构就能够满足下面两种不同的任务:
1. 给出局部数字id和对应的命名空间,查找此二元组对应的task_struct(依据tasks哈希表)
a). 这里会根据命名空间中的pid_hash(以upid.nr和upid.ns为key进行hash)找到对应的struct pid
b). 找到struct pid之后,根据对应的type,去结构体当中的tasks中找到对应的task_struct

点击(此处)折叠或打开

  1. struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns)
  2. {
  3.     RCU_LOCKDEP_WARN(!rcu_read_lock_held(),
  4.              "find_task_by_pid_ns() needs rcu_read_lock() protection");
  5.     return pid_task(find_pid_ns(nr, ns), PIDTYPE_PID);
  6. }
  7. struct task_struct *pid_task(struct pid *pid, enum pid_type type)
  8. {
  9.     struct task_struct *result = NULL;
  10.     if (pid) {
  11.         struct hlist_node *first;
  12.         first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
  13.                      lockdep_tasklist_lock_is_held());
  14.         if (first)
  15.             result = hlist_entry(first, struct task_struct, pid_links[(type)]);
  16.     }
  17.     return result;
  18. }


2. 给出task_struct、id类型、命名空间,取得命名空间局部id(依据pid当中的numbers数组)
a). 先根据task_struct当中的pid_links找到对应的pid结构,如果是pid,直接thread_pid,其他的需要pid_links来辅助进行
b). 获得pid实例之后,从struct pid的numbers数组中的相关信息,就可以得到对应的数字id:

点击(此处)折叠或打开

  1. pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
  2. {
  3.     struct upid *upid;
  4.     pid_t nr = 0;

  5.     if (pid && ns->level <= pid->level) {
  6.         upid = &pid->numbers[ns->level];
  7.         if (upid->ns == ns)
  8.             nr = upid->nr;
  9.     }
  10.     return nr;
  11. }
文章最后说一下内核为啥用struct pid这么个结构体来表示进程在内核当中的标识吧:
如果只用pid_t变量存储的话,当进程结束后又有新的进程复用pid_t的时候,会有问题;
如果用task_struct 来保存,当进程退出的时候,这个结构还被占用,占用空间太大;
使用一个占用空间小,且独立的结构能够解决上述两个问题,so~~~
阅读(363) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~