内核剖析- PID Namespace 生命周期

生命周期流程

这篇文章讲一下 Linux PID Namespace的生命周期。

笔者后续还会发一些云原生、操作系统、AI、Rust 等计算机相关知识文章,觉得有帮助的朋友可以点点赞,点点关注。

本文默认读者具有一些前置知识,本文关注点在于整个生命周期,具体的细节,读者可以自行学习补充。

本文源码来自 Linux-6.1.9

笔者还在学习中,如有错误,请仔细查证,多多包涵。

1. 用户程序调用clone

c 复制代码
// 用户程序代码
clone(child_func, stack_ptr, CLONE_NEWPID | SIGCHLD, NULL);

child_func 是一个函数指针,指向新进程启动时要执行的函数,新进程会从这里开始运行;

stack_ptr 指向新进程的栈顶。新进程会用这块栈空间。通常需要分配一块内存作为新进程的栈;

CLONE_NEWPID 和 SIGCHLD 这两个都是 flags 参数;

CLONE_NEWPID 表示新进程会被放入一个新的 PID namespace;也就是说它看到的 PID 1 是自己,实现进程号隔离;

SIGCHLD 表示当子进程退出时,父进程会收到 SIGCHLD 信号;

最后一位是传递给 函数指针的参数,由于没有参数,就传 NULL

其中 CLONE_NEWPID 是创建新PID namespace的关键,它会告诉内核:

"接下来的子进程不再复用父进程的 PID namespace,请新建一个"

2. 系统调用处理

进入clone系统调用的具体实现,继续运行进入 进程复制逻辑 copy_process

c 复制代码
// kernel/fork.c
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, ...)
    ↓
pid_t kernel_clone(struct kernel_clone_args *args)
    ↓
copy_process(args)  // 进程复制

kernel_clone 是内核内部通用的 clone 实现,参数被封装成struct kernel_clone_args 结构体,便于扩展和管理

copy_process(args) 进程复制

3. 复制namespace

c 复制代码
// kernel/fork.c -> copy_process()
retval = copy_namespaces(clone_flags, p);
    ↓
// kernel/nsproxy.c
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
    ↓
create_new_namespaces(flags, tsk, user_ns, tsk->fs)
    ↓
// 检查CLONE_NEWPID标志
new_nsp->pid_ns_for_children = copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);

copy_process() 在创建新进程的过程中,发现 args->flags 带了 CLONE_NEWPID,于是会走到copy_namespaces(clone_flags, p)

copy_namespaces() 的工作是"为 当前进程 准备一个新的 struct nsproxy,并把需要新建的 namespace 逐个填进去"。

然后调用 copy_pid_ns 来检查标志

4. 创建新的 pid namespace

c 复制代码
// kernel/pid_namespace.c
struct pid_namespace *copy_pid_ns(unsigned long flags, struct user_namespace *user_ns, struct pid_namespace *old_ns)
{
    if (!(flags & CLONE_NEWPID))
        return get_pid_ns(old_ns);  // 不创建新namespace

    return create_pid_namespace(user_ns, old_ns);  // 创建新namespace
}
    ↓
static struct pid_namespace *create_pid_namespace(struct user_namespace *user_ns, struct pid_namespace *parent_pid_ns)
{
    // 分配namespace结构
    ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);

    // 初始化IDR
    idr_init(&ns->idr);

    // 设置层级关系
    ns->level = parent_pid_ns->level + 1;
    ns->parent = get_pid_ns(parent_pid_ns);
    ns->child_reaper = NULL;  // 此时还没有init进程
    ns->pid_allocated = PIDNS_ADDING;

    return ns;
}

copy_pid_ns() 发现 flags & CLONE_NEWPID 为真,才会创建 pid_namespace

最后返回新创建的 namespace 给进程

5. 为新进程分配 pid

copy_process 继续执行

  • 当创建新进程时,需要为其分配一个唯一的 PID
  • 但 init 进程是系统第一个进程,它的 PID 是固定的(通常为 1),不需要动态分配
  • 对于其他所有进程,则通过 alloc_pid() 函数动态分配 PID
c 复制代码
// kernel/fork.c -> copy_process()
if (pid != &init_struct_pid) {
    pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid, args->set_tid_size);
}
    ↓
