【Linux系统】16. 进程程序替换

一. 程序替换的现象

1)通过现象直观看程序替换是什么

我们靠这些程序替换函数实现进程的程序替换(man 3 exec),先不用管他们具体怎么用,我们先看效果:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("进程: %d 运行中\n", getpid());

    // 使用程序替换函数
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

    printf("进程: %d 依旧在运行\n", getpid());
    printf("进程: %d 依旧在运行\n", getpid());
    printf("进程: %d 依旧在运行\n", getpid());
    printf("进程: %d 依旧在运行\n", getpid());

    return 0;
}

很明显,运行完ls -a -l之后程序并没有往下运行来打印 进程: 25039 依旧在运行 ,而是直接结束了。所以,程序替换的现象就是当前进程转而执行另一个程序,并且不会再转回来。

2)fork的两种用法

  1. 回到进程创建时,我们说过fork的两种用法:

之前我们一直用的是第一种,父进程创建子进程,子进程执行父进程的一部分,通过条件判断语句进行父子分流。但是我们还会有第二种需求,希望子进程去执行另外一个程序。这就需要用到程序替换函数(系统调用,及其封装)。

二. 程序替换的原理

操作系统会根据新程序代码和数据的大小对内存区域做调整,但进程的PCB没变,pid还是原来的pid,所以没创新进程。

三. 程序替换的本质

将磁盘上的代码和数据拷贝到内存当中,这是一个IO的过程。我们之前说过,因为体系结构规定,CPU必须与内存交互,所以程序运行之前必须要被加载到内存当中,那么程序是如何被加载到内存当中的呢?怎么做到的呢?
只有OS有资格做这种将数据从一个硬件搬到另一个硬件的工作,因为它是硬件的管理者。OS做这件事是应用户的请求,所以OS必须提供一个系统调用给用户来完成这个加载的过程。这个系统调用就是程序替换函数。
所以程序替换的本质就是加载

其实系统里所有进程在启动前都是一个加载器,这个加载器在磁盘上扫描要加载的数据,找到后调用程序替换函数,用对应的内容替换掉加载器的代码和数据,这样就把程序加载进来了。

四. 程序替换函数


1)父子进程是相互独立的。原本父子共享、指向同一块代码和数据段,当子进程要发生程序替换时,绝对不能直接替换,不然父进程的代码和数据也被换了。所以子进程要替换时,会发生**写时拷贝,**重新开辟自己的空间,从此父子彻底分离(PCB、代码、数据都不再共享)。

fork替换的这种用法最常见的就是bash,所以bash下的命令、子进程都可以执行自己的逻辑。



进行程序替换就是希望执行我们想要的程序,执行任何程序都需要:1. 我们要执行谁?找到并加载它-->需要路径+程序名。2. 我们想怎么运行?由程序和选项决定。

2)execl

  1. 程序替换函数的函数名前半部分都是exec,后面跟的字母都是有特殊含义的,l:代表list列表,这决定了传参方式都是以列表形式一个一个传。

  2. 参数含义

  3. 程序替换的函数的部分参数确实可以省略,不如下面的"ls",因为操作系统会自动做补全,但是非常不建议省略,有的可省有的不可省,弄来弄去记忆成本高还易出错。

  1. 所有程序替换函数在替换成功时都是没有返回值 ,失败时返回-1,并设置errno。

① 什么情况下会失败?路径错 或者 指令错。

演示一种路径错误的情况:将退出码设置为5,如果程序执行到exit表示替换失败。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    printf("进程: %d 运行中\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        // 使用程序替换函数
        execl("/usr/bin/lll", "ls", "-a", "-l", NULL);
        exit(5);
    }
   
    int status = 0;
    pid_t rid = waitpid(id, &status,0);
    if(rid > 0)
    {
        printf("wait success,exit code: %d\n", WEXITSTATUS(status));
    }

    return 0;
}

3)execlp

4)execv

5)execvp

6)execvpe

  1. execvpe的第三个参数envp是给替换后的新程序用的,而不是用来找这个替换程序的。

所以想找到我们自己的程序得先把它所在的路径添加到环境变量中(因为execvpe带p,只用传文件名他会到PATH中找)

bash 复制代码
[lsy@hcss-ecs-116a code_exec]$ cat ./cmd/othercmd.cpp
#include <iostream>
#include <stdio.h>

