Linux【进程控制】总结

学习目标

首先进程控制分为四大部分:进程创建、进程退出、进程等待、进程替换;

        第一步:学习如何来创建一个进程,一般我们会使用fork函数来创建子进程,创建子进程之后,就要去探索子进程与父进程的相关联系;

        第二步:学习如何让一个进程退出,需要认识并熟练使用exit、_exit、return函数来完成进程退出,了解进程退出码,进程退出码的组成,如何获取进程退出码;

        第三步:学习如何等待一个进程和进程等待的相关函数,通过函数来获取进程退出码;

        第四步:学习如何进行进程替换,了解并熟练使用进程替换的接口exec*函数

一、进程创建

进程创建必备函数:fork函数的介绍:

在已知进程中创建一个新的进程,已知进程为父进程,新进程为子进程

fork函数声明:

pid_t fork(void);

头文件:

#include <unistd.h>

返回值:

返回值为0,表示为子进程
返回的为子进程的pid,但是此时是父进程
返回值为-1,创建子进程失败

说明:

  • fork之后,子进程与父进程共享代码,如果某一进程要修改某一段代码,会发生写时拷贝
  • 通常使用  if-else  语句让父子进程执行不同的代码
  • fork通常会让子进程去执行别的程序,也就是程序替换

提问:

1. 为什么fork之后,对代码修改会发生写时拷贝?

        首先fork之后,父子进程共享父进程的代码,当这段 被共享的数据 需要修改的时候,为了保证进程的独立性,操作系统会介入其中,发生写时拷贝,然后去修改拷贝后的数据,保证父子进程互不影响。(谁被修改,就把谁写时拷贝,其他的不变)

2. 为什么同一个变量可以有不同的值?

        (1) 首先Linux支持同一个变量名表示不同的内存,因为变量最终都是存放在内存中的,而变量名是给人看的,计算机只看二进制,所有即使同一个变量名,但是内存空间的地址不同,就可以区分;

        (2) 这里也发生了写时拷贝,上面的图是一个简单版本,下面有一个复杂版本;

        (3) 页表、虚拟空间地址都相同,但是映射过去的物理地址空间不同;

二、进程退出

       进程退出,我们就会想到之前讲过的僵尸进程,先复习一下僵尸进程,当子进程退出时,子进程会把大部分资源(代码段、数据段、页表、地址空间等)还给操作系统,唯独保留下该进程的PCB,保留PCB的原因是,PCB中记录了一个进程退出时的退出码,退出码会反馈进程退出的原因(执行完结果正确、执行完结果错、没执行完,异常退出)。而退出码是会被父进程读取的,这也是必须要读取的。若没有父进程读取子进程的退出码,那该子进程就会变成僵尸进程。

        而读取进程退出码的原因是,我们必须知道子进程是因为什么退出!

说到这里,我们会清楚两点:

        1. 进程退出时,必须提供进程的退出码,以供父进程读取

        2. 父进程读取子进程的退出码的方式为:进程等待

所以,我们学习进程退出,要学会下面三个方面:

        1. 进程退出的场景

        2. 进程退出的方法

        2. 进程退出码的解读

1. 进程退出的场景

        这里我们应该深有体会,有时候进程在执行一部分代码的时候就直接终止,比如野指针的使用、除0操作;又或者执行结束,但是结果不正确;最常见的就是执行结束,也得到了想要的结果。所以我们将进程退出分为以下三个场景:

1. 程序执行结束,结果正确
2. 程序执行结束,结果不正确
3. 程序没执行结束,异常终止

2. 进程退出的方法

        我们在学习C/C++时,通常会在main函数的末尾写上一个return 0,这表示main函数返回值为0,main函数返回成功。其实这就是我们程序的退出方法之一。

        下面列举了进程退出的方法汇总和逐方法讲解:

return num

必须在主函数中使用

在其他函数体使用,仅仅代表函数的结束

exit(num)可以在任意地方调用,代表直接退出进程
_exit(num)同上

