【操作系统】5.进程控制和shell的实现(附代码)
目录一、进程创建1. fork 函数2. 写时拷贝 (Copy-On-Write, COW)3. 为什么要写时拷贝4. fork 失败的原因二、进程终止1. 退出场景2. 获取返回值3. 退出码标准4. exit 退出5. exit 和 _exit 区别三、进程等待1. 为什么要等待子进程结束2. 如何等待四、进程程序替换1. 原理2. 返回值3. execl 函数格式4. 在子进程中执行5. exec 系列函数五、简单 Shell 的实现1. 获取用户信息2. 输出提示符如 [userhost path]$3. 获取和解析命令4. 创建子进程执行外部命令5. 内建命令 (Built-in Command)一、进程创建1.fork函数分配资源为子进程分配task_struct、mm_struct等内核数据结构。拷贝数据将父进程的 PCBtask_struct、页表等拷贝到子进程中。加入调度将子进程添加到系统的进程列表中并加入到调度队列的expired组。返回fork函数返回。如果创建的子进程专门执行系统内核代码这就是内核级虚拟机如KVM的原理。2. 写时拷贝 (Copy-On-Write, COW)创建子进程时操作系统会将父、子进程页表的映射权限设置为只读。当子进程或父进程需要修改一个共享的变量时虽然地址没问题但权限不符此时会触发写时拷贝。操作系统会为修改者子进程重新开辟一块物理内存拷贝数据并修改其页表映射最终完成写入。3. 为什么要写时拷贝加快子进程创建速度无需立即拷贝全部数据创建瞬间非常快。减少内存资源浪费由于子进程通常不会使用父进程的全部数据对于不修改的部分父子可以共享无需额外占用内存。写时拷贝是一种精细化的内存管理策略。4.fork失败的原因系统中进程总数过多。用户的进程数超过了限制。二、进程终止1. 退出场景正常结束结果正确返回0。正常结束结果错误返回非0即退出码。异常结束返回值无意义程序被信号终止。2. 获取返回值使用echo $?命令可以打印最近一个进程的退出码。进程的退出码存储在task_struct结构体内。父进程回收僵尸进程时可以从中获取退出码。再次执行echo $?会显示0因为这时获取到的是echo命令本身的退出码。3. 退出码标准Linux 规定了一系列退出码strerror函数可以将整数错误码转换为对应的字符串描述。​​​​​​​从0到133共有 134 个数字对应不同的描述字符串。示例FILE* f fopen(a.txt, r); return errno;errno会返回失败操作的错误码。echo $?会输出2表示文件不存在。同样ls -l a.txt打开一个不存在的文件也会返回2。​​​​​​​异常退出示例int p 10; p / 0; // 除零异常 return 87;程序异常终止echo $?可能会输出136或其他值代表信号SIGFPE此时return 87没有意义。因此异常退出时退出码无意义。4.exit退出main函数的返回代表进程结束而普通函数的返回只代表该函数结束。如果要在函数中就结束整个进程可以使用exit。void fun() { exit(0); } int main() { fun(); return 1; }最后的退出码是0exit(0)而不是1。5.exit和_exit区别exit是 C 语言库函数_exit是操作系统提供的系统调用。​​​​​​​printf(打印); sleep(2); _exit(0); // 或 exit(0); return 0;两个函数对于缓冲区的刷新策略不同exit退出时会刷新fflush缓冲区将“打印”输出出来。_exit则不会刷新缓冲区直接退出屏幕上可能看不到“打印”。C 语言库的exit函数底层会调用_exit但在调用前会进行一些清理工作如刷新缓冲区。结论输出缓冲区这里指stdout的缓冲区的管理位于 C 语言标准库层面而不是操作系统内核。三、进程等待1. 为什么要等待子进程结束如果父进程不管子进程子进程结束会变成僵尸进程造成内存泄漏即使kill命令也无法杀死僵尸进程。父进程需要知道子进程的执行结果是否成功、退出码等以便进行后续处理。2. 如何等待wait函数等待任意一个子进程结束并回收。父进程会阻塞直到有子进程退出。#include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include string.h #include sys/wait.h int main() { int id fork(); if(id 0) { // 子进程 int cnt 5; while(cnt--) { printf(子进程pid%dppid%d\n,getpid(),getppid()); sleep(1); } exit(0); } // 父进程 sleep(10); int i wait(NULL); if(i 0) printf(回收子进程%d\n, i); return 0; }waitpid函数pid_t waitpid(pid_t pid, int* status, int options)pid_t成功时返回被等待子进程的 PID。pid_t pid指定等待哪个子进程-1表示等待任意子进程。int options0表示阻塞等待WNOHANG表示非阻塞轮询。status参数的构成这是一个输出型参数获取到的status是一个整型通常 32 位其低 16 位有效。低 16 位中高 8 位bit 15~8存储子进程的退出码正常终止时。低 7 位bit 6~0存储导致子进程终止的信号编号异常终止时。第 7 位bit 7是 core dump 标志位。正常退出示例子进程exit(1)status值为2561 8右移 8 位后得到1。​​​​​​​异常退出示例用kill -9杀死子进程此时退出码部分为0信号部分为9。​​​​​​​可以用kill -l命令查看信号编号与名称的对应关系。通过位运算取出信息if ((*status) 0x7F) { // 低7位不为0说明是信号终止 printf(回收子进程%d退出信号 %d\n, i, (*status) 0x7F); } else { // 正常退出 printf(回收子进程%d退出值 %d\n, i, ((*status) 8) 0xFF); }(*status) 0x7F取出低 7 位信号。((*status) 8) 0xFF取出高 8 位退出码。其他常见退出信号野指针信号11(SIGSEGV)​​​​​​​除零错误信号8(SIGFPE)​​​​​​​使用宏简化判断WIFEXITED(status)判断子进程是否正常退出为真表示正常。WEXITSTATUS(status)提取子进程的退出码仅在正常退出时有效。非阻塞等待 (WNOHANG)将waitpid的第三个参数改为WNOHANG。返回值成功等待到子进程结束返回子进程的PID。子进程尚未结束返回0。出错返回-1。与阻塞等待的区别阻塞等待会一直等待直到子进程结束。非阻塞等待会立即返回允许父进程在等待期间做其他事情轮询效率更高。这个while循环就是非阻塞轮询。int sta 0; int *status sta; while (1) { int i waitpid(id, status, WNOHANG); if (i 0) { // 子进程已结束处理 status break; } else if (i 0) { // 子进程未结束 printf(waiting...\n); sleep(1); } else { // 错误处理 break; } }四、进程程序替换execl(/usr/bin/ls, ls, -l, -a, NULL);执行这条语句当前进程的代码就会被替换为ls -l -a的代码。1. 原理进程 内核数据结构 代码和数据。程序替换就是将新的程序代码和数据加载到当前进程的地址空间中并调整页表映射但进程的 PID、内核数据结构等保持不变。2. 返回值替换失败返回-1程序继续执行当前代码。替换成功不返回因为原代码已被覆盖新的程序从main函数开始执行。因为代码会被替换所以返回值在替换成功的情况下没有意义。3.execl函数格式int execl(const char* path, const char* arg, ...);path要执行的程序的路径包含程序名。arg命令行参数列表...表示可变参数以NULL结尾。4. 在子进程中执行int id fork(); if (id 0) { // 子进程 printf(子进程 pid: %d\n, getpid()); execl(./other, ./other, NULL); // 如果 execl 失败才会执行下面 perror(execl); exit(1); } else { // 父进程 waitpid(-1, NULL, 0); }由子进程执行替换不会干扰父进程。为什么不会干扰因为进程具有独立性。数据和代码通过写时拷贝不会相互干扰。注意这里说的“代码不干扰”是指替换后子进程的代码完全独立。程序替换前后子进程的 PID 不变证明了没有新建进程只是替换了内容。​​​​​​​进程替换使得我们可以用 C 程序作为加载器来执行任何语言编写的程序。5.exec系列函数exec函数簇本质上是一个加载器负责将磁盘上的可执行程序加载到内存并执行。execlpp代表PATH环境变量。执行系统命令时不需要写完整路径。execlp(ls, ls, -l, -a, NULL);注意第一个参数ls是去PATH中查找程序第二个参数ls是传递给程序的argv[0]。execvv代表vector传递一个指针数组。char* const argv[] { going, a, b, NULL }; execv(./other, argv);被替换的程序other的main函数可以接收到这个参数数组。因此子进程能够拿到父进程传递的命令参数是exec系列函数的功能。int main(int argc, char* argv[]) { std::cout 替换成功: getpid() std::endl; for(int i 0; i argc; i) { std::cout i : argv[i] std::endl; } return 0; }execvp是v和p的组合既支持PATH查找又使用指针数组传参。execvpe/execlee代表environment可以传递一个新的环境变量数组覆盖原有的环境变量。新进程将使用env数组中的环境变量原有的环境变量被覆盖。char* const argv[] {going, a, b, NULL}; char* const env[] {newenv111, newenv2222, NULL}; execvpe(./other, argv, env);如何新增环境变量而不覆盖方法一使用putenv函数修改当前进程的环境变量然后使用execv它会自动传递当前的环境变量。putenv(a222); execv(./other, argv);方法二先putenv然后获取当前环境变量数组environ再传给execvpe。putenv(a222); extern char** environ; execvpe(./other, argv, environ);execve这是 Linux 操作系统直接提供的系统调用接口是exec函数簇的底层实现。其他函数如execl,execvp等都是 C 库在此基础上进行的封装。五、简单 Shell 的实现1. 获取用户信息从环境变量中获取用户名、主机名和当前路径。const char* get_user() { const char* user getenv(USER); return user ? user : unknown; } const char* get_hostname() { const char* hostname getenv(HOSTNAME); return hostname ? hostname : unknown; } const char* get_pwd() { const char* pwd getenv(PWD); return pwd ? pwd : unknown; }2. 输出提示符如[userhost path]$#define FORMAT [%s%s %s]# #define MAX_COMMANDSIZE 256 void MakeCommandLine(char cmd_prompt[], int size) { snprintf(cmd_prompt, size, FORMAT, get_user(), get_hostname(), get_pwd()); } void printcommandprompt() { char prompt[MAX_COMMANDSIZE]; MakeCommandLine(prompt, sizeof(prompt)); printf(%s, prompt); fflush(stdout); // 立即刷新输出 }使用snprintf将格式化的字符串写入prompt数组中然后打印。3. 获取和解析命令bool GetCommandLine(char* c, int size) { char* command fgets(c, size, stdin); if (command nullptr) return false; c[strlen(c) - 1] \0; // 去掉末尾的换行符 if (strlen(c) 0) return false; return true; } #define SEP char* g_argv[64]; int g_argc 0; bool CommandSplice(char* commandline) { g_argc 0; g_argv[g_argc] strtok(commandline, SEP); while ((g_argv[g_argc] strtok(NULL, SEP))); g_argc--; return true; }fgets从标准输入读取一行命令包括换行符。strtok按空格分隔命令字符串存入g_argv数组。4. 创建子进程执行外部命令void Execute() { int id fork(); if (id 0) { // 子进程 execvp(g_argv[0], g_argv); perror(execvp); // 如果替换失败 exit(1); } // 父进程等待 int rid waitpid(id, nullptr, 0); }5. 内建命令 (Built-in Command)有些命令需要 Shell 自己执行而不是创建子进程例如cd命令改变当前 Shell 进程的工作目录。void cd() { if (g_argc 1) { // 只有 cd切换到 home 目录 const char* home getenv(HOME); chdir(home); } else { // cd path chdir(g_argv[1]); } // 更新 PWD 环境变量 char cwd[256]; getcwd(cwd, sizeof(cwd)); char pwd_env[300]; snprintf(pwd_env, sizeof(pwd_env), PWD%s, cwd); putenv(pwd_env); } bool builtin() { string cmd g_argv[0]; if (cmd cd) { cd(); return true; } // 可以添加其他内建命令如 exit, export 等 return false; }chdir系统调用改变当前进程的工作目录。putenv设置或修改环境变量PWD。在 Shell 主循环中先判断是否为内建命令如果是则直接执行否则创建子进程执行。int main() { while (1) { printcommandprompt(); char commandline[MAX_COMMANDSIZE]; if (!GetCommandLine(commandline, sizeof(commandline))) continue; if (!CommandSplice(commandline)) continue; if (builtin()) continue; // 是内建命令直接执行 Execute(); // 外部命令创建子进程执行 } return 0; }注意像$?这样的命令也需要内建实现因为$?需要 Shell 进程父进程获取上一个命令的退出状态这无法通过创建子进程来完成。代码#includeiostream #includestdlib.h #includestring.h #includestdio.h #includesys/types.h #includesys/wait.h #includeunistd.h using namespace std; const int MAX_COMMANDSIAE1024; #define FORMAT [%s%s %s]# const int MAX_SIZE128; char* g_argv[MAX_SIZE]; int g_argc0; const char* get_user() { const char* usergetenv(USER); return user; } const char* get_hostname() { const char*hostnamegetenv(HOSTNAME); return hostname; } const char* get_pwd() { char cwd[100]; const char*pwdgetcwd(cwd,sizeof(cwd)); char p[100]; snprintf(p,sizeof(p),PWD%s,pwd); putenv(p); return pwd; } const char*get_home() { const char*homegetenv(HOME); return home; } void MakeCommandLine(char cmd_prompt[],int size) { snprintf(cmd_prompt,size,FORMAT,get_user(),get_hostname(),get_pwd()); } void printcommandprompt() { char prompt[MAX_COMMANDSIAE]; MakeCommandLine(prompt,sizeof(prompt)); printf(%s,prompt); fflush(stdout); } bool GetCommandLine(char*c,int size) { char*commandfgets(c,size,stdin); if(commandnullptr)return 0; c[strlen(c)-1]0; if(strlen(c)0)return 0; return 1; } bool CommandSplice(char*commandline) { #define SEP g_argc0; g_argv[g_argc]strtok(commandline,SEP); while(g_argv[g_argc]strtok(nullptr,SEP)); g_argc--; return 1; } void Excute() { int idfork(); if(id0) { execvp(g_argv[0],g_argv); exit(0); } int ridwaitpid(id,nullptr,0); } void cd() { if(g_argc1) { string homeget_pwd(); chdir(home.c_str()); } else { string whereg_argv[1]; chdir(where.c_str()); } } bool builtin() { string metg_argv[0]; if(metcd) { cd(); return 1; } return 0; } int main() { while(1) { printcommandprompt(); char commandline[MAX_COMMANDSIAE]; if(!GetCommandLine(commandline,sizeof(commandline)))continue; CommandSplice(commandline); if(g_argc0)continue; if(builtin())continue; Excute(); } return 0; }

