PID即进程描述符在linux kernel中的分配和管理比较复杂。 本文分析了其相关数据结构以及函数。 (代码基于v3.0.3)

和PID相关的数据结构有

[cpp] view plaincopy
  1. struct pid
  2. {
  3. atomic_t count;
  4. unsigned int level;
  5. struct hlist_head tasks[PIDTYPE_MAX];
  6. struct rcu_head rcu;
  7. struct upid numbers[1];
  8. };

其中 count是指向该数据结构的引用次数。

level是该pid在pid_namespace中处于第几层。当level=0时表示是global namespace,即最高层。pid_namespace这个数据结构将在后面进行解释。

tasks[PIDTYPE_MAX]数组中每个元素都代表了不同的含义。PIDTYPE_MAX表示pid所表示的类型的最大数。该值定义在enum pid_type中

[cpp] view plaincopy
  1. enum pid_type
  2. {
  3. PIDTYPE_PID,
  4. PIDTYPE_PGID,
  5. PIDTYPE_SID,
  6. PIDTYPE_MAX
  7. };

PIDTYPE_PID代表进程描述符(PID) 。 PIDTYPE_PGID代表一组进程描述符。 一组进程(process)可以组成一个群组,并且有一个组描述符。 这样的好处是如果有一个信号是针对这个组描述符,该群组内的所有进程都可以接受到。 PIDTYPE_SID是对组描述符再做一个群组,形成一个session。这是更高一个层次的抽象。

tasks[i]指向的是一个哈希表。譬如说tasks[PIDTYPE_PID]指向的是PID的哈希表。

rcu域我也没有搞明白到底是做什么的:(

numbers[1]域指向的是upid结构体。 numbers数组的本意是想表示不同的pid_namespace。 一个PID可以属于不同的namespace, numbers[0]表示global namespace,numbers[i]表示第i层namespace,i越大所在层级越低。目前该数组只有一个元素, 即global namespace。所以namepace的概念虽然引入了pid,但是并未真正使用,在未来的版本可能会用到。

接下来我们再看看upid这个数据结构

[cpp] view plaincopy
  1. struct upid {
  2. int nr;
  3. struct pid_namespace  × ns;
  4. struct hlist_node pid_chain;
  5. };

pid结构体中的numbers域指向了upid结构体。该结构体中

nr是pid的值, 即 task_struct中 pid_t pid域的值。

ns指向该pid所处的namespace。

linux内核将所有进程的upid都存放在一个哈希表中(pid_hash),以方便查找和统一管理。因此,pid结构体中的numbers[0]指向的upid instance存放在pid_hash里。通过pid_chain即哈希表的节点就能够找到该upid所在pid_hash中的位置。

接下来再看看pid_namespace结构体

[cpp] view plaincopy
  1. struct pid_namespace {
  2. struct kref kref;
  3. struct pidmap pidmap[PIDMAP_ENTRIES];
  4. int last_pid;
  5. struct task_struct *child_reaper;
  6. struct kmem_cache *pid_cachep;
  7. unsigned int level;
  8. struct pid_namespace *parent;
  9. };

kref表示指向pid_namespace的个数。

pidmap结构体表示分配pid的位图。当需要分配一个新的pid时只需查找位图,找到bit为0的位置并置1,然后更新统计数据域(nr_free)。

[cpp] view plaincopy
  1. struct pidmap {
  2. atomic_t nr_free;
  3. void *page;
  4. };

nr_free表示还能分配的pid的数量。

page指向的是存放pid的物理页。

所以pidmap[PIDMAP_ENTRIES]域表示该pid_namespace下pid已分配情况。

last_pid用于pidmap的分配。指向最后一个分配的pid的位置。(不是特别确定)

child_reaper指向的是一个进程。 该进程的作用是当子进程结束时为其收尸(回收空间)。由于目前只支持global namespace,这里child_reaper就指向init_task。

pid_cachep域指向分配pid的slab的地址。

level表示该namespace处于哪一层, 现在这里显然是0。

parent指向该namespace的父亲namespace。 现在一定是NULL。

介绍完pid_namespace相关的数据结构,我们来看看设计它们的本意是什么。 Linux中增加namespace这个概念的目的是为了虚拟化和方便管理。 比如在不同的namespace中可以有pid相同的进程。 pid_namespace的结构是层次化的。而且在child namespace中的进程一定会有parent namespace的映射。这句话可能不太好理解。可以结合下面这张图

以上图为例子,此时pid_hash全局哈希表中此时会存放15个(9+3+3)upid的instance。

前面介绍了这么多关于pid, upid, pid_namespace的概念, 接下来我们再来看看它们和task_struct之间的关系

右下角的椭圆形虚线框是全局pid_hash,所有已分配的upid都会保存在该hash表中。

左下角的椭圆形虚线框表示的是pid_namespace的关系。 当然目前只有一层。

Linux内核通过task_struct来管理进程。在task_struct中,和pid相关的域有

[cpp] view plaincopy
  1. struct task_struct
  2. {
  3. ...
  4. pid_t pid;
  5. pid_t tgid;
  6. struct task_struct *group_leader;
  7. struct pid_link pids[PIDTYPE_MAX];
  8. struct nsproxy *nsproxy;
  9. ...
  10. };

pid指该进程的进程描述符。 后面会介绍在fork函数中如何对其进行赋值的。

tgid指该进程的线程描述符。在linux内核中对线程并没有做特殊的处理,还是由task_struct来管理。所以从内核的角度看, 用户态的线程本质上还是一个进程。对于同一个进程(用户态角度)中不同的线程其tgid是相同的,但是pid各不相同。 主线程即group_leader(主线程会创建其他所有的子线程)。如果是单线程进程(用户态角度),它的pid等于tgid。

对于用户态程序来说,调用getpid()函数其实返回的是tgid。想想是为什么?:)

