进程控制

一、进程创建

1、fork函数写时拷贝

调用fork函数创建子进程时,并不会直接完整拷贝父进程的内存数据,而是采用写时拷贝机制优化内存占用。子进程会复制父进程的页表,创建完成的瞬间,父子进程的页表项均指向同一个物理内存页面,同时该物理页的引用计数会加1。

在后续运行过程中,若父子进程仅对该内存数据执行读操作 ,系统会保持当前共享状态,无需额外内存开销;若任意一个进程执行写操作,系统才会触发真正的内存拷贝:以内存页为最小分配粒度(仅拷贝被修改数据所在的内存页,而非单个变量或字节),为修改方分配全新的物理内存页,拷贝原页面数据后更新对应进程的页表映射关系,最后将原共享物理页的引用计数减1。该机制极大提升了进程创建效率,避免了无意义的内存资源浪费。

二、进程终止

进程终止的核心本质是系统回收进程占用的所有资源,主要包括进程运行过程中申请的各类内核数据结构、程序代码、运行数据、文件资源等,彻底释放系统内存与硬件资源,避免资源闲置占用。

2.1 进程退出场景

进程退出分为三类核心场景,覆盖所有进程终止情况:

  • 程序代码正常运行完毕,执行结果正确,进程正常退出

  • 程序代码正常运行完毕,但执行结果不符合业务预期,进程正常退出

  • 程序运行过程中出现异常,触发报错、非法操作等问题,进程异常终止

2.2 进程常见退出方法

(1)正常终止

程序逻辑正常执行完毕,主动触发的退出方式,包含三种:main函数return退出、调用exit函数退出、调用_exit函数退出。

(2)异常退出

程序非主动终止,由外部信号或程序异常触发,最常见方式为通过快捷键ctrl+c发送终止信号,强制结束进程。

(3)退出码

退出码是标识进程执行状态的数值,可直观反馈进程最终的执行结果。命令执行结束后,可通过退出码判断程序运行状态:0代表程序执行成功非0的任意数值均代表程序执行失败或异常退出,不同非0数值可对应不同的错误类型,方便开发者排查问题。

(4)exit和_exit函数区别

exit函数和_exit函数最终都会调用内核级的_exit系统调用完成进程终止,但exit函数在调用内核接口前,会额外执行一系列收尾工作,是更安全的主动退出方式,具体流程如下:

  • 优先执行用户通过atexit或on_exit函数注册的自定义清理函数,完成资源自定义释放

  • 关闭进程所有已打开的文件流、数据流

  • 刷新缓冲区数据,将所有缓存中的未写入数据落地保存

  • 最后调用_exit函数,完成内核层面的进程资源回收

而_exit函数为纯粹的内核系统调用,无任何前置收尾操作,直接终止进程、释放资源,不会处理缓冲区数据和自定义清理函数。

(5)return退出

return是main函数中最常用的进程退出方式,本质上与exit函数逻辑互通。在main函数中执行return n,等价于执行exit(n),操作系统会将main函数的返回值n,直接作为进程的退出码,完成进程退出。

三、进程等待

1、进程等待的必要性

子进程退出后,若父进程未做任何处理,子进程会转变为僵尸进程,引发一系列系统问题,因此父进程必须通过进程等待机制处理子进程退出状态,核心原因如下:

  • 避免内存泄漏:僵尸进程不会主动释放内核资源,会长期占用系统内存,持续积累会导致内存泄漏,影响系统运行性能

  • 无法强制销毁僵尸进程:僵尸进程仅保留PCB等少量内核数据结构,无活跃运行逻辑,kill命令仅能终止正在运行的进程,对僵尸进程无效,只能通过父进程回收资源彻底释放

  • 获取子进程执行结果:父进程可通过进程等待,获取子进程的退出状态、退出码,判断子进程任务是否正常完成、执行失败原因等信息

  • 规范资源回收:通过进程等待,父进程主动回收子进程所有占用的系统资源,保证系统资源合理复用

2、进程等待方法

2.1 wait函数

wait函数是基础的进程等待接口,用于阻塞等待任意子进程退出并回收资源。

头文件:

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

函数原型:pid_t wait(int* status);

返回值:等待成功返回退出的子进程PID;等待失败返回-1。

参数说明:status为输出型参数,用于接收子进程的退出状态信息;若无需获取子进程退出状态,可直接传入NULL。

运行特性:默认阻塞等待,若当前无已退出的子进程,父进程会一直阻塞休眠,直到有子进程退出后唤醒并回收资源。

2.2 waitpid函数