相关新闻

学长亲荐2026继续教育TOP10AI论文软件:开题报告文献综述全测评

学长亲荐2026继续教育TOP10AI论文软件:开题报告文献综述全测评

学长亲荐2026继续教育TOP10AI论文软件:开题报告文献综述全测评 2026继续教育AI论文工具测评:精准匹配学术需求的实用指南 随着人工智能技术在学术领域的深入应用,越来越多的研究者开始依赖AI工具辅助论文写作。然而,面对市场上琳琅…

2026/7/6 9:33:07 阅读更多 →
AI动态权益定价系统架构:智能数字权益管理平台商业化设计

AI动态权益定价系统架构:智能数字权益管理平台商业化设计

AI动态权益定价系统架构:智能数字权益管理平台商业化设计 摘要/引言 在当今数字化经济飞速发展的时代,智能数字权益管理平台在企业的运营和商业拓展中扮演着越来越重要的角色。然而,传统的权益定价方式往往基于静态规则和历史数据,难以适应市场的快速变化和用户的多样化需…

2026/7/8 5:28:55 阅读更多 →
Google排名优化需要做什么?深度实战避坑指南

Google排名优化需要做什么?深度实战避坑指南

做外推和SEO的朋友这两年应该都有个明显的体感:以前那种“堆砌关键词发外链”的粗暴打法,效果越来越差了。现在的Google算法,尤其是经历了数次核心更新和“有用内容”(Helpful Content)系统升级后,变得比以…

