【Linux】进程控制

个人主页~


进程控制

一、进程创建

我们在前面的文章中多次使用过fork函数,我们在这里再来简单概括一下进程的创建

fork可以在已有的进程中创建出一个新进程,老进程为父进程,新进程为子进程

c 复制代码
#include <unistd.h>
pid_t fork(void);
//在父进程中返回子进程id,在子进程中返回0,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核分配新的内存块和内核数据结构给子进程,然后将父进程部分数据结构内容拷贝至子进程,再添加子进程到系统进程列表当中,最后fork返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程,它们各自开始继续往下运行,执行先后顺序由调度器决定

fork后子进程遵循写实拷贝,在修改数据时才开辟新空间

fork也可能因为系统中已经有了太多进程和实际用户的进程数超过了限制调用失败

二、进程终止

1、进程退出情况

进程退出一共有三种场景,第一种是代码运行完毕,结果正确,第二种是代码运行完毕,结果不正确,第三种是代码异常终止

第一二种属于是正常终止,第三种属于是异常终止,正常终止我们常见的有main函数返回,调用exit和_exit,异常退出有ctrl+c,信号终止

正常终止可以通过echo $?查看最近一个关闭的进程的退出码

2、exit函数和_exit函数

(一)二者的区别和联系

c 复制代码
#include <unistd.h>
void exit(int status);
c 复制代码
#include <unistd.h>
void _exit(int status);

exit函数最终也会调用_exit函数,但在调用之前还做了一些工作,这里的atexit注册退出处理函数就是清理函数,然后刷新缓冲区

这里我们通过程序来验证一下,一个简单的打印程序,然后我们可以很清晰的看到exit和_exit的区别

(二)参数status

status虽然是int,但是只有低八位能够被父进程所用,它有0~133一共134个参数,其中个数字都对应一个退出状态,除了0是正确成功退出以外,其他所有的退出数字都是退出时的报错

其中strerror是用来将错误数字打印成我们人可以查看的字符串,而我们有一个全局变量erron,头文件为<erron.h>,它负责存储进程错误数字,我们可以通过strerror(erron);的方式来读取错误信息

3、exit退出和return退出

exit的作用是直接终止程序,不管是在main函数还是其他函数中,只要遇到exit函数就直接终止,并且main函数会返回exit的参数

return退出我们都很了解了,它与exit不同,在main函数中return代表程序终止,但是在其他函数中return表示返回值

如果main函数退出时因为exit,那么exit的参数就是main函数的返回值

4、退出异常

程序退出异常本质上是进程收到了对应的信号,这部分内容很多给到信号部分详细解释

三、进程等待

1、必要性

子进程退出,如果父进程不做处理,那么子进程就会成为僵尸进程,进而造成内存泄漏的问题,并且如果进程一旦成为僵尸进程,就无法被杀死,因为它不再运行,只是剩下了一堆的垃圾资源占用内存,父进程可以通过进程等待的方式回收子进程资源,获取子进程退出信息

2、方法

(一)wait方法

c 复制代码
#include<sys/types.h>
#include<sys/wait.h>
 
pid_t wait(int*status);
 
//成功返回被等待进程pid,失败返回-1
//返回的进程pid是随机的,如果有多个被等待的pid,它会随机选择一个
//status是输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

我们现在简单使用wait是不需要关心子进程的退出状态的,所以我们直接设置status为NULL

这里我们写了一个程序,父子进程运行,子进程执行5秒先退出,我们发现它的状态就是僵尸状态,然后父进程执行完10秒的while后,wait检测到子进程,然后子进程就被回收了

通过命令while :; do ps ajx | head -1 && ps ajx | grep myproc; sleep 1;done对myproc程序进行实时监控

status参数

这是一个输出型参数,由操作系统填充,就是说我们可以拿一个变量在参数这一栏里拿到一个数值,这里的数值是子进程的退出状态

status指向的是一个位图,而不是一个简单的整形,其中int类型占4个字节,共32个比特位,它的低16位用来表征终止状态

低16位中的高8位用来存储子进程正常退出调用exit时传入的退出状态码,低7位用来存储导致子进程终止的信号编号,中间那一位core dump标志用于指示子进程在终止时是否产生了核心转储文件(当程序在运行过程中出现严重错误而崩溃时,操作系统会将该程序当时的内存状态、寄存器状态、栈信息等核心数据保存到一个文件中,这个文件就是核心转储文件,也称为 Core 文件)

我们要读取status表达的退出状态或者终止信号的话,我们可以通过位操作和宏

量\方法 位操作
退出状态 (*status>>8)&0XFF WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
终止信号 *status&0X7F WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)

(二)waitpid方法

c 复制代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int*status, int options);
//返回值和status都与wait是一样的,可以说wait是waitpid封装的一个函数,当pid==-1,option==0,那么waitpid==wait

//第一个参数pid:
//pid > 0:等待进程 ID 等于 pid 的子进程
//pid == -1:等待任意子进程
//pid == 0:等待与调用进程(父进程)属于同一进程组的所有子进程

//第三个参数options:
//options == 0,意味着这个选项没有任何用处,未开启选项
//options == WNOHANG,若pid指定的子进程没有结束,则waitpid直接返回0,不等待,若正常结束,则返回子进程pid 

因为有可能在父进程等待的过程中,子进程还没有跑完,那么父进程就进入了阻塞等待,需要先等子进程跑完自己才能跑,这样大大降低了效率,我们可以根据最后一个选项的作用,做到进程的非阻塞等待