// kernel/pid.c
struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid, size_t set_tid_size)
{
    // 分配pid结构
    pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
    pid->level = ns->level;

    // 关键:分层分配PID,从深层到浅层
    tmp = ns;
    for (i = ns->level; i >= 0; i--) {
        // 第一个进程会得到PID 1
        int pid_min = 1;
        if (idr_get_cursor(&tmp->idr) > RESERVED_PIDS)
            pid_min = RESERVED_PIDS;

        // 在当前层级分配PID号
        nr = idr_alloc_cyclic(&tmp->idr, NULL, pid_min, pid_max, GFP_ATOMIC);

        // 记录在该层级的PID信息
        pid->numbers[i].nr = nr;
        pid->numbers[i].ns = tmp;

        tmp = tmp->parent;  // 向上到父namespace
    }

    // 将PID注册到所有层级的IDR中
    for (upid = pid->numbers + ns->level; upid >= pid->numbers; --upid) {
        idr_replace(&upid->ns->idr, pid, upid->nr);
        upid->ns->pid_allocated++;
    }

    return pid;
}

6. 设置init进程 (child_reaper)

copy_process 继续执行

is_child_reaper() 设置 child_reaper ( 把 PID 1 设为 init 进程)

is_child_reaper() 只做一次检查

只要成立,当前进程就是这一层 PID namespace 的 init,也是将来最后一个退出的进程。

c 复制代码
// kernel/fork.c -> copy_process()
init_task_pid(p, PIDTYPE_PID, pid);
if (thread_group_leader(p)) {
    init_task_pid(p, PIDTYPE_TGID, pid);

    // 检查是否是该namespace的第一个进程(PID 1)
    if (is_child_reaper(pid)) {
        ns_of_pid(pid)->child_reaper = p;  // 设置为init进程
        p->signal->flags |= SIGNAL_UNKILLABLE;  // 设置为不可杀死
    }
}

// include/linux/pid.h
static inline bool is_child_reaper(struct pid *pid)
{
    return pid->numbers[pid->level].nr == 1;  // 检查是否是PID 1
}

完成进程创建,进入运行状态

7. namespace正常运行

进程创建完成

此时 namespace 中有了 init 进程,可以正常运行

后续 fork 不再带 CLONE_NEWPID,init 进程里再 fork 子进程时,调用链一样走到 alloc_pid(),但不会再新建 namespace,而是直接复用

idr_alloc_cyclic() 会给出 2、3、4......直到 pid_max

所有进程共享同一个namespace

去向:进程正常运行,或者 fork 更多子进程

8. 后续进程的 fork

来源: init进程或其子进程调用fork

函数: 再次调用 copy_process() → alloc_pid()

c 复制代码
// 后续进程fork时(不带CLONE_NEWPID标志)
// kernel/fork.c -> copy_process()
pid = alloc_pid(p->nsproxy->pid_ns_for_children, NULL, 0);

// kernel/pid.c -> alloc_pid()
// 这次分配会得到PID 2, 3, 4...
// 因为idr_get_cursor(&tmp->idr) > RESERVED_PIDS,所以pid_min = RESERVED_PIDS
// 但实际上会循环分配下一个可用的PID

去向:新进程会在同一namespace中运行

9. 进程退出

来源: 进程结束或被杀死

函数: do_exit() → exit_task_namespaces() → free_pid()

文件: kernel/exit.c → kernel/pid.c

c 复制代码
// kernel/exit.c
void __noreturn do_exit(long code)
{
    // ...
    exit_task_namespaces(tsk);
    // ...
}
    ↓
// kernel/pid.c
void free_pid(struct pid *pid)
{
    // 从所有层级移除PID
    for (i = 0; i <= pid->level; i++) {
        struct upid *upid = pid->numbers + i;
        struct pid_namespace *ns = upid->ns;

        // 更新计数
        switch (--ns->pid_allocated) {
        case 2:
        case 1:
            // 如果只剩init进程,唤醒它
            wake_up_process(ns->child_reaper);
            break;
        case PIDNS_ADDING:
            // 处理第一个进程fork失败的情况
            ns->pid_allocated = 0;
            break;
        }

        // 从IDR中移除
        idr_remove(&ns->idr, upid->nr);
    }

    // RCU延迟释放
    call_rcu(&pid->rcu, delayed_put_pid);
}

