内核剖析- 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);
}
相关推荐
吉吉615 小时前
docker的搭建
spring cloud·docker·容器
江湖有缘5 小时前
Docker实战:使用Docker部署IT工具箱Team·IDE
ide·docker·容器
David爱编程6 小时前
Kubernetes 中的 Ingress 详解:HTTP 负载均衡、TLS 与路径转发实践
云原生·容器·kubernetes
sohoAPI6 小时前
Docker入门笔记
笔记·docker·容器
赶路人儿9 小时前
mac OS上docker安装zookeeper
docker·zookeeper·容器
bahdkdsq14 小时前
Docker——Redis
运维·docker·容器
村东头老张21 小时前
通过 Docker 安装 MySQL
mysql·docker·容器
mysql学习中21 小时前
k8s集群搭建
云原生·容器·kubernetes
hzulwy21 小时前
k8s运行应用
云原生·容器·kubernetes