2026/7/8 4:31:20 阅读更多 →

最新新闻

熬夜观赛适合喝哪款养生酒?聊聊复盛公六味地黄酒优势

熬夜观赛适合喝哪款养生酒?聊聊复盛公六味地黄酒优势

说实话,以前熬夜看球赛,我总习惯拿瓶啤酒或者红酒凑合,但第二天醒来总觉得头重脚轻、口干舌燥。后来朋友推荐我在观赛时试试养生酒,我研究了一圈,最后锁定了复盛公六味地黄酒,今天聊聊它在我这儿的真实体验…

2026/7/8 18:15:18 阅读更多 →
界面控件DevExtreme v26.1新版亮点——AI Agent Skills

界面控件DevExtreme v26.1新版亮点——AI Agent Skills

DevExtreme包括4个单独的产品套件,具有80多个UI组件和集成的开发模板/dev工具,帮助用户轻松创建高影响力和响应性的桌面和移动Web应用程序。 在接下来的系列文章中,我将为大家一一介绍DevExtreme v26.1在新版本中的更新亮点,希望…

2026/7/8 18:15:18 阅读更多 →
MATLAB版R-CNN汽车检测完整工程包:含样本图、训练脚本与检测演示

MATLAB版R-CNN汽车检测完整工程包:含样本图、训练脚本与检测演示

