ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux PID 0/1/2:进程树、内核线程与容器实践

Linux PID 0/1/2:进程树、内核线程与容器实践 第一次在服务器上敲ps -ef | head看到 PID 1 那一行的 PPID 写着 0顺手ls /proc想找那个0目录结果压根不存在。那一刻我才意识到Linux 的进程树并不是从 1 开始的1 前面还站着一个看得见编号、看不见实体的 0。后来又发现ps --ppid 2能刷出一整屏带方括号的内核线程而 PID 2 自己的 PPID 也是 0于是0、1、2 这三个号到底是什么关系就成了一个绕不过去的问题。这篇就把这条线从头到尾捋一遍谁创建了谁、为什么顺序不能反、内核在这三个号上做了哪些特殊处理以及在容器和日常运维里这三个号会以什么形态咬你一口。内容偏向内核启动流程加实战验证会看进程、写过 systemd 服务或者被容器 PID 1 坑过的人读起来应该会顺畅一些。1. 把 pid 0、pid 1、pid 2 摆到同一张表里三个元老进程的身份与分工很多讲进程管理的资料直接从 PID 1 开始讲把 0 和 2 一笔带过结果就是读者脑子里始终留着两个疙瘩为什么ps里 PID 1 的父进程是 0但/proc/0不存在为什么内核线程的父进程统一是 2而不是挂在 systemd 底下先把这三个号的身份标签贴清楚后面所有的行为都能对号入座。编号常见名字本质是否出现在 /proc父进程PID 0swapper / idle静态定义的init_task空转任务否无它是所有任务的起点PID 1init → systemd 等用户态第一个进程命名空间里的 child_reaper是0PID 2kthreadd所有内核线程的父进程是01.1 pid 0 不是被 fork 出来的它是编译期就存在的那个 task_struct这一点是理解整条启动链的钥匙。任何一个普通进程都来自fork()或clone()也就是复制一份现有的 task_struct但init_task没有这个东西可以复制——它是内核编译期就摆在数据段里的一个静态变量定义大致长这样/* init/init_task.c */ struct task_struct init_task INIT_TASK(init_task);配套的内核栈也是静态的那段内存叫init_thread_union。为什么必须静态因为这个 task_struct 要用的那一刻kmalloc和 slab 分配器都还没就绪谁也没法给一个还不存在的分配器申请内存。所以内核只能先人工造出一个进程上下文硬编码在镜像里让start_kernel()有东西可以跑等内存管理、调度器、中断子系统都初始化完了再用动态分配去创建后面所有的进程。INIT_TASK宏里会把comm设成swapper这就是为什么老一些的top输出里会出现swapper这么一行——空闲 CPU 的记账被算到了它头上。它的 PID 是 0准确说是init_task的pid字段初始化为 0。这个任务永远不会退出也永远不会被调度器挑中去运行用户代码它只做一件事在没有任何可运行任务的时候占住 CPU 执行cpu_idle循环。1.2 pid 1 的 task_struct 和 pid 0 是同一套模板造出来的区别只在最后那次 execve容易被忽略的一点是PID 1 最初并不叫 init它的comm一开始是swapper之外的另一个名字——内核里管它叫kernel_init本质上是一段内核代码跑在内核态。它是rest_init()里通过kernel_thread()复制init_task得到的所以天然继承了init_task作为父进程PPID 就是 0。这就是ps -ef里那一行1 0的来源。然后它执行自己的函数体kernel_init()在里面做完一大堆内核收尾工作下面第 2 节会细讲最后调用run_init_process()用kernel_execve把/sbin/init或者/init的可执行文件加载进来覆盖掉自己现在的地址空间。注意这里是execve不是fork——进程号不变还是 1但代码段、数据段、堆栈全部换成了用户态那个 init 程序。你可以理解成同一张工牌同一个工位人换了。这一步决定了 PID 1 的双重身份在execve之前它是一段内核代码之后才是用户态的第一个进程。如果你手快在内核还没走到run_init_process的时候去看ps理论上是看不到名字叫 init 的进程的只能看到内核日志里那句Run /sbin/init as init process。1.3 pid 2 的定位所有内核线程的统一入口同样在rest_init()里紧接着创建 PID 1 之后内核又复制了一份任务出来当kthreadd拿到 PID 2。它和 PID 1 的关键差异在于它是一段永远不 exec 的内核代码一辈子待在内核态。它的职责非常单一——守在kthread_create_list这个链表上。任何内核模块或者内核子系统要起一个内核线程就调kthread_create()把请求塞进链表kthreadd醒了以后就create_kthread()把真正的线程 fork 出来。因为 fork 的动作是在kthreadd这个上下文里做的所以所有内核线程的 PPID 都是 2。这就是ps -ef里那些[kworker/0:1]、[ksoftirqd/0]、[rcu_gp]的父进程一栏全是 2 的原因。顺带说一个历史包袱PID 2 这个位置在很老的内核上并不是 kthreadd。2.4 时代它通常是keventd内核事件守护更早的 2.0/2.2 时代PID 2、3、4 是被几个固定守护进程占着的像kflushd、kupdate、kswapd这类。2.5 之后内核引入了kthreadd统一管理内核线程PID 2 才固定成今天这个样子。所以在很老的资料上看到PID 2 是什么什么 daemon不用怀疑自己记错了那只是版本差异。2. 启动链拆解rest_init() 那三行代码决定了整台机器的进程树形状理解了三个身份接下来看它们是怎么被生出来的。整条链子的分水岭就是start_kernel()的最后一步转进rest_init()这台机器的进程树形状在这一刻就被定死了。2.1 为什么注释里强调必须先创建 initrest_init()的代码不长但里面藏着一条很关键的因果链。核心片段大致是/* init/main.c */ noinline void __ref rest_init(void) { struct task_struct *tsk; int pid; rcu_scheduler_starting(); /* * 必须先创建 init这样它才能拿到 pid 1 * 但 init 后面又会去创建内核线程 * 如果现在就跑它会出问题。 */ pid kernel_thread(kernel_init, NULL, CLONE_FS); rcu_read_lock(); tsk find_task_by_pid_ns(pid, init_pid_ns); set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id())); rcu_read_unlock(); numa_default_policy(); pid kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); rcu_read_lock(); kthreadd_task find_task_by_pid_ns(pid, init_pid_ns); rcu_read_unlock(); system_state SYSTEM_SCHEDULING; complete(kthreadd_done); schedule_preempt_disabled(); cpu_startup_entry(CPUHP_ONLINE); }先说顺序问题。PID 是按创建顺序发的增量号谁先被kernel_thread出来谁就是 1。内核明确希望 init 拿 1所以kernel_init必须排在kthreadd前面。注释里那句however的转折说的就是矛盾点init 拿 1 没问题但 init 后面要做的事里包含触发 initcall而很多 initcall 会调kthread_create()这又依赖kthreadd已经能干活了——如果kthreadd还没创建好init 一跑就会把自己的请求塞进一个没人消费的链表然后死等。另外两句也很值得看。set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()))是把 init 钉在启动 CPU 上因为sched_init_smp()还没跑跨 CPU 迁移的机制还不完整这时候让 init 到处飘是要出事的。kthreadd_task这个全局变量在这里被赋值也是后面 init 等它用的那个 rendezvous 点。最后schedule_preempt_disabled()加cpu_startup_entry(CPUHP_ONLINE)意思是当前这段代码执行完之后这个任务就正式转成了 idle 循环。也就是说PID 0 的人生在这里发生了转折——它前面是那个跑start_kernel的启动上下文到这里它把自己的角色彻底交给了init_task的空转身份。2.2 kernel_init 一上来就卡在 kthreadd_done 上kernel_init被创建出来之后第一件事不是急着去启动用户态而是先等kthreadd就绪。这个等待在 5.x 之后的内核里通常写在kernel_init_freeable()的开头static noinline void __init kernel_init_freeable(void) { /* 调度器已经完全就绪可以做阻塞分配了 */ gfp_allowed_mask __GFP_BITS_MASK; set_mems_allowed(node_states[N_MEMORY]); /* 等 kthreadd 建好 */ wait_for_completion(kthreadd_done); smp_prepare_cpus(setup_max_cpus); workqueue_init(); ... smp_init(); sched_init_smp(); ... do_basic_setup(); ... }这个wait_for_completion(kthreadd_done)和rest_init里那句complete(kthreadd_done)是一对。信号量的配对关系非常清楚rest_init在把kthreadd_task存好、system_state置成SYSTEM_SCHEDULING之后放行kernel_init这才能往下走。这个设计其实是在给内核线程基础设施做一次握手保证后面do_basic_setup()里那一大串 initcall 调用kthread_create()的时候链表的消费者已经在线了。do_basic_setup()里面会跑各种do_initcalls()从early_initcall一路到late_initcall。你会看到 PID 3、PID 4、PID 5 这些号被迅速占掉rcu_gp、rcu_par_gp、kworker之类的内核线程就是在这一批 initcall 里通过kthreadd创建出来的。所以在一个刚起来的系统上ps -e -o pid,ppid,comm会看到一条非常整齐的规律PID 1 是 initPID 2 是 kthreaddPID 3 往后一大片的内核线程 PPID 全是 2。2.3 从 /init 到 /sbin/initinit 程序查找顺序与 switch_root 的交接kernel_init把内核侧的事情做完之后就会进入查找 init 程序的阶段。这个阶段有几个容易搞混的优先级我整理成了一张表优先级来源说明1rdinit内核参数指定 rootfs 上的 init默认值是/init2rootfs 上的/init存在即用这是 initramfs 的入口3init内核参数显式指定要启动的 init失败直接 panic4/sbin/init找不到就往下一个试5/etc/init→/bin/init→/bin/sh依次尝试全失败则 panic日志里那句Run /sbin/init as init process就是run_init_process()打出来的pr_info级别进dmesg。这句日志的价值极高——当你怀疑某个系统到底加载的是 initramfs 里的 init 还是根分区的 init直接dmesg | grep as init process就能看到实际走了哪一条。有个细节在kernel_init_freeable()里if (!ramdisk_execute_command) ramdisk_execute_command /init; if (sys_access((const char __user *) ramdisk_execute_command, 0) ! 0) { ramdisk_execute_command NULL; prepare_namespace(); }它的含义是内核先假设 rootfs 上可能有个/init用sys_access探一下如果没有就把ramdisk_execute_command清空转去prepare_namespace()挂真正的根文件系统。所以有没有 initramfs这件事内核是靠rootfs 上有没有 /init来判断的不是靠别的标志位。那 initramfs 场景下 PID 1 怎么延续initramfs 里的/init通常是一段 shell 脚本它负责加载必要的内核模块、挂载真正的根分区然后调switch_rootutil-linux或者run-initinitramfs-tools。这两个工具做的事情一样把新根上的/proc、/sys、/dev迁移过去chroot到新根然后exec真正的 init。因为最后一步是exec而不是fork所以从 initramfs 的 /init 到真正的 /sbin/init进程号一直是 1。如果你在 initramfs 里ps看到 PID 1 是/init切根之后再看 PID 1 变成 systemd不要以为是两个进程它就是同一张 task_struct 换了张脸。2.4 用 dmesg 把这条链子原样打印出来想实际观察这条链最省事的方式是给内核加initcall_debug参数然后dmesg。initcall_debug会把每个 initcall 的耗时打出来你能清晰地看到kthreadd相关的初始化分布在哪一段也能看到各个 initcall 里 fork 出来的内核线程名字。如果只是想确认 init 走的哪条路dmesg | grep -i init process足够了。另一个容易被忽略的观察点/proc/sys/kernel/pid_max在启动早期就已经可读说明 PID 分配器在很早就初始化完了。PID 号是从kthreadd和 init 之后开始批量消耗的这个数字的消耗速度本身就是判断系统起来后干了多少事的一个粗略指标。3. 证据链验证只用 ps 和 /proc 把 pid 0 的存在感找回来光看代码容易产生一种这些都是理论的错觉实际上你在任何一台 Linux 上都能用现成命令把这三个 PID 的关系验一遍不需要装任何工具。3.1 PPid0 是 pid 0 唯一露脸的地方ps -eo pid,ppid,comm | head -5典型输出PID PPID COMMAND 1 0 systemd 2 0 kthreadd 3 0 rcu_gp # 有的内核版本 PPID 显示为 2取决于版本细节 4 0 rcu_par_gp这里有个非常有意思的现象PID 1 和 PID 2 的 PPID 都是 0也就是它们都挂在 swapper 底下但/proc/0不存在ps -p 0也查不出东西。这就等于内核把 pid 0 的存在感压缩成了一个数字只在别的进程的 PPID 字段里留下痕迹。核对一下就能确认这一点ls /proc | sort -n | head -3 # 1 # 2 # 3/proc下最小的目录就是 1没有 0。ps -p 0 -o pid,comm会直接告诉你 PID 0 不存在。3.2 /proc/1/status 与 /proc/2/status 的字段对照想看得更细直接读 statusgrep -E ^(Name|Pid|PPid|Uid|NSpid) /proc/1/status grep -E ^(Name|Pid|PPid|Uid|NSpid) /proc/2/status在我这台跑 systemd 的机器上PID 1 是Name: systemd Pid: 1 PPid: 0 NSpid: 1PID 2 是Name: kthreadd Pid: 2 PPid: 0 NSpid: 2两个进程的Uid都是 0root区别在于 PID 1 有完整的用户态身份——/proc/1/exe指向/usr/lib/systemd/systemd/proc/1/root指向//proc/1/cwd也有具体值。而/proc/2/exe这类链接在 kthreadd 身上是空的或者不可用因为它根本没有用户态地址空间。还有一条命令特别直观readlink /proc/1/exe # /usr/lib/systemd/systemd readlink /proc/2/exe # 报错或者空kthreadd 没有用户态可执行文件这条差异正好印证了前面说的PID 1 在某个时刻做过execvePID 2 从来没有。3.3 for_each_process() 为什么天然跳过 pid 0为什么/proc里看不到 0追到内核里其实一句话就能解释。/proc遍历任务用的宏是for_each_process()展开之后大致是#define for_each_process(p) \ for (p init_task ; (p next_task(p)) ! init_task ; )注意起点是init_task然后立刻执行next_task(p)等于从 init_task 的下一个任务开始而且循环条件又是! init_task。所以init_task自己永远落在遍历范围之外——/proc从设计上就不打算把空闲任务暴露出来。这个细节我觉得比因为它是 idle 所以不显示这种模糊说法更有说服力。顺带补一个容易踩的坑在某个 PID namespace 里读一个不属于该 namespace 的进程的status时NSpid字段里会出现 0含义是这个进程在这一层命名空间里不可见。这里的 0 是个占位符跟 idle 任务那个 PID 0 没有任何关系。我见过有人把它当成这个进程的 PID 是 0然后排查跑偏了半天。4. pid 1 的三条硬规矩孤儿回收、信号豁免、退出即终结命名空间PID 1 之所以特殊不是因为它号小而是内核在它身上写了三段专门的逻辑。这三段逻辑在容器里会被无限放大先讲清楚原理。4.1 child_reaper孤儿进程为什么都跑去认 init 当爹一个进程的父进程先退出了怎么办内核不会让它变成没有父进程的野进程而是把它过继给一个 reaper。在根命名空间里这个 reaper 就是 PID 1。具体实现在kernel/exit.c的find_new_reaper()一带。父进程退出时内核遍历它的子进程链表把每个子进程的parent指针改指向 reaper同时发一个SIGCHLD通知新的父进程。这就是为什么你在一个长期跑的服务里杀掉它的父进程之后ps -o ppid查出来会变成 1。从内核 3.4 开始多了一个更细的机制prctl(PR_SET_CHILD_SUBREAPER, 1)。一个进程可以把自己注册成子收割者那么挂在它子树下面的孤儿会被过继给它而不是一路送到 PID 1。它的实际意义在于PID 1 要处理整个系统的孤儿压力大而且语义混乱一个进程管理器据我了解systemd 的用户实例、一些会话管理器都会设置这个标志在自己的小树里当 reaper能把回收职责收敛在更近的层级。这里有个实操层面的注意点成为 subreaper 意味着你必须自己调wait()否则僵尸进程就堆在你这里。这个坑下面第 5 节会展开。4.2 kill -9 1 打不动它不是权限问题很多人第一次遇到kill -9 1没反应第一反应是权限不够其实不对称。内核在信号投递路径上做了明确的豁免核心判断在kernel/signal.c的sig_task_ignored()里逻辑大致是如果目标是全局 init根命名空间的 init并且信号是 SIGKILL 或 SIGSTOP直接忽略如果目标身上带着SIGNAL_UNKILLABLE标记且它没有为这个信号注册处理函数并且不是被强制执行的内核信号也忽略内核线程只接受内核自己发的特定信号。SIGNAL_UNKILLABLE会被打在命名空间 init 的signal_struct上。这套规则合起来的效果就是凡是 init 没有显式注册处理函数的信号一律被丢掉。所以# 容器里以 root 身份执行 kill -TERM 1 # 如果 PID 1 没处理 SIGTERM等于没发 kill -9 1 # 同一个命名空间内发被静默丢弃SIGKILL 和 SIGSTOP 无法被捕获按理说应该一击必杀但内核在 init 上又加了例外。这个例外的目的是防止某个脚本手滑把系统或者容器干掉。值得注意的是这个豁免有边界来自祖先命名空间的 SIGKILL/SIGSTOP 是可以真正送达的。这正是docker stop超时后docker kill能生效的原因——那个 SIGKILL 是从宿主机祖先命名空间发出的绕过了豁免检查。4.3 find_child_reaper() 里的两条分支panic 还是清扫命名空间PID 1 退出会发生什么这个逻辑集中在kernel/exit.c的find_child_reaper()里判断非常干脆static struct task_struct *find_child_reaper(struct task_struct *father, ...) { struct pid_namespace *pid_ns task_active_pid_ns(father); struct task_struct *reaper pid_ns-child_reaper; if (likely(reaper ! father)) return reaper; if (unlikely(pid_ns init_pid_ns)) { panic(Attempted to kill init! exitcode0x%08x\n, father-signal-group_exit_code ?: father-exit_code); } zap_pid_ns_processes(pid_ns); ... }分两种情况。如果退出的是根命名空间的 init内核直接panic屏幕上那句Attempted to kill init!就是从这里来的后面跟一个 exitcode。系统到这一步基本就停在原地了什么服务都不会再起来。如果是某个子命名空间的 init 退出典型场景就是容器内核转去调zap_pid_ns_processes(pid_ns)它做的事情分三步先把该命名空间的 PID 分配关掉disable_pid_allocation()之后在这个命名空间里fork新进程会失败然后给里面所有还活着的进程发 SIGKILL最后由父命名空间的 reaper 把这些已经过继出来的进程收尸。这条逻辑解释了一个很常见的现象容器的主进程一退出容器瞬间全灭不是因为 docker 在里面杀了谁而是内核在清扫整个命名空间。反过来说如果你在容器里跑一个 shell 然后exit里面所有后台进程也会跟着消失这是同一套机制。5. 容器场景下的 pid 1三个最常见的坑与对应的处理方式上面这些规则在普通服务器上很少被感知因为 systemd 作为 PID 1 已经把所有脏活干得漂漂亮亮。一旦进了容器PID 1 变成你自己的业务进程问题就全冒出来了。5.1 容器里 Ctrl-C 没反应、docker stop 要等满 10 秒的真实链路先复盘一下现象docker run -it一个只有 shell 的镜像敲 Ctrl-C 大概率没反应docker stop一个不处理 SIGTERM 的服务固定要等 10 秒才结束。这两个现象其实是同一条因果链的两端。Ctrl-C 这一侧你按下的组合键由宿主机侧的 pty 主设备接收但产生 SIGINT 的终端驱动是绑定在从设备上的而那个从设备属于容器的命名空间。所以这个 SIGINT 相当于命名空间内部发出的信号走到sig_task_ignored()那里发现 PID 1 是 init、没有注册 SIGINT 处理函数、信号也不是被强制执行的于是丢掉。整个过程没有任何报错看起来就像按键失灵了。docker stop那一侧docker 先向容器的 PID 1 发 SIGTERM。如果业务进程没处理按上面的规则被丢弃。等超时默认 10 秒之后docker 从宿主机发 SIGKILL因为发送者在祖先命名空间这次绕过豁免容器 git 立刻死掉然后内核清理整个命名空间。所以10 秒不是 docker 慢而是它在给一个根本不响应的进程留足面子。5.2 entrypoint 脚本里那个 exec决定了信号能不能穿透到应用比上面更隐蔽的是 shell 包装导致的信号丢失。看这个典型的 DockerfileCMD [/app/start.sh]start.sh里写着#!/bin/sh /app/server --port 8080这种情况下/bin/sh是 PID 1/app/server是 PID 1 的子进程。docker 发的 SIGTERM 落在 sh 身上sh 收到之后……什么也不会做因为它没有转发信号的义务而且 sh 作为 init 时很多信号本来就被忽略。结果就是服务进程压根收不到停止信号只能等最后的 SIGKILL 强杀进程没机会做优雅退出日志没刷盘连接没关干净。修法很简单加一个exec#!/bin/sh exec /app/server --port 8080exec会让 sh 用新的可执行文件覆盖自己PID 保持不变于是/app/server直接继承了 PID 1 的身份信号就能直达了。这是我见过最容易改也最容易忘的一处。判断当前容器有没有踩这个坑一条命令就够docker exec container ps -p 1 -o pid,ppid,comm,args如果下面是/bin/sh或者/bin/bash而你的业务进程 PPID 是 1那就说明包装层没消掉。5.3 僵尸进程堆积--init、tini、dumb-init 该怎么选僵尸进程的问题同样源于 PID 1 的身份。容器里任何子进程死亡如果它的父进程先走了就会过继给 PID 1等着被wait()回收。业务进程通常不会实现循环 wait 任意子进程的逻辑哪怕它写得很好也只 wait 自己 fork 出来的那几个于是过继来的僵尸就一直挂着ps里一堆 Z 状态堆久了还可能撞上 PID 上限。三种常见解法各有取舍docker run --init让 docker 注入一个极小的 init现版本是 tini作为 PID 1业务进程变成它的子进程。改动成本最低加个参数就行代价是多一层进程而且有些依赖自己必须是 PID 1的程序要重新验证一下。tini -g -- cmd在 Dockerfile 的ENTRYPOINT里显式包一层。-g的参数是把信号发给整个进程组适合自己的服务还会 fork 子进程的场景。tini 本身就是设计来做这件事的写得很小。dumb-init -- cmd思路和 tini 类似早期在有些环境里更常见现在功能上基本重合选哪个看团队习惯。我的经验是如果只是想让信号正常传递加回收僵尸--init最省事如果服务本身会派生进程组且希望停止时整组一起收tini -g更可控。另外有极少数程序明确要求自己必须是 PID 1比如某些自带的 init 模式这种情况就别掺和让它自己去当 init但要确认它真的实现了回收逻辑。6. 几个容易被误判的细节与我的几条经验前面把主干讲完了最后补几个散点。这些细节单独看不重要但排查问题时它们经常是那个看起来对不上的地方。6.1 NSpid 里的 0 和老内核里 pid 2 的变迁NSpid字段我前面提过一次这里再说透一点。它是给跨命名空间观察用的从读/proc的这个进程所在的命名空间开始逐层往里列出目标进程在每一层的 PID。如果目标在最外层就不可见比如一个纯内核线程只属于根命名空间对应位置就可能出现 0。所以看到NSpid: 2 0这种值不要慌它只是两层视角里面那层看不到我。另一个历史坑是 PID 2 的名字。如果你手里有很老的运维手册上面写着PID 2 是 keventd或者更早的PID 2 是 kflushd那是 2.4 及更早内核的行为。2.6 之后kthreadd接管了这个位置所有内核线程归它管。翻旧资料对不上时先确认内核版本别急着怀疑自己。6.2 pid 分配的上限、RESERVED_PIDS 和 pid 复用想知道这台机器还能撑多少进程看这几个数cat /proc/sys/kernel/pid_max cat /proc/sys/kernel/threads-max cat /proc/sys/kernel/pid_max /dev/null # 默认值通常是 32768pid_max默认 32768上限是 4194304改大之后 PID 号会一直往上走好处是复用周期变长坏处是有些老程序假设 PID 是 16 位整数撑不住大的号码。改之前建议先在测试环境验证一圈。还有个冷知识根命名空间的 PID 分配有一个保留区内核里定义RESERVED_PIDS为 300正常情况下新进程不会拿到 300 以下的号除了启动早期的那批这是为了避免 PID 复用太快。所以你在容器里看到的 1、2、3 是命名空间视角的编号跟宿主机上的实际号完全是两套体系这一点在做日志关联分析时特别容易混。6.3 我自己的排查顺序清单被 PID 1 相关问题缠上时我一般按这个顺序走基本能定位到八成以上的问题先确认容器的 PID 1 到底是谁ps -p 1 -o pid,ppid,comm,args。是业务进程还是 shell 包装决定了后面一切的排查方向。看它忽略哪些信号grep ^Sig /proc/1/statusSigIgn位图里有什么就能知道哪些信号会被吞掉。确认内核实际加载的 initdmesg | grep as init process在虚拟机/物理机上排查启动问题时极其有效。看僵尸ps -eo stat,pid,ppid,comm | grep ^Z有僵尸就说明 reaper 没干活回头查 PID 1 有没有 wait 逻辑。看内核线程归属ps -eo pid,ppid,comm | awk $22 | head如果这里大量出现异常增长说明有内核模块在频繁创建线程。有一次我在一个自建镜像上排查容器停不掉前三步走完就定位到了PID 1 是/bin/shSigIgn位图里 SIGINT/SIGTERM 都在业务进程 PPID 是 1。改动就是把启动脚本里那一行加上exec一行代码问题消失。这种问题的诱人之处在于它看起来像网络或者存储层面的卡顿实际上根子就在进程号和信号这两件最基础的事情上。
返回列表