进程控制
进程创建
fork函数
子进程是拷贝父进程的PCB的,子进程的大部分数据是来源书父进程,例如:内存指针(数据段、代码段)
父进程创建子进程成功之后,父子进程是独立的两个进程(进程的独立性),父子进程的调度取决于操作系统的内核
进程是抢占式执行的,父子进程谁先运行是不能确定的
写时拷贝
fork调用失败的原因:
系统有太多的进程
实际用户的进程数超过了限制
vfork函数
调用栈混乱的问题的解决:子进程先调用,子进程调用完毕后父进程再调用
进程终止
进程终止的场景
从 main 函数的 return 返回
能得到的结果:
代码执行完毕,没有获得既定的结果
代码执行完毕,获得了既定的结果
程序崩溃
程序终止的方法
从 main 函数的 return 返回
写一个程序,本身可以直接 return 结束,但是为了看的更清楚,所以打印一下进程号
使用 getpid 函数需要调用<unistd.h>头文件
运行后发现打印了一个进程号,即刚才的进程的进程号
exit函数–库函数
void exit(int status);
status会被进程等待接口的参数获取到
还是刚才的程序,这次使用 exit 函数
使用 exit 函数需要调用<stdlib.h>头文件
跑起来看看,确实是没有走到打印
此时可以通过 echo $? 来查看退出码
也就是刚刚在代码中写的 exit(10);
_exit函数–系统调用函数
void _exit(int status);
接下来尝试一下 _exit 函数
更改的地方很简单,目的也是一样,不想让程序走到打印那一步,运行看看
发现 _exit 也达成了希望的目标
exit 和 _exit 的区别
对刚才的代码做一些更改,首先是 exit 函数
此时希望的是能够打印出“oh ho”,而不打印进程号
完成了目标,再此修改程序,使用 _exit 函数
再运行代码
???
东西咧???
但是如果加上一个 \n 呢?
\n:想不到吧,我才是那个关键点哒
可能都没发现删掉了这个吧
现在是 exit 函数,运行试试
显而易见的成功了,那接着换成 _exit 函数
运行一下
发现这次成功了
原因在于,当我们使用 printf 函数打印时,数据是放在缓冲区中的,而使用 \n 会刷新缓冲区,当使用 _exit 函数时,由于是系统调用函数,所以不会刷新缓冲区
执行用户定义的清理函数
int atexit(void (*function)(void));
继续刚才的代码
那么此时出现了两种情况
①代码走到了 atexit 函数后进入 func 函数,打印后退出 func,再打印 oh ho,exit函数退出
②代码先打印 oh ho,在进行到exit函数时进入到 atexit 中打印 func 函数
所以到底是哪种情况?
还是直接运行来看看吧
看来是第二种情况了
再次修改代码
通过这次的修改,代码在走到打印 oh ho 结束后进入死循环,那此时会怎样输出?
看起来连输出都没有输出
atexit 函数就是在C库中注册一个函数,举个例子就像是我们订了一个闹铃,但是在到时间之前根本不会响,而 exit 函数就相当于闹铃响的时间,只有当代码运行到 exit 函数时才会使 atexit 在C库中注册的函数展现出来,即这是一个回调函数
进程等待
进程等待的作用
父进程进行进程等待,子进程在先于父进程退出之后,由于父进程在等待子进程,所以父进程会回收子进程的退出资源,从而防止子进程产生僵尸进程
进程等待的接口
wait 函数
pid_t wait(int *status);
返回值:
成功:返回子进程的 pid 号
失败:-1
参数:整形指针,int*,占用4个字节
处于高位的两个字节没有被使用,低位的两个字节中高位的一个字节用于存放进程退出码,低位字节中一个bit位用于存放coredump标志符,剩下的7bit位用于存放终止信号
coredump标志位:
1:如果取值为1,则表示当中的进程是由coredump(核心转储文件)产生
0:如果取值为0,则表示当前进程没有coredump产生
终止信号:当前程序是由什么终止的
参数 int* status:是一个出参,调用者准备一个 int 类型的变量,将地址传递给 wait 函数,wait 函数在自己实现的内部进行赋值,当 wait 函数返回之后,调用者就可以通过 int 变量,获取进程退出的信息
使用 wait 函数时需要包含一个名为 <sys/wait.h> 的头文件
让代码运行起来看看
可以看到已经输出了子进程和父进程的 pid 号
给父进程加一个死循环,再试着查看他们的进程
再去查查子进程的 pid,看看是不是僵尸进程
现在再在代码中删除 wait
可以看到产生了僵尸进程
如何获取进程退出码?
(status >> 8) & 0xff
如何获取终止信号?
status & 0x7f
如何获取coredump?
(status >> 7) & 0x1
拿到了进程退出符
再看看异常的情况
此时根本不会退出,直接就崩溃了
可以看到也输出了
waitpid 函数
pid_t waitpid(pid_t pid, int *status, int options);
pid:
-1:等待任一子进程,一旦等待到了,则返回
>0:等待特定的子进程,大于0的值就是子进程的 pid 号
status:出参,同 wait 函数
options:
WNOHANG:非阻塞等待方式--需要搭配循环去使用,直到完成函数功能
0:阻塞等待方式
进程等待的非阻塞模式
第一步,需要创建子进程,并模拟让子进程先于父进程退出
第二步,在父进程的逻辑中执行进程等待,同时使用非阻塞的等待方式
代码如下
#include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main()
7 {
8 pid_t pid = fork();
9 if(pid < 0)
10 {
11 perror("fork file");
12 return 0;
13 }
14 else if(pid == 0)
15 {
16 //child
17 printf("i am child, my pid is %d\n, ppid is %d\n",getpid(), getppid());
18 sleep(5);
19 exit(1);
20 }
21 else
22 {
23 //father
24 //当没又等待到相应的子进程,waitpid的返回值为0;
25 //当等待到了相应的子进程,waitpid的返回值为等待到的
26 //进程的pid号,即大于0
27 pid_t ret = 0;
28 do
29 {
30 ret = waitpid(-1,NULL,WNOHANG);
31 if(ret == 0)//输出child的情况
32 {printf("child is running\n");}
33 }
34 while(ret == 0);
35
36 sleep(20);
37 printf("i am father, my pid is %d, exit...\n", getpid());
38 }
39 }
随后建立Makefile文件后运行,同时查看进程,发现存在两个进程
再看一次,发现只剩下一个进程了
更改一下代码,使父进程能获取子进程的pid随后输出
再对比一下
进程程序替换
作用
将正在运行的进程替换为另一个程序的代码
原理
①进程程序替换就是将进程的代码段、数据段替换成为新的程序的低吗和数据
②更新堆栈信息
应用场景
①守护进程(本质上是为了让服务不间断,但是当子进程由于代码崩溃退出后,虽然父进程会立即重新启动一个子进程,让子进程进行程序替换,我们一定要知道,导致子进程退出的原因是崩溃,而崩溃并没有被修复,是一种治标不治本的行为)
②bash(命令行解释器)
接口
exec函数簇,这是一堆的接口
execl
int execl(const char *path, const char *arg, ...);
path:并不是单纯的路径,而是 路径+可执行程序名称
可以总结为带路径的可执行程序
arg:给待要替换的可执行程序传递的参数
规定:
1、第一个参数是可执行程序本身
2、多个可执行参数,使用“,”进行间隔
3、参数的最后需要以NULL结尾
…:可变参数列表
使用which命令可以查看命令的位置
返回值:替换成功后,该函数是没有返回值的,也不知道返回值应返回给谁,只有替换失败之后才会有返回值,返回值 < 0
由上述返回值来看,第一个 printf 可以打印出来,但是第二个 printf 无法打印
建立Makefile文件后运行
已经显示出了我们想要的内容
进程替换成功后,进程的PID是否发生了变化?答案是没有,但是不好验证
execlp
int execlp(const char *file, const char *arg, ...);
参数:
file:只需要传递待替换的可执行程序
注意: 1、如果只传递可执行程序的名称,则该可执行程序一定要在环境变量PATH中可以找到
2、也可以将待替换的可执行程序的路径写全
3、函数名中带有‘p’说明当前的函数会自动搜索环境变量PATH
arg:同execl
替换也成功了
execle
int execle(const char *path, const char *arg, ..., char *const envo[]);
path:带有路径的可执行程序
arg:同 execl
envp[]:指针数组,本质是数组,数组的每一个元素都是一个char*,程序员需要自己组织环境变量,发到envp[]数组当中,最后一个元素需要放入NULL
函数名称当中带有 ‘e’ 则表示当前的exec函数需要自己组织环境变量,放到envp指针数组当中,以NULL结尾
函数名称当中带有 ‘l’ 则表示给待替换的可执行程序,传递的命令行参数为可变参数列表
需要引用一下
替换成功了
execv
int execv(const char *path, char *const argv[]);
path:带有路径的可执行程序
argv[]:指针数组,命令行参数的数组,存放的是传递给可执行程序的命令行参数
规则: 1、第一个参数是可执行程序本身
2、参数的最后需要以NULL结尾
7.8更新
_exit()函数之所以不会刷新缓冲区,是因为_exit函数是内核代码,直接操作内核结束了进程,而此时不会通知上层C库,
所以,C库维护的缓冲区完全在无感知的情况下进程就终止了