深入解析进程创建与终止机制
1.进程的创建
1.1.fork()函数

进程调⽤fork,当控制转移到内核中的fork代码后,内核做:
1.分配新的内存块和内核数据结构给子进程
2.将父进程部分数据结构内容拷贝至子进程
3.添加子进程到系统进程列表当中
4.fork返回,开始调度器调度
看如下的一段代码:

1.2.写时拷贝

有了写时拷贝,代码的运行和空间的节省达到了最大化
2.进程终止
2.1.进程状态码
为什么要创建子进程:就是为了让它去做某项任务,而状态码就是描述子进程完成任务的情况如何,状态码是写在task_struct 内部的,此时父进程才能拿到子进程的转台码

进程退出有下面三种场景:
1.代码运行完毕,结果正确
2.代码运行完毕,退出码不正确
errno自动帮你检查错误对应的进程码
3.代码异常终止,此时的退出码无意义
此时的退出码不是return和exit()返回的,是运行到错误段时,系统帮你返回
1.SIGSEGV
(段错误,信号编号 11):退出码 139
(128+11),常见于内存越界(如数组下标越界、空指针解引用)。
2.SIGFPE
(浮点异常,信号编号 8):退出码 136
(128+8),常见于除零错误、浮点运算溢出。
3.SIGABRT
(异常终止,信号编号 6):退出码 134
(128+6),由 abort()
函数调用或内存分配检测(如 malloc
失败后 free
无效指针)触发。
常见的退出码:
退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0时表示执行成功,没有问题。代码 1或。 以外的任何代码都被视为不成功。Linux Shel 中的主要退出码
2.2.exit()和_exit()的比较:
exit():是c语言写的
_exit():是系统的
之间我们有个结论:库函数的调用其实就是对系统函数调用的一层封装,所以exit()的调用其实底层就是_exit()。
观察下面代码的结果:
结论:
进程如果exit退出的时候,exit(),进程退出的时候,会进行缓冲区的刷新
进程如果exit退出的时候,exit(),进程退出的时候,不会进行缓冲区的刷新
3.进程等待
之前讲过,子进程退出,父进程如果不管不顾,就可能造成'僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
总结:进程等待是必要的,作用回收子进程资源,获取子进程退出信息
3.1.进程等待的方法
3.1.1.子进程退出状态分析函数
1.WIFEXITED(status)
作用:判断子进程是否 正常退出(通过 exit()
函数或 return
语句结束,而非信号终止)。
返回值:
1. 非零(真):子进程正常退出(如 exit(0)
、return 5
)。
2. 0(假):子进程因信号终止(如 SIGKILL
、SIGSEGV
)或未退出(极少情况)。
2.
WIFSIGNALED(status)
作用:判断子进程是否 因未捕获的信号终止(如 kill -9
发送的 SIGKILL
,或程序崩溃产生的 SIGSEGV
)。
返回值:
非零(真):子进程被信号终止(非正常退出)。
0(假):子进程正常退出(或未退出,极少情况)。
3.
WTERMSIG(status)
作用:获取 终止子进程的信号编号(仅当 WIFSIGNALED(status)
为真时有效)。
返回值:
返回信号编号(如 SIGKILL
是 9,SIGTERM
是 15,SIGSEGV
是 11 等)。
4.WEXITSTAUS(status)
作用:获取 子进程正常退出时的返回值(即 exit()
或 return
语句传递的值)。
仅当 WIFEXITED(status)
为真时有效,否则结果未定义。
四者搭配使用:
先使用
WIFEXITED:判断是否是正常退出
WEXITSTAUS:获取正常退出的退出码
WIFSIGNALED:判断是否是信号终止
WTERMSIG:获取信号终止的信息
3.1.2.wait()
这是测试wait()函数的一段代码:
此时有两种情况:
1.正常结束:
wait_pid:就是被捕获的子进程pid;
status:返回的状态码,上面返回的是5;
2.异常结束(假设是被kill-9杀死)
3.1.2.waitpid():
waitpid()代码的好处就是我并不需要傻乎乎的去阻塞等待子进程,在等待子进程的时候,我还可以执行别的代码
这是测试waitpid()函数的一段代码:
4.进程替换
fork()函数之后,子进程继承了父进程的代码,但是子进程想要执行全新的进程,就需要使用到进程的替换
4.1.替换原理
1.一旦替换成功,就去执行新的代码,后面的代码已经是不存在了
2.exec*函数只有失败返回值,没有成功返回值,所以不用做返回值判断,只要返回就是失败
3.程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间 中(就像是子进程去更改数据的时候,就是在物理内存上开辟一段新的空间,进行存储更改的数据,保证了进程的独立性)
4.2.替换函数
一共有下面6种替换函数:
1.execl
函数原型:
参数:
path
:新程序的完整路径(如/bin/ls
)arg...
:参数列表(argv[0]
,argv[1]
, ..., 必须以NULL
结尾
示例:
2.execlp
函数原型:
参数:
file
:程序名(自动在PATH
环境变量目录中查找)arg...
:参数列表(以NULL
结尾)
示例:
3.execle
函数原型:
参数:
path
:完整路径arg...
:参数列表(以NULL
结尾)envp[]
:自定义环境变量数组(以NULL
结尾)
示例:
4.execv
函数原型:
参数:
path
:完整路径argv[]
:参数指针数组(以NULL
结尾)
示例:
5.execvp
函数原型:
参数:
file
:程序名(搜索PATH
)argv[]
:参数指针数组(以NULL
结尾)
示例:
6.execve
函数原型:
参数:
path
:完整路径argv[]
:参数指针数组envp[]
:自定义环境变量数组
示例:
5.自主shell命令解释器
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include <string>#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>using namespace std;const int basesize = 1024;
const int argnum = 64;
const int envnum = 64;// 全局的命令行参数表
int argc = 0;
char *argv[argnum];// 全局的变量
int lastcode = 0;// 我的系统的环境变量
char *genv[envnum];// 全局的当前shell工作路径
char pwd[basesize];
char pwdenv[basesize];#define TrimSpace(pos) do{ \while(isspace(*pos)){ \pos++; \} \
}while(0)string GetUserName()
{string name = getenv("USER");return name.empty() ? "None" : name;
}string GetHostName()
{string hostname = getenv("HOSTNAME");return hostname.empty() ? "None" : hostname;
}string GetPwd()
{if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);putenv(pwdenv); // PWD=XXXreturn pwd;
}string LastDir()
{string curr = GetPwd();if(curr == "/" || curr == "None") return curr;// /home/xxx/xxxsize_t pos = curr.rfind("/");if(pos == std::string::npos) return curr;return curr.substr(pos+1);
}string MakeCommandLine()
{// lwh@bfe1t-alicloud myshell$char command_line[basesize];snprintf(command_line, basesize, "[%s@%s %s]# ", \GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());return command_line;
}void PrintCommandLine() // 1. 命令行提示符
{printf("%s", MakeCommandLine().c_str());fflush(stdout);
}bool GetCommandLine(char command_buffer[], int size) // 2. 获取用户命令
{// 我们认为:我们要将用户输入的命令行,当做一个完整的字符串// "ls -a -l -n"char *result = fgets(command_buffer, size, stdin);if(!result){return false;}command_buffer[strlen(command_buffer)-1] = 0;if(strlen(command_buffer) == 0) return false;return true;
}void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{(void)len;memset(argv, 0, sizeof(argv));argc = 0;// "ls -a -l -n"const char *sep = " ";argv[argc++] = strtok(command_buffer, sep);// 是刻意写的while((bool)(argv[argc++] = strtok(nullptr, sep)));argc--;
}void debug()
{printf("argc: %d\n", argc);for(int i = 0; argv[i]; i++){printf("argv[%d]: %s\n", i, argv[i]);}
}// 在shell中
// 有些命令,必须由子进程来执行
// 有些命令,不能由子进程执行,要由shell自己执行 --- 内建命令 built command
bool ExecuteCommand() // 4. 执行命令
{// 让子进程进行执行pid_t id = fork();if(id < 0) return false;if(id == 0){//子进程// 1. 执行命令execve(argv[0], argv, genv);// 2. 退出exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)){lastcode = WEXITSTATUS(status);}else{lastcode = 100;}}return true;
}void AddEnv(const char *item)
{int index = 0;while(genv[index]){index++;}genv[index] = (char*)malloc(strlen(item)+1);strcpy(genv[index], item);genv[++index] = nullptr;
}// shell自己执行命令,本质是shell调用自己的函数
bool CheckAndExecBuiltCommand()
{if(strcmp(argv[0], "cd") == 0){// 内建命令if(argc == 2){chdir(argv[1]);lastcode = 0;}else{lastcode = 1;}return true;}else if(strcmp(argv[0], "export") == 0){// export也是内建命令if(argc == 2){AddEnv(argv[1]);lastcode = 0;}else{lastcode = 2;}return true;}else if(strcmp(argv[0], "env") == 0){for(int i = 0; genv[i]; i++){printf("%s\n", genv[i]);}lastcode = 0;return true;}else if(strcmp(argv[0], "echo") == 0){if(argc == 2){// echo $?// echo $PATHif(argv[1][0] == '$'){if(argv[1][1] == '?'){printf("%d\n", lastcode);lastcode = 0;}else{printf("%s\n", getenv(argv[1]+1));lastcode = 0;}}else{printf("%s\n", argv[1]);lastcode = 0;}}else{lastcode = 3;}return true;}return false;
}// 作为一个shell,获取环境变量应该从系统的配置来
// 我们今天直接从父shell中获取环境变量
void InitEnv()
{extern char **environ;int index = 0;while(environ[index]){genv[index] = (char*)malloc(strlen(environ[index])+1);strcpy(genv[index], environ[index]);index++;}genv[index] = nullptr;
}int main()
{InitEnv();char command_buffer[basesize];while(true){PrintCommandLine(); // 1. 命令行提示符// command_buffer: > out putif( !GetCommandLine(command_buffer, basesize) ) // 2. 获取用户命令{continue;}// printf("%s\n", command_buffer);ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析if( CheckAndExecBuiltCommand() ){continue;}ExecuteCommand(); // 4. 执行命令}return 0;
}