waitpid函数是wait函数的增强版,支持精准指定等待的子进程、支持非阻塞等待,使用更灵活。

函数原型:pid_t waitpid(pid_t pid, int *status, int options);

返回值:分为三种情况:

  • 正常等待到已退出子进程,返回对应子进程的PID

  • 设置WNOHANG非阻塞模式,且无已退出子进程时,返回0

  • 调用出错时,返回-1,同时系统会赋值errno,标识具体错误原因

参数说明:

  • pid:指定等待的子进程

    • pid = -1:等待任意一个子进程,功能与wait函数完全等效

    • pid > 0:精准等待PID与参数值一致的指定子进程

  • status:输出型参数,本质是位图数据,数值本身无直接意义,需搭配系统宏函数解析子进程退出状态,传入NULL则不获取状态

  • options:设置等待模式,默认值为0(阻塞等待)

    • 0:阻塞模式,无退出子进程时父进程持续阻塞

    • WNOHANG:非阻塞模式,检测到无已退出子进程时立即返回0,父进程不会阻塞,可继续执行后续逻辑

2.3 获取子进程status状态信息

wait与waitpid函数的status参数均由操作系统自动填充,用于存储子进程退出的详细信息,仅需开发者通过对应宏解析,核心规则如下:

  • status参数为位图结构,仅需关注低16比特位,包含进程退出原因、退出码等核心信息

  • WIFEXITED(status):判断子进程是否为正常退出,正常终止返回真(非0),异常终止返回假(0)

  • WEXITSTATUS(status):仅当WIFEXITED返回真时生效,用于提取子进程的具体退出码

四、进程程序替换

1、替换原理

通过fork创建的子进程,初始会拷贝父进程的代码和数据,与父进程执行相同程序、不同代码分支。若需要子进程执行全新的程序,需调用exec系列函数完成进程程序替换。

替换核心逻辑:调用exec函数后,进程的用户空间原有代码、数据会被全新程序的代码和数据完全覆盖,进程从新程序的启动入口开始执行。进程替换不会创建新进程,因此替换前后进程的PID、PCB标识保持不变,仅运行的程序内容发生改变。

2、替换函数

Linux系统提供6个exec系列程序替换函数,统一头文件如下:

#include <unistd.h>

六个函数原型:

cpp 复制代码
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 execve(const char *path, char *const argv[], char *const envp[]);
2.1 函数核心特性
  • 调用成功:系统加载新程序并从头执行,函数无返回值,原有后续代码不再执行

  • 调用失败:返回-1,保留原进程程序逻辑,可继续执行后续代码

  • 核心结论:exec系列函数仅存在错误返回值,无成功返回值

2.2 函数命名规则解析

六个函数命名均由功能后缀组合而成,可通过后缀快速区分函数特性:

  • l(list):命令参数以列表形式逐个传入,以NULL结尾

  • v(vector):命令参数以字符串数组形式批量传入

  • p(path):自动读取系统PATH环境变量,可直接传程序名,无需填写完整文件路径

  • e(env):支持自定义传入环境变量数组,覆盖系统默认环境变量

2.3 函数调用示例
cpp 复制代码
#include <unistd.h> 
int main() 
{ 
// 定义命令参数数组 
char *const argv[] = {"ps", "-ef", NULL}; 
// 定义自定义环境变量数组 
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; 
// 1. execl:全路径+列表传参 
execl("/bin/ps", "ps", "-ef", NULL); 
// 2. execlp:自动匹配PATH+列表传参,无需全路径 
execlp("ps", "ps", "-ef", NULL); 
// 3. execle:全路径+列表传参+自定义环境变量 
execle("ps", "ps", "-ef", NULL, envp); 
// 4. execv:全路径+数组传参 
execv("/bin/ps", argv); 
// 5. execvp:自动匹配PATH+数组传参 
execvp("ps", argv); 
// 6. execve:全路径+数组传参+自定义环境变量(原生系统调用) 
execve("/bin/ps", argv, envp); 
exit(0); 
}
2.4 函数层级关系

六个替换函数中,execve是唯一真正的内核原生系统调用,属于底层接口,在man手册第2节;其余五个函数均为用户层封装函数,底层最终都会调用execve完成程序替换,在man手册第3节。

五、自定义shell命令行解释器

自定义shell的核心原理:模拟系统shell的执行逻辑,通过获取用户输入、解析命令、区分内建/外部命令、创建子进程替换执行、回收进程资源,实现基础的命令行交互功能。整体遵循固定执行流程,同时维护全局运行变量保障功能稳定。

1、全局变量维护