本文还有配套的精品资源,点击获取 简介:直接在MATLAB里跑起来的R-CNN汽车检测方案,带30张实拍道路车辆图(image_00001.jpg到image_00278.jpg),覆盖多角度、不同光照和部分遮挡场景。包里有RCNN.m主脚本、…

2026/7/8 18:13:18 阅读更多 →
2026封神!5款AI写作辅助软件实测,解决内耗焦虑,论文速成不熬夜!

2026封神!5款AI写作辅助软件实测,解决内耗焦虑,论文速成不熬夜!

对于学生、科研工作者而言,论文写作常常面临诸多挑战:文献资料筛选耗时冗长、格式排版反复调整、重复率控制难度大、逻辑结构梳理不清,这些问题严重制约了写作效率与学术成果的规范性。随着AI技术在2026年的持续突破,各类AI论文写…

2026/7/8 18:09:12 阅读更多 →
Java个人记账系统源码:SpringBoot后端+Thymeleaf页面,含MySQL建表脚本与完整分层结构

Java个人记账系统源码:SpringBoot后端+Thymeleaf页面,含MySQL建表脚本与完整分层结构

本文还有配套的精品资源,点击获取 简介:直接可运行的Java个人理财管理项目,后端用SpringBoot搭建,Controller、Service、DAO、Entity分层清晰,所有代码按标准Maven结构组织;前端用Thymeleaf渲染&#xf…