int main(int argc, char *argv[], char *env[])
{
    for(int i = 0; i < argc; i++)
    {
        printf("argv[%d]: %s\n", i, argv[i]);
    }
    std::cout << "\r\n";
    for(int j = 0; env[j]; j++)
    {
        printf("env[%d]->%s\n", j, env[j]);
    }
    std::cout << "成功替换为自己的C++程序" << std::endl;
    return 0;
}
bash 复制代码
[lsy@hcss-ecs-116a cmd]$ pwd
/home/lsy/code/code_exec/cmd
[lsy@hcss-ecs-116a cmd]$ PATH=$PATH:/home/lsy/code/code_exec/cmd
[lsy@hcss-ecs-116a cmd]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/lsy/.local/bin:/home/lsy/bin:/home/lsy/code/code_exec/cmd
cpp 复制代码
void test01()
{
    printf("进程:%d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        printf("当前是子进程\n");
        
        char* myargv[] = {(char*)"./othercmd", (char*)"-a", (char*)"-b", NULL};
        //char* myenv[] = {(char*)"PATH=/home/lsy/code/code_exec/cmd", NULL};
        char* myenv[] = {(char*)"PATH=llllssa", NULL}; // 不会替换失败,因为它不是用来找替换的文件的
       
        //extern char** environ;

        //execvpe("othercmd", myargv, environ);
        execvpe("othercmd", myargv, myenv);
        exit(5);
    }

    int status = 0;
    pid_t rid = waitpid(id, &status,0);
    if(rid > 0)
    {
        printf("wait success,exit code: %d\n", WEXITSTATUS(status));
    }
}
  1. 如果使用了execvpe就必须传第三个参数,也就是环境变量表:

① 如果想传自己的,就自定义一张环境变量表传给子进程。

② 如果不想改,就想用原来继承来的环境变量表,可以使用environ

cpp 复制代码
void test01()
{
    printf("进程:%d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        printf("当前是子进程\n");
        
        char* myargv[] = {(char*)"./othercmd", (char*)"-a", (char*)"-b", NULL};
        char* myenv[] = {(char*)"PATH=/home/lsy/code/code_exec/cmd", NULL};
       
        extern char** environ;
        execvpe("othercmd", myargv, environ);
        exit(5);
    }

    int status = 0;
    pid_t rid = waitpid(id, &status,0);
    if(rid > 0)
    {
        printf("wait success,exit code: %d\n", WEXITSTATUS(status));
    }
}

③ 想在原来的环境变量上新增或修改:putenv 。putenv不是为全局的环境变量新增,父子进程在进程地址空间中有各自不同的环境变量区,谁调用改变谁。

  1. l(list): 参数采用列表

  2. v(vector): 参数用数组

  3. p(PATH): 自动搜索环境变量PATH

  4. e(env): 表示可以自己传入一个自定义的环境变量数组,之后完全使用自定义的,舍弃原本继承来的。
    总结

  5. 子进程的环境变量不是必须继承父进程的,也可以根据自己的需求定制。

  6. 命令行参数表和环境变量表都是父进程通过exec系列的程序替换函数传递的

6)exec系列函数对比

  1. 只有execve 是真正的系统调用

  2. 其他六个都是C 标准库提供的封装函数,并不是系统提供的,底层都是调execve的。

7)可替换任意程序

程序替换不仅可以将系统命令替换进去,还可将自己写的任意语言替换进去 ,但是注意替换的是编译好的二进制程序,不是源文件。(比如实现用C语言的程序调C++、Python、Java...甚至shell、bash脚本都可以)

bash 复制代码
[lsy@hcss-ecs-116a cmd]$ cat othercmd.cpp
#include <iostream>
using namespace std;

int main()
{
    cout << "成功替换成自己的C++程序" << endl;
    return 0;
}
[lsy@hcss-ecs-116a cmd]$ cat test.sh
#!/bin/bash

echo "hello world"
echo "hello shell"
printf("hello exec");
[lsy@hcss-ecs-116a cmd]$ cat test.py
print("成功替换成自己的python");
cpp 复制代码
void test02()
{
    printf("当前进程pid: %d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        printf("当前是子进程:\n");
        //execl("./cmd/othercmd", "./othercmd", NULL);
        //execl("/usr/bin/bash", "bash", "./cmd/test.sh", NULL);
        execl("/usr/bin/python3", "python3", "./cmd/test.py", NULL);
        exit(5);
    }
    
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        printf("wait success,exit code: %d\n", WEXITSTATUS(status));
    }
}
相关推荐
70asunflower1 小时前
Ubuntu `tree` 命令完全指南:让目录结构一目了然
linux·数据库·ubuntu
ch3nyuyu2 小时前
IO缓冲区
linux·服务器
wheeldown2 小时前
2026年4月横评三款主流远控软件实况实测:UU远程,Todesk,向日葵,综合性能 UU 远程表现最佳
linux·运维·服务器
诗句藏于尽头2 小时前
CentOS 7 源码编译安装 Python 3.11 完整教程
linux·centos·python3.11
bksczm2 小时前
Linux之基础开发工具(Ubuntu)之apt 、vim
linux·ubuntu·php
java_logo2 小时前
Docker 部署 Open WebUI + Ollama 完整教程(Windows / Linux 通用)—— 打造自己的本地OpenAI
linux·docker·容器·ollama·open-webui·open-webui部署·open-webui教程
小夏子_riotous2 小时前
Docker学习路径——8、Dockerfile
linux·运维·docker·容器·系统架构·centos·运维开发
叶子上的考拉3 小时前
解决远程连接服务器反应较慢问题
linux·运维·服务器
JackSparrow4143 小时前
彻底理解Java NIO(一)C语言实现 单进程+多进程+多线程 阻塞式I/O 服务器详解
java·linux·c语言·网络·后端·tcp/ip·nio