目录
一.进程创建
1.fork创建子进程,操作系统做了什么?
2.fork的用法以及失败的原因
二.进程终止
1.三种退出情况
2.main函数为什么一般情况下都要return 0
3.进程的退出码
1).0与非0
2).linux用指令查看一个进程结束后的退出码
3).程序崩溃时退出码无意义
4).被信号杀掉的进程
4.exit与_exit
1).exit
2).exit与_exit的区别
3).exit与return的区别
三.进程等待
1.进程等待的经典场景
2.wait
3.waitpid
4.wait/waitpid的本质
本质就是:操作系统新创建了一个进程
(进程 = 内核数据结构 + 进程的代码和数据)
1.创建子进程,操作系统需要为子进程分配新的内核数据结构和内存块
2.拷贝父进程的虚拟地址空间,但映射到物理内存时采用写时拷贝
3.共享父进程fork之后的代码,而数据分离(代码是只读的,代码共享并不影响进程的独立性)
4.添加子进程到系统进程列表中
5.fork返回,开始由调度器调度
fork的用法
父进程通过复制自己来同时执行不同的代码段,
如:父进程等待用户请求,生成子进程来完成这个请求
或一个进程要执行不同的程序
fork失败的原因
系统中的进程过多
实际用户拥有的进程数超过了限制
正常退出且结果正确
正常退出且结果不正确
非正常退出(程序崩溃)
在我们之前写程序时,int main{ return 0; }就像是固定模板一样,但是,为什么一般都要这么写呢?
为什么要return 0呢?return 1可不可以?
我们以上面这个程序为例,当main函数内return 1时好像也不会报错
实际上,return 0就代表的是: 程序结束,且正常退出
这便是程序的退出码
我们正式引出程序的退出码这一概念!
0: 代表程序正常退出且结果正确
非0: 代表程序正常退出且结果有误
这里的非零, 可以是1/2/3/4/ ... n(具体n是多少呢?), 且每一个数字都有自己的退出时的含义(结果为什么有误)
解释问题一: 具体n是多少呢,一共有多少退出码??
这里用到strerror函数
#include
#include
int main()
{
for(int i = 0; i < 150; i++)
{
printf("[%d]: %s\n", i, strerror(i));
}
return 0;
}
指令: echo $?
本质: $?其实就是bash中的一个变量, bash做为main函数的父进程, main函数退出后返回的退出码给了bash中的$?变量
用一个ls指令来显示一个不存在的文件,会提示No such file or directory, 这就是刚刚用strerror打印出来的下标为2对应的结果
这个进程的退出码的函数是可以自己定义的,但是推荐使用strerror函数中定义的
当程序崩溃,通常情况下都是没有运行到return的,这个时候将不再关心退出码
//当程序崩溃时退出码将无意义,此时观察一下程序退出码是什么
int main()
{
printf("开始实验\n");
int *p = NULL;
*p = 10;//在这里程序崩溃(野指针)
return 0;
}
上述程序在访问野指针的时候崩溃了,此时还没有执行到return语句,可以看到指令框弹出了一条Segmentation fault(段错误)
此时程序崩溃,退出码已经无效,那么Segmentation fault又是谁给弹出来的呢?
其实是信号,操作系统给该进程发送了一个信号,并且强制结束掉了该进程(信号部分先暂时不详细说明)
被信号杀掉的进程的退出码是无意义的, 因为这本质上与进程崩溃属于同一类, 并没有执行到return语句
exit直接结束掉进程, 这并不属于程序崩溃, exit(), 括号中是一个int类型, 表示程序的退出码
//观察exit的退出码
int main()
{
printf("传参为5, 观察exit函数结束掉的进程退出码是否为exit传入的参数");
exit(5);
return 0;
}
exit要包含头文件stdlib.h
_exit要包含头文件unistd.h
//观察exit与_exit的区别
int main()
{
printf("传参为5, 观察exit函数结束掉的进程退出码是否为exit传入的参数");
_exit(5);
return 0;
}
我们可以看到退出码仍然还是我们传入的参数5,这一点与exit一样
但是,有一个地方明显有问题,printf打印的那句话去哪了?
为什么exit就有printf打印的那句话,而_exit却没有?
下面画一张图,来解释以上的问题
exit是C语言的库函数, _exit是系统调用
其实exit的底层就是调用了_exit, exit是_exit的一层封装, 多加入了一些功能
(所以这同时也进一步揭示了缓冲区实际是在库中维护的)
1.return只有在main函数中, 才可视为进程结束且返回退出码
2.exit在任意位置, 可以直接结束进程, 并且以exit函数参数做为进程结束的退出码
3.return n等同于exit(n), 因为调用main函数且main函数return n后
调用main函数的函数会将main函数的返回值n当作exit的参数, 传给exit
当父进程有某项任务要交给子进程去做时, 父进程是一定要需要知道子进程完成的情况的
这也就是父进程是一定要接收子进程的退出状态, 并且检验然后才可完由其父进程回收子进程
父进程以进程等待的方式, 等待接收子进程的退出信息, 并且回收子进程
以下介绍两个函数wait与waitpid
wait是系统调用, 阻塞式的等待回收子进程,
如果回收子进程失败wait会返回-1, 如果回收子进程成功wait会返回子进程的pid, wait的返回值是pid_t类型
#include
#include
#include
//目标,让父进程通过使用wait系统调用阻塞式的等待回收子进程
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int count = 5;
while(count)
{
printf("I am child, 我将在%d秒后结束, 我的pid: %d\n", count, getpid());
count--;
sleep(1);
}
}
else
{
//父进程
printf("I am father, 我开始等待回收子进程\n");
pid_t res = wait(NULL);
if(res > 0)
{
printf("I am father, 等待子进程(pid: %d)成功\n", res);
}
else if(res == -1)
{
printf("I am father, 等待子进程失败\n");
}
}
return 0;
}
wait的参数只有一个, 是一个输出型参数, 当调用wait回收子进程时, 会将子进程的退出状态填入这个输出型参数中
这个输出型参数status是一个32位的数字, 低七位存放信号部分(第8位先不考虑), 次低八位存放退出码
当程序正常退出, 则信号部分将不被关心
当程序异常崩溃, 则退出码部分将不被关系(异常崩溃的本质就是操作系统给这个异常进程发送了信号并且强制终止)
#include
#include
#include
#include
//目标,让父进程通过使用wait系统调用阻塞式的等待回收子进程
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int count = 5;
while(count)
{
printf("I am child, 我将在%d秒后结束, 我的pid: %d\n", count, getpid());
count--;
sleep(1);
}
exit(55);//子进程结束, 退出码为55
}
else
{
//父进程
int status = 0;
printf("I am father, 我开始等待回收子进程\n");
pid_t res = wait(&status);
if(res > 0)
{
printf("I am father, 等待子进程(pid: %d)成功\n", res);
printf("wait中输出型参数status:%d\n", status);
printf("status中的次低八位(退出码): %d\n", (status >> 8) & 0xFF);
printf("status中的低七位(信号): %d\n", status & 0x7F);
}
else if(res == -1)
{
printf("I am father, 我回收子进程失败\n");
}
}
return 0;
}
以上子进程正常退出的情况, 信号不被关心, 默认为0, 如果是被父进程kill掉呢?
进程被信号所杀, 退出码不再关心默认为0
参数:
第一个参数pid:
pid > 0, 回收为对应pid的子进程
pid == -1, 回收任意一个子进程
第二个参数status:
与wait的参数是同一个, 都是输出型参数
第三个参数options:
options传参为0: 阻塞式的等待回收子进程
options传参为WNOHANG: 非阻塞式的等待回收子进程
(WNOHANG是一个宏, #define WNOHANG 1)
返回值:
回收子进程成功, 返回子进程的pid
回收子进程过程中出错, 返回-1
如果使用非阻塞等待(WNOHANG)式的调用, 子进程还未结束, 返回0
两个宏函数
WIFEXITED(status): 若正常终止子进程返回状态, 则为真 (判断子进程是否是正常退出的)
WEXITSTATUS(status): 若子进程是正常退出的, 则表示正常退出的退出码
重点讲解options, 阻塞/非阻塞等待
阻塞等待:
父进程不在继续向后执行, 而是一直在waitpid调用处阻塞式的等待, 直到回收掉子进程, 父进程才可以执行接下来的逻辑
#include
#include
#include
#include
//目标,让父进程通过使用wait系统调用阻塞式的等待回收子进程
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int count = 5;
while(count)
{
printf("I am child, 我将在%d秒后结束, 我的pid: %d\n", count, getpid());
count--;
sleep(1);
}
//观察进程崩溃(异常退出)
//int *p = NULL;
//*p = 10;
exit(55);
}
else
{
//父进程
int status = 0;
printf("I am father, 我开始等待回收子进程\n");
//阻塞式等待
pid_t res = waitpid(id, &status, 0);
if(res > 0)//子进程回收成功
{
if(WIFEXITED(status))//子进程正常退出
{
printf("I am father, 等待子进程(pid: %d)成功\n", res);
printf("子进程正常退出且退出码为: %d\n", WEXITSTATUS(status));
}
else//子进程异常退出
{
printf("I am father, 等待子进程(pid: %d)成功\n", res);
printf("子进程异常退出且信号为: %d\n", status & 0x7F);
}
}
else if(res == -1)//子进程回收失败
{
printf("子进程回收失败\n");
}
}
return 0;
}
非阻塞等待:
父进程在等待回收子进程的同时, 仍可以执行自己的逻辑(非阻塞等待采用轮询检测方案)
#include
#include
#include
#include
#include
#include
typedef void (*handler_t)(); //函数指针类型
void fun1()
{
printf("正在执行任务一\n");
}
void fun2()
{
printf("正在执行任务二\n");
}
void Load(std::vector& handlers)
{
printf("正在装载任务...\n");
handlers.push_back(fun1);
handlers.push_back(fun2);
printf("装载完成!\n");
}
//目标: 使用waitpid的非阻塞式调用 并且使用宏函数WIFEXITED WEXITSTATUS
int main()
{
std::vector handlers;//定义了一个函数指针数组
pid_t id = fork();
if(id == 0)
{
//子进程
int count = 5;
while(count)
{
printf("I am child, 我将在%d秒后结束, 我的pid: %d\n", count, getpid());
count--;
sleep(1);
}
//观察进程崩溃(异常退出)
//int *p = NULL;
//*p = 10;
exit(55);
}
else
{
//父进程
int status = 0;
printf("I am father, 我开始等待回收子进程\n");
//非阻塞式等待
int quit = 0;//子进程退出状态 --- 0:未退出 1:已退出
while(!quit)
{
pid_t res = waitpid(id, &status, WNOHANG);
if(res > 0)//子进程回收成功(等待子进程成功且子进程已经结束)
{
if(WIFEXITED(status))//子进程正常退出
{
printf("I am father, 等待子进程(pid: %d)成功\n", res);
printf("子进程正常退出且退出码为: %d\n", WEXITSTATUS(status));
quit = 1;
}
else//子进程异常退出
{
printf("I am father, 等待子进程(pid: %d)成功\n", res);
printf("子进程异常退出且信号为: %d\n", status & 0x7F);
quit = 1;
}
}
else if(res == 0)//等待子进程成功但子进程并未结束
{
//to do something
printf("已经等待到子进程, 但子进程仍在执行任务还未结束, 我是父进程, 我将继续执行我的任务, 稍后在回收子进程\n");
if(handlers.empty())
{
Load(handlers);//装载任务
}
for(auto elem : handlers)
{
elem();
}
sleep(1);
}
else//子进程回收失败
{
printf("子进程回收失败\n");
quit = 1;
}
}
}
return 0;
}
一段关于waitpid内部的伪代码
(便于理解阻塞与非阻塞的区别)
wait/waitpid都是系统调用, 本质就是去子进程的PCB(task_struct)中读取两个东西(即子进程的退出码)
int exit_code/int exit_signal
当进程结束时, main函数最终return一个数, 之后会调用exit 将return的这个数 做为exit的参数, 而exit的底层又是调用的_exit
所以最终会由系统调用将进程的退出状态(即退出码与信号写入到进程的PCB中, 即int exit_code与int exit_signal)
然后由父进程调用wait/waitpid系统调用去子进程的PCB中读取这两个值, 然后再以某种形式写入到status中