文章目录
1.进程创建
1.1 fork函数
在Linux中,fork函数是一个比较重要的函数,它能从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
函数头文件和原型
#include<unistd.h> pid_t fork(void)
其中,pid_t 是一个整型,若是创建子进程失败,则返回-1,若是创建成功,对于父进程而言,返回 子进程的PID号,对于子进程而言,返回 == 0 的数
关于fork函数需要注意的几点:
① 子进程是拷贝于父进程的PCB的,子进程大部分数据都是来源于父进程(其中就包括了PCB内存指针中的进程地址空间和对应的页表映射关系)
②父进程创建子进程成功之后,父子进程是独立的两个进程(体现了进程的稳定性),父子进程的调度取决于操作系统的内核
③ 进程是抢占式执行的,父子进程谁先运行是不能确定的
④ 子进程拷贝父进程时是写时拷贝。写时拷贝的具体含义实现可以看我之前写的这篇文章中的疑惑验证小节
关于fork函数的更详细解释,可以查看我之前写的详解进程的相关概念(冯诺依曼体系,操作系统,PCB,进程状态,进程创建)中的进程创建小节。
1.2 vfork函数
vfork函数可以理解为旧版的fork的函数,但是他和fork函数是不一样的,它也能从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程,但与fork函数不同的是,父子进程是共享同一个进程地址空间的,并且是子进程先运行,但子进程结束后父进程才能继续运行。
函数头文件和原型
#include<unistd.h> pid_t vfork(void)
返回值和fork函数一样,对子进程返回0,对父进程返回子进程的PID号,如果创建失败就返回-1。
因为vork函数创建出的子进程是和父进程共享进程地址空间的并且是子进程先运行,那肯定就存在着一定的问题,比如若是子进程一直在运行,一直在进程地址空间中循环,子进程一直迟迟不退出,那么带来的影响就是父进程就会一直无法获取资源,只有等待子进程结束后,他才会运行。
但是该函数也有属于他的用途,他可以应用于那种软件24小时不停止运作的情况;也能用于顶雷的情况,即若是由于某些原因导致程序挂掉,子进程退出,但不影响父进程,父进程可以再起一个子进程,继续保证该程序大多数功能都能继续运行下去,父进程在这个过程中起的作用就是监督子进程,若是子进程挂了,就快速再创建一个进程。
2. 进程终止
2.1 进程终止的场景
① 从main函数的return函数返回
- 代码执行完毕,没有获得既定的结果
- 代码执行完毕,获得了既定的结果
② 程序崩溃(异常终止)
ctrl + c:使信号终止
2.2 进程终止的方法
① 从main函数的return进行返回
②exit函数(库函数)
库函数:库函数是对系统调用函数进行封装,通过它再调用操作系统的内核
#include <stdlib.h> void exit(int status);
int status:定义了进程的终止状态,父进程通过wait(进程等待参数)来获取该值
③_exit函数(系统调用函数)
系统调用函数:直接调用操作系统的内核
#include <unistd.h> void _exit(int status);
同exit函数,传入给定的进程退出码,由父进程的进程等待参数来获取
exit和_exit函数的最大区别就是是否会刷新缓存区, exit函数在退出的时候会对缓存区中的内容进行刷新,而_eixt函数不会。
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit 或 on_exit 定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
具体过程如图:
这其实也算是库函数和系统调用函数的一个区别
清理缓存区的方式:
- ’\n’
- fflush函数
- exit
- 从main函数返回的return
2.3 atexit 清理函数
atexit函数是一个库函数,当程序正常终止时,调用指定的函数func,并执行该函数。你可以在任何地方注册你的终止函数,但它会在程序终止时被调用。
函数头文件和原型
#include <stdlib.h> int atexit(void (*func)(void))
分析它的参数可知,它的参数是一个函数指针,那么在调用它的时候就要传入一个函数的首地址(函数的首地址,通常可以直接传函数名即可)
当atexit函数成功的时候,就会返回一个0,否则就会返回一个非0的数。
注:它会在程序终止时调用
3. 进程等待
3.1 进程等待的作用
① 是解决僵尸进程和僵尸状态的最佳方法,能够有效的防止内存的泄漏。
② 获取子进程的状态,比如,父进程派给子进程的任务完成的如何,子进程运行完成,结果对还是不对,或者子进程是否正常退出,等等这些都需要进程等待
③父进程要通过进程等待的方式,回收子进程资源,获得子进程的退出信息。
3.2 进程等待的方法
3.2.1 wait方法
wait函数会暂停当前进程的执行,直到有信号来到或者子进程结束。进程一旦调用了wait函数,就会进入阻塞状态,由wait函数自动分析当前进程的某个子进程已经退出,如果让它找到了一个已经变成僵尸的子进程(子进程先于父进程退出),wait就会收集这个子进程的信息,并把它彻底销毁后再返回;如果没有找到这样的子进程,wait函数就会一直进行阻塞,直到有一个僵尸进程出现为止。
函数头文件和原型
#include<sys/types.h> //提供了类型pid_t的定义 #include<sys/wait.h> pid_t wait(int* status);
返回值:如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1。
参数说明
int* status
首先,该参数类型是一个整型指针,占4个字节,该参数是一个输出型参数,由操作系统填充,如果传递NULL,则表示不关心子进程的退出状态信息;否则操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能当成一个简单的整型指针来看待,可以将其看待为位图,即只有效占用了低两位的字节,高两位的字节没有使用,具体细节如图
再这低两位的字节中,又可划分为3类:进程终止码(8bit)、coredump标志位(1bit)和终止信号(7bit),具体细节如图:
这些进程退出码、coredump标志位和终止信号是由操作系统内部自动进行赋值的,但是也分为两种情况:正常退出和异常终止。
- 正常退出:只会生成进程退出码,不生成其他两个
- 异常退出:只会生成coredump标志位和终止信号,不会生成进程退出码
碎片知识:coredump被称为核心转储文件
进程退出码
在Linux操作系统中,程序可以在执行终止后传递值给其父进程,这个值被称为退出码(exit code)或退出状态(exit status)
coredump标志位
由于coredump标志位只有一个比特位的大小,因此它只有 0 和 1 两种状态
- 0:如果取值为0,表示当前进程 没有 coredump文件产生
- 1:如果取值为1,则表示当前进程 有 coredump文件产生
那什么是coredump文件呢?
程序由于各种异常或者bug导致在运行过程中异常退出或者中止,并且在满足一定条件下会产生一个叫做core的文件。通常情况下,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成第一个文件,许多的程序出错的时候都会产生一个core文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。
终止信号
表示当前进程是由什么信号导致退出的
对wait函数的一些深入理解
调用者准备一个int类型的变量,将该变量的地址传给wait函数后,wait函数在自己实现的内部内部进行赋值,当wait函数返回后,调用者就可以通过定义的int变量,来获取进程的退出信息。
这里有一个很重要的命令:pstack [PID]
,它可以用来查看进程调用堆栈,以此来验证当前进程处于什么状态(阻塞、就绪和运行)。
碎片知识:当前执行流调用某一个函数的时候,该函数一直不返回,称此现象为阻塞
而一旦调用wait函数,它会使当前进程进入阻塞状态,然后一直等待由子进程返回的信号,等接收到信号之后,wait函数才会返回,当前进程会继续运行
子进程在退出时,会告知父进程它退出了,此处的告知即就是发送一个SIGCHLD信号,但是该信号在所有的进程中,是被忽略掉的,不做处理的,因此才会产生僵尸进程。但是wait函数接收就是该信号,接收到后,就会进行返回。
获取进程退出码
(status >> 8) & 0xff
将status右移 8 位,再与 1111 1111 (16进制为0xff)做 & 运算
终止信号
status & 0x7f
直接与 0111 1111 (0x7f) 做 & 运算
coredump标志位
(status >> 7) & 0x1
将status右移 7 位,再与 1(0x1) 做 & 运算
3.2.1 waitpid方法
waitpid函数和wait函数功能均相同,均是用来使进程等待,解决僵尸进程。
函数头文件和原型
#include<sys/types.h> //提供了类型pid_t的定义 #include<sys/wait.h> pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数说明
pid_t pid
:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
int* status
:
和wait函数中的参数一样,产生进程退出码、coredump标志位和终止信号
但是可以用WIFEXITED函数来进行查看:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
int options
:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的PID。(非阻塞等待方式,即若条件不满足,则返回),需搭配循环去使用,直到完成函数功能。(一般都是do…while();循环)
0 :效果等同于wait。(阻塞等待方式,即陷入函数内部的逻辑,一直进行等待)
注:
- 如果子进程已经退出,调用 wait/waitpid 时,wait/waitpid 会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用 wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
4.进程程序替换
4.1 概念(作用)
进程程序替换就是指将正在运行的进程替换成为另外一个程序,使该进程运行另外一个程序的代码。
原理
假设当前正在运行的程序是test,当test运行的时候,会先加载到内核当中,然后内核要对该程序进行管理(描述),就会生成一个PCB,在Linux中就是一个task_struct的结构体,在这个结构体中,有一个内存指针mm,mm会指向进程虚拟地址空间,而进程程序替换就是将当前进程的进程虚拟地址空间中的数据段和代码段替换成为另一个程序中的数据段和代码段,然后进程就会在 test 中执行另一个程序的代码,最终产生不同的结果。
如果感觉不想看字,直接看下面的图
总结一下,进程程序替换就是将进程的代码段和数据段替换为新的程序的数据段和代码段,并更新堆栈的信息
4.2 应用场景
4.2.1 守护进程
守护进程就是父进程创建出子进程,然后用子进程进行进程程序替换,完成不同的功能。
守护进程的本质上是为了提供7 * 24小时的服务,服务不间断;但是,当子进程由于代码崩溃退出之后,虽然父进程会立即重新启动一个子进程,让子进程进行进程程序替换,但是我们一定要知道,导致进程退出的原因是程序崩溃,而崩溃(代码)并没有被修复。
4.2.2 bash(命令行解释器)
仔细思考的话,我们写代码编译出来的程序其实也是进行进程程序替换之后的结果,任何程序编译出来后,都有一个共同的父进程bash,这些程序的运行就是进程程序替换之后的结果。
4.3 接口
exec函数簇:包含了一堆的进程程序替换的接口
4.3.1 execl
int execl(const char *path, const char *arg, ...);
参数:
path
:这并不是单纯的路径,而是路径+可执行程序名称(带路径的可执行程序)arg
:给待替换的可执行程序传递参数规定:
① 第一个参数为可执行程序本身
② 多个可执行参数,使用,
进行间隔
③ 参数的最后需要以NULL结尾
...
:可变参数列表
返回值
- 替换成功之后,该函数是没有返回的,也不知都返回值是多少,替换成功之后,进程PID和替换之前的进程PID一样
- 替换失败后,则返回一个小于0的数。
下面几个函数的返回值都一样
举个例子:
#include <unistd.h>
execl("/user/bin/ls","ls","-l","-a",NULL);
将当前进程替换成为ls
进程
函数名中带l
的,表示给待替换的可执行程序传递的命令行参数为可变参数列表(即为...
)
4.3.2 execlp
int execlp(const char *file, const char *arg, ...);
参数:(参数相同的将不在解释)
file
:只需要传递待替换的可执行程序
但需要注意的是:
- 若只传递可执行程序的名称,则该可执行程序一定要在环境变量PATH中找到
execlp("ls","ls","-l",NULL);
- 也可以将待替换的可执行程序路径写全
execlp("../test","test",NULL);
函数名称带p
,则表示当前函数会自动搜索环境变量PATH
4.3.3 execle
int execle(const char *path, const char *arg, ..., char * const envp[]);
参数:
*envp[]
:指针数组,本质是数组,数组每一个元素都是char *
,我们需要自己组织一个环境变量,放入envp[]数组中,最后一个元素需放入NULL。
extern char** environ;
execle("/user/bin/ls","ls","-l",environ);
environ,是C库中直接进行维护的,可以直接拿来用,表示的是当前系统的环境变量。
函数中带有e
,则表示当前exel函数需要自己组织环境变量,放入envp指针数组中,以NULL结尾
4.3.4 execv
int execv(const char *path, char *const argv[]);
argv[]
:指针数组,它是命令行参数的数组,存放的是传递给可执行程序的命令行参数规定:
① 第一个参数为可执行程序
② 参数的最后需要以NULL结尾
char* argv[1024] = {0};
argv[0] = "ls";
argv[1] = "-l";
argv[2] = NULL;
execv("/user/bin/ls",argv);
函数名称中带有v
,表示给待替换的可执行程序传递的命令行参数,是指针数组
4.3.5 execvp 和 execve
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
参数功能都同上。
4.3.6 exec函数簇小结
废话不多说,直接上图