news 2026/2/25 21:02:02

【操作系统】5.进程控制和shell的实现(附代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【操作系统】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. 输出提示符(如 [user@host path]$)

3. 获取和解析命令

4. 创建子进程执行外部命令

5. 内建命令 (Built-in Command)


一、进程创建

1.fork函数

  1. 分配资源:为子进程分配task_structmm_struct等内核数据结构。

  2. 拷贝数据:将父进程的 PCB(task_struct)、页表等拷贝到子进程中。

  3. 加入调度:将子进程添加到系统的进程列表中,并加入到调度队列的expired组。

  4. 返回fork函数返回。

  • 如果创建的子进程专门执行系统内核代码,这就是内核级虚拟机(如KVM)的原理。

2. 写时拷贝 (Copy-On-Write, COW)

  • 创建子进程时,操作系统会将父、子进程页表的映射权限设置为只读

  • 当子进程(或父进程)需要修改一个共享的变量时,虽然地址没问题,但权限不符,此时会触发写时拷贝

  • 操作系统会为修改者(子进程)重新开辟一块物理内存,拷贝数据,并修改其页表映射,最终完成写入。

3. 为什么要写时拷贝?

  1. 加快子进程创建速度:无需立即拷贝全部数据,创建瞬间非常快。

  2. 减少内存资源浪费:由于子进程通常不会使用父进程的全部数据,对于不修改的部分,父子可以共享,无需额外占用内存。

  • 写时拷贝是一种精细化的内存管理策略。

4.fork失败的原因

  1. 系统中进程总数过多。

  2. 用户的进程数超过了限制。

二、进程终止

1. 退出场景

  1. 正常结束,结果正确:返回0

  2. 正常结束,结果错误:返回非0(即退出码)。

  3. 异常结束:返回值无意义(程序被信号终止)。

2. 获取返回值

  • 使用echo $?命令可以打印最近一个进程的退出码。

  • 进程的退出码存储在task_struct结构体内。父进程回收僵尸进程时,可以从中获取退出码。

  • 再次执行echo $?会显示0,因为这时获取到的是echo命令本身的退出码。

3. 退出码标准

  • Linux 规定了一系列退出码,strerror函数可以将整数错误码转换为对应的字符串描述。​​​​​​​

  • 0133,共有 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. 为什么要等待子进程结束?

  1. 如果父进程不管子进程,子进程结束会变成僵尸进程,造成内存泄漏,即使kill命令也无法杀死僵尸进程。

  2. 父进程需要知道子进程的执行结果(是否成功、退出码等),以便进行后续处理。

2. 如何等待?

  1. 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:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } exit(0); } // 父进程 sleep(10); int i = wait(NULL); if(i > 0) printf("回收子进程:%d\n", i); return 0; }

  2. waitpid函数

    • pid_t waitpid(pid_t pid, int* status, int options)

    • pid_t:成功时返回被等待子进程的 PID。

    • pid_t pid:指定等待哪个子进程,-1表示等待任意子进程。

    • int options0表示阻塞等待;WNOHANG表示非阻塞轮询。

  3. 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命令查看信号编号与名称的对应关系。

  4. 通过位运算取出信息

    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 位(退出码)。

  5. 其他常见退出信号

    • 野指针:信号11(SIGSEGV)​​​​​​​

    • 除零错误:信号8(SIGFPE)​​​​​​​

  6. 使用宏简化判断

    • WIFEXITED(status):判断子进程是否正常退出(为真表示正常)。

    • WEXITSTATUS(status):提取子进程的退出码(仅在正常退出时有效)。

  7. 非阻塞等待 (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函数簇本质上是一个加载器,负责将磁盘上的可执行程序加载到内存并执行。

  1. execlp

    • p代表PATH环境变量。执行系统命令时,不需要写完整路径。

    execlp("ls", "ls", "-l", "-a", NULL);
    • 注意:第一个参数"ls"是去PATH中查找程序,第二个参数"ls"是传递给程序的argv[0]

  2. execv

    • v代表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; }

  3. execvp

    • vp的组合,既支持PATH查找,又使用指针数组传参。

  4. execvpe/execle

    • e代表environment,可以传递一个新的环境变量数组,覆盖原有的环境变量。

      • 新进程将使用env数组中的环境变量,原有的环境变量被覆盖。

    char* const argv[] = {"going", "a", "b", NULL}; char* const env[] = {"newenv=111", "newenv2=222", NULL}; execvpe("./other", argv, env);

  5. 如何新增环境变量而不覆盖?

    • 方法一:使用putenv函数修改当前进程的环境变量,然后使用execv(它会自动传递当前的环境变量)。

      putenv("a=222"); execv("./other", argv);
    • 方法二:先putenv,然后获取当前环境变量数组environ,再传给execvpe

      putenv("a=222"); extern char** environ; execvpe("./other", argv, environ);
  6. 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. 输出提示符(如[user@host 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 进程(父进程)获取上一个命令的退出状态,这无法通过创建子进程来完成。

代码

#include<iostream> #include<stdlib.h> #include<string.h> #include<stdio.h> #include<sys/types.h> #include<sys/wait.h> #include<unistd.h> using namespace std; const int MAX_COMMANDSIAE=1024; #define FORMAT "[%s@%s %s]# " const int MAX_SIZE=128; char* g_argv[MAX_SIZE]; int g_argc=0; const char* get_user() { const char* user=getenv("USER"); return user; } const char* get_hostname() { const char*hostname=getenv("HOSTNAME"); return hostname; } const char* get_pwd() { char cwd[100]; const char*pwd=getcwd(cwd,sizeof(cwd)); char p[100]; snprintf(p,sizeof(p),"PWD=%s",pwd); putenv(p); return pwd; } const char*get_home() { const char*home=getenv("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*command=fgets(c,size,stdin); if(command==nullptr)return 0; c[strlen(c)-1]=0; if(strlen(c)==0)return 0; return 1; } bool CommandSplice(char*commandline) { #define SEP " " g_argc=0; g_argv[g_argc++]=strtok(commandline,SEP); while(g_argv[g_argc++]=strtok(nullptr,SEP)); g_argc--; return 1; } void Excute() { int id=fork(); if(id==0) { execvp(g_argv[0],g_argv); exit(0); } int rid=waitpid(id,nullptr,0); } void cd() { if(g_argc==1) { string home=get_pwd(); chdir(home.c_str()); } else { string where=g_argv[1]; chdir(where.c_str()); } } bool builtin() { string met=g_argv[0]; if(met=="cd") { cd(); return 1; } return 0; } int main() { while(1) { printcommandprompt(); char commandline[MAX_COMMANDSIAE]; if(!GetCommandLine(commandline,sizeof(commandline)))continue; CommandSplice(commandline); if(g_argc==0)continue; if(builtin())continue; Excute(); } return 0; }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/25 10:50:40

宏智树AI——ChatGPT学术版驱动的论文写作全流程智能平台

学术创作的核心价值&#xff0c;在于思想的沉淀与观点的创新&#xff0c;而非被繁琐的流程消耗精力。宏智树AI作为专为论文写作量身打造的学术辅助平台&#xff0c;依托ChatGPT学术版模型的深度赋能与AI5.0技术架构的强大算力&#xff0c;构建起从大纲生成到最终定稿的一站式学…

作者头像 李华
网站建设 2026/2/25 16:07:10

AI取代前端?我的亲身经历:从失业到4x程序员的转型之路,必看收藏

本文分享了AI时代前端开发者的转型历程。作者从被裁员开始&#xff0c;通过社交媒体建立个人品牌&#xff0c;学习全栈开发&#xff0c;并利用AI工具(Cursor、Claude Code)将效率提升至4倍。面对AI可能取代传统前端的趋势&#xff0c;作者探索差异化路径&#xff0c;学习设计理…

作者头像 李华
网站建设 2026/2/15 9:40:59

2026年最值得all in的岗位:AI产品经理

文章指出AI产品经理是未来5年最有"钱"景的职业&#xff0c;分析了三类想进入AI领域人群的状态&#xff08;观望者、探索者、跑偏者&#xff09;&#xff0c;介绍了AI产品经理的三个层次&#xff08;工具型、应用型、专业型&#xff09;&#xff0c;强调应用型是普通人…

作者头像 李华
网站建设 2026/2/24 22:20:41

导师推荐9个AI论文网站,助你高效完成研究生毕业论文!

导师推荐9个AI论文网站&#xff0c;助你高效完成研究生毕业论文&#xff01; AI 工具助力论文写作&#xff0c;让学术之路更顺畅 在研究生阶段&#xff0c;撰写毕业论文是一项既重要又复杂的任务。随着人工智能技术的不断进步&#xff0c;AI 工具逐渐成为学生和科研工作者的重要…

作者头像 李华