一文搞懂Docker容器里进程的 pid 是如何申请出来的?

news2024/11/17 23:50:13

如果大家有过在容器中执行 ps 命令的经验,都会知道在容器中的进程的 pid 一般是比较小的。例如下面我的这个例子。

# ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 ./demo-ie
   13 root      0:00 /bin/bash
   21 root      0:00 ps -ef

不知道大家是否和我一样好奇容器进程中的 pid 是如何申请出来的?和宿主机中申请 pid 有什么不同?内核又是如何显示容器中的进程号的?

事实上进程的 pid 命名空间、pid 也都是在这个过程中申请的。我今天就来带大家深入理解一下 docker 核心之一 pid 命名空间的工作原理。

一、Linux 的默认 pid 命名空间

我们提到了进程的命名空间成员 nsproxy。

//file:include/linux/sched.h
struct task_struct {
 ...
 /* namespaces */
 struct nsproxy *nsproxy;
}

Linux 在启动的时候会有一套默认的命名空间,定义在 kernel/nsproxy.c 文件下。

//file:kernel/nsproxy.c
struct nsproxy init_nsproxy = {
 .count = ATOMIC_INIT(1),
 .uts_ns = &init_uts_ns,
 .ipc_ns = &init_ipc_ns,
 .mnt_ns = NULL,
 .pid_ns = &init_pid_ns,
 .net_ns = &init_net,
};

其中默认的 pid 命名空间是 init_pid_ns,它定义在 kernel/pid.c 下。

//file:kernel/pid.c
struct pid_namespace init_pid_ns = {
 .kref = {
  .refcount       = ATOMIC_INIT(2),
 },
 .pidmap = {
  [ 0 ... PIDMAP_ENTRIES-1] = { ATOMIC_INIT(BITS_PER_PAGE), NULL }
 },
 .last_pid = 0,
 .level = 0,
 .child_reaper = &init_task,
 .user_ns = &init_user_ns,
 .proc_inum = PROC_PID_INIT_INO,
};

在 pid 命名空间里我觉得最需要关注的是两个字段。一个是 level 表示当前 pid 命名空间的层级。另一个是 pidmap,这是一个 bitmap,一个 bit 如果为 1,就表示当前序号的 pid 已经分配出去了。

另外默认命名空间的 level 初始化是 0。这是一个表示树的层次结构的节点。如果有多个命名空间创建出来,它们之间会组成一棵树。level 表示树在第几层。根节点的 level 是 0。

INIT_TASK 0号进程,也叫 idle 进程,它固定使用这个默认的 init_nsproxy。

//file:include/linux/init_task.h
#define INIT_TASK(tsk) \
{ 
 .state  = 0,      \
 .stack  = &init_thread_info,    \
 .usage  = ATOMIC_INIT(2),    \
 .flags  = PF_KTHREAD,     \
 .prio  = MAX_PRIO-20,     \
 .static_prio = MAX_PRIO-20,     \
 .normal_prio = MAX_PRIO-20,     \
 ...
 .nsproxy = &init_nsproxy,    \
 ......
}

所有进程都是一个派生一个的方式生成出来的。如果不指定命名空间,所有进程使用的都是使用缺省的命名空间。

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈

二、Linux 新 pid 命名空间创建

在这里,我们假设我们创建进程时指定了 CLONE_NEWPID 要创建一个独立的 pid 命名空间出来(Docker 容器就是这么干的)。

整个创建过程的核心是在于 copy_process 函数。

在这个函数中会申请和拷贝进程的地址空间、打开文件列表、文件目录等关键信息,另外就是pid 命名空间的创建也是在这里完成的 。

//file:kernel/fork.c
static struct task_struct *copy_process(...)
{
 ...
 //2.1 拷贝进程的命名空间 nsproxy
 retval = copy_namespaces(clone_flags, p);

 //2.2 申请 pid 
 pid = alloc_pid(p->nsproxy->pid_ns);

 //2.3 记录 pid 
 p->pid = pid_nr(pid);
 p->tgid = p->pid;
 attach_pid(p, PIDTYPE_PID, pid);
 ...
}

