进程控制:
进程控制包括1. 进程创建, 2. 进程终止, 3. 进程等待 4.进程程序替换
进程创建
- fork
int fork(void) ; 创建子进程
返回值:
成功: 返回两次
大于0: 返回给父进程逻辑
等于0: 返回给子进程逻辑
失败: 返回 -1
子进程拷贝父进程的PCB;
- vfork:
int vfork(void)
功能:创建子进程的接口
创建出来的子进程拷贝部分父进程的PCB, 和父进程共用同一个虚拟地址空间; 如果父子进程同时并行运行 有可能导致调用栈混乱的问题,
vfork解决方案:
让子进程先运行, 子进程运行完毕后,再让父进程运行
vfork函数创建子进程已经被淘汰了
进程终止:
1. 含义: 进程终止的含义就是一个进程的退出
2. 场景:
2.1 程序跑完了所有的代码, 从main函数退出
main函数代码执行完毕, 结果正确
main函数代码执行完毕, 结果错误
2.2 程序没有跑完所有代码, 程序崩溃掉了
3. 退出的方式:
3.1 main函数的return返回;
3.2 exit函数:(库函数);
退出码:
函数在退出的时候, 返回的值;
来源于main函数的返回值,
或者exit的参数;
3.3 _exit函数:(系统调用)
exit函数比_exit函数多两个步骤终止进程:
第一步: 执行自定义处理函数
int atexit(void(*function)(void));
参数是一个函数指针, 可以接收没有返回值, 没有参数的函数地址
作用: 调用atexit函数, 将参数传入的函数地址告诉内核. 当程序员需要退出的时候, 才调用传入的函数; 这样的函数叫做回调函数;
void func(void)
int main(){
atexit(func);
printf("xxx\n");
return 0;
}
第二步:刷新缓冲区
刷新缓冲区的方式:
1. 从main函数的return返回会冲刷缓冲区;
2. \n也会冲刷缓冲区;
3. fflush函数强制刷新缓冲区;
4. 调用exit函数也会冲刷缓冲区
原因: 缓冲区是C库当中维护的, 并不是内核维护的;
如果直接调用_exit, 直接执行了内核代码, 不会刷新缓冲区
如果调用exit, 在结束之前, 会先将缓冲区当中的数据进行刷新
异常
代码没有执行完毕, 程序崩溃掉了(解引用空指针; 内存访问越界, double free; "crtl+c"; "kill命令")
进程等待
1.作用 : 防止僵尸进程的产生
2. 方法:
2.1 pid_t wait(int* status): 阻塞接口;
status: 该参数是一个出参 , 供调用wait函数的进程获取子进程的退出信息的 ;
占用了4个字节, 实际有效部分为2个字节(后面低两个88字节), 倒数第二个字节为退出码(第一个字节, 第二个字节, 第三个字节, 第四个字节)
2.1.1 进程正常退出:
高16位当中高8位代表退出码, 16位当中低8位全为0
获取退出码:
(status >> 8) & 0xFF
向右移动8位变为低8位 再&0XFF, 可以看到哪一个位为1
2.1.2 进程异常退出:
16位中低8位被划分为2个部分, 第8位和第7位: 第8位代表coredump标志位; 第7位为终止信号
获取coredump标志位(判断是否有coredump文件产生) :
(status >> 7) & 0x1
等于0: 没有coredump文件产生
等于1: 则表示有coredump文件产生
获取终止信号(判断程序是否是正常退出的) :
status & 0x7f
等于0: 正常退出
大于0: 异常退出, 有终止信号
阻塞: 当调用函数需要等待一定条件成熟的时候, 成熟则返回; 如果条件一直不成熟, 则一直等待
非阻塞: 当调用函数需要等待一定条件成熟的时候, 成熟则返回; 如果条件不成熟, 也报错返回
信号:
子进程退出的时候会通过信号的方式告知父进程 ----> SIGCHLD(默认处理方式是忽略)
进程间通信:
status进程间通信 --> 子进程将自己的退出信息告诉父进程;
int child_exit_info ===> status = child_exit_info
wait函数是阻塞等待: 谁调用谁阻塞等待, 直到有子进程退出,才返回; wait接口的实现就是调用waitpid实现的
2.2 pid waitpid(pid_pid, int* status, int options)
pid: 等待子进程的pid号 不仅仅可以传入pid号 还可以传入-1
pid = -1: 表示等待任意的子进
pid > 0: 表示等待指定进程号为pid的子进程
status: 子进程的退出信息
options: 设置waitpid是阻塞的还是非阻塞的
0: 表阻塞
WNOHANG: 代表非阻塞(当我们调用非阻塞的waitpid函数接口时,如果等待的子进程没有退出, waitpid也不会等待, 直接报错返回 , 执行后面逻辑; )
非阻塞需要搭配循环去使用:
如果非阻塞的接口在调用waitpid时, 没有等待到子进程退出, 则循环去调用waitpid;
返回值:
非阻塞模式:
返回0 : 表示没有等待到子进程, 需要循环调用;
返回-1: 表返回错误;
大于0 : 才表示等待到了子进程
waitpid(pid>0, status, 0); 就相当于wait接口
如果进程退出的时候, 返回-1, 则退出码取到多少? --> 255
1: 正数在计算机中的存储方式: 正数的反码就是本身 补码也是本身
正数原码: 00000001
正数反码: 00000001
正数补码: 00000001
正数和负数: 最高符号位 正数为0, 负数为1,
-1: 负数的反码就是将原码0变1 1变0, 符号位不动; 补码是反码加1
负数原码: 10000001
负数反码: 11111110
负数补码: 11111111 ==>255
为什么返回的是一个int类型 但是最终计算出来的结果是255
就是由于退出码在内核中只占用了1个字节**
进程程序替换:
-
原理
创建进程同时创建一个结构体task_struct, 结构体的内存指针指向进程虚拟地址空间(并不具备存储数据的能力, 真正存储数据是在物理内存中) , 物理内存的数据来源于磁盘(test可执行程序被执行后, 数据/代码加载到物理内存), 通过内存指针找到进程虚拟地址空间的数据段/代码段,再通过页表映射将数据段和代码段映射为新的程序在物理内存当中保存的数据和代码
进程程序替换: 用新程序的数据段和代码段替换正在运行的程序的数据段和代码段, 更新堆,栈 ( 促使重新加载可执行程序到物理内存中 ) , 调用exec并不创建新进程,所以调用exec前后该进程的id并未改变, 只是执行的数据段和代码段变了而已 命令行参数会进行更新, 环境变量是继承之前的环境变量(也不改变) -
进程程序替换接口:
exec函数簇: 不是一个函数, 而是多个函数
int execl(const char* path , const char* arg, ...)
path: 带路径的可执行程序, 要替换哪一个可执行程序
arg: 给可执行程序传递的参数, 规定: 第一个参数必须是可执行程序的名称
...: 可变参数列表, 必须以NULL结尾, 告诉execl函数读到NULL的时候就是命令行参数结尾地方
execl("usr/bin/ls", "ls", "-a", NULL);
返回值:
只有替换失败的时候才有返回值, 返回-1; 替换成功了, 则直接执行替换的程序
int execlp(const char* file, const char* arg, ...);
file: 可执行程序的名称,由于可执行程序没有带路径, 所以这个可执行程序是必须在环境变量PATH当中可以找到的 (用$PATH查看 用export将要执行的程序路径临时插入PATH后) ; 也可以直接将可执行程序的绝对路径放在可执行程序的名称前面;
arg: 给可执行程序传递的参数, 规定: 第一个参数必须是可执行程序的名称
...: 可变参数列表, 必须以NULL结尾, 告诉execl函数读到NULL的时候, 就是命令行参数结尾地方.
exec函数带有p和不带有p的区别是:
带有p的exec函数会搜索环境变量, 不带的话,则不会搜索环境变量
对于所有的可执行程序而言, 在执行的时候, 第一个命令行参数都是自己的可执行程序的名称;
int execle(const char* path, const char* arg, ..., char* const envp[])
path: 带路径的可执行程序
arg: 可执行程序的参数 , NULL结尾
envp: 操作系统不会为我们替换成功的进程组织环境变量, 而是程序员自己指定环境变量放到envp这个指针数组中, 如果不传入环境变量则认为当前进程没有环境变量
如果自己组织环境变量, 一定需要以NULL结尾, 如果不以NULL结尾, 就会报错Bad Address
exec函数带e和不带e的区别:
如果不带e: 则不需要程序员去组织环境变量, 内核会将环境变量继承下去;
如果说带e, 则需要程序员去组织环境变量, 如果程序员传入NULL, 则替换的程序当中没有环境变量, 如果传入非NULL, 则指针数组的环境变量需要使用NULL结尾;
int execv(const char* path, char* const argv[]); -----> 定参的函数
path: 带路径的可执行程序
argv: 给替换的程序传递命令行参数, 如果不传, 则替换成功的进程当中没有命令行参数;
如果传参, 第一个参数是该程序的名称, 参数必须以NULL结尾;
exec函数簇当中带有l和带有v的区别:
l: 表示命令行参数为可变参数列表
v: 表示命令行参数为指针数组
int execvp(const char* filename, char* const argv[]);
int execve(const char* filename, char* const argv[], char* const envp[]);
唯独该函数为系统调用
filename: 可执行程序的名称
argv: 传递给可执行程序的参数, 第一个参数是该程序的名称, 以NULL结尾;
envp: 程序员自己组织环境变量, 如果不传入环境变量则认为当前进程没有环境变量
如果自己组织环境变量, 一定需要以NULL结尾, 如果不以NULL结尾, 就会报错Bad Address.
注: 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。