2026/9/27 2:12:40

第 2 天:Shell 是怎么启动另一个程序的?

第 2 天:Shell 是怎么启动另一个程序的? 昨天我们运行了./hello。程序从磁盘上的文件变成了进程但中间留了一个问题正在运行的 Shell怎样让另一个程序跑起来在 Linux 中常见做法可以概括为三个动作fork创建子进程 exec让子进程运行新程序 wait父进程等待并取得结果只记住这三个词还不够。真正容易弄混的是fork之后父子进程各从哪里继续运行exec会不会再创建一个进程子进程结束后又为什么还需要wait我们用两个小程序把它们看清楚。先写一个要被启动的程序把下面的代码保存为worker.c#includestdio.h#includeunistd.hintmain(void){printf(新程序正在运行PID%ld\n,(long)getpid());return7;}它只做两件事打印自己的进程号然后以状态码7退出。接着写启动它的程序保存为launcher.c#includestdio.h#includesys/types.h#includesys/wait.h#includeunistd.hintmain(void){printf(父进程PID%ld\n,(long)getpid());fflush(stdout);pid_tpidfork();if(pid-1){perror(fork);return1;}if(pid0){printf(子进程exec 前 PID%ld\n,(long)getpid());fflush(stdout);execl(./worker,worker,(char*)NULL);perror(execl);_exit(127);}printf(父进程创建了子进程 %ld等待它结束\n,(long)pid);intstatus;if(waitpid(pid,status,0)-1){perror(waitpid);return1;}if(WIFEXITED(status)){printf(父进程子进程的退出码是 %d\n,WEXITSTATUS(status));}return0;}编译并运行gcc-Wall-Wextra-oworker worker.c gcc-Wall-Wextra-olauncher launcher.c ./launcher你会看到类似结果父进程PID3200 父进程创建了子进程 3201等待它结束 子进程exec 前 PID3201 新程序正在运行PID3201 父进程子进程的退出码是 7中间父进程和子进程的打印先后可能变化最后一行一定要等子进程结束后才会出现。更值得留意的是exec前后子进程的 PID 都是 3201。fork之后为什么两边都继续运行程序运行到这一行pid_tpidfork();操作系统创建一个子进程。从这一行返回后父进程和子进程都会继续执行后面的代码只是拿到的返回值不同谁在执行fork()的返回值父进程子进程的 PID是一个正数子进程0创建失败-1不会产生子进程因此同一段代码可以通过if (pid 0)判断“现在运行到这里的是子进程”。子进程也不是从main的第一行重新跑一遍。它从fork返回的位置继续执行。父子进程此后各走各的父进程可以等待子进程可以去运行别的程序。常说fork“复制进程”但别理解为它立刻把所有内存逐字节复制一遍。父子进程起初看到的内存内容相同现代 Linux 通常使用写时复制先共享相关的物理内存页等一方修改时再为它准备独立的页。对程序来说它们仍是两个拥有各自地址空间的进程。fork还会让子进程继承父进程已打开的文件描述符。Shell 正是利用这一点在运行新程序前接好输入、输出或管道。我们讲管道时会用到它。exec做的不是“再生一个进程”子进程接着执行execl(./worker,worker,(char*)NULL);它要求运行./worker。这个函数属于exec函数族最终会使用execve系统调用。**exec把当前进程运行的程序换掉但进程仍然是这个进程。**它的 PID 不变所以例子里worker打印出的 PID与调用execl前子进程打印的 PID 相同。替换之后原来launcher在子进程中的代码、栈等程序内容不再照旧运行新程序从自己的入口开始执行最终进入worker的main。父进程则没有被替换它仍在launcher里等待。这也解释了代码中一个看着有点奇怪的地方execl(./worker,worker,(char*)NULL);perror(execl);_exit(127);如果execl成功它不会返回到下一行。只有找不到文件、没有执行权限等导致它失败时才会执行perror。随后用_exit(127)结束子进程避免它误跑到后面属于父进程的逻辑。这里的127是这个示例为启动失败选用的退出码不是exec规定的统一错误码。父进程等的是什么父进程拿到子进程的 PID 后调用waitpid(pid,status,0);它会等待指定的子进程结束并取得子进程的终止信息。等待期间父进程不需要自己写一个循环反复检查操作系统可以让它暂时休眠把 CPU 用在别的任务上。status不能直接当成退出码使用。我们先用WIFEXITED(status)判断子进程是否正常退出再用WEXITSTATUS(status)取出worker返回的7。如果子进程是被信号终止的就属于另一种情况。那不调用waitpid行不行子进程结束后它占用的大部分资源会被释放但内核还要暂时保留少量记录包括 PID 和退出状态好让父进程来领取结果。在父进程尚未领取的这段时间里它被称为僵尸进程。“僵尸”听起来很吓人实际上它已经不执行代码了问题在于如果父进程长期不回收大量子进程这些记录会持续占用系统资源。wait或waitpid就是父进程领取结果、完成回收的方式。再看一次 Shell 做的事当你在交互式 Shell 中输入./hello通常会发生这样的过程Shell │ ├─ fork创建子进程 │ │ │ └─ exec子进程运行 ./hello │ └─ wait等待子进程结束取得执行结果程序结束后Shell 还在所以你会重新看到命令提示符。如果在命令后加让它在后台运行Shell 对等待的安排就会不同但“创建进程再运行程序”这条主线没有变。今天最该弄清的不是三个函数名而是它们各自解决的问题fork解决“谁来运行”exec解决“运行什么”wait解决“运行完怎样收尾”。明天继续追一个问题Shell、浏览器和音乐播放器都准备好了CPU 究竟先运行谁