2.1 创建进程时构造新命名空间

在上面的 copy_process 代码中我们看到对 copy_namespaces 函数的调用。命名空间就是在这个函数中操作的。

//file:kernel/nsproxy.c
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{
 struct nsproxy *old_ns = tsk->nsproxy;
 if (!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
    CLONE_NEWPID | CLONE_NEWNET)))
  return 0;

 new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs);
 tsk->nsproxy = new_ns;
 ...
}

如果在创建进程时候没有传入 CLONE_NEWNS 等几个 flag,还是会复用之前的默认命名空间。这几个 flag 的含义如下。

  • CLONE_NEWPID: 是否创建新的进程编号命名空间,以便与宿主机的进程 PID 进行隔离
  • CLONE_NEWNS: 是否创建新的挂载点(文件系统)命名空间,以便隔离文件系统和挂载点
  • CLONE_NEWNET: 是否创建新的网络命名空间,以便隔离网卡、IP、端口、路由表等网络资源
  • CLONE_NEWUTS: 是否创建新的主机名与域名命名空间,以便在网络中独立标识自己
  • CLONE_NEWIPC: 是否创建新的 IPC 命名空间,以便隔离信号量、消息队列和共享内存
  • CLONE_NEWUSER: 用来隔离用户和用户组的。

因为我们本节开头假设传入了 CLONE_NEWPID 标记。所以会进入到 create_new_namespaces 中来申请新的命名空间。

//file:kernel/nsproxy.c
static struct nsproxy *create_new_namespaces(unsigned long flags,
 struct task_struct *tsk, struct user_namespace *user_ns,
 struct fs_struct *new_fs)
{
 //申请新的 nsproxy
 struct nsproxy *new_nsp;
 new_nsp = create_nsproxy();
 ......
 //拷贝或创建 PID 命名空间
 new_nsp->pid_ns = copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns);
}

create_new_namespaces 中会调用 copy_pid_ns 来完成实际的创建,真正的创建过程是在 create_pid_namespace 中完成的。

//file:kernel/pid_namespace.c
static struct pid_namespace *create_pid_namespace(...)
{
 struct pid_namespace *ns;

 //新 pid namespace level + 1
 unsigned int level = parent_pid_ns->level + 1;

 //申请内存
 ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);
 ns->pidmap[0].page = kzalloc(PAGE_SIZE, GFP_KERNEL);
 ns->pid_cachep = create_pid_cachep(level + 1);

 //设置新命名空间 level
 ns->level = level;

 //新命名空间和旧命名空间组成一棵树
 ns->parent = get_pid_ns(parent_pid_ns);

 //初始化 pidmap
 set_bit(0, ns->pidmap[0].page);
 atomic_set(&ns->pidmap[0].nr_free, BITS_PER_PAGE - 1);

 for (i = 1; i < PIDMAP_ENTRIES; i++)
  atomic_set(&ns->pidmap[i].nr_free, BITS_PER_PAGE);

 return ns;
}

在 create_pid_namespace 真正申请了新的 pid 命名空间,为它的 pidmap 申请了内存(在 create_pid_cachep 中申请的),也进行了初始化。

另外还有一点比较重要的是新命名空间和旧命名空间通过 parent、level 等字段组成了一棵树。其中 parent 指向了上一级命名空间,自己的 level 用来表示层次,设置成了上一级 level + 1。

其最终的效果就是新进程拥有了新的 pid namespace,并且这个新 pid namespace 和父 pidnamespace 串联了起来,效果如下图。

如果 pid 有多层的话,会组成更直观的树形结构。

2.2 申请进程id

创建完命名空间后,在 copy_process 中接下来接着就是调用 alloc_pid 来分配 pid。

//file:kernel/fork.c
static struct task_struct *copy_process(...)
{
 ...
 //2.1 拷贝进程的命名空间 nsproxy
 retval = copy_namespaces(clone_flags, p);
 ...

 //2.2 申请 pid 
 pid = alloc_pid(p->nsproxy->pid_ns);
 ...
}

注意传入的参数是 p->nsproxy->pid_ns。前面进程创建了新的 pid namespace,这个时候该命名空间就是 level 为 1 的新 pid_ns。我们继续来看 alloc_pid 具体 pid 的过程。

