文章目录
前言
进程控制是 Linux 系统编程的核心基石,直接决定了程序如何创建、终止、等待子进程,以及如何实现多任务协作。理解进程终止的场景、进程等待的意义、程序替换的原理,不仅能解决僵尸进程、资源泄漏等实际问题,更能让你掌握编写多进程程序、模拟 shell 等高级功能的底层逻辑。
本文将从进程终止的三种场景与退出方法入手,深入剖析进程等待(wait/waitpid)的实现机制与核心作用,详解 exec 函数族的程序替换原理与用法,最终通过完整代码实现简易 shell,串联所有知识点。全文兼顾理论深度与实操性,每个核心接口都配套示例代码,文末还附上高频面试习题及解析,帮助你夯实基础、检验学习成果。无论你是刚接触 Linux 多进程编程的初学者,还是想深耕底层的开发者,都能通过本文彻底理清进程控制的完整逻辑链,实现从 "理解原理" 到 "实战应用" 的跨越。
进程终止
进程退出的场景
代码运行完毕,结果正确 --退出码为0
代码运行完毕,结果不正确 -- 退出码为非0,具体是多少要看是什么问题
代码异常终止 --比如整数以0,此时就是异常终止--这个时候的退出码不重要了--因为代码都运行不完
进程出现异常,本质是进程收到了对应的信号父进程会关心子进程运行的情况
查询退出码的办法:$?--里面有最近一次进程退出时的退出码
basheg: echo $?
C标准库里面有官方的退出码:
errno,然后可以用strerror(errno)转换成字符串形式的错误描述当系统调用(如
open、read、write等)或库函数(如malloc、fopen等 )调用失败时,errno会被设置为对应的错误码
cpp用法: int* ptr = (int*)malloc(1000*1000*1000*4); if (ptr == NULL) printf("error: %d - %s\n", errno, strerror(errno));注意:
strerror不只是只能识别errno的错误码哈1
当然,也可以自己设计一套自己的退出码体系这样
进程常见的退出方法
1.
return2.exit3._exit区别:
exit和return的区别:在main函数里面,return和exit一样 但是在调用的函数里面,exit是退出当前进程,return是结束这个函数
exit和_exit的区别:exit在退出进程前会刷新缓冲区,关闭流,执行用户定义的清理函数,但是_exit不会
cppeg: 用法: return 0; exit(0); _exit(0);
引申:标准输出流会先把数据写入缓冲区中,合适的时候再进行刷新
--由上图可知,缓冲区肯定不在内核中
进程等待
进程等待:通过系统调用
wait/waitpid,来对子进程进行状态检测与回收的功能--如果没有这个,子进程的僵尸状态将一直保持(除非等到
init自动调用wait)
进程等待的意义:1.僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题
2.父进程需要通过进程等待来获得子进程的退出情况(父进程可能关心,可能不关系哈)
通过进程等待可以保证父进程是多个进程里面最后一个退出的进程
然后进程等待有两种方法:一种是wait,一种是waitpid父进程只能等待自己的子进程哈--等待子进程的子进程或者其他人的进程都是不行的
引申:子进程结束,也不是需要立马就去回收哈
问题:父进程要拿子进程的状态数据,为什么必须要用wait等系统调用?--因为进程具有独立性
进程等待的作用机理:子进程执行完毕后,其PCB里面还存储着退出信息,父进程调用
wait这些时,就会让内核去查找子进程的PCB,读取退出信息到status
wait
wait是等待到任意一个子进程退出时就释放那个子进程(子进程的状态会从Z变成X)想让
wait回收多个进程的话就搞个循环
cpppid_t wait(int*status); 头文件是: #include <sys/types.h> #include <sys/wait.h> 用法eg: pid_t ret = wait(NULL);返回值是
pid_t类型的(Linux下其实就是int),成功的话就返回那个子进程的pid,失败就返回-1这个
status是输出型参数,不关心子进程退出信息的话就传个NULL目前的话只研究
status的低16个比特位低7位存的是终止信号是啥--0的话表示没有异常
低第8位是
core dump状态剩下那8位是退出码--可以通过退出码看程序有无出错以及出错的原因
这个
status还有两个宏:(变成其他变量名也行哈)
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
引申:这个
<defunct>也表示这个进程正在僵尸状态
waitpid
cpppid_ t waitpid(pid_t pid, int *status, int options); 头文件: #include <sys/types.h> #include <sys/wait.h>返回值是
pid_t类型,为0表示等待的条件还没有就绪,不等了;<0表示失败,>0返回的是子进程的pid
status跟上面的那个一样这个
pid的话,如果填-1,就是等待任一一个子进程;填>0的数的话,就是等待ID跟这个pid相等的子进程这个
options的话,可以填WNOHANG:这样的话就表示非阻塞轮询+父进程可以干自己的事(但是活不能太重) --不想这样的话就填0
cpp引出阻塞: 如果子进程还没退出,父进程在wait的时候此时叫做阻塞状态--父进程也不会去干其他事
进程程序替换
概念:就是在程序运行到程序替换的时候,直接去执行另一个程序了,这个程序的后续将不再执行
--进程还是原来那个进程
--程序替换只进行进程的程序代码和数据的替换
--在程序替换中,环境变量也是原来的(除非用的
execle和execvpe)
子进程进行程序替换,是不会影响父进程的
Linux中的可执行程序是ELF格式的 可执行程序的入口地址在那个ELF的表头
用于程序替换的6个函数
exec函数只有出错的返回值(-1),没有成功的返回值--因为函数调用成功的话就去执行新程序了,不再返回
这里列的是函数调用接口--当然还要系统调用接口eg:
execve
cpp
#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[]);
//cahr*后面的const表示东西写进去就不能变了
这几个函数统称为exec函数
命名的记忆:
带l是参数采用可变参数列表的形式 带v是参数采用的数组方式 --这里要填的其实就是命令行上填的东西(把换行换成NULL就行了)
带e是自己传环境变量 带p是文件去PATH里面找
参数的含义:
带envp[]的要自己导入环境变量:可以传系统的eg:environ(记得extern声明)
path的话表示要传路径进去 file的话写文件名就行,会去环境变量PATH里面找
argv[]的话表示命令行参数用数组传过去
注意:这里的不管是可变参数列表还是数组的方式:最后那个都得是NULL
eg: char *const argv[] = {"ls","-a","-l",NULL};
cpp
用法:
execl("/user/bin/ls","ls","-a","-l",NULL);
execlp("ls","ls","-a","-l",NULL);
execle("/user/bin/ls","ls","-a","-l",NULL,environ);
execv("/bin/ps", argv);//argv是数组
注意:execlp("./otherExe","./otherExe",NULL);
execlp("./otherExe","otherExe",NULL); 也行
只要第一个参数传对,第二个参数传的是路径也行--
带p的传路径也是可以的
程序替换时想给子进程传递增多的环境变量的方法:
1.父进程的地址空间里面
putenv2.彻底替换--eg:
execle(这里填的环境变量不是追加!!!)
引申:C++的源文件常见后缀:.cpp .cc .cxx
代码里面把程序替换成其他语言的程序也是可以的--无论是可执行程序还是脚本,都能跨语言调用--原因:所以语言运行起来,本质都是进程
模拟实现shell
这里的话是粗略模拟实现Linux的命令行
包括对个别内建命令的识别(比如
echo --但是没考虑到eg: echo "aaaa")但是不包括eg:Linux里面命令行的Tab键的作用
自己模拟实现的shell能执行普通命令的原因:普通命令是可执行程序,相当于运行程序了内建命令的话要单独处理,因为需要父进程进行操作
cpp
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
// 自定义环境变量表
char myenv[LINE_SIZE];
const char *getusername()
{
return getenv("USER");
}
const char *gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd, sizeof(pwd));
}
void interact(char *cline, int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
char *s = fgets(cline, size, stdin);//会把\n也读进来,size-1的话就是\n不读进来
assert(s);
(void)s;//用来骗编译器--release没有assert(s),s只定义了但没使用
cline[strlen(cline)-1] = '\0';//\0 \n这些算一个字符哈
}
//这样就可以读到ls -a -l这样了
int splitstring(char cline[], char *_argv[])//返回的是命令行参数个数
{
int i = 0;
argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL, DELIM));
//这个;要注意哈while后面是空语句的话要跟个;
//这样写的话还把最后一个位置置为了NULL
return i - 1;
}
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0){
perror("fork");
return;
}
else if(id == 0){
//让子进程执行命令
execvp(_argv[0], _argv);
//用这个或者execvpe会好些;因为有_argv,并且没有绝对路径提供给这个函数
exit(EXIT_CODE);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
int buildCommand(char *_argv[], int _argc)
{
if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"), "%s", pwd);
//将环境变量PWD设置成pwd的值 --PWD是环境变量,不是指令!
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "export") == 0){
strcpy(myenv, _argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
if(strcmp(_argv[1], "$?") == 0)
{
printf("%d\n", lastcode);
lastcode=0;
}
else if(*_argv[1] == '$'){
char *val = getenv(_argv[1]+1);//注意这里的+1的含义
if(val) printf("%s\n", val);
}
else{
printf("%s\n", _argv[1]);//打出$
}
return 1;
}
// 特殊处理一下ls,让他有"颜色"
if(strcmp(_argv[0], "ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit){
// 1. 交互问题,获取命令行
interact(commandline, sizeof(commandline));
// 2. 子串分割的问题,解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
// 3. 指令的判断
//内键命令,本质就是一个shell内部的一个函数
int n = buildCommand(argv, argc);
// 4. 普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
引申:
strtok:C语言中用作字符串的分割
cppchar *strtok(char *str, const char *delim);
str在首次调用时传入待分割的字符串,后续调用传入NULL,表示 "从上次分割的位置继续"
delim:分隔符
chdir:改变进程当前的工作目录
cppint chdir(const char *path); 会改成path的
putenv:直接让环境变量表指向参数的地址--所以这个环境变量要小心失效--这个参数最好搞在堆上引申:环境变量表里面存的是环境变量的地址
Linux的shell命令行的一开始的环境变量是从哪搞的?
--当用户登录时,shell读取了目录里面的某个文件,那里面保存了导入环境变量的方式
引申:有些编译器对定义了但没使用的变量是会报警告的--此时可以eg:(void)s;这样来骗编译器
作业部分
cpp
下面哪些属于,Fork后子进程保留了父进程的什么?[多选] (AC)
A.环境变量
B.父进程的文件锁,pending alarms和pending signals
C.当前工作目录
D.进程号
cpp
通过fork和exec系统调用可以产生新进程,下列有关fork和exec系统调用说法正确的是? [多选](AB)
A.fork生成的进程是当前进程的一个相同副本
B.fork系统调用与clone系统调用的工作原理基本相同
//clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现
C.exec生成的进程是当前进程的一个相同副本//exec是程序替换函数,本身并不创建进程
D.exec系统调用与clone系统调用的工作原理基本相同
不算 main 这个进程自身,创建了多少个进程(B)
cppint main(int argc, char* argv[]) { fork(); fork() && fork() || fork(); fork(); }A.18
B.19
C.20
D.21
解法:
1,2进程的话,创建了3,4进程,1,2进程又创建了5,6进程--他们给1,2进程返回的都是自己的pid,然后在自己进程中给自己返回的是0
5,6进程&&前面的那个fork拿到的值是父进程拿到的同款值--他跟父进程的区别是在产生5,6进程的fork那里开始的
引申:逻辑与(&&)的优先级高于逻辑或(||)所以:三个
fork那里可以写成:(fork() && fork()) || fork()




