目录
前言:
一、main函数的返回值
二、退出码有什么用?
三、perror/strerror/erron
四、erron变量
五、exit函数
六、_exit变量
七、初始缓冲区
八、wait函数和folk函数的返回值
九、父进程获取子进程退出信息waitpid函数
1.返回值
2.第一个参数(pid_t pid)
3.第二个参数(int* status)
3.1重谈进程退出
3.2观察进程退出信号
3.3WIFEXITED(status)宏和WEXITSTATUS(status)宏
4.第三个参数(int options)非阻塞等待
5.参数总览
总结:
前言:
我们已经知道了很多的进程属性,还有进程调度算法,进程地址空间和写时拷贝等等(这是一系列文章,大家感兴趣可以在我的主页追剧,零基础就从基础命令开始看起)。本篇来聊一聊一个进程退出时肯定也需要让OS知道其是如何退出的,包括我们经常说到的缓冲区,我们这篇先来对缓冲区有一个初步的认识,这部分知识很重要!
一、main函数的返回值
main函数的返回值是什么?是返回给父进程/OS的。我们写一个C++代码,并执行,之后echo $?
我们已经在关于进程的第二篇讲过echo $?是用来查看上次进程退出信息的。
二、退出码有什么用?
退出码有什么用?表明错误原因:0表示成功,非0表示错误。
用不用的数字表明不同的原因。系统提供了一批错误码,约定退出码(C/C++提供)。
三、perror/strerror/erron
各位如果学过C,是否还记得里面的这几个内容,我们先使用系统手册来查看这三个是什么东西:
这里提前再次声明系统手册:
由于这几个函数/变量都是C语言的,所以查3号手册。
man 3 perror
有可能你使用Xshell连接会有显示问题,这时候推荐在vim编译器中打开查看,在底行模式中(一直点Esc,之后输入":"(冒号)),输入以下命令:
!man 3 perror
strerror函数是将erron传入,之后返回字符串,打印错误信息。
erron是一个全局变量,是一个错误码。其在errno.h头文件中。
perror = printf + strerror
我们举一个perror的例子:
#include<stdio.h> #include<stddef.h> #include <stdlib.h> #include <errno.h> int main() { FILE* file = fopen("non_existent_file.txt", "r"); if (file == NULL) { perror("Error opening file"); return EXIT_FAILURE; } // 文件操作... fclose(file); return EXIT_SUCCESS; }
注意程序最后的"代码为1"是获取的erron
四、erron变量
我们举一个栗子:
打开一个文件以读的方式,这个文件并不存在,所以会报错,我们在文件读取前打印错误码和错误信息;在文件读取后打印错误码和错误信息。
#include<iostream> #include<string.h> #include<cstdio> #include<error.h> int main() { printf("before: errno: %d, errstring: %s\n", errno, strerror(errno)); FILE *fp = fopen("./log.txt", "r"); if (fp == NULL) { printf("after: errno: %d, errsting: %s\n", errno, strerror(errno)); return errno; } return 10; }
错误码(errno)就是给系统/父进程看的,错误信息(strerror)是给人看的。我们不知道一共有多少个错误信息,这里更改代码,上限为200全部打印一遍。
#include<iostream>
#include<string.h>
#include<cstdio>
#include<error.h>
int main()
{
for (int i = 0; i < 200; i++)
{
std::cout << "code: " << i << ",errstring: " << strerror(i) << std::endl;
}
}
里面可以看到我们很常见的错误信息。 我们可以演示一下,比如错误码2(code: 2,errstring: No such file or directory):
五、exit函数
我们今天来正式认识一下exit函数,我们编写process.cc文件,调用一个函数,打印之后执行exit(100):
所以exit里面接受的也就是错误码,这就意味着退出码可以自定,不必要一定使用系统的退出码。
我们将函数的exit修改为return,再次编译执行观察结果:
所以可以可以得出结论,在代码的任何地方调用exit表示进程结束。
六、_exit变量
我们来认识一个新的变量:_exit。
它的用法和exit的使用方法几乎一致。
可以看出这是Linux的系统操作手册中的变量。
这两个是有区别的,还是否记得我们之前有缓冲区的概念?exit会将缓冲区的内容刷新,而_exit不会将缓冲区的内容刷新。
七、初始缓冲区
这里直接上图:
对于缓冲区的详细知识,我们后期会详细讲解,这里先抛砖引玉。
八、wait函数和folk函数的返回值
大家是否还记得,当子进程死亡以后,没有人回收它,子进程就会僵尸,等待父进程回收,今天我们就要完善这个代码,让父进程回收它。
我们先来看folk函数的返回值:
我们先用一段代码说明一下,这段代码创建子进程,如果folk创建失败返回-1,对父进程返回子进程pid;对子进程返回0,我们让子进程一共打印3次信息,并退出时返回0(exit返回),父进程死循环打印:
#include<iostream>
#include<string.h>
#include<cstdio>
#include<error.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("errno: %d, errstring: %s\n", errno, strerror(errno));
return errno;
}
else if (id == 0)
{
int cnt = 3;
while (cnt)
{
printf("子进程运行中, pid: %d\n", getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
//父进程
while(true)
{
printf("我是父进程:pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
并再开一个窗口进行监视。
还是否记得我们循环检测进程的语句?
while :; do ps ajx | head -1 ; ps ajx | grep process | grep -v grep; sleep 1; done
小提示:当XShell发生BUG时,特别是vim编辑器会发生BUG,可以查看进程那些启动了。杀死一些无关的vim进程。
此时父进程并没有回收子进程,子进程处于僵尸状态。
对于创建的子进程,父进程就必须等待子进程(回收子进程),当子进程变成僵尸之后,父进程就要对其回收,使用系统提供的wait函数回收。在父进程中写入wait函数使其等待。
在父进程等待期间,子进程不退出,父进程就需要阻塞在wait函数内部,类似于scanf。
wait等待任意一个子进程,返回值大于0就是等待成功,返回的是子进程的PID;返回-1就是等待失败,可能是没有子进程造成的。
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("errno: %d, errstring: %s\n", errno, strerror(errno));
return errno;
}
else if (id == 0)
{
int cnt = 3;
while (cnt)
{
printf("子进程运行中, pid: %d\n", getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
//父进程
pid_t rid = wait(nullptr); //这里调用wait函数
if (rid > 0)
{
printf("wait sub process success, rid = %d\n", rid);
}
while(true)
{
printf("我是父进程:pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
九、父进程获取子进程退出信息waitpid函数
但是还有问题,父进程等待完毕以后,肯定也要获取子进程的对应退出信息,也就是想知道子进程完成任务到底怎样,所以我们平时使用的函数是waitpid函数。
1.返回值
我们先来介绍一下waitpid的返回值:
> 0: 等待成功,返回的是子进程pid
== 0: 等待成功,但是子进程没有退出(重要)
< 0: 等待失败
2.第一个参数(pid_t pid)
我们先来从第一个参数开始了解:
所以我们将代码做一些调整:
#include<iostream>
#include<string.h>
#include<cstdio>
#include<error.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("errno: %d, errstring: %s\n", errno, strerror(errno));
return errno;
}
else if (id == 0)
{
int cnt = 3;
while (cnt)
{
printf("子进程运行中, pid: %d\n", getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
//父进程
//pid_t rid = wait(nullptr);
//pid_t rid = waitpid(-1, nullptr, 0); // == wait
pid_t rid = waitpid(id, nullptr, 0); //返回给父进程的本身就是子进程的pid
if (rid > 0)
{
printf("wait sub process success, rid = %d\n", rid);
}
while(true)
{
printf("我是父进程:pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
当然,我们也可以查看进程等待失败的情况,将waitpid的id值+1观察情况:
3.第二个参数(int* status)
waitpid第二个参数status,它会帮助父进程获取子进程的退出信息。
我们定义一个status变量,并传入,之后将子进程的退出码设置为1,并打印status。
#include<iostream>
#include<string.h>
#include<cstdio>
#include<error.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("errno: %d, errstring: %s\n", errno, strerror(errno));
return errno;
}
else if (id == 0)
{
int cnt = 3;
while (cnt)
{
printf("子进程运行中, pid: %d\n", getpid());
sleep(1);
cnt--;
}
exit(1); //设置退出码为1
}
else
{
//父进程
//pid_t rid = wait(nullptr);
//pid_t rid = waitpid(-1, nullptr, 0); // == wait
int status = 0;
pid_t rid = waitpid(id, &status, 0); //返回给父进程的本身就是子进程的pid
if (rid > 0)
{
printf("wait sub process success, rid = %d, status = %d\n", rid, status);
}
else
{
perror("waitpid"); //等待失败的情况
}
while(true)
{
printf("我是父进程:pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
但结果并不是1,而是256。
因为status不仅仅包含进程退出码,我们写的exit和_exit都是程序正常退出,但是程序只会正常退出吗?
进程结束分为两种:
1.正常结束
2.异常结束
因为代码可能跑不到exit和return,在中途可能会有野指针段错误等直接就崩溃了,退出码也就没有意义了。
所以异常退出也属于进程退出的信息。
所以status看起来是一个整数,实际上是一个位图,一共32位:
status低16为的次8位记录退出码,所以我们这样修改代码,通过位运算获取退出码:
if (rid > 0)
{
printf("wait sub process success, rid = %d, status code = %d\n", rid, (status >> 8) & 0xFF);
}
你可能会问,子进程可不可以通过一个全局变量把退出码给父进程,这是不行的,因为修改全局变量之后,发生写时拷贝,即使地址一样,父进程也拿不到,更好验证了进程具有隔离性,只能通过系统调用函数来完成。
3.1重谈进程退出
1.代码跑完了,结果对,return 0。
2.代码跑完了,结果不对,return !0。
3.进程异常了。
只要进程出错,OS必须知道,一般就会把这个进程通过信号干掉(以后我们可以有方法让它不崩掉退出)。3退出进程退出信息中,会记录下来自己的退出信号。
status低7位表示退出信号的值。
所以退出码范围为[0, 255],退出信号值范围为[0, 127]。
3.2观察进程退出信号
接下来我们再次修改代码,将进程的退出信号也打印出来观察:
if (rid > 0)
{
printf("wait sub process success, rid = %d, status code = %d, exit signal: %d\n", rid, (status >> 8) & 0xFF, status & 0x7F);
}
接下来我们在子进程中制造几种错误:
我们先来观察一个正常的程序跑出野指针错误会报什么错误。 新写一个.c文件:
其实这个即使OS发送了 11) SIGSEGV 这个信号。
我们验证一下,写一个死循环程序并给他发送这个信号:
使用之前子进程的指针问题代码打印出来其OS发送的信号并验证信号:
再看信号列表中的字段:
进程的退出信息会维护在PCB中。
3.3WIFEXITED(status)宏和WEXITSTATUS(status)宏
其实写成位操作去观察退出码的方法并不优雅,系统提供了宏可以直接查看。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
我们一般将其两者连用,所以我们再次修改代码:
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("errno: %d, errstring: %s\n", errno, strerror(errno));
return errno;
}
else if (id == 0)
{
int cnt = 3;
while (cnt)
{
printf("子进程运行中, pid: %d\n", getpid());
sleep(1);
cnt--;
}
exit(1); //设置退出码为1
}
else
{
//父进程
//pid_t rid = wait(nullptr);
//pid_t rid = waitpid(-1, nullptr, 0); // == wait
int status = 0;
pid_t rid = waitpid(id, &status, 0); //返回给父进程的本身就是子进程的pid
if (rid > 0)
{
if (WIFEXITED(status))
{//程序正常退出
printf("wait sub process success, rid: %d, status code: %d\n", rid, WEXITSTATUS(status));//打印退出码
}
}
else
{
perror("child process quit error\n"); //等待失败的情况
}
while(true)
{
printf("我是父进程:pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
所以回答最开始的问题,为什么"退出码"是256?因为退出码是1,信号是0(因为进程正常退出,系统发送信号0),所以第9位是1,低8位是0,所以是256。
我们一直在讲创建子进程,但是我们创建出来子进程以后就只是完成了打印,并没有做一些实质上的任务,而且我们说过退出码可以由程序员自行约定,接下来我们写一个小项目来说明。
我们一直在添加数据(在vector中添加数据),之后每隔10s都要向一个文件中写入,这个文件以时间戳命名。在备份的时候,不能影响主逻辑,也就是说备份时主进程数据还在添加(其实还是阻塞等待了,但是这里演示一下)。
#include<iostream>
#include<vector>
#include<cstdio>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
std::vector<int> data;
void Save()
{
//创建子进程
pid_t id = fork();
if (id == 0)
{
//子进程
exit(0);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("wait child sucee, exit code: %d\n", WEXITSTATUS(status));
}
else
{
perror("waitpid");
}
}
int main()
{
int cnt = 1;
while(true)
{
data.push_back(cnt++);
sleep(1);
if (cnt % 10 == 0)
{
Save();
}
}
return 0;
}
此时子进程就要开始写文件。 这时候我们定义一个SaveBegin的函数,因为要写文件,所以文件可能会打开失败,所以可以使用枚举常量来自行约定退出码:
#include<iostream>
#include<vector>
#include<cstdio>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
std::vector<int> data;
enum CLASS
{
OK,
OPEN_FILE_ERROR
};
const std::string sep = " ";
int SaveBegin()
{
//获取时间 以时间命名文件
std::string name = std::to_string(time(nullptr));
name += ".backup";
FILE* fp = fopen(name.c_str(), "w"); //以写的方式
//文件打开失败
if (fp == nullptr)
{
return OPEN_FILE_ERROR;
}
std::string dataStr;
for (auto d : data)
{
dataStr += std::to_string(d);
dataStr += sep;
}
fputs(dataStr.c_str(), fp);
fclose(fp);
return OK;
}
void Save()
{
//创建子进程
pid_t id = fork();
if (id == 0)
{
//子进程
int code = SaveBegin();
exit(code);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
int code = WEXITSTATUS(status);
if (code == 0) printf("备份成功, exit code: %d\n", code);
else printf("备份失败, exit code: %d\n", code);
}
else
{
perror("waitpid");
}
}
int main()
{
int cnt = 1;
while(true)
{
data.push_back(cnt++);
sleep(1);
if (cnt % 10 == 0)
{
Save();
}
}
return 0;
}
其实有两种等待方式,我们刚才以阻塞等待方式完成,父进程其实还是在等待子进程。还可以非阻塞等待,这和waitpid第三个参数有关。
4.第三个参数(int options)非阻塞等待
非阻塞等待由我们自己循环调用非阻塞接口,完成轮询检测,让父进程可以做更多自己的事情。
服务器中,当其卡住时我们称其hang住了;当其崩溃我们称其宕机了。
我们先举一个非阻塞的例子:
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
while (true)
{
printf("我是子进程, pid: %d\n", getpid());
sleep(1);
}
exit(0);
}
//父进程
while(true)
{
sleep(1);
pid_t rid = waitpid(id, nullptr, WNOHANG);
if (rid > 0)
{
printf("等待子进程 %d 成功\n", rid);
break;
}
else if (rid < 0)
{
printf("等待子进程失败\n");
break;
}
else
{
printf("子进程尚未退出\n");
}
}
return 0;
}
5.参数总览
总结:
我们又学习了很多新内容,进程退出信息非常重要,明白了进程退出时需要向上级汇报工作,也知道了一些关于缓冲区的知识,但是这些只是还是凤毛麟角,我们接下来要讲解进程替换和其他重磅内容,大家继续追剧哦!