前言
在此之前,我们学过进程的概念,进程的状态,进程地址空间等一系列进程相关的问题。本章我们继续学习进程,我们要来学习一下进程的控制,关于进程等待,等问题。
目录
- 1.再次认识Fork函数
- 1.1 fork()之后操作系统做了什么
- 2.进程终止
- 2. 1 进程终止的常见方式:
- 2. 2 进程退出常见操作方法:
- 3 .进程等待
- 3.1 进程等待的必要性:
- 3.2 进程等待的方法
- 3.3 获取子进程status:
- 3.4 阻塞等待:
- 3.5 非阻塞等待:
- 4.makefile的新增知识点:
1.再次认识Fork函数
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
-
fork之前父进程独立执行,fork之后,父子两个执行流分别执行
-
fork之前只有父进程执行
-
fork之后父子进程代码共享
-
进程具有独立性,代码和数据必须独立的
-
因为代码只能读取,所以就不会有人写入,更不会发生写实拷贝,所以这是父子进程代码共享的原因。
-
通常父子代码共享,父子在不写入时,数据也是共享的
-
当任意一方试图写入,便以写时拷贝的方式各自一份副本
-
fork之后,是否只有fork之后的代码是被父子进程共享的?
fork之后,父子共享所有的代码!只不过子进程只能从这里(fork)开始执行。
fork之后,父子进程谁先运行并不能给答案,是调度器给答案
1.1 fork()之后操作系统做了什么
- 进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据(task_ struct)结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
- 进程 = 内核的进程数据结构 + 进程的代码和数据
创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表) + 代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立。
fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid
写时拷贝
- 写时拷贝是一种延迟拷贝的策略~
- 写时拷贝本身就是有OS的内存管理模块完成的!
2.进程终止
进程终止的时候,操作系统做了什么?
1.进程终止的时候,首先会进入Z状态,父进程会去等待它回收子进程的信息,读取退去时的一些信息,然后将进程设置为X状态,这时才真正的退出,释放内核结构,释放曾静进程加载到内存所对应的到吗和数据。
2. 当然要释放进程申请的相关内核数据结构和对应的数据和代码(本质就是释放系统资源)
2. 1 进程终止的常见方式:
- 正常退出:
a.代码跑完,结果正确 b.代码跑完,结果不正确
main函数的返回值意义是什么?
答:return的意义是返回给上一级进程,用来评判该进程执 行结果用的。(main函数崩溃,返回值就没意义了)
- 异常退出:
代码没有跑完,程序崩溃了(信号部分coredump,涉及到一点点)
2. 2 进程退出常见操作方法:
- 正常退出:(可以通过echo $? 查看进程退出码)
1.从main函数 Return返回
2.调用exit( n)
3.调用_exit
注意:在函数内部中,使用Return() 代表返回函数调用。
exit在代码任何地方调用,都表示直接终止进程。
main函数的返回值,是进程退出码(strerror())
- 异常退出:
1.Ctrl+C :信号终止
程序崩溃的时候,退出码没有意义了(异常退出)
3 .进程等待
3.1 进程等待的必要性:
1.之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。。
2. 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。。
3. 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。。
4.父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。。
3.2 进程等待的方法
- wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
代码演示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程创建成功
while(1)
{
printf("我是子进程, 我正在运行...Pid: %d\n", getpid());
sleep(1);
}
}
else
{
//父进程
//想看到等待成功之后僵尸进程就不见了
printf("我是父进程:pid:%d, 我准备电脑等待子进程啦\n", getpid());
sleep(30);
pid_t ret = wait(NULL);
if(ret < 0)
{
printf("等待失败!\n");
}
else
{
printf("等待成功: result: %d\n", ret);
}
//父进程等20后再退出
sleep(20);
}
}
-
wait()的方案可以解决回收子进程Z状态,让子进程进入X
-
waitpid()方法
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- 返回值大于0:等待子进程成功,返回值就是子进程的进程ID pid
- 返回值小于0:等待失败
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
参数:
Pid:
- pid值大于0:是几就代表等待哪一个子进程, 指定等待
- pid值等于-1:等待任意进程
status:
- 是一个输出型参数
- 通过调用 WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
子进程会将自己的退出信息写入task_ struct~~
子进程一旦死掉,父进程直接把子进程退出码拷贝到自己(通过waitpid传进来的int status参数,父进程就拿到了子进程的退出结果)*
options:
- 0:阻塞等待
- WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。(非阻塞等待)
3.3 获取子进程status:
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息.
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程.
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
次低八位得到子进程的退出码,最低七位是终止信号。
3.4 阻塞等待:
如果子进程还在运行,则父进程会被阻塞等待,直到子进程退出或被终止,才能继续执行下去。
阻塞等待,父进程等的时候,子进程压根没退出的,父进程只能阻塞式的等,只有等到子进程退出之后才能正式拿出来这里的int* status。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程创建成功
while(1)
{
printf("我是子进程, 我正在运行...Pid: %d\n", getpid());
sleep(1);
int* p = NULL;
*p = 100;
}
}
else
{
//父进程
int status = 0;
printf("我是父进程:pid:%d, 我准备电脑等待子进程啦\n", getpid());
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
//status >> 8 并不影响status的值
//status >>= 8 才影响status的值
// printf("wait success, ret : %d, 我所等待子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0x7F);
if(WIFEXITED(status))
{
//子进程是正常退出的
printf("子进程执行完毕,字进程的退出码:%d\n",WEXITSTATUS(status));
}
else
{
printf("子进程异常退出:%d\n",WIFEXITED(status));
}
}
}
return 0;
}
我们只需要对status进行位操作,就能拿到对应的退出码和退出信号。
我们很显然做了一个对空指针的解引用:
我们能看到退出信号是11号信号,我们来看一下11号信号是什么:
SIGSEGV的全称是Segmentation Violation,即”段错误”。
3.5 非阻塞等待:
与阻塞等待不同的是,非阻塞等待不是子进程不退出就一直在那里等,而是多次调用非阻塞接口轮询检测!
非阻塞等待时,条件不满足会直接就返回,此时用户不会因为调用了waitpid ()
,让自己阻塞住,一旦条件不满足该函数会立刻返回,父进程或者用户可以继续在返回之后空闲时间段内,做自己的事,做一会之后再去调用waitpid(),这种监测方式叫做非阻塞。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//非阻塞等待验证
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
//死循环跑不完,但是代码出现异常了,进程收到信号,信号终止了子进程,父进程就要知道
while(1)
{
printf("我是子进程,我的PID : %d, 我的PPID : %d\n", getpid(), getppid());
sleep(3);
}
exit(111);
}
else if(id > 0)
{
//父进程
//基于非阻塞的轮询等待方案
int status = 0;
while(1)
{
pid_t ret = waitpid(-1, &status, WNOHANG);
if(ret > 0)
{
printf("等待成功, %d, exit sig: %d, exit code: %d\n", ret, status & 0x007F, (status & 0xFF00) >> 8);
break;
}
else if(ret == 0)
{
//等待成功了,但是子进程没有退出 -- 函数调用成功了,只不过是在非阻塞状态
printf("子进程好了没,还没,那么父进程就做其他事情...\n");
sleep(1);
}
else
{
//出错了,暂时不处理
}
}
}
else
{
//do nothing
}
return 0;
}
- 轮训检测:
在非阻塞等待的时候,子进程还没退出,父进程虽然是在等子进程,但是不是卡住在那里等待,而是可以做其他的事情。
补充知识:
- 阻塞的本质是进程阻塞,把进程阻塞是要改进程状态的,R -> S。
- 把进程的PCB从运行队列放到等待队列,这都是操作系统干的.
- 把进程的PCB从运行队列放到等待队列,这都是操作系统干的
4.makefile的新增知识点:
如果想要生成两个,makefile代码如下:
尾声
看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