ctrl + c 

kill -num pid

在命令行中使用信号杀死进程,属于异常终止

注意:

  • exit 和 _exit可以在任意地方调用,都是用来退出进程的;但是return必须在主函数中使用,在其他函数体使用,仅仅代表函数的结束
  • exit支持刷新缓冲区,_exit不支持
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//    printf("exit()会刷新缓冲区");printf("_exit()不会刷新缓冲区");_exit(0);
//    exit(0);
}
这里的结果显而易见,_exit不会刷新缓冲区,也就是根本不会打印任何东西;反之exit则会打印出来;但是在每个printf语句里末尾都加一个 '\n' ,他们都会打印出来
        这是因为 ' \n '是有刷新缓冲区的作用
  •  exit在底层封装了_exit,比_exit多了个刷新缓冲区

思考:这个缓冲区是操作系统内部的吗?

        首先该缓冲区一定不是操作系统内部的,因为_exit 是一个系统调用接口,_exit被调用的时候,释放了资源,但是并没有刷新缓冲区,而exit函数封装之后才会刷新缓冲区,所以这个缓冲区一定不是操作系统的缓冲区,而是用户层的。

3. 进程退出码

       进程退出码:记录进程退出时的情况,正常或异常,正确或错误。

进程退出码的组成:

                                        进程退出码 = 退出码 + 核心转储标志 + 异常码

注意:

  • 进程退出码以位图的方式呈现,我们只关心低16位
  • 异常码为0到6比特位,表示进程出异常收到的异常编号
  • 退出码为8到15比特位,表示进程正常退出的退出码
  • 异常码 = 0,表示进程执行过程中无异常,看退出码
  • 异常码 ≠ 0,表示进程执行出异常,退出码无意义

获取进程退出码的方法:

指令:echo $?
进程等待函数wait、waitpid的输出型参数status

三、进程等待

1. 为什么要进行进程等待?

        1. 子进程退出时,父进程如果不进行进程等待,子进程会变成僵尸进程,造成内存泄漏

        2. 进程一旦变成僵尸进程,无法使用kill命令来杀死

        3. 子进程退出时,会将其退出情况存放到进程退出码中,这个退出码就放在PCB里,所以父进程进行进程等待,也可以获取子进程的退出情况

2. 进程等待必要性

回收子进程资源(必做),获取子进程退出码(选做)

3. 进程等待的方法

(1)wait函数

函数声明:

pid_t wait(int* status)

头文件:

#include<sys/types.h>
#include<sys/wait.h>

返回值:

成功:返回被等待进程的 pid ;

失败:返回  -1;

status:

输出型参数,获取子进程退出码,不获取则可以设置成为NULL

示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{printf("I am a child , pid = %d\n", getpid());exit(1);}int status = 0;if(wait(&status) == id) //等待成功,返回被等待进程pidprintf("status = %d\n", status);else //失败,返回-1printf("wait error\n");return 0;
}

(2)waitpid函数

函数声明:

pid_ t waitpid(pid_t pid, int* status, int options)

头文件:

#include <sys/types.h>
#include <sys/wait.h>

返回值:

正常返回:waitpid返回等待到的子进程pid;
设置选项WNOHANG:而调用中waitpid发现没有已退出的子进程可收集,则返回0;
调用中出错:则返回-1

status:

输出型参数,依旧是获取子进程的退出码,不获取可设置为NULL。

查看进程是否是正常退出:

WIFEXITED(status)

若为正常终止子进程返回的状态,则为真

异常退出为假,可查看进程异常退出信号

查看进程的退出码:

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。

options:

当options传入 WNOHANG 时 (一个宏,值为1)

代表当pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。

若正常结束,则返回该子进程pid

options传入的整型值,不是1,就会一直等待子进程的退出,父进程会阻塞等待;

为WNOHANG,则不会等待,父进程会执行后面的代码,是非阻塞等待