shell运行前需初始化全局变量,统一管理运行状态,包含四类核心变量:命令行参数数组、环境变量数组、未处理的原始命令行字符串、命令退出码(初始值为0,标识初始状态执行成功)。

2、核心功能实现流程

2.1 输出规范命令行提示符

系统shell提示符标准格式:[用户名@主机名 当前目录尾项]$ ,需精准获取三类信息并格式化输出,具体实现方式:

  • 用户名、主机名:通过getenv函数读取USER、HOSTNAME环境变量直接获取

  • 当前目录尾项 :不使用PWD环境变量(存在更新延迟隐患),调用内核级getcwd函数,从进程task_struct结构体中读取真实绝对路径,保证路径准确性;再通过字符串逆向查找rfind函数定位最后一个/,通过substr函数截取末尾目录名称

  • 格式输出:通过snprintf函数格式化拼接信息,通过宏定义统一提示符格式,规范输出样式

2.2 获取用户输入命令

使用fgets函数读取标准输入的用户命令,该函数会完整记录输入内容(包含空格、特殊字符),不会自动跳过字符。读取内容会保留末尾换行符\n,需手动清理该换行符,避免影响后续命令解析。

2.3 解析处理重定向符号

采用从后往前遍历的方式识别命令中的重定向符号,区分三类重定向类型并标记、记录文件名:

  • 输入重定向 <:识别符号后,向后遍历跳过空格,定位目标文件名起始位置,标记为INPUT_REDIR

  • 输出重定向 > :判断前一位字符是否为>,区分覆盖/追加模式

  • 追加输出重定向 >>:双大于号为追加模式,标记为APPEND_REDIR;单大于号为覆盖模式,标记为OUTPUT_REDIR,最终均定位并记录目标文件名

2.4 拆分命令参数

通过strtok函数对清理后的命令字符串进行拆分,以空格为分隔符,将完整命令拆分为独立的参数数组,方便后续逻辑判断:

  • 首次调用:传入原始命令字符串,拆分首个参数,将分隔符替换为\0并返回参数指针

  • 后续调用:第一个参数传NULL,从上次拆分结束位置继续遍历,依次拆分剩余参数

  • 终止条件:遍历至字符串末尾,无有效参数时返回NULL,拆分结束

2.5 处理内建命令

内建命令是shell内置功能,无独立可执行文件,无法通过exec系列函数执行,需shell内部手动实现逻辑,核心实现cd、pwd、echo、export、exit五大基础内建命令:

  • cd命令:无参数时,通过GetHome函数定位用户家目录,调用chdir切换到家目录;携带参数时,直接读取参数目录路径,通过chdir完成目录切换

  • echo命令 :支持三类场景,参数为$?时,输出上一条命令的全局退出码;参数以$开头时,截取后续字符串作为环境变量名,通过getenv读取并输出环境变量值;普通文本参数直接原样输出

  • pwd、export、exit命令:分别实现打印当前目录、设置环境变量、退出当前shell的基础功能

2.6 执行外部命令

非内建命令均为外部可执行程序,执行流程标准化:通过fork创建子进程,在子进程中调用exec系列函数完成程序替换,执行用户输入的外部命令;父进程通过waitpid等待子进程退出,回收子进程资源,并更新全局命令退出码,完成一次完整的命令执行流程。

相关推荐
寒水馨2 小时前
Linux下载、安装PowerShell-v7.6.4(附安装包powershell-7.6.4-linux-x64.tar.gz)
linux·跨平台·自动化运维·devops·命令行·powershell·脚本语言
lsh曙光2 小时前
scp和rsync远程同步
linux
Kina_C3 小时前
HAProxy 负载均衡算法全解析:从静态轮询到一致性哈希的实战指南
linux·算法·负载均衡·哈希算法·haproxy
~光~~3 小时前
【嵌入式linux学习_OV8858 bring up】L2_V4L2与Ov8858
linux·数据库·学习
Kina_C3 小时前
HAProxy 负载均衡实战:从安装配置到高级参数详解
linux·运维·负载均衡·haproxy
Zhang~Ling3 小时前
手写进程池:基于匿名管道的 Master-Slave 架构实战
linux·服务器·架构
wunaiqiezixin3 小时前
MIT 6.S081 syscall 实验篇(lab2):System call tracing (moderate)
linux·unix·os·xv6·mit6.s081
凉、介3 小时前
ARMv8-A 指令学习
linux·笔记·学习·嵌入式·arm·指令
_张一凡4 小时前
Ubuntu 完美安装 FT_SCServo_Debug_Qt 飞特SCS_STS舵机调试工具
linux·ubuntu·舵机调试