目录
2.父子进程共享fork之前和fork之后的所有代码,只不过子进程只能执行fork之后的
(1)低16个比特位的次低8比特位(次低8比特位) 是退出码
一.fork函数初识
1.概念
2.父子进程共享fork之前和fork之后的所有代码,只不过子进程只能执行fork之后的
fork之前父进程独立执行,fork之后, 父子两个执行流分别执行。
那么fork之后,是否只有fork之后的代码是被父子进程共享的? ?
fork之后,父子共享所有的代码,但fork之前的代码也是父子共享的,只不过子进程只能执行fork之后的
子进程执行的后续代码! =共享的所有代码,只不过子进程只能从这里开始执行! !
为什么呢:
CPU中有一个程序计数器叫eip,用途是eip叫做,保存当前正在执行指令的下一条指令!
eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处开始执行啦! !
如果我想让子进程拿到fork之前的代码,可以让子进程把CPU中的eip改成main函数入口就可以执行fork之前的代码。
3.fork之后,操作系统做了什么?
进程=内核的进程数据结构+进程的代码和数据
创建子进程的内核数据结构(struct task_ struct + struct mm_ struct +页表) +代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立!
进程具有独立性,代码和数据必须独立的
数据通过写时拷贝保证独立性
代码因为是只读的,不可修改
写时拷贝
fork常规用法
fork调用失败的原因
4.Fork后子进程保留了父进程的什么?
A.环境变量
B.父进程的文件锁,pending alarms和pending signals
C.当前工作目录
D.进程号
fork函数功能是通过复制父进程,创建一个新的子进程。
- A选项正确:环境变量默认会继承于父进程,与父进程相同
- B选项错误:信号相关信息各自独立,并不会复制
- C选项正确:工作路径也相同
- D选项错误:每个进程都有自己独立的标识符
根据理解分析,正确选项为A和C选项
5.fork和exec系统调用
- fork生成的进程是当前进程的一个相同副本(fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样)
- fork系统调用与clone系统调用的工作原理基本相同(fork创建子进程就是在内核中通过调用clone实现)
- exec是程序替换函数,本身并不创建进程
- clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现,而exec函数中本身并不创建进程,而是程序替换,因此工作机理并不相同
二.进程终止
1.常见进程退出
1.代码跑完,结果正确 |
2.代码跑完,结果不正确 |
3.代码没跑完,程序异常了 |
——————————————————
2.关于进程终止的正确认识
C/C++的时候,main函数为什么 return 0; ?
a.return 0,给谁return
b.为何是0?其他值可以吗?
return 0表示进程代码跑完,结果正确
return 非零:结果不正确
在main函数中return代表进程结束。其他非main 函数return 代表函数调用结束
(1)进程退出码
代码跑完,结果正确就没什么好说的就exit(0)/return 0返回码是0;如果代码跑完,结果不正确,那我们最想知道的是失败的原因!
所以:非零标识不同的原因! 比如exit(13)
return X的X叫做进程退出码,表征进程退出的信息,是让父进程读取的! !
echo $? 查看进程退出码
echo $? :在bash中,最近一次执行完毕时,对应进程的退出码
解释这里:第一次 echo $? 打印了进程退出码 123 ,第二次 echo $? 打印的是上一次 echo $?的进程退出码,因为上一次 echo $? 执行成功了,所以进程退出码是0,。
一般而言,失败的非零值我该如何设置呢? ?以及默认表达的含义?
可以自定义,也可以用系统定义的sterror。
错误码退出码可以对应不同的错误原因,方便定位问题!
(2)关于终止的常见做法——exit()
1. 在main函数中return代表进程结束。其他非main 函数return 代表函数调用结束
2.在自己的代码任意地点中main函数/非main函数,调用exit()都叫进程退出,exit(X)中的X是退出码
(3)exit和_exit
exit终止进程刷新缓冲区
_exit,是系统调用,直接中止进程,不会有任何刷新操作
终止进程推荐exit或main函数中的return。
会打印: hello bit,即刷新缓冲区
如果是_exit(0),就不会打印任何东西,即不刷新缓冲区
(4)关于终止,内核做了什么?
进程 = 内核结构 + 进程代码 和 数据
进程代码 和 数据一定会释放,但是 task/struct && mm_ struct:操作系统可能并不会释放该进程的内核数据结构
因为再次创建对象:1.开辟空间 2.初始化 都要花时间。
linux会维护一张废弃的数据结构链表叫 obj,若释放进程,对应的进程的数据结构会被维护到这个链表中,这个链表没有释放空间,只是被设成无效,需要时就拿,节省开辟时间(这样的链表也称 内核的数据结构缓冲池,操作系统叫:slab分派器)
三.进程等待
1.为什么要进行进程等待
①为了解决僵尸进程内存泄漏问题
子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力。
②为了获取子进程的退出状态
2.wait与waitpid
wait()的方案可以解决回收子进程z状态,让子进程进入X十,wait是等待任意一个退出的子进程
wait演示:
waitpid:
pid_ t (int) 的返回值 :
>0:等待子进程成功,返回值就是子进程的pid
<0:等待失败
=0:等待成功,但子进程没有退出
第一个参数 pid:
>0:是几,就代表等待哪一个子进程退出,比如pid=1234, 指定等待
-1:等待任意进程退出(通常是最后退出的那个进程)
第三个参数 option:
0:标识阻塞等待(就是父进程等待子进程死,子进程死后就回收它)
当options被设置为WNOHANG则函数非阻塞(Wait No Hang 夯住了),且当没有子进程退出时,waitpid返回0
第二个参数status:
int* status:是一个整数,是输出型参数:父进程调用waitpid,可以通过status拿到子进程的退出码(具体过程:子进程退出后变为Z状态—子进程代码释放,只是维护着子进程的进程控制块 task_struct;此时子进程的进程控制块 task_struct中 有两个整形退出码int exit_ code和退出信号 exit_ signal 会被填充,然后父进程会拿到这两个值放入status中,所以输出型参数要先定义然后传参时取地址 waitpid(id, &status, 0),或者传nullptr 是不需要退出码 )
只需要关心改整数的低16个比特位!
(1)低16个比特位的次低8比特位(次低8比特位) 是退出码
证明:让子进程先睡眠5秒,然后退出,退出码设为0。子进程睡眠5秒期间父进程用 wait/waitpid 设成阻塞态,已知返回值:
pid_ t (int) 的返回值 :
>0:等待子进程成功,返回值就是子进程的pid
<0:等待失败 =0:等待成功,但子进程没有退出
利用ret接收返回值,当接收成功时,打印ret(这里的ret就是子进程的pid),
并打印(status>>8) &0xFF ,status>>8是 次低8比特位开始,与上0xFF(8个1),就是退出码。我们会发现status中的退出码确实记录了子进程的退出码
(2)低8个比特位是终止信号
(kill -l 可查退出信号,异常就是因为收到信号,status&0x7F 可打印终止信号)
status&0x7F:status的低8位与上111 1111 ,就可得到终止信号
没有0号信号:
另一个接收
waitpid():
阻塞等待和非阻塞等待
当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待,本质:就是当前进
程自己变成阻塞状态,等条件就绪的时候,在被唤醒!(这里的条件不就绪可能是任意的软硬件条件!)
3.父进程非阻塞等待(WNOHANG)
waitpid(-1,&status, WNOHANG);
WNOHANG 父进程为非阻塞等待 (Wait No Hang 夯住了)
返回值:=0,等待成功,但子进程没有退出 ;等待成功返回子进程pid,等待失败返回-1。
(1)父进程基于非阻塞的轮询等待的 例子:
waitpid(-1,&status, WNOHANG); 在while循环内每次 waitpid执行一次,就检测一次子进程,如果子进程退出,就等待成功返回子进程pid;如果子进程没有退出,因为是非阻塞等待就等待成功返回0;
(2)父进程基于非阻塞的轮询等待,父进程也有任务的例子
#include <iostream>
#include <vector>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
typedef void (*handler_t)();
//方法集
std::vector<handler_t> handlers;
void fun1()
{
printf("hello, 我是方法1\n");
}
void fun2()
{
printf("hello, 我是方法2\n");
}
void Load()
{
//加载方法
handlers.push_back(fun1);
handlers.push_back(fun2);
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
printf("我是子进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(104);
}
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&0x7F, (status>>8)&0xFF);
break;
}
else if(ret == 0)
{
//等待成功了,但是子进程没有退出
printf("子进程好了没,奥, 还没,那么我父进程就做其他事情啦...\n");
if(handlers.empty())
Load(); 添加任务
for(auto f : handlers)
{
f(); //回调处理对应的任务,即执行任务
}
sleep(1);
}
else{
//出错了,暂时不处理
}
}
}