//file:kernel/pid.c
struct pid *alloc_pid(struct pid_namespace *ns)
{
 //申请 pid 内核对象
 pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);

 //调用到alloc_pidmap来分配一个空闲的pid
 tmp = ns;
 pid->level = ns->level;
 for (i = ns->level; i >= 0; i--) {
  nr = alloc_pidmap(tmp);
  if (nr < 0)
   goto out_free;

  pid->numbers[i].nr = nr;
  pid->numbers[i].ns = tmp;
  tmp = tmp->parent;
 }
 ...
 return pid;  
}

在上面的代码中要注意两个细节。

  • 我们平时说的 pid 在内核中并不是一个简单的整数类型,而是一个小结构体来表示的(struct pid)。
  • 申请 pid 并不是申请了一个,而是使用了一个 for 循环申请多个出来

之所以要申请多个,是因为对于容器里的进程来说,并不是在自己当前的命名空间申请就完事了,还要到其父命名空间中也申请一个。我们把 for 循环的工作工程用下图表示一下。

首先到当前层次的命名空间申请一个 pid 出来,然后顺着命名空间的父节点,每一层也都要申请一个,并都记录到 pid->numbers 数组中。

这里多说一下,如果 pid 申请失败的话,会报 -ENOMEM 错误,在用户层看起来就是“fork:无法分配内存”,实际是由 pid 不足引起的。

2.3 设置整数格式 pid

当申请并构造完 pid 后,将其设置在 task_struct 上,记录起来。

//file:kernel/fork.c
static struct task_struct *copy_process(...)
{
 ...
 //2.2 申请 pid 
 pid = alloc_pid(p->nsproxy->pid_ns);

 //2.3 记录 pid 
 p->pid = pid_nr(pid);
 p->tgid = p->pid;
 attach_pid(p, PIDTYPE_PID, pid);
 ...
}

其中 pid_nr 是获取的根 pid 命名空间下的 pid 编号,参见 pid_nr 源码。

//file:include/linux/pid.h
static inline pid_t pid_nr(struct pid *pid)
{
 pid_t nr = 0;
 if (pid)
  nr = pid->numbers[0].nr;
 return nr;
}

然后再调用 attach_pid 是把申请到的 pid 结构挂到自己的 pids[PIDTYPE_PID] 链表里了。

//file:kernel/pid.c
void attach_pid(struct task_struct *task, enum pid_type type,
  struct pid *pid)
{
 ...
 link = &task->pids[type];
 link->pid = pid;
 hlist_add_head_rcu(&link->node, &pid->tasks[type]);
}

task->pids 是一组链表。

三、容器进程 pid 查看

pid 已经申请好了,那在容器中是如何查看当前层次的进程号的呢?比如我们在容器中看到的 demo-ie 进程的 id 就是 1。

# ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 ./demo-ie
    ...

内核提供了个函数用来查看进程在当前某个命名空间的命名号。

//file:kernel/pid.c
pid_t pid_vnr(struct pid *pid)
{
 return pid_nr_ns(pid, task_active_pid_ns(current));
}

其中在容器中查看进程 pid 使用的是 pid_vnr,pid_vnr 调用 pid_nr_ns 来查看进程在特定命名空间里的进程号。

函数 pid_nr_ns 接收连个参数

  • 第一个参数是进程里记录的 pid 对象(保存有在各个层次申请到的 pid 号)
  • 第二个参数是指定的 pid 命名空间(通过 task_active_pid_ns(current)获取)。

当具备这两个参数后,就可以根据 pid 命名空间里记录的层次 level 取得容器进程的当前 pid 了

//file: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_nr_ns 中通过判断 level 就把容器 pid 整数值查出来了。

四、总结

最后,举个例子,假如有一个进程在 level 0 级别的 pid 命名空间里申请到的进程号是 1256,在 level 1 容器 pid 命名空间里申请到的进程号是 5。那么这个进程以及其 pid 在内存中的形式是下图这个样子的。

