进程的程序替换

文章目录

一、进程的程序替换(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:所有命令参数的指针数组整体。argv[0]:数组首元素,固定为当前程序名


问题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,符合命令行参数表的设定。赋值之后,会将argv[argc]作为while的判断条件,最后退出循环

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

6. 执行命令

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

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

7. 检查与处理内建命令

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

查看命令是否为内建命令:看g_argv[0]是否为内建命令之一即可

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

让shell进程执行chdir

相关推荐
liulian09162 小时前
Flutter 依赖注入与设备信息库:get_it 与 device_info_plus 的 OpenHarmony 适配指南总结
flutter·华为·学习方法·harmonyos
划水的code搬运工小李2 小时前
ubuntu下使用opencode
linux·运维·ubuntu
ZPC82102 小时前
Ubuntu 实时性优化(专属定制版,适配 fast_shm 通信)
linux·数据库·postgresql
郝学胜-神的一滴2 小时前
epoll 边缘触发 vs 水平触发:从管道到套接字的深度实战
linux·服务器·开发语言·c++·网络协议·unix
萌新小码农‍2 小时前
机器学习概述 学习笔记day2
笔记·学习·机器学习
韩明君2 小时前
OpenClaw安全部署实现
linux·人工智能·安全·debian·本地部署·ai agent·openclaw
daanpdf2 小时前
大一《中国近代史纲要》题库及答案PDF知识点整理笔记
笔记·pdf
曦月逸霜2 小时前
区块链技术与应用学习笔记(持续更新中)
笔记·学习·区块链
代码中介商2 小时前
Linux 文件操作系统调用完全指南:从 open 到 close
linux·运维·服务器