引入
在写 C 语言程序的时候,我们必写的结构就是:
int main()
{
return 0;
}
在学习 C 语言的时候,我们好像并没有讨论过这个 return 0
有什么用,是干什么的!return 1
可以吗?return
的返回值给谁看?这样的问题!
那么今天我们就会浅浅地解决一下这些问题!
进程退出的场景
一个进程被创建出来就是用来执行特定任务的!比如你写了一个快速排序的代码,他的任务就是排序!那么当这个进程退出的时候会有以下三种情况:
- 代码执行完毕,结果正确。
- 代码执行完毕,结果不正确。
- 代码异常终止。
设想一下,如果你的代码执行完毕,结果正确!我们还要不要关心他为什么正确呢?就比如你考试考了满分💯,你的家长要不要问你为什么要考满分?
因此,只有当进程没有完成任务,我们才会去关心:
- 代码执行完毕结果不正确,那么是那里不正确呢?
- 代码异常终止了,又是哪里出了问题呢?
回到一开始的问题,main
函数中 return
的值有什么意义呢?如果 main
函数返回 0,代表代码执行完毕,结果正确(约定俗称 0 表示这个意义);如果 return
返回其他值,我们就能通过返回值的不同得到进程退出时出错的原因!我们称进程结束时,返回给操作系统的数值称为退出码!
进程的退出码
退出码表征了进程运行的状态/结果,关心这个退出码的一定是创建这个进程的父进程!其实本质上关心出错原因的应该是用户!
在 Linux
操作系统中,有一组通用的退出码以及其大致含义:
- 0:成功,表示程序或者命令成功执行。
- 1:通用错误,通常表示一般性错误,没有特定的详细信息。
- 2:误用命令,表示命令的使用方式错误。
- 126:表示命令无法执行。
- 127:表示系统找不到要执行的命令
如果你不想用系统的,进程的退出码可以自定义,我们可以自定义退出码的描述信息,比如你可以设置进程退出码为 1 表示什么什么错误;退出码为 2 表示什么什么错误 ······
查看进程的退出码
想要查看进程的退出码可以使用:
echo $?
该命令可以查看最近的一个进程的退出码!
return 设置退出码
在下面的代码中我们通过 return 11
将退出码设置为 11。./test
运行程序,使用 echo $?
打出了最近的一个进程的退出码,的确是 11。我们发现再次使用 echo $?
打印出来的结构就变成了 0,这是为什么呢?因为 echo
本身也是一个进程嘛,在运行 ./test
之后第二次使用 echo
这个 echo
就是最近的那个进程啦~而 echo
又是成功运行的,当然就是 0 啦!
return 设置错误码仅限在 main 函数中呢!因为在其他函数中 return
仅仅表示结束当前函数嘛!
exit 设置退出码
exit
函数可以直接结束当前进程,在任何地方!参数用来设置退出码!
在下面的代码中,我们定义了一个函数 Div
,在函数体中检测到除数如果为 0 我们直接终止进程,退出码设置为 1,如果你自定义了退出码 为 1 时为除零错误,就能更好的展示错误信息啦!
#include<stdio.h>
#include<stdlib.h>
int Div(int x, int y)
{
if(y == 0)
exit(1);
return x / y;
}
int main()
{
Div(1, 0);
return 0;
}
我们看到退出码为 1,并不是 0 说明,exit() 的确能够终止进程并设置退出码的!
补充:exit
VS _exit
相同点:
- 可以在程序的任意位置终止进程。
- 可以设置进程的退出码
不同点:
exit
是 C 语言的库函数(#include <stdlib.h>
),_exit
是Linux
操作系统的系统调用函数(#include<unistd.h>
)。exit
函数除了终止进程,设置进程的退出码之外,还会执行用户定义的清理函数,冲刷缓冲区,关闭流等!;_exit
仅仅是终止进程,设置进程的退出码!
我们可以用下面的程序进行验证:
我们使用 printf
函数打印 “hello linux” 这个字符串,不加换行符的打印哦!然后使用 exit
函数终止进程!
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("hello linux");
exit(11);
return 0;
}
我们可以看到顺利将 hello linux
这个字符串打印出来了!
再来看下面的程序:同样是不带换行符打印 hello linux
这次我们使用 _exit
终止进程!
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello linux");
_exit(11);
return 0;
}
我们看到使用 _exit
函数终止进程没有打印 hello linux
!
这是为什么呢?我们知道在 Linux
操作系统中,printf
的刷新策略是行刷新!也就是说在没有遇到换行符或者没有将缓冲区写满之前,printf
的数据都是不会刷新到显示器上的!结合 exit
会在进程终止之前刷新缓冲区!因此我们可以得出结论:缓冲区绝对不在内核中!而是在用户区!
如果缓冲区是在内核中的话,_exit
也一定会把数据刷新到显示器!不然为什么要向内核中写数据呢!!!
可见:exit
函数是在刷新缓冲区,关闭流等工作完成之后调用的 _exit
系统调用。
在平时的使用中还是建议使用
exit
因为刷新缓冲区,关闭流的确是需要的!
错误码
再讲完了退出码,就不得不提错误码的概念!错误码通过全局变量 errno
来表示,errno
是一个整数,用于最近一次系统调用或者库函数导致的错误,这个错误提供了一种标准化的方式来处理和传递错误信息!
我们也不知道系统提供了多少错误码,我们可以打印错误码,以及他的描述信息来看看!查看错误码对应描述信息可以使用 strerror
函数哈!
#include<stdio.h>
#include<string.h>
int main()
{
for(int i = 0; i < 200; i++)
{
printf("%d: %s\n", i, strerror(i));
}
return 0;
}
我们看到错误码系统提供到了 133 号!
如果你不喜欢系统提供的错误码,也可以自定义错误码对应的描述信息!
我们来使用使用错误码:
我们调用 fopen
函数以读的方式打开一个不存在的文件,在 fopen
函数的描述中,可以看到如果有错误,错误码就会被设置,我们可以通过 errno
变量来打印错误的描述信息。也可以通过 perror
函数来打印错误的描述信息,这些都是在 C 语言阶段学习过的,就不详细解释了!
errno 的使用需要包含头文件:#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
extern int errno;
fopen("test.txt", "r");
printf("%d: %s\n", errno, strerror(errno));
perror("fopen");
return 0;
}
代码异常
如果代码出现了异常,那么退出码还有意义嘛?
代码异常说明代码很可能没跑完,return 语句都没有执行,用户都不知道这个退出码怎么来的,因此是没有任何意义的!就比如你作弊考了 100 分,你爸会关心你的 100 分嘛,直接就是辣椒炒肉了!
代码异常引起的进程终止,我们要关心的是为什么异常了!发生了什么异常!
在 Linux
中代码异常的本质是进程收到了信号!信号我们后续会详解!之前我们不是还用过 9 号信号嘛:杀死一个进程!
我们可以写一个会发生异常的代码:
#include<stdio.h>
int main()
{
int a = 10;
a /= 0;
return 0;
}
怎么验证代码异常的本质是进程收到信号了呢?很简单只需要我们手动给进程发信号就行了,上面的异常是几号信号呢?我们使用 kill -l
查看一下所有信号,仔细找找发现是 8 号信号:
因此我们手动给进程发 8 号信号看看结果:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("hello linux, pid: %d\n", getpid());
sleep(1);
}
return 0;
}
可以看到发送 8 号信号之后,进程也是出现了浮点异常!说明代码异常的本质就是进程受到了信号
知识点小结:
- 进程终止的三种情况
- 退出码
- 错误码