2026/7/8 18:07:11 阅读更多 →
Apriori和FP-Growth算法Python实现对比包(含可运行代码+详细说明)

Apriori和FP-Growth算法Python实现对比包(含可运行代码+详细说明)

本文还有配套的精品资源,点击获取 简介:提供两个主流关联规则挖掘算法的轻量级Python实现:Apriori和FP-Growth。每个算法封装为独立脚本(Apriori.py、FP_growth.py),支持灵活设置最小支持度、最小置信度…

2026/7/8 18:05:10 阅读更多 →

日新闻

3大核心能力重塑《明日方舟》游戏体验:MAA自动化助手的革命性突破

3大核心能力重塑《明日方舟》游戏体验:MAA自动化助手的革命性突破

3大核心能力重塑《明日方舟》游戏体验:MAA自动化助手的革命性突破 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目地址: …

2026/7/8 0:00:48 阅读更多 →
MyBatis 批量操作深度优化——从 N+1 到批处理的全路径

MyBatis 批量操作深度优化——从 N+1 到批处理的全路径

MyBatis 批量操作深度优化——从 N1 到批处理的全路径 一、从"功能正确"到"性能可接受"——MyBatis 批量操作的三段式进化 MyBatis 在日常增删改查场景中几乎是无感的——实体映射直观、SQL 控制灵活。但当数据量从千级上升到十万级、百万级,许…

2026/7/8 0:00:48 阅读更多 →
工业负载控制方案:TPD2015FN与PIC18F45K22应用解析

工业负载控制方案:TPD2015FN与PIC18F45K22应用解析

1. 工业负载控制方案概述在工业自动化、电机驱动和照明控制等高需求场景中,可靠地控制电感和电阻负载是核心挑战之一。TPD2015FN作为东芝的8通道高端智能功率开关IC,配合PIC18F45K22微控制器,能够构建一套稳定、高效的负载控制系统。这套组合…

2026/7/8 0:02:48 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/8 16:14:06 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/7 12:34:47 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/8 16:59:55 阅读更多 →

月新闻