一、进程创建 在linux中系统给我们提供了一个调用接口,用于进程的创建:
pid_t fork(void)
返回值:可以通过返回值来区分父子进程,比如父进程的返回值是子进程的pid而子进程的返回值是0;
特点:通过复制子进程,创建一个子进程;代码共享,数据独有。
可以通过以下图示来进行理解
文章图片
数据独有指的是:
当子进程要修改数据时,再给他开辟新的物理空间,这是一种写时拷贝技术
【Linux|Linux之进程创建/进程终止/进程等待/程序替换】写时拷贝技术:子进程创建出来后,各个虚拟地址字物理内存中的指向与父进程完全相同,这是为了提高子进程的创建效率(否则要为子进程开辟物理内存,拷贝数据过来,更新页表,整个过程比较慢,并且有可能子进程根本就不使用这些数据)扩展:与fork函数相似的还有一个
若某一块物理内存中数据即将发生改变(某个进程要对这块内存进行写操作),则给子进程相应数据开辟物理内存,拥有自己的数据。
pid_t vfork(void)
也是用来创建子进程并阻塞父进程,特点是创建子进程会比较的快。
直到子进程exit退出或者程序替换之后,父进程才会运行 因为同时运行会造成调用栈混乱
vfork创建子进程效率比较高,因为vfrok创建子进程之后父子进程共用同一个虚拟地址空间
二、进程终止 – 进程的退出 (1)在main()函数中return退出 例如:
文章图片
文章图片
可以看到终端没有任何的信息打印,是因为进程已经退出。
也可以通过查看进程信息来查看:
#include
#include
int main()
{
sleep(20);
//这里让他先睡上个20秒
return 0;
printf("helloworld\n");
}
再在另一个终端查看该进程的信息:
文章图片
可以看到进程在运行过了一会就退出了。
注意:return 退出只能在main() 函数中退出进程,而函数中的return只是函数的退出,而不是进程的退出。(2)调用库函数 exit() 退出 这里可能会有疑惑,为什么有一个exit() 还有一个系统调用接口 _exit() 首先这里可以说明一下两者之间的关系:
库函数封装了系统调用接口 系统调用接口不太好用这里使用手册 man 查看一下两者之间的关系:
文章图片
这里详细介绍了 exit函数,这里左上角部分显示的是一个 exit(3)
这里是因为 如果我们直接 man exit 是不会出现这个结果的。
man 1 exit 查看exit命令的手册
man 2 exit 查看系统调用接口的手册
man 3 exit 查看库函数的手册
文章图片
可以看到
函数:void exit(int status);
头文件:
返回值:无返回值
参数 :status 将status作为返回值返回给父进程
这里的返回值的作用会在后面的进程等待中的 wait 函数中介绍exit() 例如:
#include
#include
void Test1()
{
printf("hello\n");
exit();
}
int main()
{
Test1();
printf("helloworld\n");
}
文章图片
exit与return 又有什么区别
return 只有在main函数中的时候才会退出进程,然而exit是在任意位置调用都会退出调用进程(3)调用系统调用接口 _exit() 头文件:
而return在其他函数 中是退出函数 ,exit在任意位置都会退出进程 而不会再进行其他的进程的调度。
参数与返回值等与exit相同;
那么前面已经有了两种退出进程的方式了,而 _exit() 又与其他两种有何区别呢? 这里要说到 printf 输出时换不换行的问题。
printf 不换行会怎么样? 会不刷新缓冲区
看一个例子:
#include
int main()
{
printf("helloworld");
return 0;
}
文章图片
再看 使用 _exit() 退出时:
#include
#include
int main()
{
printf("helloworld");
_exit(0);
}
文章图片
这里可以看到 当使用 _exit()退出时 并没有输出 helloworld 这是因为:
exit 以及 return 都会刷新缓冲区 但是_exit会直接释放资源,不刷新缓冲区注:
以上几种进程退出方式,都属于进程的正常退出 — 但是会根据返回值向父进程表示为什么退出了
但是进程退出也有可能异常退出 --通常指程序崩溃 – 程序没有运行完毕突然因为某种错误退出,具体的会在下面的进程等待解释。
三、进程等待 父进程等待子进程的退出,获取子进程的返回值 ,释放子进程资源,避免产生僵尸进程
当父进程不等待子进程的退出,就会产生僵尸进程。注意只有当进程是正常退出才可以获取返回值,如果进程是异常退出的,那么这个返回值是没有意义的。
例如:
#include
#include
int main()
{
pid_t pid=fork();
if(pid==0)
{
printf("我先睡5秒钟,然后退出\n");
sleep(5);
exit(0);
}
while(1)
{
printf("------父进程正在打游戏---------\n");
sleep(1);
}return 0;
}
文章图片
使用 ps -aux | grep 进程名 查看该进程的详细信息
文章图片
可以看到当 父进程不等待子进程的退出时,就会成为僵尸进程,至于僵尸进程的危害,就不再这里阐述了,因此引入了进程等待的概念,避免僵尸进程的产生。
进程等待的两个方法:
前提了解一下阻塞与非阻塞的概念:
阻塞:为了完成某个功能发起一个调用,若当前不具备完成功能的条件,则调用不返回一直等待(1)pid_t wait(int *status); --阻塞等待任意一个子进程的退出,获取返回值 返回值: 成功返回被等待进程pid,失败返回-1。
非阻塞:为了完成某个功能发起一个调用,若当前不具备完成功能的条件,则立即报错返回 非阻塞操作通常需要进行循环操作 是一个相对于阻塞较好的操作,对cpu利用更加的充分,因为不在一直等待了,但是必须循环进行操作
参数: 传入一个int 空间的首地址 ,获取子进程的返回值,若父进程不关心则可以设置为NULL
例如:
#include
#include
int main()
{
pid_t pid=fork();
if(pid==0)
{
sleep(5);
printf("休息一会子进程退出\n");
exit(0);
}
printf("等待子进程的退出\n");
wait(NULL);
int i=0;
for(i=0;
i<5;
i++)
{
sleep(1);
printf("父进程正在运行\n");
}
return 0;
}
文章图片
文章图片
如上图所示,当父进程等待子进程退出之后,就不会产生僵尸进程。
(2)pid_t waitpid(pid_t pid, int *status, int options); 这两个函数意义一样不过只是参数不一样
参数:
1.pid: Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。
2.status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
3.options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该 子进程的ID
返回值 :若没有子进程退出,则返回0;若出错则返回-1;否则返回退出的子进程的pid
其实也就是说:wait(status)=waitpid(-1,status,0);
例如:
#include
#include
#include
int main()
{
pid_t pid=fork();
if(pid ==0 )
{
//表示为子进程
sleep(5);
exit(0);
//等待5秒钟后再退出 ,父进程什么都不做就会成为僵尸进程
}
int ret;
//这里还可以定义 status 进行一个传入一个int 空间的首地址 ,获取子进程的返回值
//例如 int sattus 但是不要定义一个 int *status 然后在传递参数时写成:
// waitpid(pid,status,WHOHANG) 这样是违法的 :正确的应该是如下的格式:
//int status;
//waitpid(pid,&status,WHOHANG)
while(ret=waitpid(pid,NULL,WNOHANG)==0)
{
printf("现在子进程还没有退出 --- 等一下在来看\n");
sleep(1);
}
while(1)
{
printf("------父进程正在干别的事情-----\n");
sleep(1);
} return 0;
}
wait 和waitpid并不只是处理刚退出的进程 ,而是对已经退出的进程进行处理(不管什么时候退出的 )并且每次只能处理一个退出的进程。
返回值的获取:子进程的退出返回值通过wait /waitpid函数的status返回给用户
注意:
int status ----status 这个变量有四个字节的空间 ,向函数传入地址异常退出: 如何判断子进程是否正常退出?
int *status — 定义了一个指针变量 ,有8个字节空间 ,存放的是另一块空间的地址
status 四个字节 但是只使用了两个字节的空间用来存放子进程的退出返回值 ,但是实际上只使用了一个字节的存储空间,也就意味值返回值最好不要大于255;
这一个字节的返回值,在status四个字节中存放的时候是存放在低16位中的高8位中
相当于真实的数据右移 8 位 & 0xff
例如:
文章图片
文章图片
我们刚了解了,在status中只有一个字节用来保存正常退出的子进程的退出码,在8-16这个比特位之间存储,而在低八位中用来保存一些特殊的数据
文章图片
core dump 核心转储–程序异常退出时,则保存程序的运行信息,便于事后调试,默认是关闭的。
信号:通知进程发生了某个事件,中断进程当前的操作,去处理这个事件。
而操作系统中的信号的体现实际是一个数字,某个数字表示某个异常事件,程序因为异常退出的事后,则会将这个异常退出事件的信号值放到低7位中
当异常退出时:获取低七位。异常退出型号值,通过是否为0判断是否进程正常退出
00000000 00000000 00000000 01111111 & status 得到低七位 也就是 0x7f & status
这是一个手动的方式判断其是否正常退出,但是也可以使用函数中的参数:
WIFEXITED(status) // 正常终止子进程返回的状态,则为真
例如:
if((WIFEXITED(status))
{
printf("正常退出的status为:\n",WEXITSTATUS(status));
//查看进程的退出码
}
程序替换: 在创建子进程的目的中我们知道,大致有两点
- 子进程可以帮父进程分担压力
- 子进程可以干其他的事情,不对父进程产生影响。
这是两个基本的特点,我们也知道通过进程的返回值来进程父子进程的一个区分例如:
pid_t pid=fork();
if(pid==0)
{
//这是子进程
//干一些其他的事情
}
但是这种方式存在缺陷:
程序的耦合度非常的强,因为所有功能代码都是在当前程序中编写的 — 如果想要改变子进程的功能处理流程,需要修改 整个代码,然后再重新编译,导致整个程序变得非常大因此引入了程序替换的概念:
程序替换:替换一个进程正在调度的程序信息(进程是pcb, 负责调度管理一个程序的运行,运行的数据和代码都在内存中,将另一个程序加载到内存中,然后让原有的pcb不再调度原程序,而去调度新的程序)
程序替换图示:
本质来说就是替换一个pcb在内存中对应的代码和数据(加载另一个程序到内存中,然后更新页表信息,初始化虚拟地址空间)这个进程pcb将重新开始调度的程序运行
文章图片
如何完成程序替换? exec函数族 --实现进程的替换 使用 man 3 exec 查看手册:
文章图片
可以看到这里有很多的函数:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg , ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execev(const char *path,char *const argv[],char *const envp[]);
演示例如:
**int execl(const char path, const char arg, …);
参数介绍:
const char *path 带有路径的新程序的名称,就是使用这个程序替换进程正在调度运行的程序const char *arg 程序参数注意:程序替换之后,当前进程运行替换后的程序就会退出,并不会再回去运行原先的程序
而后面的省略号表示这是一个不定参的参数,可以将新程序的运行参数,通过不定参的形式传递进入新的程序,以NULL作为结尾
这个程序主要演示将程序替换为 ls 程序,我们可以通过以下指令找到他的位置下面绿色的就是ls程序,我们通过程序替换ls来看一下程序替换效果:
文章图片
//演示程序替换
#include
#include
#include
int main(int argc, char *argv[])
{printf("helloworld\n");
execl("/usr/bin/ls","ls","-l",NULL);
return 0;
}
文章图片
文章图片
通过这个演示 我们可以看到程序被替换为了 ls -l 这里是调用了 ls 指令 参数是 -l
execl("/usr/bin/ls",“ls”,"-l",NULL);而这里的参数,可以通过一个小程序来来演示一下:
#include
#include
#include
int main(int argc, char *argv[])
{
//argc 这个main函数的第一个参数 -- 用于表示当前程序有多少个运行参数
//argv这个字符串指针数组用于存储当前程序的运行参数
int i=0;
for(i=0;
i
文章图片
我们可以看到 这里 prcretest 程序的第一个参数 也就是 第0个参数是 ./prcretest 我们再以空格进行参数输入时 可以看到 程序的 若干个参数,例如 ls 指令 ls -a / ls -l 就是通过程序不同参数调用不同的功能。
以上是第一个函数:int execl(const char *path, const char *arg, …); 的使用方法
函数详细介绍: 1.
1.int execl(const char *path, const char *arg, ...);
4.int execv(const char *path, char *const argv[])
而这两个 第一个 和第二个的区别就是 第一个是不定参,而第二个是一个字符串数组,将通过以下代码进行演示:
通过 myprctest 程序 调用prcretest 程序 打印出其参数
//prcretest 程序
#include
#include
#include
int main(int argc, char *argv[])
{
//argc 这个main函数的第一个参数 -- 用于表示当前程序有多少个运行参数
//argv这个字符串指针数组用于存储当前程序的运行参数
int i=0;
for(i=0;
i
//myprctest 程序
#include
#include
#include
int main(int argc, char *argv[])
{
//传递一个字符串数组 表示参数
char *new_argv[]={"prcretest","-l","-a","-p",NULL};
execv("./prcretest",new_argv);
return 0;
}
文章图片
注意 :无论是不定参还是指定的字符串数组的最后一个元素必须是 NULL
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,需要自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execev | 数组 | 不是 | 不是,需要自己组装环境变量 |
推荐阅读
- 【LINUX】进程创建-进程等待-进程替换-进程终止
- 学习工具|关于嵌入式容器技术的调研
- Centos|Linux机器密码正确,但无法登陆
- Linux下安装JDK
- ubuntu|opencv图像基本操作
- 网络|万字长文详解Istio
- 服务器|【SOC】经典输出hello world
- 云存储|ArcGIS与MINIO系列文章(1)-MINIO搭建
- Linux|详解linux权限操作(概念、chown、chmod、chgrp、umask、粘滞位)