【Linux】【实战向】Linux 进程替换避坑指南:从理解 bash 阻塞等待,到亲手实现能执行 ls/cd 的 Shell

前言:欢迎各位光临本博客,这里小编带你直接手撕,文章并不复杂,愿诸君耐其心性,忘却杂尘,道有所长!!!!

在这里插入图片描述


IF’Maxue:个人主页

 🔥 个人专栏:
《C语言》
《C++深度学习》
《Linux》
《数据结构》
《数学建模》

⛺️生活是默默的坚持,毅力是永久的享受。不破不立!

文章目录

    • 一、为啥sleep命令会“卡住”?——bash的阻塞等待
    • 二、进程替换到底是啥?——不建新进程,只换“代码和数据”
      • 关键问题:为啥exec系列函数“只有失败返回值”?
    • 三、exec系列接口怎么用?——5个函数,记住“字母含义”就够了
      • 1. execl:传全路径+参数列表
      • 2. execlp:不用写路径,靠PATH找
      • 3. execv:用数组传参数
      • 4. execvp:结合v和p的优点
      • 5. execvpe:自定义环境变量
    • 四、为啥子进程替换,父进程没事?——写时拷贝的“保护”
    • 五、进程替换能跨语言吗?——当然能!不管啥语言,能执行就行
      • 例子1:C程序执行C++程序
      • 例子2:C程序执行Python脚本
    • 六、怎么证明没建新进程?——看PID就知道
    • 七、自己写个简单Shell?——4步搞定:循环、获取命令、解析、执行
      • 1. 第一步:显示命令提示符(用户名@主机名:路径$)
      • 2. 第二步:获取用户输入(处理回车)
      • 3. 第三步:解析命令(按空格切割)
      • 4. 第四步:执行命令(子进程替换,父进程等待)
      • 问题:cd命令为啥没用?——内建命令的必要性
    • 总结

一、为啥sleep命令会“卡住”?——bash的阻塞等待

先从一个小问题切入:为啥用execl调用sleep时,程序像“卡住”了?
其实这不是卡住,是bash在等它执行完
比如你写了个程序,里面用execl调用系统的sleep命令(如图1、图2所示):

// 类似图中代码的逻辑
#include <unistd.h>
int main() {// 执行sleep 1秒:路径/bin/sleep,参数sleep、1,末尾NULLexecl("/bin/sleep", "sleep", "1", NULL); return 0;
}

当你运行这个程序时,bash(你的命令行父进程)会“阻塞等待”这个子进程(你写的程序)执行完。而你的程序又被sleep替换了,所以bash会等sleep跑完1秒才恢复,看起来就像“卡住”——这其实是正常的等待逻辑,如图1、图2里的执行效果所示。

image.png
(图1:程序执行execl调用sleep的效果)

image.png
(图2:sleep执行时bash阻塞等待的状态)

二、进程替换到底是啥?——不建新进程,只换“代码和数据”

很多人以为“执行新程序”就是“新建进程”,其实不是!进程替换的核心是:不创建新进程,只把当前进程的“代码和数据”换成新程序的

就像一个人(进程)换衣服(代码和数据),身份证(PID、PCB进程控制块)没变,还是同一个人,只是穿的衣服不一样了。如图3、图4所示:进程的“结构”(PID、PCB)早就建好,替换时只把里面的“代码段、数据段”覆盖掉。

image.png
(图3:进程替换前的结构,PID等信息已存在)

image.png
(图4:替换时只覆盖代码和数据,结构不变)

关键问题:为啥exec系列函数“只有失败返回值”?

你查man execl会发现:exec*函数成功时没有返回值,只有失败时返回-1(如图5所示)。为啥?

因为一旦替换成功,当前进程的代码已经被新程序覆盖了——原来的代码(包括exec之后的返回语句)全没了,根本没法返回!只有替换失败时,原来的代码还在,才能返回-1告诉你“没换成”。

image.png
(图5:man execl说明“只有错误时有返回值”)

比如图6的代码:execl之后写了printf("替换后执行");,但如果execl成功,这段代码会被覆盖,永远不会执行;只有execl失败(比如路径写错),才会打印“替换失败”。

image.png
(图6:exec成功后,后续代码不执行的验证)

三、exec系列接口怎么用?——5个函数,记住“字母含义”就够了

