首先我们要搞清楚_exit()和exit()的区别, 在7.3节已经讲过了, _exit()是一个系统调用, 而exit()是ISO C的函数库. 调用_exit()的时候会立刻陷入内核, 然后执行进程的退出工作, 而在exit()中调用了_exit(), 但是在之前还做了一些清理的工作, 比如说执行atexit()注册的回调函数, 清理标准IO等.
本题让我们把子进程中的_exit()替换为exit()就是希望利用exit()对标准IO的清理, 关闭标准输出. 因为使用vfork()时, 子进程和父进程使用了同样的地址空间, 所以如果子进程fclose(stdout), 那么父进程中的printf也会失效, 无法打印到终端.
代码见8_1, 如果只是将_exit()替换为exit(), 在我的机器上结果并没有发生改变. 所以我在子进程中显式关闭了stdout, 结果发现父进程果然没有打印.
测试代码见8_2, 运行的结果是程序会coredump. 我们回忆一下vfork()这个调用的功能, 它和fork()一样会创建一个子进程, 和fork()的不同之处主要在于两点:
- vfork()创建的子进程和父进程共享一个地址空间.
- vfork()会阻塞父进程, 直到子进程退出为止.
那么测试代码中的coredump可以这样解释:调用vfork()后, 子进程马上返回, 从而导致子进程的func1()的栈桢被回收, 最终当子进程执行完main()函数退出时, 寄存器%rbp指向的位置已经不是之前func1()这个函数的栈帧底部了, 这时候父进程解除阻塞, 希望从fun1()返回, 但是利用%rbp去寻找返回地址的时候, 会得到一个错误的返回地址, 跳到这个错误地址后继续执行后面的代码从而导致了coredump.
通过这个题目我们刚好回顾一下进程退出时的相关知识, 首先进程退出分为正常退出和异常退出. 正常退出的场景一般都是通过exit的方式退出:
- 通过exit(),_exit(),_Exit()退出.
- return返回, 这也和exit退出等价
- 进程的所有线程都返回
异常退出, 一般是由于接受到了信号, 中断了正常的处理流程:
- 接收到了某些导致进程中止的信号, 例如SIGABORT, SIGKILL, SIGSEGV等信号
- 线程的取消请求
不管进程是正确退出还是异常退出, 最终都会执行内核的同一段代码, 用于回收进程占用的资源. 但是由于我们希望能够保留进程的终止状态, 也就是说我们希望父进程能够有办法知道子进程是正常退出还是异常退出, 正常退出的返回码是多少, 导致异常退出的信号是什么. 所以内核中回收进程资源的代码并不会将所有的进程信息全部回收, 而是留了一部分内存保存每个进程的终止信息, 而wait的一系列函数就是为了获取并释放这些终止信息, 所以只有进程被父进程wait之后, 才会真正终止, 否则就会变成僵尸进程.
回到我们这个题目上来, 代码见8_3, 其实就是考察的waitid()的应用, 还是比较简单的.
图8.13中的代码目的是在子进程打印之前先阻塞, 等待父进程打印完了之后, 解除子进程的阻塞, 然后子进程打印. 这里只是对打印顺序做了同步, 并没有对进程的退出做同步, 也就是说父进程打印完后可能马上退出, 运行另一个a.out, 这样就导致第二次调用a.out的输出可能和第一次调用a.out中子进程的打印同时进行. 本质上其实就是没有对a.out中父进程退出和子进程输出这两件事的顺序进行同步. 如果让子进程先输出的话, 就不会有这个问题, 因为可以保证父进程退出的时候, 子进程的打印一定已经完成了, 所以顺序不会乱.
首先回顾一下exec系列函数的关系, 其实真正底层的系统调用就只有一个execve().
- 带l后缀的exec, 都会先parse参数列表, 然后得到一个argv[], 再调用其带v后缀的版本, 例如execl会调用execv, execlp会调用execvp.
- 带p的版本, 都会调用不带p的版本. 这个p其实就是PATH的意思, 处理过程就是轮流测试把PATH中的每一个 + file_name, 看是不是一个存在的path_name, 找到第一个合法的path_name后就直接用这个path_name去调用不带p的版本, 例如execvp()会调用execv()
- 不带e的版本, 最后都会调用带e的版本, 这个e其实就environment的意思, 对于不带e的版本, 会把当前环境变量表environ传给带e的版本, 例如execv()会调用execve().
所以这里题目说把execl改为execlp, 而且PATH中又有/home/sar/bin, 所以实际的效果应该是先在/home/sar/bin中找到了testinterp, 然后再用path_name = /home/sar/bin/testinterp作为参数调用execl, 最后的结果应该是一样的.
代码见8_6, 产生一个僵尸进程很简单, 只要父进程不要调用wait, 并让子进程退出就好了. 这里简单通过sleep()保证子进程先成为僵尸进程, 然后再调用ps打印进程状态, 结果如下:
$./8_6
#stat pid ppid cmd
S+ 10521 1966 ./8_6
Z+ 10522 10521 [8_6] <defunct>
S+ 10523 10521 sh -c ps -A -o stat,pid,ppid,cmd | grep 8_6
S+ 10525 10523 grep 8_6pid = 10522的这个进程成为了僵尸进程.
代码见8_7, 这个题目其实和exec没啥关系, 只是想说明opendir()和open()的关系. exec的时候, 是否关闭fd是根据文件的CLOEXEC这个flag来决定的, 在opendir()中, 会将这个flag设为1, 而在open中这个flag默认为0.