进程控制——进程创建、终止、等待、程序替换
本文主要介绍了进程控制中关于进程创建、进程终止、进程等待、进程程序替换的相关内容。
目录
进程创建
一、fork()
二、fork()内部操作
三、用户空间&内核空间
四、写时拷贝(写时复制)
五、fork创建子进程特性
六、fork特殊用法
进程终止
一、进程终止
四、exit和_exit的区别
五、刷新缓冲区的方式
六、缓冲方式
进程等待
一、进程等待的意义
二、wait函数
三、waitpid函数
进程程序替换
一、原理
二、exec函数簇
进程创建
一、fork()
概念:
fork()可以让正在运行的进程创建出一个新的进程,新进程为子进程,原进程为父进程,二者相互独立。
fork()返回值:
pid_t [返回值变量] = fork();
如果使用fork创建子进程成功,则在父进程和子进程中分别返回:
父进程返回大于0的数(子进程的pid)
子进程的返回值为0
如果使用fork创建子进程失败,则返回-1
二、fork()内部操作
父进程创建子进程,子进程拷贝父进程的PCB
给子进程分配新的内存块和内核数据结构(task_struct)
子进程拷贝部分父进程部分数据结构内容
添加子进程到系统进度列表当中,添加到双向链表当中
修改子进程PCB内容(子进程pid、信号级等)
fork返回,开始调度器调度(操作系统开始调度)
三、用户空间&内核空间
概念:
1. 用户空间:应用程序和程序员代码运行的空间
2. 内核空间:Linxu操作系统、驱动程序、系统调用函数运行的空间
补充:
1. 如果程序员代码调用系统调用函数,则会由用户空间 “ 切换 “ 到内核空间,执行完系统调用函数后再 ” 切换回 “ 用户空间
2. fork()函数时系统调用函数,属于内核空间
四、写时拷贝(写时复制)
概念:
创建子进程时,子进程会拷贝父进程的PCB和页表(虚拟内存地址和物理内存地址的映射关系),同时会将内存设置为只读,当父进程(或子进程)对内存进行修改操作时,便会触发缺页异常,然后触发写时拷贝机制:将原来的内存地址复制一份并重新设置页表,将其赋予父进程(子进程),最后将父子进程的内存地址都设置为可读写。
解释说明:
所以在开始时,同一个变量在父子进程中的虚拟内存地址和物理内存地址的映射关系是一样的,也意味着操作系统没有给子进程中的变量在物理内存中分配空间进程存储,同一个变量父子进程使用同一个物理地址。
当变量发生改变时,才以写时拷贝的方式进行拷贝一份,此时父子进程通过各自的页表,指向不同的物理地址
当变量不发生改变时,父子进程共享同一个数据(共享内存)
扩展:
共享内存:不同的虚拟内存地址通过各自的页表指向相同的物理内存地址,那么就实现了共享内存的机制。
五、fork创建子进程特性
父子进程相互独立运行,互不干扰,在各自的进程虚拟地址空间和页表下读取数据
父子进程抢占式运行,被执行的顺序本质上由操作系统的调度决定(也和自身准备的情况相关)
子进程是从fork之后开始运行的(程序计数器+上下文执指针)
父子进程代码共享(代码相同),数据独有(页表及其对应的地址空间不同)
六、fork特殊用法
守护进程:
父进程创建子进程,利用子进程去执行业务(进程程序替换),当子进程意外 “ 挂掉 ”时,父进程则可以重新启动子进程,继续执行业务提供服务。此时的父进程称为守护进程。
备注:
1. 守护进程可以使业务程序提供持续监控,防止因意外而中断服务,但无法解决业务程序本身的BUG,因为守护程序本质上是父进程,程序代码与业务程序(子进程)相同。
2. 守护进程是提高程序 “ 高可用 ” 的一种手段。
进程终止
一、进程终止
正常终止:
echo $? 查看进程退出码
从main函数当中return返回 echo $? 查看值为return返回值
调用exit()函数(库函数) echo $? 查看值为exit()返回值
调用_exit()函数(系统调用函数) echo $? 查看值为_exit()返回值
异常终止:
解引用空指针,解引用野指针(垂悬指针) 段错误,产生核心转储文件
double free 产生核心转储文件
内存访问越界(访问别人的内存) 操作系统告知非法访问,进程会被强杀
补充:进程被强杀的情况
1. 内存访问越界(非法访问)
2. 进程持续申请空间,导致操作系统没有空间可以使用,为保护自身正常运行会强杀进程
四、exit和_exit的区别
exit函数比_exit函数多做的事情:
1. 执行用户自定义的清理函数
void (*function)(void):函数指针
函数指针指向函数的地址(保存函数的地址)
atexit:注册函数指针保存的函数地址到内核当中(注册不等同于调用函数指针指向的函数)
2. 刷新缓冲区
将缓冲区的内容刷新到屏幕上
扩展:
回调:当程序达到特定场景的时候才会调用函数,这种方法称为回调
回调函数:atexit_callback
五、刷新缓冲区的方式
\n
从main函数return返回
exit函数
fflush(stdout)函数(标准刷新)
六、缓冲方式
全缓冲:当缓冲区写满的时候,才进行IO
行缓冲:当在输入和输出中遇到换行符时,标准I/O库执行I/O操作
不缓冲:不带缓冲,标准I/O库不对字符进行缓冲存储,立即刷新
进程等待
一、进程等待的意义
父进程进行进程等待,等待子进程退出后,回收子进程的退出状态信息,防止子进程变成僵尸进程
二、wait函数
函数原型:
#incldue<sys/wait.h>
pid_t wait(int *status);
示例:
wait(NULL);
或
wait(int &status)
wait函数是阻塞调用,当父进程运行到wait函数时会停止运行,直到子进程运行结束后将返回信息船体给父进程,父进程才会继续执行。
返回值:
成功返回进程pid
失败返回-1
进程退出状态(int *status):
该值是int类型,由4个字节组成,但进程退出状态只利用了后两个字节,具体如下图:
(1)退出信号:用于判断程序退出类型
标准:退出信号是否有值
正常退出:退出信号=0,此时有退出码
异常退出:退出信号>0,此时退出码无意义
(2)coredump标志位:用于判断是否产生核心转储文件
1:产生核心转储文件
0:没有产生核心转储文件
(3)退出码:反应程序退出信息(仅在正常退出下有效)
(4)status:进程退出状态信息
获取退出信号:
status & 0x7f
获取coredump标志位:
(status >> 7) & 0x1
获取退出码:
(status >> 8) & 0xff
参数:
输出型参数,该参数的值由wait函数进行赋值,程序员在wait函数调用完毕后通过该参数获取到退出进程的退出状态信息,不关心可以设置为NULL。
扩展:
1. 输入型/输出型参数
输入型:程序员赋值给程序
输出型:程序自动赋值,程序员查看该值进行后续操作
2. pstack
查看进程的调用堆栈(查看进程正在运行什么代码)
三、waitpid函数
函数原型:
#include<sys/wait.h>
pid_t wait(pid _t pid,int *status,int options);
示例:
参数说明:
pid:
pid = -1,等待任意一个子进程,与wait等效
pid > 0,等待其进程ID与pid相等的子进程
status:退出状态信息(等同于wait函数的参数)
options:
WNOHANG:设置waitpid为非阻塞状态
若pid为指定的子进程没有结束,则waitpid()函数返回0,不予等待
若pid为指定的子进程正常结束,则返回子进程的ID
结论:
非阻塞调用一定要搭配循环使用
返回值:
当正常返回时,waitpid返回收集到的子进程的进程ID
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可以收集,则返回0
如果调用中出错,则返回-1,此时errno会被设置成相应的值以指示错误所在
进程程序替换
一、作用
父进程创建出来的子进程和父进程拥有相同的代码段,当我们想让子进程执行不同的程序时,就需要让子进程调用进程程序替换的接口,从而让子进程执行不一样的代码。
二、原理
替换进程的代码段和数据段,更新堆栈
三、exec函数簇
(1)execl:
#include <unstd.h>
int execl(const char *path,const char *arg,...);
示例:
execl("/usr/bin/ls","-a","-l",NULL);
参数说明:
path:带路径的可执行程序(需要路径)- 替换成为这个程序
arg:给程序传递命令行参数
1. 第一个命令行参数是程序本身
2. 最后一个参数以NULL结尾
返回值:函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;
失败则返回-1
(2)execlp:
#inlcude <unistd.h>
int execlp(const char *file,const char *arg,...);
示例:
execlp("ls","ls","-l",NULL);
参数说明:
file:可执行程序,可以带有路径,也可以不带有路径
arg:传递给可执行的程序的命令行参数
1. 第一个参数,需要可执行程序本身,如果需要传递多个参数,则用“,”进行间隔
2. 末尾以NULL结尾
返回值:这些函数如果调用成功则加载新的程序从启动代码开始执行
如果调用出错则返回-1
(3)execle:
#include <unistd.h>
int execle(const cahr *path,const char *arg,...,char *const envp[]);
示例:
extern cahr** environ;
execle("/usr/bin/ls","ls","-l",NULL,environ);
参数:
path:带路径的可执行程序(需要路径)
arg:传递给可执行程序的命令行
1. 第一个参数需要可执行程序本身
2. 如果需要传递多个参数,则用“,”进行间隔,末尾以NULL结尾
envp:程序员传递环境变量(调用该函数需要自己组织环境变量传递给函数)
返回值:这些函数如果调用成功则加载新的程序从启动代码开始执行
如果调用出错则返回-1
(4)execv:
#include <unistd.h>
int execv(const char *path,char *const argv[]);
示例:
char* argv[10] = {0};
argv[0] = "ls";
argv[1] = "-l";
argv[2] = "NULL";
execv("/usr/bin/ls",argv);
参数:
path:带路径的可执行程序(需要路径)
argv:传递给可执行的程序的命令行参数,以指针数组的方式进行传递
1. 第一个参数,需要可执行程序本身
2. 多个参数都放到数组当中
3. 末尾以NULL结尾
返回值:这些函数如果调用成功则加载新的程序从启动代码开始执行
如果调用出错则返回-1
(5)execvp:
#include <unistd.h>
execvp(const cahr *file,char *const argv[]);
示例:
char* argv[10] = {0};
argv[0] = "ls";
argv[1] = "-l";
argv[2] = "NULL";
execvp("ls",argv);
参数:
file:可执行程序,可以带有路径,也可以不带有路径
argv:传递给可执行的程序的命令行参数,以指针数组的方式进行传递
1. 第一个参数,需要可执行程序本身
2. 多个参数都放到数组当中
3. 末尾以NULL结尾
返回值:这些函数如果调用成功则加载新的程序从启动代码开始执行
如果调用出错则返回-1
(6)exceve:
#include <unistd.h>
int execve(const char *path,char *const argv[],cahr *const envp[]);
示例:
extern cahr** environ;
char* argv[10] = {0};
argv[0] = "ls";
argv[1] = "-l";
argv[2] = "NULL";
execvp("/usr/bin/ls",argv,environ);
参数:
path:带路径的可执行程序(需要路径)
argv:传递给可执行的程序的命令行参数,以指针数组的方式进行传递
1. 第一个参数,需要可执行程序本身
2. 多个参数都放到数组当中
3. 末尾以NULL结尾
返回值:这些函数如果调用成功则加载新的程序从启动代码开始执行
如果调用出错则返回-1
总结:
结论:
(1)函数名中带有“l”:
传递给可执行程序的参数是以可变参数列表的方式进行传递
1. 第一个参数,需要可执行程序本身
2. 如果需要传递多个参数,则用“,”进行间隔
3. 末尾以NULL结尾
(2)函数名中带有“P”:
可以使用环境变量PATH,无需写全路径。(函数会搜索可执行程序PATH,找到可执行程序,所以不用写路径)
(3)函数名中带有“e”:
程序员传递环境变量(程序员调用该函数时需要自己组织环境变量传递给函数)
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/w2583467558/article/details/130261256
版权声明:
作者:SE_Yang
链接:https://www.cnesa.cn/2116.html
来源:CNESA
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论