一、进程创建
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等待子进程退出,回收子进程资源,并更新全局命令退出码,完成一次完整的命令执行流程。