(1)进程的阻塞等待方式

在子进程跑的时候父进程在阻塞等待,不可运行,子进程跑完父进程才开始跑

(2)进程的非阻塞等待方式


(3)子进程执行结束后资源释放与退出信息留存机制

当子进程完成其执行流程后,用户空间的代码段与数据段所占用的资源会被系统回收,这些资源包含了程序指令以及诸如栈、堆、全局变量等数据,然而,子进程对应的 PCB,并不会立即被销毁,它存储着子进程的重要状态信息,特别是退出状态相关数据,如正常退出时的退出码或者因信号导致异常终止时的退出信号等,这些信息会保留,等待父进程通过调用 wait、waitpid 等系统调用进行读取,待父进程成功获取这些退出信息后,系统才会对该 PCB 进行销毁操作,释放其占用的内核资源

四、进程程序替换

1、替换原理

每个进程在 Linux 系统中都有一个进程地址空间,它包含了进程运行所需的各种元素,如代码段、数据段、堆、栈等,代码段包含了进程要执行的机器指令,数据段存储了程序中的全局变量等数据,堆用于动态内存分配,栈用于存储函数调用的上下文等信息,当进行程序替换时,实际上就是用新程序的相应部分去替换原进程映像中的对应部分,这一部分就是我们提到的代码段和数据段

我们通过exec家族函数来进行进程替换,首先我们先不要讨论execl函数怎么使用,我们只需要知道这个函数可以进行进程替换,而参数提供的就是ls -a -l,打印目录,而我们发现,在进程替换之后,原来进程的final process没有打印出来,说明这个替换不是该行代码暂时的替换,而是在整个进程结束之前长时间的替换

2、exec函数家族

c 复制代码
//库函数,它们5个最终都是调用的execve
#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[],char *const envp[]);
//系统调用函数
int execve(const char *path, char *const argv[], char *const envp[]);

exec进程替换家族一共有七个,其中它们的命名是有规律的,除了它们的"姓氏"exec以外,它们的名字分别有l、p、e、v

名字 理解
l list,表示中间参数采用列表
p path,自动搜索环境变量PATH
e env,自己维护环境变量
v vector,表示中间参数用数组

我们先来说一下最简单的execl函数,它的第一个参数就是路径,第一个参数到最后一个参数之间的参数叫做可变参数列表,也就是说可以有多个,它和我们在命令行使用的命令是一样的,只不过我们命令行的分隔是空格,而这里的分隔是逗号和引号,最后一个参数必是NULL,表示该表的末尾边界

execlp函数,除了第一个参数和execl不一样以外,其他的格式都一样,p说明它会先从环境变量PATH中找,所以我们要执行命令可以不用带路径,直接写命令名字就行了,和上面那段代码等效的代码就是

c 复制代码
execlp("ls","ls","-l","-a",NULL);

execle函数,除了比execl多了一个最后一个参数以外,其他都不变,只是NULL值要在倒数第二个参数,同上方的代码应该是如下所示

c 复制代码
extern char** environ;
execlp("/usr/bin/ls","ls","-l","-a",NULL,envrion);

这样表示传递了原来进程的环境变量,当然这个表可以是我们自己写的表

c 复制代码
char* const my_environ[]
{	
	"SUPER=1",
	"LITTLE=2",
	"MONSTER=3",
	NULL	
};
execle("/usr/bin/ls","ls","-l","-a",NULL,my_environ);

虽然效果在我们看起来是相同的,但是我们自定义的环境变量已经导入到我们替换的进程中了

最后一个说一下execv,这样大家都清楚这些函数的命名方法了,比起execl,它们之间的差别就是中间参数的组织方法,execv是用一个vector组织起来

c 复制代码
char* const myargv[] = 
{
	"ls",
	"-l",
	"-a",
	NULL
};
execv("/usr/bin/ls",myargv);

3、调用其他进程

命令行的本质就是程序,我们能通过进程替换调用命令行,我们就可以通过进程替换调用其他程序,例如cpp,Python,Java等等,因为任何语言,不管它的逻辑如何,跑起来都是一个进程,只要是进程就可以被exec家族函数调用

这里我们写一个c程序调用c++程序的例子,因为我只会c和c++,命令行参数可以传递过去,并且我们前面所说的我们自己写的环境变量表也可以被使用并且作为被替换程序的环境变量


今日分享就到这里了~

相关推荐
鸡鸭扣34 分钟前
Docker:3、在VSCode上安装并运行python程序或JavaScript程序
运维·vscode·python·docker·容器·js
A ?Charis1 小时前
k8s-对接NFS存储
linux·服务器·kubernetes
饮长安千年月2 小时前
Linksys WRT54G路由器溢出漏洞分析–运行环境修复
网络·物联网·学习·安全·机器学习
红花与香菇2____2 小时前
【学习笔记】Cadence电子设计全流程(二)原理图库的创建与设计(上)
笔记·嵌入式硬件·学习·pcb设计·cadence·pcb工艺
人工干智能4 小时前
科普:“Docker Desktop”和“Docker”以及“WSL”
运维·docker·容器
落笔画忧愁e4 小时前
FastGPT及大模型API(Docker)私有化部署指南
运维·docker·容器
一天八小时4 小时前
Docker学习进阶
学习·docker·容器
前端没钱4 小时前
前端需要学习 Docker 吗?
前端·学习·docker
郭顺发_4 小时前
NPM环境搭建指南
经验分享
前端郭德纲4 小时前
前端自动化部署的极简方案
运维·前端·自动化