文章目录
1.进程创建补充
关于进程创建及其本质都在前面的文章有详细介绍了,这里就不叙述太多,只进行细节补充
当子进程继承父进程的数据段的时候,无论该部分的权限之前如何,系统会将数据段的权限都设置成只读,当子进程需要修改共享数据时,此时会触发只读权限,系统不会将该修改识别为异常,而是自动修改权限并赋予子进程新的数据空间,实现写时拷贝
2.进程终止
进程退出的三种情况:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.1 查看进程退出
[zzh_test@hcss-ecs-6aa4 PC]$ echo $?
0
比如最熟悉的 main
函数,最后总要加个 return 0;
表示进程退出状态正常,$?
查看的是最近一次执行进程的退出码
0
表示正常退出,除此以外用 strerror
解释退出码,可以看到多个退出原因
当进程异常时同样会返回退出码,但此时更重要的是异常的原因,即程序中断原因,应该查看信号,因为进程退出异常的本质就是收到了信号,这个后面会讲
2.2 exit 和 _exit
通过代码发现,exit
是用于返回进程码的函数,但是 exit
后的代码就再也没有执行了,这也就说明 exit
是进程退出,return
表示的是函数退出,二者不一样
我们知道对于 printf
来说 \n
的作用不仅是换行,更是起到刷新缓冲区让字符串强制输出的作用
去掉 \n
,进行两种函数的对比,发现两种函数对于返回退出码的作用是一样的,但是一个输出了一个没输出,这是因为 _exit
是系统级别的调用,直接调用系统终止进程;exit
最终也是调用了 _exit
来终止进程的,但是还做了清理函数,刷新缓冲区的工作
3.进程等待
🤔为什么需要进程等待?什么是进程等待?
之前讲过如果子进程在父进程还在运行的时候进行了退出,父进程此时不对子进程进行处理,那么子进程会变成僵尸进程,此时连
kill -9
都无法杀死该进程,因为一个已经死掉的进程无法被杀掉,父进程派给子进程的任务完成的如何,我们需要知道,子进程运行完成,结果对还是不对,或者是否正常退出。因此父进程通过进程等待的方式,调用wait
/waitpid
回收子进程资源,获取子进程退出信息
3.1 wait 和 waitpid
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
}
else if(id > 0)
{
int cnt = 10;
while(cnt)
{
printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
pid_t ret = wait(NULL);
if(ret == id)
{
printf("wait successful, ret: %d\n", ret);
}
sleep(5);
}
else
{
printf("fork fail\n");
}
return 0;
}
以上是一个使用 wait
回收子进程的例子
pid_t wait(int *status)
,该函数 status
用于获取子进程的退出状态,成功返回被等待进程 pid
,失败返回 -1
如果不关心子进程的退出状态,可传入
NULL
,就像代码中pid_t ret = wait(NULL);
这种写法如果想获取子进程退出状态,可定义一个
int
类型变量,将其地址传入。通过一些宏来检查和解析状态信息,比如WIFEXITED(status)
:判断子进程是否正常退出,若正常退出返回非零值,否则返回0
。WEXITSTATUS(status)
:在WIFEXITED
为真时使用,用于获取子进程通过exit
或return
返回的退出码
if (WIFEXITED(status))
{ // 判断子进程是否正常退出
printf("子进程正常退出\n");
printf("子进程的退出码是:%d\n", WEXITSTATUS(status)); // 获取子进程的退出码
}
else
{
printf("子进程异常退出\n");
}
🔥值得注意的是: WIFEXITED
可以记忆为 wait if exited
(等待是否退出),WEXITSTATUS
可以记忆为 wait exited status
(等待退出状态)
通过执行代码,可以发现僵尸进程确实是被回收了,再深度思考,我们如何获取子进程的退出状态?比如异常了是被什么信号打断了?正常运行但是结果有错是什么原因造成的,此时处于什么状态?那么这个时候就用到了 waitpid
函数
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else if(id > 0)
{
int cnt = 10;
while(cnt)
{
printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
int status;
pid_t ret = waitpid(id, &status, 0);
if(ret == id)
{
printf("wait successful, ret: %d\n, status: %d", ret, status);
}
else
{
printf("wait fail\n");
}
}
else
{
printf("fork fail\n");
}
return 0;
}
要传入参数 &status
,所以需要在外设置变量 status
获取子进程的退出状态(系统会自动捕捉子进程退出状态给到 status
)
观察 status
的值,256
不是正常的退出码值,为什么会出现这种情况?我们要知道,父进程等待子进程期望获得哪些信息?
- 子进程代码是否异常
- 没有结果,结果异常是为什么
所以 status
一定不是单纯的整数类型而已
对于 status
我们只看最低的 16
位,因为这里存储的是有效信息
低 7
位(第 1 - 7
位)为 0
,表示是正常终止而非被信号杀死,第 8
位啥意思现在先不用管,高 8
位(第 8 - 16
位)存储 “退出状态”(即子进程通过 exit
或 return
指定的退出码,比如 exit(1)
里的 1
)。0000 00001 0000 0000
化为二进制是 0X7F
,即 256
,刚好就是打印出来的 status
✏️函数解析:
pid_ t waitpid(pid_t pid, int *status, int options)
返回值:
当正常返回的时候 waitpid
返回收集到的子进程的进程 ID
如果设置了选项 WNOHANG
,而调用中 waitpid
发现没有已退出的子进程可收集,则返回 0
如果调用中出错,则返回 -1
,这时 errno
会被设置成相应的值以指示错误所在
参数:
- pid:
pid=-1
,等待任一个子进程,与wait
等效
pid>0
,等待其进程ID
与pid
相等的子进程 - status:
WIFEXITED(status)
:若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status)
:若WIFEXITED
非零,提取子进程退出码(查看进程的退出码) - options:
WNOHANG
:若pid
指定的子进程没有结束,则waitpid()
函数返回0
,不予以等待。若正常结束,则返回该子进程的ID
3.2 阻塞和非阻塞轮询
如果子进程一直不结束,那么父进程 wait
岂不是要一直进行等待?确实是这样的,这种情况就是阻塞,那我们要如何优化呢?
waitpid
中将 option
参数设置成 WNOHANG
的方式就能避免阻塞的情况,他使用的是非阻塞轮询的方式
阻塞就是父进程一直等待子进程结束,因此会很耽误父进程自己的效率,非阻塞轮询不同的地方在于他是间歇性的询问子进程是否结束,如果没结束,父进程会继续干自己的事,比如打印日志等,就这样不断重复询问,直到子进程结束父进程就能回收了
4.进程替换
前面遇到的情况都是父子进程共用同一套代码,但是如果子进程想要实行另一套代码呢?那就需要用到进程替换了
4.1 进程替换本质
子进程进行进程替换时,数据代码父子共享,就有人会问了:那是不是就得创建新的进程来放新代码?还是进行写时拷贝?都不是!我们这里是整体替换,而不是部分修改,所以不涉及写时拷贝,更不涉及新进程创建
真正的方式: 清空子进程之前从父进程继承的内存映射(包括代码段、数据段等),从硬盘读取新程序,然后为子进程建立新的虚拟内存映射,并将文件内容加载到对应物理内存页中,该替换是由 exec
系列函数实现的
🔥值得注意的是:
- 进程替换成功之后,
exec*
后续的代码不会被执行,替换失败才可能执行后续代码,只有失败有返回值(比如exit(1)
),能看到exit(1)
对应的返回值就表示进程替换失败了 - 替换程序的时候,会将新程序的表头先加载进去,当真正要使用的时候才全部加载,这也是懒加载的一种表现
- 环境变量默认不会被替换
4.2 进程替换函数
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("替换前\n");
execl("/usr/bin/ls", "-ls", "-a", "-l", NULL);
printf("替换后\n");
int cnt = 3;
while (cnt)
{
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else if (id > 0)
{
int cnt = 5;
while (cnt)
{
printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
int status;
pid_t ret = waitpid(id, &status, 0);
if (ret == id)
{
printf("wait successful, ret: %d, WIFEXITED: %d, WEXITSTATUS: %d\n", ret, WIFEXITED(status), WEXITSTATUS(status));
}
sleep(5);
}
else
{
printf("fork fail\n");
}
return 0;
}
在子进程进行主体函数运行时,添加一个 ececl
进程替换函数
根据结果,可以发现子进程程序替换之后的代码都没有执行
由 exec
开头的函数,这一系列的都是程序替换的函数,这些函数都封装了 execve
函数(系统调用函数)来间接调用
l(list)
:表示参数采用列表
v(vector)
:参数用数组
p(path)
:有 p
自动搜索环境变量 PATH
e(env)
:表示自己维护环境变量
使用示例:
char *argv[] = {"ls", "-a", "-l", NULL};
char *envp[] = {"VAR1=value1", "VAR2=value2", NULL};
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execlp("ls", "ls", "-a", "-l", NULL);
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp);
execv("/usr/bin/ls", argv);
execvp("ls", argv);
execve("/usr/bin/ls", argv, envp)
execl
和 execv
是一组的,第一个参数都是替换程序的绝对或相对路径,后面填的就是可变参数,指令怎么用这里就怎么填,记得要用 NULL
结尾,至于为什么,和命令行参数的道理是一样的,这两不同的就在于一个是直接传入,另一个是把参数放在数组里然后传入
传送门:命令行参数
execlp
和 execvp
是一组的,唯一的区别就是不用写路径,而是直接写要替换程序的文件名,前提是该文件名要在 PATH
环境变量下。execle
和 execve
是一组的,唯一的区别就是可以传入自己的环境变量,环境变量采用的是覆盖写入而不是追加
🔥值得注意的是:
引入 exec
系列函数时,通常会包括 extern char **environ
,有人就问了,不是从父进程继承了吗?为什么还要写这条代码,确实你从父进程继承了该环境变量数据,但是你不知道在哪啊,需要外部声明来找到位置。补充一个知识点,除了可以 expot
写入 $PATH
,还可以用 putenv()