group_leader除了在多线程的模式下指向主线程,还有一个用处, 当一些进程组成一个群组时(PIDTYPE_PGID), 该域指向该群组的leader。

nsproxy指针指向namespace相关的域。

[cpp] view plaincopy
  1. struct nsproxy {
  2. atomic_t count;
  3. struct uts_namespace *uts_ns;
  4. struct ipc_namespace *ipc_ns;
  5. struct mnt_namespace *mnt_ns;
  6. struct pid_namespace *pid_ns;
  7. struct net           *net_ns;
  8. };

通过nsproxy域可以知道该task_struct属于哪个pid_namespace, 当然现在一定是global namespace。(已经讲了很多次了:))

其他一些域也是namespace相关,这里就不展开解释了。

pids[PIDTYPE_MAX]指向了和该task_struct相关的pid结构体。

pid_link的定义如下

[cpp] view plaincopy
  1. struct pid_link
  2. {
  3. struct hlist_node node;
  4. struct pid *pid;
  5. };

在linux内核中如果想获得该task_struct所对应的pid可以调用task_pid()函数, 这个函数的实现非常简单

[cpp] view plaincopy
  1. static inline struct pid *task_pid(struct task_struct *task)
  2. {
  3. return task->pids[PIDTYPE_PID].pid;
  4. }

自此我已将pid相关的数据结构介绍完了, 下面我们再看看和pid相关的使用。

(1)fork函数中如何分配一个新的pid?

fork(), vfork()还有clone()函数最终都是通过调用do_fork()来进行工作。 分配新的pid是在copy_process()函数实现的。 do_fork()函数会调用copy_process(), 它们之间的关系我会在以后的文章中进行介绍。

[cpp] view plaincopy
  1. static struct task_struct *copy_process(unsigned long clone_flags,
  2. unsigned long stack_start,
  3. struct pt_regs *regs,
  4. unsigned long stack_size,
  5. int __user *child_tidptr,
  6. struct pid *pid,
  7. int trace)
  8. {
  9. ...
  10. if (pid != &init_struct_pid) {
  11. retval = -ENOMEM;
  12. pid = alloc_pid(p->nsproxy->pid_ns);
  13. if (!pid)
  14. goto bad_fork_cleanup_io;
  15. }
  16. p->pid = pid_nr(pid);
  17. p->tgid = p->pid;
  18. if (clone_flags & CLONE_THREAD)
  19. p->tgid = current->tgid;
  20. ...
  21. }

我只将和pid分配的代码列出来了。

alloc_pid函数将分配一个新的pid struct。 简单的说该函数的功能是在pidmap上找到一个未用的pid bit,如若找不着,着说明已经没有可用的pid了,该namespace所在pid配给全部用完。 然后将其保存到pid_hash的哈希表里,然后再将pid结构体返回。

pid_nr函数的实现也很简单

[cpp] view plaincopy
  1. static inline pid_t pid_nr(struct pid *pid)
  2. {
  3. pid_t nr = 0;
  4. if (pid)
  5. nr = pid->numbers[0].nr;
  6. return nr;
  7. }

返回该pid所在global namespace的值。

后面几行代码用于区分进程和线程中tgid的值。

和pid相关的数据结构,函数定义可以在 include/linux/pid.h include/linux/pid_namespace.h 以及 kernel/pid.c kernel/pid_namespace.c中找到。

