生命周期流程
这篇文章讲一下 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);
}