Docker内核技术原理(二)之PID Namespace

概述

上一篇整体介绍了namespace,这篇文章我们一起了解一下PID Namespace。在用户态看来,进程启动的时候会为每个进程分配一个本系统中唯一的一个进程号,子进程通过PPID标识自己的父进程,进程之间形成树状层级结构,系统第一个启动的进程ID为1,譬如Systemd进程。

当创建一个新的PID namespace后,那么进程的ID将从1重新计算,但其实这只是一个“障眼法”,Linux通过将PID namespace中进程的ID映射到上一级的PID namespace中从而实现进程隔离。如下图所示,在系统启动后默认有level 0的PID namespace,当创建一个新的level 1 PID namespace后,进程ID重新从1开始计数,但level 1中的进程ID 为1进程其实是映射到level 0进程ID为5的进程。


Docker内核技术原理(二)之PID Namespace

上一级的PID namespace可以看到下一级的所有进程,但下一级的PID namespace却看不到上一级进程。这就解释了我们可以Docker宿主机上看到docker容器里面的进程,只不过进程ID和容器内不相同罢了,但在容器里面看不到宿主机的进程。

内核原理

内核中pid结构体的定义中通过level定义PID属于哪个level中。同一个进程,在不同的level中分配不同的PID.

//内核pid 结构
struct pid
{
	unsigned int level; //pid的level,也就是pid所在ns的level
	struct hlist_head tasks[PIDTYPE_MAX]; //使用该pid结构体的进程描述符集合
	struct upid numbers[1];  //存储每层的pid信息的变成数组,长度就是上面的level
};

// pid 和 ns 组合结构体,当我们说到某个pid 的时候,一定是关联到某个ns
struct upid {
	int nr; //该层pid ns 的PID值
	struct pid_namespace *ns; //该层pid ns结构体地址
};

当然分配PID的也需要考虑PID Namespace,所以内核分配pid的函数alloc_pid 需要将ns作为参数传入。

//参数是新进程pid ns,返回值是申请的pid结构体
struct pid *alloc_pid(struct pid_namespace *ns) 
{
	tmp = ns;
	pid->level = ns->level;  //将ns的level赋值给pid level
  //下面通过for循环,遍历遍历所有父ns,然后在每个服ns里面都重新分配一个ID
	for (i = ns->level; i >= 0; i--) {
		int pid_min = 1;     //如果ns是新创建的,则pid值从1开始

    //真正PID分配的方法
		nr = idr_alloc_cyclic(&tmp->idr, NULL, pid_min,
			      pid_max, GFP_ATOMIC);
		spin_unlock_irq(&pidmap_lock);
		idr_preload_end();

		pid->numbers[i].nr = nr;  //在pid结构体中记录每个父ns分配的pid值
		pid->numbers[i].ns = tmp; //关联每个父ns
		tmp = tmp->parent;
	}
	return pid;
}

DEMO演示

我们可以通过C语言编写创建一个PID命名空间的例子,核心就是通过CLONE_NEWPID这个flag指定创建新的PID命名空间。

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
char* const child_args[] = {
  “/bin/bash“,
  NULL
};
int child_main(void* args) {
  printf(“在子进程中!
“);
  execv(child_args[0], child_args);
  return 1;
}
int main() {
  printf(“程序开始: 
“);
  int child_pid = clone(child_main, child_stack+STACK_SIZE,
           CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS 
           | SIGCHLD, NULL);
  waitpid(child_pid, NULL, 0);
  printf(“已退出
“);
  return 0;
}

编译并运行

# gcc -Wall pid.c -o pid.o && ./pid.o

通过$打印当前进程的ID

root@tim:~# echo $
1
root@tim:~# exit
exit

可以看到在新的PID空间内,进程的ID又开始从1计数了。如果大家对Go语言比较熟悉,附上一份Go语言的实现,代码类似

package main

import (
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

执行的效果也是类似

# go run main.go
# echo $
1
展开阅读全文

页面更新:2024-06-16

标签:内核   障眼法   组合   宿主   遍历   层级   容器   进程   分配   类似   原理   定义   参数   语言   结构   技术   科技   空间

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top