那么容器在查看进程的 pid 号的时候,传入容器的 pid 命名空间,就可以将该进程在容器中的 pid 号 5 给打印出来了!!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/349988.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

始于日志,不止于日志,Elastic Stack全面介绍

1、Elastic Stack是什么&#xff1f; 说Elastic Stack之前&#xff0c;先说一下ELK Stack。这个词相信很多人都是耳熟能详的&#xff0c;作为一个著名的日志系统解决方案&#xff0c;应用非常广泛。 “ELK”是三个开源项目的首字母缩写词&#xff1a;Elasticsearch、Logstash…

第五章.与学习相关技巧—Batch Normalization

第五章.与学习相关技巧 5.3 Batch Normalization Batch Norm以进行学习时的mini_batch为单位&#xff0c;按mini_batch进行正则化&#xff0c;具体而言&#xff0c;就是进行使数据分布的均值为0&#xff0c;方差为1的正则化。Batch Norm是调整各层激活值的分布使其拥有适当的广…

进程组和用处

进程组&#xff1a;一个或多个进程的集合&#xff0c;进程组id是一个正整数。组长进程&#xff1a;进程组id 进程id组长进程可以创建一个进程组&#xff0c;创建该进程组的进程&#xff0c;终止了&#xff0c;只要进程组有一个进程存在&#xff0c;进程组就存在&#xff0c;与…

卷积神经网络(CNN)

目录The Basic Usage of CNNPadding&#xff08;填充&#xff09;Weights&#xff08;权重&#xff09;PoolingThe Basic Usage of CNN What are Convolutional Neural Networks? They’re basically just neural networks that use Convolutional layers&#xff08;卷积层…

家政服务小程序实战教程13-接入客服

小程序在微信里使用&#xff0c;以其无需安装随用随走为特点。但是有个问题是&#xff0c;如果提供商品或者服务的&#xff0c;用户如果有问题往往希望平台的运营方给出专业的解答。为了满足这类需求&#xff0c;就需要我们提供客服接入的功能&#xff0c;用户可以点击客服图标…

Linux使用定时任务监控java进程并拉起

需求描述&#xff1a; 设计一个脚本&#xff0c;通过Linux定时任务&#xff0c;每分钟执行一次&#xff0c;监控jar包进程是否存在&#xff0c;存在则不做动作&#xff0c;不存在则重新拉起jar包程序。 定时任务配置&#xff1a; */1 * * * * bash -x /root/myfile/jars/che…

stk 根据六根数文件生成卫星轨迹(一)

先简单介绍下上面的参数。 Propagator预报轨道模型。 TwoBody为二体&#xff08;开普勒运动模型&#xff09;。HPOP为高精度轨道模型。目前只用到这两个。 下图为六根数参数 Orbit Epoch&#xff1a;为根数时间&#xff08;UTC&#xff09; Semimajor Axis&#xff1a;长半…

软考高项——第五章进度管理

范围管理进度管理总线索规划进度管理定义活动活动排序估算活动资源估算活动时间制定进度管理计划控制进度进度管理总线索 进度管理的总线索包括&#xff1a; 1&#xff09;规划进度管理 2&#xff09;定义活动 3&#xff09;活动排序 4&#xff09;估算活动资源 5&#xff09;…

pandas基本操作

df.head()/tail() 查看头/尾5条数据&#xff1b;df.info 查看表格简明概要&#xff1b;df.dtypes 查看字段数据类型&#xff1b;df.index 查看表格索引&#xff1b;df.columns 查看表格列名&#xff1b;df.values 以array形式返回指定数据的取值&#xff1b;list(dt.groupby(&q…

vue2 使用 cesium 篇

vue2 使用 cesium 篇 今天好好写一篇哈&#xff0c;之前写的半死不活的。首先说明&#xff1a;这篇博文是我边做边写的&#xff0c;小白也是&#xff0c;实现效果会同时发布截图&#xff0c;如果没有实现也会说明&#xff0c;仅仅作为技术积累&#xff0c;选择性分享&#xff0…

远程管理时代,还得是智能化PDU才靠得住!

