Docker 学习笔记10 容器技术原理 PID Namespace

一、PID namespace介绍

Linux通过命名空间管理进程pid,对于同一进程,在不同的命名空间中,看到的pid号不同。
每个pid命名空间有一套自己的pid管理方法 ,所以在不同的命名空间中调用getpid(),看到的pid号是不同的。

  • PID namespace可以嵌套,就是有父子关系
  • 父 namespace可以看 以所有后代namespace的进程信息
  • 子namespace看不到父namespace 或 兄弟namespace的进程信息
  • 目前PID namespace最多可以嵌套32层,在内核中使用MAX_PID_NS_LEVEL来定义。

Linux下每个进程都有一个对应的/proc/PID目录,该目录包含了当前进程的信息。
对一个PID namespace而言,/proc目录只包含当前namespace和它所有后代namespace里的进程信息。

  • Linux中的进程 ID从1累加、不能重复、可回收再次利用。
  • 进程ID为1的进程是内核启动的第一个应用层进程 ,一般是init进程。现在采用systemd的系统第一个进程是systemd。
  • 当系统中一个进程的父进程退出时,内核会指定init进程成为这个进程的新父进程。
  • 当init进程退出时,系统也将退出。

二、实验观察

ubuntu 18.04 环境(测试CentOS7对fork还没完全支持)

1. 简单示例

(1) 查看当前pid namespace的ID

readlink /proc/self/ns/pid

在这里插入图片描述

(2) 启动新的pid namespace

启动新的pid namespace、uts mount namespace

sudo unshare --uts --pid --mount --fork /bin/bash

显示结果:

xundh@xundh-To-be-filled-by-O-E-M:~$ sudo unshare --uts --pid --mount --fork /bin/bash
[sudo] xundh 的密码: 
root@xundh-To-be-filled-by-O-E-M:~#

参数注释

  • --uts 新的uts 生成新hostname
  • --mount 新的mount namespace ,方便修改新namespace里的mount信息
  • --fork ,让unshare进程fork一个新的进程出来,然后再用bash替换掉新的进程

(3) 命名hostname,进入容器环境

hostname container001
exec bash

显示结果:

root@xundh-To-be-filled-by-O-E-M:~# hostname 
xundh-To-be-filled-by-O-E-M
root@xundh-To-be-filled-by-O-E-M:~# hostname container001
root@xundh-To-be-filled-by-O-E-M:~# exec bash
root@container001:~#

(4) 查看进程关系

pstree -pl

在这里插入图片描述

(5) 查看pid的namespace

readlink /proc/2526/ns/pid

在这里插入图片描述

2. PID namespace嵌套

  • 调用 unshare或 setns函数后,当前进程的namespace不会发生变化 ,不会加入到新的namespace,而子进程会加入到新的namespace;
  • 在一个PID namespace里的进程,它的父进程可能不在当前namespace中,而是父namespace里,这类进程的ppid=0。
  • 可以在祖先namespace中看到子namespace的所有进程信息,且可以发信号给子namespace进程,但进程在不同namespace里的PID不一样。

(1) 记下最外层namespace ID

xundh@xundh-To-be-filled-by-O-E-M:~$ readlink /proc/$$/ns/pid
pid:[4026531836]

(2) 创建新的pid namespace

xundh@xundh-To-be-filled-by-O-E-M:~$ sudo unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@xundh-To-be-filled-by-O-E-M:~# hostname container001
root@xundh-To-be-filled-by-O-E-M:~# exec bash
root@container001:~# readlink /proc/$$/ns/pid
pid:[4026532324]
root@container001:~# 

(3) 再创建新的pid namespace

root@container001:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@container001:~# hostname container002
root@container001:~# exec bash
root@container002:~# readlink /proc/$$/ns/pid
pid:[4026532327]

(4) 再创建新的pid namespace

root@container002:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@container002:~# hostname container003
root@container002:~# exec bash
root@container003:~# readlink /proc/$$/ns/pid
pid:[4026532330]

另外开一个窗口查看namespace关系:

pstree -pl

在这里插入图片描述

3. "init"示例

当一个进程的父进程被kill掉后,该进程将会被当前namespace中pid 为1的进程接管,而不是被最外层的系统级别的init 进程接管 。
当pid为1的进程停止运行后,内核将会给这个namespace及其子孙namespace里的所有其它进程发送SIGKILL信号,使其它进程都停止,当前namespace及后代namespace都会销毁。

(1) 接上面示例,在container003中启动3个新的bash

