大家好,我是飞哥!
如果你有在容器中执行ps命令的经验,你就会知道容器中进程的pid一般都很小以我下面的例子为例
# PS—efpidusertimecommand 1 root 0:00。/demo—ie13 root 0:00/bin/bash 21 root 0:00 PS—ef
不知道大家是否和我一样好奇容器进程中的pid是如何申请的主机应用pid和主机有什么区别内核如何在容器中显示进程号
在上一篇文章中,Linux进程是如何创建的中介绍了流程的创建过程实际上,进程的pid命名空间和pid也应用在这个进程中今天我就带大家深入了解一下docker核心之一pid namespace的工作原理
首先,Linux的默认pid名称空间
上一篇文章《Linux进程是如何创建的我们提到了进程的名称空间成员nsproxy
//file:include/Linux/sched . hstructtask _ struct/* namespaces */struct proxy * n proxy,
Linux在启动时会有一个默认的名称空间,这个名称空间是在kernel/n proxy . c文件中定义的。
默认的pid名称空间是init_pid_ns,它是在kernel/pid.c下定义的
//file:kernel/PID . cstructpid _ namespace init _ PID _ ns =kref=refcount=ATOMIC_INIT,,PID map == ATOMIC _ INIT,NULL,last_pid=0,级别=0,child _ reaper = ampinit_task,user _ ns = amp初始化用户
在pid名称空间中,我认为最需要注意的是两个字段一个是level,表示当前pid名称空间的级别另一个是pidmap,是位图如果某个位为1,则表示当前序列号的pid已被分配
此外,默认命名空间的级别初始化为0这是一个表示树的层次结构的节点如果创建了多个名称空间,它们将形成一棵树指示树位于哪个级别根节点的级别为0
INIT_TASK 0进程,也称为空闲进程,使用这个默认的init _ nsproxy。
//file:include/Linux/init _ TASK . h # define init _ TASK state=0,stack = amp初始化线程信息用法=ATOMIC_INIT,flags=PF_KTHREAD,普里奥=马克斯_PRIO—20静态优先级=最大PRIO—20正常优先级=最大PRIO—20
所有流程都是一个一个生成的如果没有指定命名空间,所有进程都使用默认命名空间
二,Linux新pid名称空间的创建
这里,我们假设在创建流程时,我们指定了CLONE_NEWpid来创建一个独立的pid名称空间。
在《Linux进程是如何创建的我们已经了解了创建流程的过程整个创建过程的核心是copy_process函数
在这个函数中,进程的地址空间,文件列表,文件目录等关键信息将被申请和复制,pid命名空间的创建也在这里完成。
//file:kernel/fork . cstatics tructtask _ struct * copy _ process//2.1命名空间nsproxyretval =复制进程的copy _ namespaces ,//2.2申请PID = alloc _ PID,//2.3记录pidp—PID = PID _ NR,p—tgid = p—PID,attach_pid,2.1创建流程时构造一个新的名称空间。
在上面的copy_process代码中,我们看到了对copy_namespaces函数的调用命名空间是在这个函数中操作的
CLONE _ new IPC | CLONE _ NEWPID | CLONE _ NEWNET)))return 0,new _ ns = create _ new _ namespaces(flags,tsk,user_ns,tsk—fs),tsk—n proxy = new _ ns,
如果在创建流程时没有传入几个标志(如CLONE_NEWNS ),那么仍然会重用以前的默认名称空间这些标志的含义如下
CLONE_NEWPID:是否要创建一个新的进程号命名空间,将其与主机的进程PID隔离。
CLONE_NEWNS:是否要创建一个新的挂载点名称空间来将文件系统与挂载点隔离开来。
CLONE_NEWNET:是否要创建一个新的网络命名空间来隔离网卡,IP,端口,路由表等网络资源。
CLONE_NEWUTS:是否要创建一个新的主机名和域名命名空间,以便在网络中独立地标识自己。
CLONE_NEWIPC:是否要创建一个新的IPC名称空间来隔离信号量,消息队列和共享内存。
CLONE_NEWUSER:用于隔离用户和用户组。
因为我们在本节开始时假设传入了CLONE_NEWPID标记因此,它将输入create_new_namespaces来申请新的名称空间
//file:kernel/nsproxy . cstaticstructnsproxy * create _ new _ namespaces//申请一个新的nsproxystructnsproxy * new _ nspnew _ NSP = create _ nsproxy(),//复制或创建PID命名空间new _ NSP—PID _ ns = copy _ PID _ ns(flags,user _ ns,tsk—ns proxy—PID _ ns),
在create_new_namespaces中会调用Copy_pid_ns来完成实际的创建,真正的创建过程是在create _ PID _ namespaces中完成的。
//file:kernel/PID _ namespace . cstaticstructpid _ namespace * create _ PID _ namespacetstructpid _ namespace * ns,//new pidnamespacelevel+1 unsigned level = parent _ PID _ ns—gt,level+1,//应用内存ns = kmem _ cache _ alloc(PID _ ns _ cachep,GFP _ kernel),ns—gt,pidmap(0)page=kzalloc(PAGE_SIZE,GFP _ KERNEL),ns—gt,PID _ cachep = create _ PID _ cachep(level+1),//设置新的命名空间级别ns—gt,水平=水平,//新命名空间和旧命名空间形成一棵树ns—gt,parent = get _ PID _ ns(parent _ PID _ ns),//初始化pidmapset_bit(0,ns—gt,pidmap(0)页面),atomic _ set(amp,ns—gt,pidmap(0)nr_free,BITS _ PER _ PAGE—1),for(I = 1,iltPIDMAP _ ENTRIESi++)atomic _ set(amp,ns—gt,pidmap(i)
在create_pid_namespace中,已经申请了新的pid名称空间,并且已经申请并初始化了其pidmap的内存。
另一个要点是,新名称空间和旧名称空间通过诸如parent和level之类的字段形成一棵树Parent指向上一级的命名空间,自己的级别用来表示层次结构,在下一级设置为+1级
最终的效果是新的进程有了一个新的pid名称空间,这个新的pid名称空间和父PID名称空间串联在一起效果如下图
如果pid有多层,会形成更直观的树形结构。
2.2申请流程id
创建名称空间后,copy_process的下一步是调用alloc_pid来分配pid。
//file:kernel/fork . cstatics tructtask _ struct * copy _ process//2.1命名空间nsproxyretval =复制进程的copy _ namespaces ,//2.2申请PID = alloc _ PID,
注意传入的参数是p—gt,nsproxy—gt,pid_ns .前面的过程创建了一个新的pid名称空间,这个名称空间就是级别为1的新pid_ns我们继续来看看alloc_pid的具体pid过程
//file:kernel/pid . cstructpid * alloc _ PID//申请PID内核对象PID = kmem _ cache _ alloc(NS—PID _ cachep,GFP _ kernel),//调用alloc_pidmap分配一个空闲的pidtmp = nspid级=ns级,for(i=ns级,I = 0,I—)NR = alloc _ PID map(tmp),ifnrlt0gotoout _ free管道仪表流程图编号(I)nr = nr管道仪表流程图编号(I)
注意上面代码中的两个细节。
我们通常说的pid在内核中不是简单的整数类型,而是一个小结构。
pid应用程序不申请一个,而是使用for循环申请多个应用程序。
之所以要申请多个应用,是因为对于容器中的进程,需要在它的父命名空间申请一个,而不是在你的当前命名空间申请一个让我们在下图中展示for循环的工作项目
首先从当前级别的命名空间申请一个pid,然后跟随命名空间的父节点,从每个级别申请一个,记录在pid—gt中,在数字数组中。
这里我多说一句,如果pid应用失败,会报错—ENOMEM错误,看起来像用户级的fork:无法分配内存,其实是pid不足造成的这个问题我写在《明明还有很多内存为什么我会报告错误无法分配内存提到了
2.3设置整数格式pid
申请并构造pid后,在task_struct上设置并记录。
//file:kernel/fork . cstatics tructtask _ struct * copy _ process//2.2申请PID = alloc _ PID,//2.3记录pidp—PID = PID _ NR,p—tgid = p—PID,attach_pid,
其中pid_nr是所获得的根pid名称空间中的pid号见pid_nr源代码
//file:include/Linux/PID . hstaticinlinepid _ tpid _ nrpid _ tnr = 0,if(pid)nr=pid数字(0)。NR,returnnr
然后调用attach_pid在你的pid链表中挂起应用的pid结构。
//file:kernel/PID . cvoidatch _ PID link = amp,任务—PID(类型),link—PID = PID,hlist _ add _ head _ rcu(amp,链接节点,ampPID—任务(类型)),
task—gt,Pids是一组链表。
三。容器过程的pid视图
pid已经申请了,那么如何查看容器中当前级别的进程号呢例如,我们在容器中看到的demo—ie进程的id是1
# PS—efpidusertimecommand 1 root 0:00。/demo—ie...
内核提供了一个函数来检查当前名称空间中进程的名称编号。
//file:kernel/PID . cpid _ tpid _ vnrreronpid _ NR _ ns(PID,task _ active _ PID _ ns(current)),
其中pid _ vnr用于查看容器中的进程pid,pid_vnr调用pid_nr_ns查看特定命名空间中进程的进程号。
函数pid_nr_ns接收几个参数。
第一个参数是进程中记录的pid对象。
第二个参数是指定的pid名称空间获取)。
有了这两个参数,就可以根据pid名称空间中记录的级别获得容器进程的当前pid。
//file:kernel/PID . cpid _ tpid _ NR _ nsstructupid * upid,PID _ tnr = 0,ifpidampampns—level = PID—level upid = amp,pid数字(ns级),ifu PID—nsns)NR = upid—NR,returnnr
在pid_nr_ns中,通过判断级别找出容器pid的整数值。
四。摘要
最后,例如,如果在0级pid名称空间中有一个进程的应用程序号是1256,则1级容器pid名称空间中的应用程序号是5那么进程及其在内存中的pid形式如下
然后容器查看进程的pid号,传入容器的pid命名空间,容器中进程的pid号5就可以打印出来了!!
。