示例1:使用WIFEXITED和WEXITSTATUS获取进程退出情况

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{printf("I am a child, pid = %d\n", getpid());exit(11); //子进程退出}int status = 0;waitpid(id, &status, 0);if(WIFEXITED(status))  //当进程正常退出,返回真printf("exit code = %d\n", WEXITSTATUS(status)); //打印进程的退出码else   //进程异常退出printf("exit signal = %d\n", WIFEXITED(status)); //打印进程的异常退出信号return 0;
}

示例2: 阻塞等待

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{int count = 5;while(count--){printf("I am a child, pid = %d\n", getpid());sleep(1);}exit(11); //子进程退出}int status = 0;while(1){int rid = waitpid(id, &status, 0);if(rid == 0)printf("child is running\n");else{printf("child exit success\n");return 0;}sleep(1);}
}

示例3:非阻塞等待

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{int count = 5;while(count--){printf("I am a child, pid = %d\n", getpid());sleep(1);}exit(11); //子进程退出}int status = 0;while(1){int rid = waitpid(id, &status, WNOHANG);if(rid == 0)printf("child is running\n");else{printf("child exit success\n");return 0;}sleep(1);}
}

四、进程替换

        进程替换就是将现在正在执行的进程换去执行另一个程序的代码

1. 进程替换的函数

(1)库函数

#include <unistd.h>
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 execvpe(const char* file, char* const argv[]s, char* const envp[])

(2)系统调用函数

#include <unistd.h>
int execve(const char *path, char *const argv[], char *const envp[]);


名词解释:

path        传程序的绝对路径
file传程序的文件名
arg

可变参数列表,依次传使用规则传入字符串

但结尾必须为NULL

argv

将上面的可变参数放在指针数组里,传数组名

数组最后一个元素必须为NULL

envp

将自己写的环境变量放在指针数组里,传数组名

数组最后一个元素必须为NULL

函数返回值:

这些函数如果调用成功,则替换后的程序从启动代码开始执行,不再返回;
如果调用出错则返回  -1;
所以exec函数只有出错的返回值而没有成功的返回值

快速记忆exec*函数的命名:

l -> list : 表示参数采用列表
v -> vector : 参数用数组
p -> path : 有p自动搜索环境变量PATH,只需要传程序名,自动搜索路径
e -> env : 表示需要自己传环境变量

2. 进程替换后的注意事项

  • 进程替换不会产生新的进程,进程pid不变
  • 进程成功替换后,不会执行exec*函数后面的代码,因为被替换掉了
  • exec*函数只有失败返回值,没有成功返回值
  • 不论什么语言,只要是一个可执行程序,我们就可以替换
  • 进程替换,只是把被替换的程序的代码段和数据段覆盖到替换之前到程序上,其他内核数据结构(PCB、页表、虚拟地址空间)不变,这也就解释了为什么没有产生新进程

3. exec函数示例

#include <unistd.h>
int main()
{char *const argv[] = {"ls", "-l", NULL};char *const envp[] = {"PATH=/usr/bin", NULL};//    execl("/usr/bin/ls", "ls", "-l", NULL);//    execlp("ls", "ls", "-l", NULL);//    execle("ls", "ls", "-l", NULL, envp);//    execv("/usr/bin/ls", argv);//    execvp("ls", argv);//    execve("/usr/bin/ls", argv, envp);return 0;
}

4. 进程替换的应用场景

通常会创建子进程,让子进程进行进程替换,利用该特性,我们可以实现一个自己的shell

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/599054.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Visual Studio 2022-C语言如何防止头文件多次引入

头文件的包含 本地⽂件包含 #include "filename" 查找策略&#xff1a;先在源⽂件所在⽬录下查找&#xff0c;如果该头⽂件未找到&#xff0c;编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件。 如果找不到就提⽰编译错误。 Linux环境的标准头⽂件的路径&…

书生·浦语训练营二期第三次笔记-茴香豆:搭建你的 RAG 智能助理