在如今这个信息技术高速发展的时代&#xff0c;数据中心IDC机房服务器数量与日俱增&#xff0c;提供DNS域名服务、主机托管服务、虚拟主机服务等服务的服务器是IDC最基本的功能之一。服务器需要7*24小时不间断持续工作&#xff0c;但当服务器数量很大&#xff0c;服务器工作、重…

.net6API使用AutoMapper和DTO

AutoMapper&#xff0c;是一个转换工具&#xff0c;说到AutoMapper时&#xff0c;就不得不先说DTO&#xff0c;它叫做数据传输对象(Data Transfer Object)。 通俗的来说&#xff0c;DTO就是前端界面需要用的数据结构和类型&#xff0c;而我们经常使用的数据实体&#xff0c;是数…

华为ensp模拟校园网/企业网实例(同城灾备及异地备份中心保证网络安全)

文章简介&#xff1a;本文用华为ensp对企业网络进行了规划和模拟&#xff0c;也同样适用于校园、医院等场景。如有需要可联系作者&#xff0c;可以根据定制化需求做修改。作者简介&#xff1a;网络工程师&#xff0c;希望能认识更多的小伙伴一起交流&#xff0c;私信必回。一、…

多元回归分析 | CNN-LSTM卷积长短期记忆神经网络多输入单输出预测(Matlab完整程序)

多元回归分析 | CNN-LSTM卷积长短期记忆神经网络多输入单输出预测(Matlab完整程序) 目录 多元回归分析 | CNN-LSTM卷积长短期记忆神经网络多输入单输出预测(Matlab完整程序)预测结果评价指标基本介绍程序设计参考资料预测结果 评价指标 训练集平均绝对误差MAE:0.69559 训练…

宝塔搭建实战php源码人才求职管理系统后台端thinkphp源码(一)

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 在开源社区里看到了这一套系统&#xff0c;骑士人才系统SE版&#xff0c;搭建测试了&#xff0c;感觉很不错。能够帮助一些想做招聘平台的朋友降低开发成本&#xff0c;就是要注意&#xff0c;想商业使用的话&…

QT+OpenGL光照2

QTOpenGL材质 本篇完整工程见gitee:QtOpenGL 对应点的tag&#xff0c;由turbolove提供技术支持&#xff0c;您可以关注博主或者私信博主 材质 在现实世界中&#xff0c;每个物体会对光照产生不同的反应 在OpenGL中模拟多种类型的物体&#xff0c;必须为每种物体分别定义一个…

IO模型--从BIO、NIO、AIO到内核select、poll、epoll剖析

IO基本概述 IO的分类 IO以不同的维度划分&#xff0c;可以被分为多种类型&#xff1b;从工作层面划分成磁盘IO&#xff08;本地IO&#xff09;和网络IO&#xff1b; 也从工作模式上划分&#xff1a;BIO、NIO、AIO&#xff1b;从工作性质上分为阻塞式IO与非阻塞式IO&#xff1b…

低代码/零代码的快速开发框架

目前国内主流的低代码开发平台有&#xff1a;宜搭、简道云、明道云、云程、氚云、伙伴云、道一云、JEPaaS、华炎魔方、搭搭云、JeecgBoot 、RuoYi等。这些平台各有优劣势&#xff0c;定位也不同&#xff0c;用户可以根据自己需求选择。 一、阿里云宜搭 宜搭是阿里巴巴集团在20…

分布式文件存储Minio学习入门

文章目录一、分布式文件系统应用场景1. Minio介绍Minio优点2. MinIO的基础概念、3. 纠删码ES(Erasure Code)4. 存储形式5. 存储方案二、Docker部署单机Minio三、minio纠删码模式部署四、分布式集群部署分布式存储可靠性常用方法冗余校验分布式Minio优势运行分布式minio使用dock…

如何设置股票接口版交易软件的指标涨跌家数?

如何设置股票接口版交易软件指标涨跌家数&#xff1f;今天小编就以通达信为例给大家介绍一下&#xff0c;很多人其实不知道通达信里面有个很厉害的股票情绪的指标&#xff0c;叫做通达信涨跌家数&#xff0c;打开在通达信软件k线界面&#xff0c;然后输入880005就可以找到了。下…