去向: 如果是普通进程,流程结束;如果是init进程,进入namespace清理

10. init进程退出,清理namespace

来源: init进程(child_reaper)退出

函数: zap_pid_ns_processes()

文件: kernel/pid_namespace.c

c 复制代码
// kernel/exit.c -> find_child_reaper()
// 当init进程退出时,会调用:
zap_pid_ns_processes(pid_ns);

// kernel/pid_namespace.c
void zap_pid_ns_processes(struct pid_namespace *pid_ns)
{
    // 1. 禁止创建新进程
    disable_pid_allocation(pid_ns);

    // 2. 忽略SIGCHLD信号
    me->sighand->action[SIGCHLD - 1].sa.sa_handler = SIG_IGN;

    // 3. 杀死所有剩余进程
    rcu_read_lock();
    read_lock(&tasklist_lock);
    nr = 2;
    idr_for_each_entry_continue(&pid_ns->idr, pid, nr) {
        task = pid_task(pid, PIDTYPE_PID);
        if (task && !__fatal_signal_pending(task))
            group_send_sig_info(SIGKILL, SEND_SIG_PRIV, task, PIDTYPE_MAX);
    }
    read_unlock(&tasklist_lock);
    rcu_read_unlock();

    // 4. 等待所有进程退出
    do {
        clear_thread_flag(TIF_SIGPENDING);
        rc = kernel_wait4(-1, NULL, __WALL, NULL);
    } while (rc != -ECHILD);

    // 5. 等待只剩init进程
    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);
        if (pid_ns->pid_allocated == init_pids)
            break;
        schedule();
    }
}

去向: 所有进程清理完毕,进入 namespace 销毁

11. 销毁namespace

来源: 所有进程清理完毕

函数: put_pid_ns() → destroy_pid_namespace()

文件: kernel/pid_namespace.c

c 复制代码
// kernel/pid_namespace.c
void put_pid_ns(struct pid_namespace *ns)
{
    struct pid_namespace *parent;

    // 从子namespace向上逐层销毁
    while (ns != &init_pid_ns) {
        parent = ns->parent;
        if (!refcount_dec_and_test(&ns->ns.count))
            break;
        destroy_pid_namespace(ns);
        ns = parent;
    }
}
    ↓
static void destroy_pid_namespace(struct pid_namespace *ns)
{
    // 释放inode编号
    ns_free_inum(&ns->ns);

    // 销毁IDR
    idr_destroy(&ns->idr);

    // RCU延迟释放namespace
    call_rcu(&ns->rcu, delayed_free_pidns);
}
    ↓
static void delayed_free_pidns(struct rcu_head *p)
{
    struct pid_namespace *ns = container_of(p, struct pid_namespace, rcu);

    // 释放资源计数
    dec_pid_namespaces(ns->ucounts);
    put_user_ns(ns->user_ns);

    // 释放namespace内存
    kmem_cache_free(pid_ns_cachep, ns);
}
相关推荐
简单点了4 小时前
Docker部署kafka实操+Java中访问
docker·容器·kafka
MANONGMN11 小时前
【Docker实战进阶】Docker 实战命令大全
docker·容器
独行soc11 小时前
2025年渗透测试面试题总结-15(题目+回答)
python·科技·docker·容器·面试·eureka
小白不想白a17 小时前
【K8s】K8s控制器——复制集和deployment
云原生·容器·kubernetes
hhzz18 小时前
一键设置 NTP & 时区的脚本(亲测,适用于部署 K8S 的前置环境)
云原生·容器·kubernetes
苏侠客8521 天前
在docker上部署fastapi的相关操作
docker·容器·fastapi
小白不想白a1 天前
【k8s】k8s中的几个概念性问题
云原生·容器·kubernetes
北巷初晴、1 天前
Kubernetes-核心概念Service
云原生·容器·kubernetes
容器魔方1 天前
KubeEdge秋季带薪远程实习来了!2025年LFX Mentorship开启申请
云原生·容器·云计算
@不会写代码的小张2 天前
K8s DaemonSet 详解
云原生·容器·kubernetes