RAG学习文档1&#xff1a; https://paragshah.medium.com/unlock-the-power-of-your-knowledge-base-with-openai-gpt-apis-db9a1138cac4 RAG学习文档2: https://blog.demir.io/hands-on-with-rag-step-by-step-guide-to-integrating-retrieval-augmented-generation-in-llms-a…

yolov9直接调用zed相机实现三维测距(python)

yolov9直接调用zed相机实现三维测距&#xff08;python&#xff09; 1. 相关配置2. 相关代码2.1 相机设置2.2 测距模块2.2 实验结果 相关链接 此项目直接调用zed相机实现三维测距&#xff0c;无需标定&#xff0c;相关内容如下&#xff1a; 1. yolov4直接调用zed相机实现三维测…

90天玩转Python—05—基础知识篇:Python基础知识扫盲,使用方法与注意事项

90天玩转Python系列文章目录 90天玩转Python—01—基础知识篇:C站最全Python标准库总结 90天玩转Python--02--基础知识篇:初识Python与PyCharm 90天玩转Python—03—基础知识篇:Python和PyCharm(语言特点、学习方法、工具安装) 90天玩转Python—04—基础知识篇:Pytho…

C语言开发实战:使用EasyX在Visual Studio 2022中创建井字棋游戏

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

c++的学习之路:16、string(3)

上章有一些东西当时没学到&#xff0c;这里学到了将在补充&#xff0c;文章末附上代码&#xff0c;思维导图。 目录 一、赋值重载 二、带模板的创建 三、析构函数 四、代码 五、思维导图 一、赋值重载 这里的赋值重载就是直接利用交换函数进行把传参生成的临时数据和需要…

2024/4/7 IOday6

1&#xff1a;有一个隧道&#xff0c;全长5公里&#xff0c;有2列火车&#xff0c;全长200米&#xff0c; 火车A时速 100公里每小时 火车B时速 50公里每小时 现在要求模拟火车反复通过隧道的场景(不可能2列火车都在隧道内运行) #include <stdio.h> #include <string.…

特别详细的Spring Cloud 系列教程1:服务注册中心Eureka的启动

Eureka已经被Spring Cloud继承在其子项目spring-cloud-netflix中&#xff0c;搭建Eureka Server的方式还是非常简单的。只需要通过一个独立的maven工程即可搭建Eureka Server。 我们引入spring cloud的依赖和eureka的依赖。 <dependencyManagement><!-- spring clo…

工单派单-saas工单处理软件效益分析,智能解决企业管理痛点亿发

企业对引入工单管理系统是有迫切需求的&#xff0c;工单管理系统可以有效地管理任务和工作流程&#xff0c;提高工作效率和客户满意度。 在没有工单管理系统之前&#xff0c;许多企业可能面临着诸如任务分配不清晰、信息不透明、工作流程混乱等管理挑战。举例来说&#xff0c;…

蓝桥杯 历届真题 杨辉三角形【第十二届】【省赛】【C组】

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 思路&#xff1a; 由于我第一写没考虑到大数据的原因&#xff0c;直接判断导致只得了40分&#xff0c;下面是我的代码&#xff1a; #…

《Ubuntu20.04环境下的ROS进阶学习6》

一、手持激光雷达建图 在上次的学习中我们已经使用hector_Mapping在仿真环境下建图了&#xff0c;那么本节我们将拿出真实雷达做一次室内的建图。我们使用的是思岚的S2L激光雷达。 二、下载思岚的应用手册 首先我们根据自己的激光雷达类型去到思岚官网下载相应的ROS包&#xff…

哪些医疗器械申请FDA,需要准备网络安全文件?需要提交的文件都是什么样的?

一、什么类型的医疗器械需要递交网络安全文件&#xff1f; FD&C法案第524B条(c) 条将“网络设备”定义为&#xff1a; 1&#xff09;经申请人验证、安装或授权的软件或设备&#xff1b; 2&#xff09;具备连接互联网的能力&#xff1b; 3&#xff09;包含经申请人验证、…