大家好,我是飞哥!如果你有在容器中执行过ps命令的经验,就会知道容器中进程的pid一般都比较小。比如我下面的例子。#ps-efPIDUSERTIMECOMMAND1root0:00./demo-ie13root0:00/bin/bash21root0:00ps-ef我想知道你是否和我一样好奇pid在容器进程是从哪里申请的?和在宿主机申请pid有什么区别?内核如何显示容器中的进程号?前面我们在《Linux进程是如何创建出来的?》中介绍了进程的创建过程。实际上,进程的pid命名空间和pid也是在这个进程中申请的。今天就带大家深入了解docker的核心之一pid命名空间的工作原理。一、Linux默认的pid命名空间在上一篇《Linux进程是如何创建出来的?》中,我们提到了进程命名空间成员nsproxy。//file:include/linux/sched.hstructtask_struct{.../*namespaces*/structnsproxy*nsproxy;}Linux在启动时会有一组默认的命名空间,定义在kernel/nsproxy.c文件中。//file:kernel/nsproxy.cstructnsproxyinit_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,};defaultpid命名空间为init_pid_ns,定义在kernel/pid.c下。//file:kernel/pid.cstructpid_namespaceinit_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,也就是位图。如果某位为1,则表示当前序号的pid已经分配完毕。另外,默认命名空间的层级初始化为0,这是一个代表树层次结构的节点。如果创建了多个命名空间,它们之间会形成一棵树。level表示树在第几层,根节点的层级为0。INIT_TASK0号进程,也叫空闲进程,它使用默认的init_nsproxy。//file:include/linux/init_task.h#defineINIT_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,\...}所有进程都是通过fork一个产生的。如果未指定命名空间,则所有进程都使用默认命名空间。2、创建一个新的Linuxpidnamespace这里,我们假设在创建进程时指定CLONE_NEWPID来创建一个独立的pidnamespace(Docker容器就是这样做的)。在《Linux进程是如何创建出来的?》一文中我们了解了创建进程的过程。整个创建过程的核心在于copy_process函数。在这个函数中,会申请并复制进程的地址空间、打开文件列表、文件目录等关键信息。另外,这里也完成了pid命名空间的创建。//file:kernel/fork.cstaticstructtask_struct*copy_process(...){...//2.1Copyprocessnamespacensproxyretval=copy_namespaces(clone_flags,p);//2.2申请pidpid=alloc_pid(p->nsproxy->pid_ns);//2.3记录pidp->pid=pid_nr(pid);p->tgid=p->pid;attach_pid(p,PIDTYPE_PID,pid);...}2.1创建进程时构造新命名空间在上面的copy_process代码中我们看到了对copy_namespaces函数的调用。在此函数中操作命名空间。//file:kernel/nsproxy.cintcopy_namespaces(unsignedlongflags,structtask_struct*tsk){structnsproxy*old_ns=tsk->nsproxy;如果(!(标志&(CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWPID|CLONE_NEWNET)))返回0;new_ns=create_new_namespaces(flags,tsk,user_ns,tsk->fs);tsk->nsproxy=new_ns;...}如果在创建进程的时候没有传入CLONE_NEWNS等几个flag,那么还是会复用之前的Defaultnamespace。这些标志的含义如下。CLONE_NEWPID:是否创建一个新的进程号命名空间来隔离宿主机的进程PIDCLONE_NEWNS:是否创建一个新的挂载点(文件系统)命名空间来隔离文件系统和挂载点CLONE_NEWNET:是否创建一个新的网络命名空间来隔离网卡、IP、端口、路由表等网络资源CLONE_NEWUTS:是否新建主机名和域名命名空间,在网络中独立标识自己CLONE_NEWIPC:是否新建IPC命名空间,隔离信号卷、消息队列和共享内存CLONE_NEWUSER:用于隔离用户和用户组。因为我们开始本节假设传入了CLONE_NEWPID标志。所以它会进入create_new_namespaces申请新的命名空间。//file:kernel/nsproxy.cstaticstructnsproxy*create_new_namespaces(unsignedlongflags,structtask_struct*tsk,structuser_namespace*user_ns,structfs_struct*new_fs){//申请一个新的nsproxystructnsproxy*new_nsp;new_nsp=create_nsproxy();......//CopyorcreatePIDnamespacenew_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.cstaticstructpid_namespace*create_pid_namespace(...){structpid_namespace*ns;//新的pid命名空间level+1unsignedintlevel=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);//设置新的命名空间级别ns->level=level;//新的命名空间和旧的命名空间形成一棵树ns->parent=get_pid_ns(parent_pid_ns);//初始化pidmapset_bit(0,ns->pidmap[0].page);atomic_set(&ns->pidmap[0].nr_free,BITS_PER_PAGE-1);对于(i=1;i