exec有5个常用函数:execlexeclpexecvexecvpexecvpe。不用死记,记住字母代表的意思:

  • l(list):参数用“列表”一个个传(比如sleep 1传成sleep, 1, NULL);
  • v(vector):参数用“数组”传(把参数放进字符数组,最后放NULL);
  • p(path):不用写全路径,系统会在PATH环境变量里找程序(比如直接写sleep,不用/bin/sleep);
  • e(environment):可以自定义环境变量(默认继承父进程环境变量)。

1. execl:传全路径+参数列表

格式:execl(全路径, 程序名, 参数1, 参数2, ..., NULL)
注意:

  • 第一个参数是“程序全路径”(比如/bin/sleep);
  • 第二个参数是“程序名”(和执行时的名字一致,比如sleep);
  • 后面跟参数,最后必须用NULL结尾(告诉系统参数传完了)。

如图7的代码,调用sleep 1

execl("/bin/sleep", "sleep", "1", NULL); // 正确,最后有NULL

如果少传NULL或路径错,会替换失败(如图8所示)。

image.png
(图7:execl的正确用法,全路径+NULL结尾)

image.png
(图8:少传NULL或路径错误导致替换失败)

2. execlp:不用写路径,靠PATH找

execl多了个p,意思是“用PATH找程序”。比如调用sleep,不用写/bin/sleep,直接写sleep

execlp("sleep", "sleep", "1", NULL); // 正确,系统会在PATH里找sleep

如图9的例子,执行ls时,execlp("ls", "ls", "-l", NULL)能成功,因为系统在PATH(比如/bin)里找到了ls

image.png
(图9:execlp用PATH找ls,不用写全路径)

3. execv:用数组传参数

execl多了个v,参数放进字符数组,最后放NULL。比如调用sleep 1

char* arg[] = {"sleep", "1", NULL}; // 参数数组,最后NULL
execv("/bin/sleep", arg); // 传数组,不用一个个写参数

如图10所示,数组必须以NULL结尾,否则系统会读错参数。

image.png
(图10:execv的参数数组必须NULL结尾)

4. execvp:结合v和p的优点

既可以用数组传参数,又不用写全路径(靠PATH找)。比如:

char* arg[] = {"sleep", "1", NULL};
execvp("sleep", arg); // 不用写路径,数组传参

如图11,执行ls -l时,execvp("ls", arg)能成功,不用写/bin/ls

image.png
(图11:execvp结合数组和PATH的用法)

5. execvpe:自定义环境变量

execvp多了个e,可以传自定义环境变量。默认情况下,子进程继承父进程的环境变量(比如PATH),但execvpe可以指定新的环境变量数组。

如图12,自定义env数组,传给execvpe,新程序会用这个环境变量:

char* arg[] = {"echo", "$MY_ENV", NULL};
char* env[] = {"MY_ENV=hello", NULL}; // 自定义环境变量
execvpe("echo", arg, env); // 传自定义env

image.png
(图12:execvpe传自定义环境变量)

四、为啥子进程替换,父进程没事?——写时拷贝的“保护”

如果直接在父进程里做替换,父进程会被改成新程序,这显然不行(比如bash自己不能被替换成sleep)。所以通常的做法是:fork一个子进程,让子进程做替换,父进程继续运行。

为啥子进程替换不影响父进程?因为Linux有“写时拷贝”机制:

  • 父进程和子进程一开始共享代码和数据;
  • 当子进程要修改代码(比如替换程序)时,系统会“拷贝一份代码”给子进程,让子进程改自己的拷贝,父进程的代码不变。

如图13的代码:fork后,子进程执行execl替换,父进程执行wait等待,父进程的代码没被改,所以能继续运行。

image.png
(图13:子进程做替换,父进程不受影响)

五、进程替换能跨语言吗?——当然能!不管啥语言,能执行就行

exec不管程序是用啥语言写的,只要是“可执行文件”(或脚本加了shebang),就能替换执行。比如:

  • C程序替换执行C++编译的程序;
  • C程序替换执行Python脚本。

例子1:C程序执行C++程序

  1. 先写个C++程序cpp_prog.cpp,编译成cpp_prog
    #include <iostream>
    int main() { std::cout << "我是C++程序" << std::endl; return 0; }
    
  2. 写C程序c_prog.c,用execl执行cpp_prog
    #include <unistd.h>
    int main() {execl("./cpp_prog", "cpp_prog", NULL); // 执行C++编译的程序return 0;
    }
    

如图14所示,C程序能成功执行C++程序,输出“我是C++程序”。

image.png
(图14:C程序替换执行C++程序的效果)