root@container003:~# bash
root@container003:~# bash
root@container003:~# bash
root@container003:~# pstree
bash───bash───bash───bash───pstree
root@container003:~# 

(2) fork一个新进程

root@container003:~# unshare --fork nohup sleep 3600&
[1] 44
root@container003:~# nohup: 忽略输入并把输出追加到'nohup.out'

root@container003:~# 

这里子进程会后台运行,sheep1小时。

现在的进程关系:

root@container003:~# pstree -p
bash(1)───bash(19)───bash(27)───bash(35)─┬─pstree(46)
                                         └─unshare(44)───sleep(45)

(3) kill掉44

root@container003:~# kill 44
root@container003:~# pstree -p
bash(1)─┬─bash(19)───bash(27)───bash(35)───pstree(47)
        └─sleep(45)
[1]+  已终止               unshare --fork nohup sleep 3600
root@container003:~# 

可以看到原来的sleep(45)被bash(1)接管了。

三、pid namespace的管理

1. pid_namespace(include/linux/pid_namespace.h)结构体

struct pid_namespace {  
          struct kref kref;                    //引用计数
          struct pidmap pidmap[PIDMAP_ENTRIES]; //pid分配的bitmap,如果位为1,表示这个pid已经分配了
          int last_pid;                      //记录上次分配的pid,理论上,当前分配的pid=last_pid+1
          struct task_struct *child_reaper; //表示进程结束后,需要这个child_reaper进程对这个进程进行托管
          struct kmem_cache *pid_cachep;     //高速缓存,这个不太清楚,待这块分析源代码
          unsigned int level;                //记录这个pid namespace的深度
          struct pid_namespace *parent;      //记录父pid namespace
  #ifdef CONFIG_PROC_FS
          struct vfsmount *proc_mnt;
  #endif
  #ifdef CONFIG_BSD_PROCESS_ACCT
          struct bsd_acct_struct *bacct;
  #endif
  };

成员变量解释:

  • pidmap 它表示在这个pid命名空间的pid的分配情况,pidmap是个数组,每一位代表这个这个偏移量的pid是否分配出去,初始这个数组只有一个元素。

pidmap结构体(include/linux/pid_namespace.h):

struct pidmap {
        atomic_t nr_free;//表示这个bitmap还有多少位为0,就是说对应的pid没有被分配出去
        void *page;//表示一段连续的内存空间,每位的0或1表示对应pid是否被分配
  };

默认情况下pid最大是32768,那么默认正好是1页能保存下的pid使用情况,linux默认一页的大小是4k=410248位=32768,如果pid的最大值超过32768那么pidmap数组就用上了,多个pidmap就是为了pid限制大于32768来设计的。

  • child_reaper的作用就是当父进程先于子进程结束的时候,就把子进程的父进程更新为child_reaper。

四、PID Namespace 程序测试

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    /* 查看子进程的PID,我们可以看到其输出子进程的 pid 为 1 */
    printf("Container [%5d] - inside the container!\n", getpid());
    sethostname("container",10); /* 设置hostname */
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}
int main()
{
    printf("Parent - start a container!\n");
  /*启用PID namespace - CLONE_NEWPID*/
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL); 
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}

运行环境:CentOS7

[root@localhost c4]# ls
a.out  main.c
[root@localhost c4]# ./a.out 
Parent - start a container!
Container [    1] - inside the container!
[root@container c4]# echo $$
1
[root@container c4]# 

这里子进程的pid为1。
在传统的UNIX系统中,PID为1的进程是init,它是所有进程的父进程,有很多特权(比如:屏蔽信号等);另外,其还会为检查所有进程的状态。
如果某个子进程脱离了父进程(父进程没有wait它),那么init就会负责回收资源并结束这个子进程。
所以,要做到进程空间的隔离,首先要创建出PID为1的进程,最好就像chroot那样,把子进程的PID在容器内变成1。

但是,我们会发现,在子进程的shell里输入ps,top等命令,我们还是可以看得到所有进程。说明并没有完全隔离。这是因为,像ps, top这些命令会去读/proc文件系统,所以,因为/proc文件系统在父进程和子进程都是一样的,所以这些命令显示的东西都是一样的。

所以,我们还需要对文件系统进行隔离。

参考文章:

  • https://coolshell.cn/articles/17010.html
  • https://blog.csdn.net/fivedoumi/article/details/51413020

猜你喜欢

转载自blog.csdn.net/xundh/article/details/106756483