linux内核PID管理--命名空间相关推荐

  1. linux内核PID管理

    PID即进程描述符在linux kernel中的分配和管理比较复杂. 本文分析了其相关数据结构以及函数. (代码基于v3.0.3) 和PID相关的数据结构有 [cpp] view plaincopy ...

  2. Linux内核-进程管理

    Linux内核-进程管理 引言 本文主要介绍Linux内核进程管理相关知识,包括进程描述符.进程创建.销毁.状态.线程的实现以及Linux进程相关命令等. 进程描述符 内核把进程的列表存放在叫做任务队 ...

  3. 【Linux 内核 内存管理】虚拟地址空间布局架构 ③ ( 内存描述符 mm_struct 结构体成员分析 | mmap | mm_rb | task_size | pgd | mm_users )

    文章目录 一.mm_struct 结构体成员分析 1.mmap 成员 2.mm_rb 成员 3.get_unmapped_area 函数指针 4.task_size 成员 5.pgd 成员 6.mm_ ...

  4. 【Linux 内核 内存管理】内存管理架构 ④ ( 内存分配系统调用过程 | 用户层 malloc free | 系统调用层 brk mmap | 内核层 kmalloc | 内存管理流程 )

    文章目录 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) 二.内存管理流程 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) " 堆内存 " ...

  5. 【Linux 内核 内存管理】内存管理架构 ② ( 用户空间内存管理 | malloc | ptmalloc | 内核空间内存管理 | sys_brk | sys_mmap | sys_munmap)

    文章目录 一.用户空间内存管理 ( malloc / free / ptmalloc / jemalloc / tcmalloc ) 二.内核空间内存管理 1.内核内存管理系统调用 ( sys_brk ...

  6. 【Linux 内核 内存管理】内存管理架构 ① ( 内存管理架构组成 | 用户空间 | 内核空间 | MMU 硬件 | Linux 内核架构层次 | Linux 系统调用接口 )

    文章目录 一.内存管理架构组成 ( 用户空间 | 内核空间 | MMU 硬件 ) 二.Linux 内核架构层次 三.Linux 系统调用接口 一.内存管理架构组成 ( 用户空间 | 内核空间 | MM ...

  7. 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )

    文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...

  8. 【Linux 内核 内存管理】RCU 机制 ② ( RCU 机制适用场景 | RCU 机制特点 | 使用 RCU 机制保护链表 )

    文章目录 一.RCU 机制适用场景 二.RCU 机制特点 三.使用 RCU 机制保护链表 一.RCU 机制适用场景 在上一篇博客 [Linux 内核 内存管理]RCU 机制 ① ( RCU 机制简介 ...

  9. pae扩展内存 linux,浅析linux内核内存管理之PAE

    浅析linux内核内存管理之PAE 早期Intel处理器从80386到Pentium使用32位物理地址,理论上,这样可以访问4GB的RAM.然而,大型服务器需要大于4GB的RAM来同时运行数以千计的进 ...

最新文章

  1. slam开发|adb的用法一些总结
  2. 【顶会资源汇总】2020最新视觉描述生成文献大列表
  3. BZOJ-1009-GT考试-HNOI2008
  4. 设置eclipse新建maven项目默认使用jdk1.8
  5. java中$和 的区别详解_Mybatis之#{}与${}的区别使用详解
  6. jsp 中包含 一个路径为变量的文件
  7. Java经典实例:实现一个简单堆栈
  8. XP蓝屏代码集(转)
  9. hmcl手机版下载_最新HMCL下载地址
  10. solidword入门使用
  11. 基于开源方案构建统一的文件在线预览与office协同编辑平台的架构与实现历程
  12. 关闭惠普计算机通电启动注册表,惠普电脑关机后自动重启的解决办法
  13. java chr()_chr码值对应列表大全
  14. 全国平均工资水平排序 北京上海西藏居前三名
  15. 字节跳动音乐梅开二度,腾讯音乐、网易云要警惕了
  16. 前端js实现本地模糊搜索
  17. MySQL SQL语句 生成32位 UUID
  18. 小程序 Base64转换字符串
  19. Dockerfile(centos7:lastest + jdk8)
  20. STM32驱动无刷直流电机学习(2)

热门文章

  1. zabbix 自动发现和注册
  2. [转载] ffmpeg超详细综合教程——摄像头直播
  3. xmpp 服务器配置 open fire for windows 及 spark 测试
  4. CentOS下查看已经登录用户并踢出的方法
  5. 2018-01-02 JavaScript实现ZLOGO: 用语法树实现多层循环...
  6. Redis概述与Redis集群(一)
  7. vue中过渡动画(类名实现方式)
  8. JavaScript七种非常经典的创建对象方式
  9. Dubbo源码解析 --- DIRECTORY和ROUTER
  10. module_init和init_module的区别