子僵尸进程调用setpgid返回ESRCH而非EACCES的原因
嘿,我来拆解下你遇到的这个问题——核心原因其实很直白:你fork出来的子进程执行的ls命令,在父进程sleep的2秒内就已经完全退出了。当父进程醒过来调用setpgid时,目标PID对应的进程已经不存在了,所以内核返回了ESRCH(找不到进程),而不是你预期的EACCES。
先理清楚手册里的错误码前提
先回顾下setpgid(2)手册里的两个错误码定义:
EACCES:尝试修改调用进程的子进程进程组ID,但该子进程已执行execve(2)(setpgid()、setpgrp())。
ESRCH:对于getpgid(),pid无匹配进程;对于setpgid(),pid既非调用进程也非其子进程。
注意EACCES的触发前提是:目标进程确实存在,只是它已经执行过exec了。而ESRCH的触发条件是内核根本找不到这个PID对应的进程——这个错误的优先级更高,只要进程不存在,就会先返回它。
你的代码里发生了什么?
你的代码逻辑是:
- 父进程fork出子进程,子进程立刻exec
ls命令 - 父进程sleep 2秒,然后调用
setpgid(ls_pid, ls_pid)
但ls是一个极短的命令:它只是列出当前目录的文件,完成后立即终止,整个过程耗时远不到2秒。等父进程sleep结束后,ls_pid对应的进程早就消失了,内核里已经没有这个PID的进程实体。这时候调用setpgid,内核找不到目标进程,自然返回ESRCH,根本轮不到触发EACCES的条件。
怎么验证这个结论?
你可以修改代码,让子进程exec一个不会很快退出的程序,比如sleep 10——这样父进程sleep 2秒后,子进程还在运行,这时候调用setpgid就会返回你预期的EACCES:
修改后的测试代码:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> void print_errno() { if (errno == EACCES) printf("errno: EACCES\n"); else if (errno == EPERM) printf("errno: EPERM\n"); else if (errno == ESRCH) printf("errno: ESRCH\n"); } #define CHECK(syscall, msg) do { \ if ((syscall) == -1) { \ print_errno(); \ perror(msg); \ _exit(1); \ } \ } while(0) int main () { int sleep_pid; char *sleep_argv[] = { "sleep", "10", NULL }; CHECK(sleep_pid = fork(), "fork error"); if (!sleep_pid) { CHECK(execvp(sleep_argv[0], sleep_argv), "execvp error"); } else { sleep(2); int ret = setpgid(sleep_pid, sleep_pid); if (ret == -1) { print_errno(); perror("setpgid error"); } else { printf("setpgid success\n"); } CHECK(wait(NULL), "wait error"); printf("Finish\n"); } }
运行这段代码,你会看到输出errno: EACCES,完全符合手册的预期。
额外提醒
如果想测试exec后的子进程触发EACCES的场景,一定要保证在调用setpgid时,子进程还处于运行状态——像ls这种瞬间完成的命令,根本满足不了这个条件。
内容的提问来源于stack exchange,提问作者K.Miao

