当前位置: 首页 > news >正文

深入解析进程创建与终止机制

1.进程的创建

1.1.fork()函数

fork函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进 程为⽗进程。

 进程调⽤fork,当控制转移到内核中的fork代码后,内核做:

1.分配新的内存块和内核数据结构给子进程

2.将父进程部分数据结构内容拷贝至子进程

3.添加子进程到系统进程列表当中

4.fork返回,开始调度器调度

看如下的一段代码:

这⾥看到了三⾏输出,⼀⾏before,两⾏after。进程43676先打印before消息,然后它有打印after。
另⼀个after消息有43677打印的。注意到进程43677没有打印before
原因是下面的执行流程:

1.2.写时拷贝

通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅ 式各⾃⼀份副本。

有了写时拷贝,代码的运行和空间的节省达到了最大化

2.进程终止

进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1.进程状态码

为什么要创建子进程:就是为了让它去做某项任务,而状态码就是描述子进程完成任务的情况如何,状态码是写在task_struct 内部的,此时父进程才能拿到子进程的转台码

Linux中查看进程码的方式:
查看的是最近的一次进程

进程退出有下面三种场景:

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(假):子进程因信号终止(如 SIGKILLSIGSEGV)或未退出(极少情况)。

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;
}

http://www.lryc.cn/news/614880.html

相关文章:

  • Linux 信号处理标志sa_flags详解
  • 有限元方法中的数值技术:Cholesky矩阵分解
  • 从零学习three.js官方文档(一)——基本篇
  • 校招秋招春招实习快手在线测评快手测评题库|测评解析和攻略|题库分享
  • 【linux基础】Linux目录和Windows目录的区别
  • 免费开发数字人API
  • Milvus 向量数据库基础操作解析
  • Kubernetes 无法识别你定义的 `CronJob` 资源*逐步解决方案
  • 不足3个细胞怎么做差异分析?
  • 目标检测数据集 - 足球场广告横幅检测数据集下载「包含VOC、COCO、YOLO三种格式」
  • 【Datawhale AI夏令营】从Baseline到SOTA:深度剖析金融问答RAG管道优化之路
  • [CUDA] CUTLASS | `CuTe DSL` 创新
  • 《TypeScript搭建的认知桥梁:游戏化学习应用的深层架构》
  • day22|学习前端ts语言
  • Javaweb - 14.1 - 前端工程化
  • 政府数字化大屏系统 - Flask实现方案
  • 使用LangGraph从零构建多智能体AI系统:实现智能协作的完整指南
  • OpenAI开源大模型 GPT-OSS 开放权重语言模型解析:技术特性、部署应用及产业影响
  • HTML金色流星雨
  • 人工智能技术发展历史演变
  • Android中RecyclerView基本使用
  • 深入理解Qt事件处理机制
  • C++实现MATLAB矩阵计算程序
  • 【Redis7.x】docker配置主从+sentinel监控遇到的问题与解决
  • Debian 系统更新命令
  • PDF 转 HTML API 数据接口
  • 免费PDF编辑软件 pdf24-creator 及其安装包
  • 力扣-74.搜索二维矩阵
  • MyBatis联合查询 - 注解篇
  • 【洛谷题单】--分支结构(三)