进程的程序替换

文章目录

一、进程的程序替换(exec系列的接口)

在Linux中,进程除了能通过fork创建子进程外,还可以通过exec系列函数进行进程替换。

为什么需要程序替换?

之前学习的fork(),创建的子进程执行的还是原来的代码。想让子进程运行新的程序运行,就要用程序替换(exec 系列函数)。进程不变,PCB 不变,只是代码和数据全换掉。

所谓程序替换,就是让一个正在运行的进程丢掉原来的程序映像,转而执行另一个可执行文件。

替换,简单理解:就是将磁盘中全新的程序 (代码和数据)覆盖式的加载到当前进程代码和数据 的位置,然后修改页表即可。程序替换本质上并不会创建新的进程(验证:可以在子输出一次pid,在替换之后再输出一次pid)

一旦程序替换成功,就去执行新代码。原来的代码将不再执行,已经不存在了

exec系列函数失败时才会返回 -1 并继续向下执行,成功时没有返回值,所以不需要做返回值判断,返回即失败

简单认识全部接口

补充:

(1)v是将一个个的参数,放在字符指针数组里;然后将数组整个的传给函数

(2)exec系列函数所有接口的调用关系

execve是系统调用,头文件为<unistd.h>

其他exec函数是库函数,头文件为<stdlib.h>,由C语言进行封装,底层会去调用系统调用execve

1. execl

execl 会将(当前进程的程序)替换为(新程序)

原进程的代码和数据被覆盖,PID保持不变,替换成功后不再返回,后续代码无法执行。

bash 复制代码
int execl(const char *pathname, const char *arg, ...);

它的第一个参数const char* pathname表明:我要执行谁 (要有路径+程序名);

剩下的参数表明:如何执行那个程序 (简单方法:命令行怎么写,我就怎么传。看下图)

execl中 l 可以理解为list风格的「变长参数列表」。(将命令行的字符串一个一个传给参数,即参数以逐个列举的形式传入。而list 风格:逐个列举)

(...代表可变参数列表)。最后一个参数必须是以NULL结尾(表明参数传递完成)

  1. execl程序替换之后的将不会再执行
bash 复制代码
#include <stdio.h>
#include <unistd.h>
int main()
{
	printf("replace begin\n");
	
	//这个效果和 ls -a -l效果一样
	execl("/usr/bin/ls","ls","-a","-l",NULL);
          // 程序路径   arg0,arg1,arg2,结束标记	
      
    //这个不会被执行,因为上一步是程序替换中的execl,
    //原来的代码(从 execl 往下)直接丢掉,不再执行
	printf("replaxe end\n");
	return 0;
}
  1. 程序替换的弊端:就是一个程序替换的代码,如果它一旦替换,就会影响到当前进程(会转而执行新程序,旧的后面就不再执行)。

解决办法:(让父进程安安心心执行这个程序,它常见其他子进程执行新的程序):

通过fork创建一个子进程,(if(fork()==0))让子进程去执行新程序,父进程在这里进行进程等待,之后继续执行原来的程序)

  • fork() 用于创建子进程。子进程和父进程最初运行的是相同的程序
  • exec() 用于替换子进程的内存映像,加载并运行新程序。(子进程一般会立刻调用 exec,以执行一个全新的程序)

子进程的代码数据都拷贝父进程的同时,这里父子进程也不再共享,子进程有自己新的代码。--->父子进程彻底分离

  1. 任何一个程序进入内存,必须先变为进程。所以加载程序 的本质就是动态创建进程的过程。

  2. 我们自己写的进程可以被替换吗?--------可以

    只要是(能转换为进程的)程序,都可以进行程序替换

