追过野蜂 發表於 2025-8-11 00:00:00

Linux从fork到exec揭秘进程替换的全流程

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>前言</li><li>进程替换简单示范</li><li>进程替换函数exec*<ul class="second_class_ul"><li>int execl(const char* ,const char*,&hellip;)</li><li>int execlp(const char*,const char* ,&hellip;)</li><li>int execv(const char* path,char* const argc[])</li><li>int execvp(consy char* file,const char* arg[])</li><li>int execle(const char* path.const arg,&hellip;,char* const envp[])</li></ul></li><li>总计<ul class="second_class_ul"></ul></li><li>Linux 下用select实现串口数据读取方法<ul class="second_class_ul"><li>示例代码(使用 select 实现)</li><li>代码解释</li></ul></li></ul></div><p class="maodian"></p><h2>前言</h2>
<p>关于进程控制,从创建进程的fork,进程等待对子进程进行回收在前面的博客中都有谈到。fork创建进程:子进程与父进程共用一份代码和数据,修改时进行写实拷贝。那如果我不希望创建的子进程依旧与父进程执行同一份代码,而是希望子进程能够执行一个全新的程序,那要怎么办呢???</p>
<p><strong>使用fork的返回值将父子进程进行分流???</strong>*</p>
<ul><li>这种方法是可以的,但是你还要将全新的程序的源代码植入到源程序中,这势必会浪费内存资源以及编译时间;</li><li>并且还有语言要求,必须是使用相同编程语言进行编写的可执行程序才能将代码插入;</li><li>我们希望的是可执行程序就在我的目录中让子进程直接去运行。此时就可以使用进程替换来实现,进程替换将当前进程替换成其他进程,以下将对进程替换的接口以及原理进行剖析。</li></ul>
<p class="maodian"></p><h2>进程替换简单示范</h2>
<p>先介绍一个进程替换接口,看看猪跑;</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115950a8.png" /></p>
<p>程序输出:</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115950109.png" /></p>
<p>上面代码预期结果:打印进程的PID,进程替换,再打印进程的PID;<br />实际结果:打印进程PID,进程替换,进程结束没有再打印进程PID。</p>
<p>根据上面输出结果不然分析,进程替换将进程的代码和数据都替换了 ,所以源代码后面的内容没有被执行。</p>
<p><strong>那么进程的PCB结构体对象有没有被替换呢???</strong></p>
<p>再看一段代码:</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115950422.png" /></p>
<p>程序输出:</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115951208.png" /></p>
<p>子进程进程替换后其PID并没有改变,说明进程的PCB对象用的还是原来的,只不过可能内部数据进行了一定的修改。</p>
<p>所以:<strong>程序替换不会创建新的进程,而是将进程的代码和数据进行修改;execl后续的代码被替换了所以没有执行,如果替换失败才可能执行</strong> 。<br />进程替换大致可以分为3个步骤:</p>
<ul><li>将进程的代码和数据进行替换,将原进程的栈和堆区进行刷新;</li><li>将页表中的映射关系进行修改,让进程地址空间能准确地映射到新代码的各个位置;</li><li>从新程序的main函数开始执行。</li></ul>
<p>进程替换后父进程仍然找到子进程的原因:子进程进行进程替换后,只有物理内存上进行了修改,子进程的PCB对象还是原来的,PID没有改变,所以父进程依旧可以拿着原来的PID找到子进程对其进行回收。</p>
<p>补充:程序替换后,CPU是怎么知道从哪里再开始执行的???</p>
<p>Linux下形成的可执行程序不是杂乱无章的,其有自己的格式&mdash;ELF,在可执行程序的起始位置有表头(专门存放可执行程序各个区段的起始位置),其中就记录了可执行程序的入口。</p>
<p class="maodian"></p><h2>进程替换函数exec*</h2>
<p>在Linux中进程替换的函数有很多,但是所以的接口都是以exec开头的,其中我们开发过程中最常用的有6个,看似很多但是基本上都是大同小异。下面对一一介绍。</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115951Z4.png" /></p>
<p>想要执行一个可执行程序,分两步:</p>
<p>告诉操作系统可执行程序在哪; 告诉操作系统要怎么样执行这个程序。<br />下面这些接口都是为了解决这两个问题,只不过处理的方式不一样。</p>
<p class="maodian"></p><h3>int execl(const char* ,const char*,&hellip;)</h3>
<p>int execl(const char* <strong>path</strong> ,const char* arg,&hellip;)第一个参数表示可执行程序的路径,在哪一个目录小;后面的参数是可变参数表示执行这个可执行程序需要带那些选项,相当于main函数的参数。</p>
<p>通过第一个参数告诉操作系统程序的位置,后面的可变参数告诉操作系统如何执行,注意:可变参数要以NULL结尾。</p>
<ul><li>关于路径,如果是自己的可执行程序,就自己手动输入路径;如果是系统提供的指令,其可执行程序一般放在/usr/bin路径下;</li><li>对于后面的可变参数,即选项:<strong>在命令行上是怎么输入的,在选项中怎么写,只不过不是用空格进行分割,而是使用字符串,以及结尾要添加NULL</strong>。</li></ul>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q11159513H.png" /></p>
<p class="maodian"></p><h3>int execlp(const char*,const char* ,&hellip;)</h3>
<p>int execlp(const char* <strong>file</strong>,const char* arg,&hellip;)与上面接口相比,该函数的区别好像就是函数名称和参数名称不一样呀。</p>
<p>是的,execl第一个参数path是路径,而execpl第一个参数是file是文件名称。<strong>execpl只有文件名称,那操作系统怎么知道找那个目录下查找这个文件</strong>,execpl接口默认了查找路径为环境变量PATH中的路径,就是指令存放的路径。</p>
<p>所以execpl函数就不需要指定路径,直接指定文件名称即可。所以如果先通过execpl进程替换为自己的可执行程序就要先将可执行程序添加到这些目录中,或将可执行程序所在的目录加到PATH下。</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115951V2.png" /></p>
<p class="maodian"></p><h3>int execv(const char* path,char* const argc[])</h3>
<p>第一个参数不用说跟上面的一样,还是指定路径。而后面不再是可变参数了,而是一个字符串数组和。<br />execv将execl后面的可变参数进行了整合,以一个数组的方式进行传递。就是main函数参数中的char* argv。将后面的argv数组传递给可执行程序中main的参数进而执行。上面的可变参数在底层也会被转换成数组的形式。</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115951J3.png" /></p>
<p class="maodian"></p><h3>int execvp(consy char* file,const char* arg[])</h3>
<p>与execv区别在于其有默认查找文件的路径PATH,只需要指定可执行程序的名称即可。</p>
<p class="maodian"></p><h3>int execle(const char* path.const arg,&hellip;,char* const envp[])</h3>
<p>此接口与execl不同于后面多了一个参数envp,此接口运行我们在进程替换之后设置新进程的环境变量。</p>
<p style="text-align:center"><img alt="" src="https://zhuji.jb51.net/uploads/allimg/20250811/2-250Q1115952537.png" /></p>
<p>注意设置环境变量后,原本的环境变量会被覆盖。</p>
<p class="maodian"></p><h2>总计</h2>
<p>上面各个库函数名称的规律:</p>
<ul><li>l(list):表示参数采用列表;</li><li>v(vector):参数用数组;</li><li>p(path):有p表示自动搜索路径环境变量PATH;</li><li>e(env):表示自己维护环境变量。</li></ul>
<table><tbody><tr><th style="text-align: center;"><strong>函数名</strong></th><th style="text-align: center;"><strong>参数格式</strong></th><th style="text-align: center;"><strong>是否带路径</strong></th><th style="text-align: center;"><strong>是否使用当前环境变量</strong></th></tr><tr><td style="text-align:center"><strong>execl</strong></td><td style="text-align:center"><strong>列表</strong></td><td style="text-align:center"><strong>不带</strong></td><td style="text-align:center"><strong>继承</strong></td></tr><tr><td style="text-align:center"><strong>execlp</strong></td><td style="text-align:center"><strong>列表</strong></td><td style="text-align:center"><strong>带</strong></td><td style="text-align:center"><strong>继承</strong></td></tr><tr><td style="text-align:center"><strong>execle</strong></td><td style="text-align:center"><strong>列表</strong></td><td style="text-align:center"><strong>不带</strong></td><td style="text-align:center"><strong>自己组装</strong></td></tr><tr><td style="text-align:center"><strong>execv</strong></td><td style="text-align:center"><strong>数组</strong></td><td style="text-align:center"><strong>不带</strong></td><td style="text-align:center"><strong>继承</strong></td></tr><tr><td style="text-align:center"><strong>execvp</strong></td><td style="text-align:center"><strong>数组</strong></td><td style="text-align:center"><strong>带</strong></td><td style="text-align:center"><strong>继承</strong></td></tr><tr><td style="text-align:center"><strong>execve</strong></td><td style="text-align:center"><strong>数组</strong></td><td style="text-align:center"><strong>不带</strong></td><td style="text-align:center"><strong>自己组装</strong></td></tr></tbody></table>
<p>exec可以去调用任何进程,不论是用什么语言进行编写的,只要是可实现程序,都能进行替换,exec相当于一个加载器,可以将各种程序加载起来 。</p>
<p><strong>在execlv中可以指定的传环境变量,那么其他没有传环境环境变量的接口怎么办???</strong></p>
<p>环境变量也是数据,在进程地址空间中也为环境环境变量和命令行参数分配了空间,子进程也会拷贝一份父进程的环境变量;当进行进程替换的时候如果没有指定修改或覆盖环境变量,那么环境依旧是原来替换之前的。</p>
<p><strong>如果想要对替换后的进程的环境变量修改怎么办???</strong></p>
<p>上面的execle是一种方法,但是还有一种方法:</p>
<ul><li>覆盖式:使用execle接口,将原来进程的环境变量全部进行覆盖,此时原来的环境变量不能再使用,用自己传的那一份;</li><li>增加式:使用put()接口,eg:put(&ldquo;HELLO=135&rdquo;)接口将HELLO=135加入到环境变量中,再以继承的方式让替换后的进程也能使用;就相当于在原进程中增加了一个环境变量,让替换后的进程直接使用这一环境变量。</li></ul>
<p>补充:在上面有各种各样的函数实现进程替换,但是他们底层都调用同一个系统调用接口:execve(),上面的库函数都是对这个接口的封装而与。</p>
<p class="maodian"></p><h2>Linux 下用select实现串口数据读取方法</h2>
<p>在 Linux 系统里,我们可以借助 select、poll 或者 epoll 这些 I/O 多路复用机制达成串口数据读取的触发方式。这些机制能够让程序在特定文件描述符(像串口设备文件描述符)有数据可读时得到通知,进而进行数据读取操作,而不是像轮询方式那样持续调用 read 函数。</p>
<p class="maodian"></p><h3>示例代码(使用 select 实现)</h3>
<div class="dxycode"><pre class="brush:cpp;">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;termios.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/select.h&gt;
// 设置串口波特率
void SetSpeed(int fd, int speed) {
    struct termios options;
    tcgetattr(fd, &amp;options);
    switch (speed) {
      case 115200:
            cfsetispeed(&amp;options, B115200);
            cfsetospeed(&amp;options, B115200);
            break;
      // 可以添加更多波特率设置
      default:
            cfsetispeed(&amp;options, B115200);
            cfsetospeed(&amp;options, B115200);
            break;
    }
    tcsetattr(fd, TCSANOW, &amp;options);
}
// 设置串口参数
void SetParity(int fd, int databits, int stopbits, char parity) {
    struct termios options;
    tcgetattr(fd, &amp;options);
    // 设置数据位
    options.c_cflag &amp;= ~CSIZE;
    switch (databits) {
      case 7:
            options.c_cflag |= CS7;
            break;
      case 8:
            options.c_cflag |= CS8;
            break;
      default:
            options.c_cflag |= CS8;
            break;
    }
    // 设置奇偶校验位
    switch (parity) {
      case 'n':
      case 'N':
            options.c_cflag &amp;= ~PARENB;
            options.c_cflag &amp;= ~PARODD;
            break;
      case 'o':
      case 'O':
            options.c_cflag |= (PARODD | PARENB);
            break;
      case 'e':
      case 'E':
            options.c_cflag |= PARENB;
            options.c_cflag &amp;= ~PARODD;
            break;
      default:
            options.c_cflag &amp;= ~PARENB;
            options.c_cflag &amp;= ~PARODD;
            break;
    }
    // 设置停止位
    switch (stopbits) {
      case 1:
            options.c_cflag &amp;= ~CSTOPB;
            break;
      case 2:
            options.c_cflag |= CSTOPB;
            break;
      default:
            options.c_cflag &amp;= ~CSTOPB;
            break;
    }
    tcsetattr(fd, TCSANOW, &amp;options);
}
int main(void) {
    int fd;
    unsigned char buf;
    unsigned short len;
    fd = open("/dev/ttyS1", O_RDWR);
    if (fd == -1) {
      perror("can't open serial\n");
      return -1;
    }
    SetParity(fd, 8, 1, 'n');
    SetSpeed(fd, 115200);
    fd_set readfds;
    struct timeval timeout;
    while (1) {
      // 清空文件描述符集
      FD_ZERO(&amp;readfds);
      // 将串口文件描述符加入读文件描述符集
      FD_SET(fd, &amp;readfds);
      // 设置超时时间
      timeout.tv_sec = 0;
      timeout.tv_usec = 20000;
      // 调用 select 函数等待事件发生
      int activity = select(fd + 1, &amp;readfds, NULL, NULL, &amp;timeout);
      if (activity &lt; 0) {
            perror("select error");
            break;
      } else if (activity &gt; 0) {
            // 检查是否是串口文件描述符有数据可读
            if (FD_ISSET(fd, &amp;readfds)) {
                len = read(fd, buf, 100);
                if (len &gt; 0) {
                  write(fd, buf, len);
                }
            }
      }
    }
    close(fd);
    return 0;
}</pre></div>
<p class="maodian"></p><h3>代码解释</h3>
<ol><li><strong><code>SetSpeed</code>&nbsp;函数</strong>:此函数用于设置串口的波特率,它借助&nbsp;<code>tcsetattr</code>&nbsp;函数来配置串口的输入和输出波特率。</li><li><strong><code>SetParity</code>&nbsp;函数</strong>:该函数用于设置串口的数据位、停止位和奇偶校验位,同样是通过&nbsp;<code>tcsetattr</code>&nbsp;函数来完成串口参数的配置。</li><li><strong><code>main</code>&nbsp;函数</strong>:<ul><li>打开串口设备文件&nbsp;<code>/dev/ttyS1</code>,并且设置串口参数。</li><li>构建一个&nbsp;<code>fd_set</code>&nbsp;类型的文件描述符集&nbsp;<code>readfds</code>,把串口文件描述符&nbsp;<code>fd</code>&nbsp;添加到该集合中。</li><li>设定一个超时时间&nbsp;<code>timeout</code>,防止&nbsp;<code>select</code>&nbsp;函数一直阻塞。</li><li>调用&nbsp;<code>select</code>&nbsp;函数等待事件发生,若有数据可读,<code>select</code>&nbsp;函数会返回大于 0 的值。</li><li>利用&nbsp;<code>FD_ISSET</code>&nbsp;宏检查是否是串口文件描述符有数据可读,若有则调用&nbsp;<code>read</code>&nbsp;函数读取数据,然后将数据回写到串口。</li></ul></li></ol>
<p>通过这种方式,程序就无需持续轮询&nbsp;<code>read</code>&nbsp;函数,而是在有数据到达时才进行读取操作。</p>
<p>以上就是【Linux手册】进程替换:从fork到exec揭秘进程替换的全流程的详细内容,更多相关资料请阅读琼殿技术社区其它文章!</p>
頁: [1]
查看完整版本: Linux从fork到exec揭秘进程替换的全流程