例子2:C程序执行Python脚本

  1. 写Python脚本py_prog.py,开头加shebang(告诉系统用Python解释器):
    #!/usr/bin/python3
    print("我是Python程序")
    
  2. 给脚本加执行权限:chmod +x py_prog.py
  3. C程序用execl执行脚本:
    execl("./py_prog.py", "py_prog.py", NULL);
    

如图15所示,C程序能成功执行Python脚本,输出“我是Python程序”。

image.png
(图15:C程序替换执行Python脚本的效果)

六、怎么证明没建新进程?——看PID就知道

前面说“替换不建新进程”,怎么证明?打印PID,替换前后PID不变,就说明没建新进程

如图16的代码:

  1. 先打印当前进程PID(getpid());
  2. 然后用execl替换成sleep,并让sleep执行echo $$$$是当前进程PID);
  3. 替换后,echo $$打印的PID和之前一致,说明还是同一个进程。

image.png
(图16:打印PID证明替换不建新进程)

如图17的输出,替换前打印的PID是1234,替换后sleep执行echo $$也打印1234,说明没建新进程。

image.png
(图17:PID不变的输出结果)

七、自己写个简单Shell?——4步搞定:循环、获取命令、解析、执行

Shell的核心是“死循环”:不断获取用户命令→解析命令→执行命令。我们一步步实现:

1. 第一步:显示命令提示符(用户名@主机名:路径$)

要显示类似user@ubuntu:~/test$的提示符,需要获取3个信息:

  • 用户名:用getenv("USER")(环境变量里有);
  • 主机名:用gethostname()函数;
  • 当前路径:用getcwd()函数。

如图18的代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {char host[256];char cwd[1024];gethostname(host, sizeof(host)); // 获取主机名getcwd(cwd, sizeof(cwd));       // 获取当前路径// 打印提示符:用户名@主机名:路径$printf("%s@%s:%s$ ", getenv("USER"), host, cwd);return 0;
}

image.png
(图18:获取用户名、主机名、路径的代码)

2. 第二步:获取用户输入(处理回车)

fgets获取用户输入(scanf会被空格截断,不适合命令行),但fgets会把末尾的回车(\n)也读进来,需要去掉。

如图19的代码:

char buf[1024];
fgets(buf, sizeof(buf), stdin); // 获取输入,比如输入"ls -l\n"
// 去掉末尾的\n:把\n换成\0
buf[strcspn(buf, "\n")] = '\0'; // strcspn找\n的位置,换成结束符

image.png
(图19:用strcspn去掉输入中的回车)

3. 第三步:解析命令(按空格切割)

strtok函数按空格切割命令,比如把“ls -l”切成“ls”和“-l”,放进参数数组(最后放NULL)。

如图20的代码:

char* arg[64] = {NULL}; // 参数数组,最多64个参数
int i = 0;
// 第一次调用strtok:传buf和分隔符" "
arg[i] = strtok(buf, " "); 
while (arg[i] != NULL) {i++;// 后续调用:第一个参数传NULL,用上次的位置继续切arg[i] = strtok(NULL, " "); 
}
// 循环结束后,arg = {"ls", "-l", NULL}

image.png
(图20:用strtok解析命令的代码)

4. 第四步:执行命令(子进程替换,父进程等待)

fork子进程,子进程用execvp执行命令(不用写路径,数组传参),父进程用wait等待子进程结束。