(例子:原本的程序是C写的(test.c),现在想在c的程序中,将C++的程序(other.cc)调起来。可以在pro.c的程序中写:execl("./myother","myother",NULL);。这里没有选项,所以不需要写

  1. 程序替换是系统级行为。以下是原因:

(1)任何程序要运行,必须先加载为进程。只要是进程,就可以通过exec系列函数完成程序替换。

(2)程序替换的代码不是自己随便换,而是操作系统帮你把进程里的代码、数据全换掉。程序替换是操作系统提供的能力。

程序替换是系统级行为:程序运行必为进程,凡是进程均可替换。

  1. 程序替换是系统级能力,C 语言进程可替换执行一切能跑成进程的程序,跨编译型 / 脚本型语言都支持。

程序替换是操作系统层面的机制。在一个 C 语言进程里,可以通过程序替换,执行任何编译型语言、脚本语言编写的程序------ 只要它最终要在系统里运行,就必须被加载为进程;而只要是进程,系统都支持对其进行程序替换,从而实现跨语言调用(前端页面类语言除外)。

  1. 程序替换的核心 exec 本质就是系统加载器,能加载并运行各类程序:对编译型语言直接加载执行,对脚本语言则加载对应的解释器来运行,实现一个进程执行任意程序。

2. execlp

bash 复制代码
int execlp(const char *file, const char *arg, ... );

execlp中,p是PATH,因为execlp会自动在环境变量PATH中查找指定命令,所以不用路径,只需要文件名即可。

l的含义同上:将参数以列表的形式传递

bash 复制代码
execlp("ls","ls","-a","-l",NULL);

3. execv

没有p(path)就说明:这里需要写路径(绝对/相对路径)

v表示vector(数组):第二个参数以数组呈现,即:字符指针数组,曾经命令行参数以列表形式传,现在将这些字符串"ls","a","l",一个一个放在数组,整体传进来。也以NULL结尾

cpp 复制代码
#include <stdio.h>
#include<unistd.h>
 
int main()
{
    char* const argv[]={
        (char* const)"ls",
        (char* const)"-l",
        NULL
    };  
 
    printf("开始\n");
    execv("/usr/bin/ls",argv);                                                                                                                                         
    printf("结束\n");
}

知识点:argv:所有命令参数的指针数组整体。argv0:数组首元素,固定为当前程序名


问题1:为什么是字符指针数组,而不是字符数组呢?

因为我们的命令行参数:"ls","a","l",它们是n个独立字符串。如果 用字符数组:char argv\[\] = "ls -l /home";内核根本分不清:哪一段是命令名/参数 1/参数 2,它只会当成一整个参数。。字符指针数组 char *argv\[\]是多个独立字符串的地址列表,内核一看就懂:第 0 项:命令名。第 1 项:参数 1。第 2 项:参数 2


问题2:char *argv[] = { "ls", "-l", "/home", NULL };中括号里的是字符串,为什么类型是字符指针


问题3:ls是不是一个二进制程序?

是的。

先了解什么是二进制文件:代码.c给程序员看的文字。经过gcc编译后生成的文件(比如 a.out、ls)就是电脑CPU能直接跑的机器码,这叫做编译好的二进制程序。(不是文本,打开是乱码;不能直接编辑修改;电脑加载进去就能直接跑;是ELF格式(Linux)、EXE 格式(Windows)

(1)当在命令行敲 ls,就是运行这个二进制程序。它不是脚本、不是别名,就是真正的程序本体。


问题4:我们自己程序的命令行参数谁传的,怎么传?

任何一个进程的命令行参数 = 父进程通过 exec 传进来的

在bash里敲命令 → bash把参数打包好 → 通过 exec 扔给你的程序。你的程序 main 函数的:int main(int argc, char *argv[])

(1)用户输入命令(终端运行bash父进程):./myprog a b c

(2)bash(父进程)解析命令,构造参数数组argv\[\]:

bash 复制代码
char *argv[] = {"./myprog", "a", "b", "c", NULL};

(3)bash 调用 fork() 创建子进程

(4)子进程调用 exec 系列函数。exec 替换子进程内存空间,变成新程序

(5)内核把 argv 数组直接塞进新程序的 main 函数里

同时,这也是LInux/Unix 程序启动的唯一标准流程


问题5:所有程序都是 bash 的子进程?父进程用 exec 传参给子进程?

在终端敲的所有命令,全是 bash 的子进程

命令行里的所有程序,都是 bash 生的子进程,参数全靠 exec 传递。


4. execvpe

  1. execvpe中:
    v的意思是:将命令行参数以(字符指针)数组的形式传递。
    p的意思是:会去环境变量找路径,不用自己写路径,直接传文件名。
    e的意思是:使用带e的exec系列函数,会舍弃继承的环境变量,而强制子进程仅使用自定义传入的环境变量。

详解e :e 对应环境变量environment,接收char* const envp[]环境指针数组;无 e 函数默认继承父进程环境全局表,携带 e 的 exec 系统调用会完全替换环境上下文,子进程独立使用自定义envp,不再继承父进程环境。

如果自己传了envp,就是用自己的。没传,就是用默认的(语言级别的environ),即从父进程的进程地址空间继承的表


问题1:什么是环境上下文?

环境上下文 = 该进程能看到的所有环境变量的整体,比如PATH,HOME,USER,LANG,PWD。这些变量合在一起,就叫环境上下文。


问题2:新增环境变量

我们还可以以新增的方式将env传给子进程,将传进去的环境变量加在原来的环境变量列表后面


问题3:创建子进程时,为什么父进程不传环境变量和命令行参数,子进程也能获得?

在 Linux 系统中,每个进程都拥有独立于堆、栈的专属内存区域,用来存放命令行参数与环境变量,并通过全局指针extern char** environ维护环境变量表;调用fork()创建子进程时,会完整拷贝父进程的地址空间,自动复刻环境变量与参数信息。不带e的 exec 函数会默认沿用全局environ,让新进程天然继承父进程全部环境上下文;而携带e的 exec 调用,会舍弃继承的所有环境配置,以开发者手动传入的envp\[\]字符指针数组作为新进程唯一的环境变量集合,完成环境上下文的完全替换。


  1. 查看环境变量可以用env,得到环境变量那个值,可以使用getenv

int putenv(char* string); putenv是在该进程的环境变量表里,新增一个环境变量

(1)改自身进程环境(全局继承式):

在当前进程直接用putenv()/setenv()新增环境变量,不会覆盖,是追加 / 修改父进程原有环境;后续调用不带 e的 exec系列函数,子进程自动继承原环境 + 你新增的变量。

即:我们直接在本进程里使用putenv()新增环境变量,之后创建子进程,调用exec后,子进程就自动继承了父的所有环境变量(包括旧的+新增的部分)

把xx用yy替换:%s/xx/yy/g

二、Shell(命令行解释器)

1. shell的核心工作流程

bash 复制代码
while(true) {
    1. 显示命令提示符
    2. 读取用户输入的命令
    3. 解析命令(分割成程序名和参数)
    4. fork创建子进程
    5. 子进程exec执行命令
    6. 父进程wait等待子进程
}
bash 复制代码
while(1) {
    // 1. 显示提示符
    printf("[user@host dir]$ ");
    // 2. 读取命令
    fgets(command, sizeof(command), stdin);
    // 3. 解析命令
    parse(command, argv);
    
    // 4. 创建子进程
    pid_t id = fork();
    if(id == 0) {
        // 5. 子进程执行命令
        execvp(argv[0], argv);
        exit(1);
    }
    else {
        // 6. 父进程等待
        waitpid(id, &status, 0);
    }
}

2. 小知识点补充

  1. 什么是内建命令,外部命令

内建命令:不需要跑到硬盘找独立二进制程序,直接藏在 Shell(bash)内部、自带的小命令。输入命令时,不创建子进程,Shell 自己当场执行。
ls:外部命令,是 /usr/bin/ls 独立二进制文件,bash 要 fork+exec 建子进程运行。
cd、exit、export:内建命令,没有单独的程序文件,就在 bash 代码里写死的,直接本机执行,不开子进程。

外部命令:必须 fork + exec 替换子进程

内建命令:不走 fork、不走 exec,直接在当前 bash 进程里运行

内建命令是 Shell 程序内部内置的指令,无独立可执行文件,执行时:不fork子进程、不调用exec系列函数,由当前Shell进程直接解析运行

  1. 为什么cd不是外部命令,而是内建?
    设想一个,fork一个子进程去执行切换路径的命令,确实是可以切换。但是子进程执行cd,那肯定是cd自己的,子进程退出后,父进程丝毫不会改变,还在原来的目录。所以cd命令不应该由子进程执行,应该由shell亲自执行

3. 显示命令提示符(接口)

获取用户名的函数:可通过环境变量表知道。获取环境变量的内容:char *getenv(const char *name);其他同理

再将函数进行封装,达到将命令行提示符打印出来的效果

4. 读取用户输入的命令

  1. 一旦有命令行之后,bash就卡在这里不动了,在等什么?等待用户输入
  2. 在现有的shell中,用户可以让shell做几次事情?无数件 (shell完成一件事情,就等着下一件,所以等待用户输入是需要循环的 )(当用户输入一条命令,回车,shell完成后会继续等待用户输入)

知识点:

用户输入ls -a -l,bash需要理解为"ls -a -l"字符串

scanf是以空格为分隔符。scanf会弄成3个字符串"ls" "-a" "-l",所以完成任务,不使用scanf,使用fgets

fgets简介:

bash 复制代码
char *fgets(char *str, int n, FILE *stream);

从指定的文件流(键盘stdin或文件)中读取一行字符,最多读取n-1个字符,自动在末尾添加\0构成合法C字符串。

输入的命令带回车,(那显式打印一下,也会有回车),如何去掉?

修改被写入的数组,将数组最后一个成员(回车)变成 0(即:清理\n)

5. 解析命令行

走到这里,说明用户输入的命令已经获取成功

  1. 我们获取到的是一串字符串"ls -a -l",在之前程序替换中,参数可以以"列表"传,以数组传,但是不能一个字符串传,所以第一步:将字符串拆分(这个命令不能直接在shell中进行,需要创建子进程去完成命令,所以这一步为后续的程序替换做铺垫了)

  2. 系统自带的shell会默认去获取:用户的命令输入,再把用户输入的信息构建成一张命令行参数表(在shell内部维护)

    所以第二步=:将之前拆分的命令行参数,放入到全局的命令行参数表中。(首先,先在myshell中,创建一个全局命令行参数表,并将参数个数初始化为0)

(修改:将return true修改为return g_argc > 0 ? true : false;)

通过这一步:获取到了命令函参数表

strtok的详细解析:

strtok 函数首次调用时,第一个参数传入待分割的原始字符串

(函数从字符串起始位置开始扫描,直至遇到指定分隔符后,截取当前位置之前的子串并返回,同时内部记录本次分割结束的位置)

后续对同一字符串的继续分割,第一个参数必须传入 NULL,函数会基于上一次记录的位置继续向后扫描、截取剩余部分,直至字符串末尾。

一直切一直切,最后会切到NULL。切到NULL就会去将argv最后一个内容置为NULL,符合命令行参数表的设定。赋值之后,会将argvargc作为while的判断条件,最后退出循环

无数次的拆分:通过循环来继续拆分,直到结束

6. 执行命令

获取到了命令函参数表,接下来执行。

但不是让当前进程去执行命令,当前进程有自己的任务。

7. 检查与处理内建命令

如果是内建命令,在封装的函数中,直接让shell执行即可,执行完回到main函数里,直接continue。(因为后面调用的是:外部命令的执行方法)

查看命令是否为内建命令:看g_argv0是否为内建命令之一即可

一个进程更改自己的工作路径,调用chdir,参数是:绝对/相对

让shell进程执行chdir

相关推荐
努力的小雨2 小时前
我用 QClaw 做了个 Web3 陪学助手,专治 Java 程序员的“概念劝退”
经验分享·ai智能
RainCity15 小时前
Java Swing 自定义组件库分享(十二)
java·笔记·后端
AlfredZhao2 天前
生产环境里,为什么不建议把普通端口直接暴露到公网?
linux·https·443·80
戴为沐3 天前
Linux内存扩容指南
linux
zylyehuo3 天前
Linux 彻底且安全地删除文件
linux
用户805533698034 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
用户034095297914 天前
linux fcitx 5 雾凇拼音 设置在中文输入法下仍然输入英文标点
linux
Web3探索者5 天前
可视化服务器管理和传统命令行区别是什么?新手教程:Linux 运维到底该用图形界面还是 SSH 命令行?
linux·ssh
zylyehuo5 天前
Linux系统中网线与USB网络共享冲突
linux
Sokach10157 天前
Linux Shell 脚本从零到能用:一个新手的一天学习总结
linux