上一篇整体介绍了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的进程。
上一级的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;
}
我们可以通过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
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号