如图21的完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>int main() {while (1) { // 死循环,Shell不退出// 1. 显示提示符char host[256], cwd[1024];gethostname(host, sizeof(host));getcwd(cwd, sizeof(cwd));printf("%s@%s:%s$ ", getenv("USER"), host, cwd);// 2. 获取输入char buf[1024] = {0};fgets(buf, sizeof(buf), stdin);buf[strcspn(buf, "\n")] = '\0'; // 去掉\n// 3. 解析命令char* arg[64] = {NULL};int i = 0;arg[i] = strtok(buf, " ");while (arg[i] != NULL) {i++;arg[i] = strtok(NULL, " ");}if (arg[0] == NULL) continue; // 输入为空,跳过// 4. 执行命令pid_t pid = fork();if (pid == 0) { // 子进程execvp(arg[0], arg); // 执行命令exit(1); // 替换失败才会到这步} else if (pid > 0) { // 父进程wait(NULL); // 等待子进程结束}}return 0;
}

image.png
(图21:简单Shell的完整代码)

问题:cd命令为啥没用?——内建命令的必要性

上面的Shell能执行lssleep,但执行cd ~会发现路径没变化。为啥?
因为cd是“内建命令”——它需要修改当前Shell进程(父进程)的路径,而不是子进程的路径。如果让子进程执行cd,子进程的路径改了,但子进程退出后,父进程的路径没变,等于白改。

解决办法:父进程自己执行chdir函数(不用子进程),比如判断如果命令是cd,就调用chdir

if (strcmp(arg[0], "cd") == 0) {if (arg[1] != NULL) chdir(arg[1]); // 父进程自己改路径continue;
}

这种“父进程亲自执行”的命令,就是内建命令(比如cdexit)。

总结

  1. sleep卡住:不是卡住,是bash作为父进程在等待子进程(sleep)执行完;
  2. 进程替换:不建新进程,只替换当前进程的代码和数据,PID不变;
  3. exec函数:记l/v/p/e的含义,execlpexecvp最常用;
  4. 子进程替换:靠写时拷贝保护父进程,父进程不受影响;
  5. 自定义Shell:死循环+获取命令+解析命令+执行命令,内建命令需父进程亲自执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/96593.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/96593.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux常用命令 (3)——系统包管理

博客主页&#xff1a;christine-rr-CSDN博客 ​​​​​ ​​ hi&#xff0c;大家好&#xff0c;我是christine-rr ! 今天来分享一下linux常用命令——系统包管理 目录linux常用命令---系统包管理&#xff08;一&#xff09;Debian 系发行版&#xff08;Ubuntu、Debian、Linux …

YOLOv8 mac-intel芯片 部署指南

&#x1f680; 在 Jupyter Notebook 和 PyCharm 中使用 Conda 虚拟环境&#xff08;YOLOv8 部署指南&#xff0c;Python 3.9&#xff09; YOLOv8 是 Ultralytics 开源的最新目标检测模型&#xff0c;轻量高效&#xff0c;支持分类、检测、分割等多种任务。 在 Mac&#xff08;…

【高等数学】第十一章 曲线积分与曲面积分——第六节 高斯公式 通量与散度

上一节&#xff1a;【高等数学】第十一章 曲线积分与曲面积分——第五节 对坐标的曲面积分 总目录&#xff1a;【高等数学】 目录 文章目录1. 高斯公式2. 沿任意闭曲面的曲面积分为零的条件3. 通量与散度1. 高斯公式 设空间区域ΩΩΩ是由分片光滑的闭曲面ΣΣΣ所围成&#x…

IDEA试用过期,无法登录,重置方法

IDEA过期&#xff0c;重置方法: IntelliJ IDEA 2024.2.0.2 (亲测有效) 最新Idea重置办法!&#xff1a; 方法一&#xff1a; 1、删除C:\Users\{用户名}\AppData\Local\JetBrains\IntelliJIdea2024.2 下所有文件(注意&#xff1a;是子目录全部删除) 2、删除C:\Users\{用户名}\App…

创建用户自定义桥接网络并连接容器

1.创建用户自定义的 alpine-net 网络[roothost1 ~]# docker network create --driver bridge alpine-net 9f6d634e6bd7327163a9d83023e435da6d61bc6cf04c9d96001d1b64eefe4a712.列出 Docker 主机上的网络[roothost1 ~]# docker network ls NETWORK ID NAME DRIVER …

Vue3 + Vite + Element Plus web转为 Electron 应用,解决无法登录、隐藏自定义导航栏

如何在vue3 Vite Element Plus搭好的架构下转为 electron应用呢&#xff1f; https://www.electronjs.org/zh/docs/latest/官方文档 https://www.electronjs.org/zh/docs/latest/ 第一步&#xff1a;安装 electron相关依赖 npm install electron electron-builder concurr…

qt QAreaLegendMarker详解

1. 概述QAreaLegendMarker 是 Qt Charts 模块中的一部分&#xff0c;用于在图例&#xff08;Legend&#xff09;中表示 QAreaSeries 的标记。它负责显示区域图的图例项&#xff0c;通常包含区域颜色样例和对应的描述文字。图例标记和对应的区域图关联&#xff0c;显示区域的名称…

linux 函数 kstrtoul

kstrtoul 函数概述 kstrtoul 是 Linux 内核中的一个函数&#xff0c;用于将字符串转换为无符号长整型&#xff08;unsigned long&#xff09;。该函数定义在 <linux/kernel.h> 头文件中&#xff0c;常用于内核模块中解析用户空间传递的字符串参数。 函数原型 int kstrtou…

LLM(三)

一、人类反馈的强化学习&#xff08;RLHF&#xff09;微调的目标是通过指令&#xff0c;包括路径方法&#xff0c;进一步训练你的模型&#xff0c;使他们更好地理解人类的提示&#xff0c;并生成更像人类的回应。RLHF&#xff1a;使用人类反馈微调型语言模型&#xff0c;使用强…

DPO vs PPO,偏好优化的两条技术路径

1. 背景在大模型对齐&#xff08;alignment&#xff09;里&#xff0c;常见的两类方法是&#xff1a;PPO&#xff1a;强化学习经典算法&#xff0c;OpenAI 在 RLHF 里用它来“用奖励模型更新策略”。DPO&#xff1a;2023 年提出的新方法&#xff08;参考论文《Direct Preferenc…

BLE6.0信道探测,如何重构物联网设备的距离感知逻辑?

在物联网&#xff08;IoT&#xff09;无线通信技术快速渗透的当下&#xff0c;实现人与物、物与物之间对物理距离的感知响应能力已成为提升设备智能高度与人们交互体验的关键所在。当智能冰箱感知用户靠近而主动亮屏显示内部果蔬时、当门禁系统感知到授权人士靠近而主动开门时、…

【计算机 UTF-8 转换为本地编码的含义】

UTF-8 转换为本地编码的含义 详细解释一下"UTF-8转换为本地编码"的含义以及为什么在处理中文时这很重要。 基本概念 UTF-8 编码 国际标准&#xff1a;UTF-8 是一种能够表示世界上几乎所有字符的 Unicode 编码方式跨平台兼容&#xff1a;无论在哪里&#xff0c;UTF-8 …

4.6 变体

1.变体简介 2.为什么需要变体 3.变体是如何产生的 4.变体带来的麻烦 5.multi_compile和shader_feature1.变体简介 比如我们开了一家餐厅, 你有一本万能的菜单(Shader源代码), 上面包含了所有可能的菜式; 但是顾客每次来点餐时, 不可能将整本菜单都做一遍, 他们会根据今天有没有…

猿辅导Android开发面试题及参考答案(下)

为什么开发中要使用线程池,而不是直接创建线程(如控制线程数量、复用线程、降低开销)? 开发中优先使用线程池而非直接创建线程,核心原因是线程池能优化线程管理、降低资源消耗、提高系统稳定性,而直接创建线程存在难以解决的缺陷,具体如下: 控制线程数量,避免资源耗尽…

【网络通信】IP 地址深度解析:从技术原理到企业级应用​

IP 地址深度解析&#xff1a;从技术原理到企业级应用​ 文章目录IP 地址深度解析&#xff1a;从技术原理到企业级应用​前言一、基础认知&#xff1a;IP 地址的技术定位与核心特性​1.1 定义与网络层角色1.2 核心属性与表示法深化二、地址分类&#xff1a;从类别划分到无类别路…

grafana实践

一、如何找到grafana的插件目录 whereis grafana grafana: /etc/grafana /usr/share/grafana插件安装目录、默认安装目录&#xff1a; 把vertamedia-clickhouse-datasource-3.4.4.zip解压到下面目录&#xff0c;然后重启就可以了 /var/lib/grafana/plugins# 6. 设置权限 sudo …

uniapp 文件查找失败:main.js

重装HbuilderX vue.config.js 的 配置 有问题main.js 框架能自动识别 到&#xff0c;不用多余的配置

KEIL烧录时提示“SWD/JTAG communication failure”的解决方法

最新在使用JTAG仿真器串口下载调试程序时&#xff0c;老是下载不成功&#xff0c;识别不到芯片&#xff0c;我尝试重启keil5或者重新插拔仿真器连接线、甚至重启电脑也都不行&#xff0c;每次下载程序都提示如下信息&#xff1a;在确定硬件连接没有问题之后&#xff0c;就开始分…

红日靶场(三)——个人笔记

环境搭建 添加一张网卡&#xff08;仅主机模式&#xff09;&#xff0c;192.168.93.0/24 网段 开启centos&#xff0c;第一次运行&#xff0c;重启网络服务 service network restart192.168.43.57/24&#xff08;外网ip&#xff09; 192.168.93.100/24&#xff08;内网ip&am…

车载网关框架 --- 车内网关IP转CAN链路解析 done

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…