30-换个姿势执行 ls 命令

本来本文的名字叫 《exec 系列函数》。可是想了想,exec 系列的函数有 6 个,实在是太多,而且功能也一样,实在没必要一一讲解。

exec 系列函数的目的,就是把本进程空间的代码和数据全部替换成你指定的数据,然后从新程序的入口点开始执行。本篇只介绍其中一个函数——execvp.

这里解释一下,函数名中的 v 代表 vector,表示参数是数组。p 代表 path,表示如果你指定的文件不包含路径,就从 path 环境变量指定的路径中搜索文件。

另外还有 execlp, execve ……,那么函数名中的字母 l 表示 list,表示函数参数是可变参数;字母 e 表示函数需要传递环境变量参数。

总结一下,exec 系列函数总体分成两大类:

  • 以 list 可变参数传参
    • execl
    • execlp
    • execle
  • 以 vector 数组传参
    • execv
    • execvp
    • execve

1 使用 execvp 启动 ls 命令

  • 代码
#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main() {
  char* argv[] = {"ls", "-l", NULL}; // 构造 vector,注意argv[0] 是占用参数
  if (execvp("ls", argv) == -1) { // 替换代码段和数据段,并重新从 ls 入口点执行
    perror("exec");
    return 1;
  }

  return 0;  
}
  • 编译
$ gcc myexec.c -o myexec
  • 运行
$ ./myexec
  • 结果

运行结果将和你使用 ls -l 命令一模一样。

2. 进程空间结构

前面花了大量篇幅讲解进程空间的概念,也知道进程空间是假的。另外,之前是通过分地和种菜的小故事来讲解进程地址空间是如何映射到了物理地址上的。

本节我们不再关心这种映射,而是关心“种菜”问题。之前你在菜地上种菜,是没有任何规划的,爱种哪种哪,没人拦的住你,可是这样你的菜地就显的不太整洁,使用效率也低。最好的办法是有所规划。


这里写图片描述
图1 两种不同的种菜方法(左边的比右边的看起来要整洁的多)

尽管图 1 中两种种菜方法得到的结果都一样,但是无论从哪方面来说左边的看起来要比右边的舒服很多。

2.1 进程的那块地

在进程空间里,整齐划一的思想也一样得到体现。


这里写图片描述
图2 进程的那块“菜地”

从图 2 中,可以清晰的看到,进程的菜地被整齐规划,不同的地方种不同的菜。这张图是你第一次见,不要求你能够熟记于心,只要有个印象就行了。

2.2 再论 exec

在开篇本文就讲到,exec 系列函数的本质就是替换代码段和数据段,然后从新的入口点重新执行。对应到图2 中,就是替换整个粉红色的区域。

在第 1 节的代码里,使用了 execvp 函数执行 ls -l命令,本质上就是 myexec 进程执行到 execvp 的时候,把 ls 这个二进制文件读取出来,然后替换掉图2 中的代码段和数据段,同时 execvp 函数会把 BSS 段重新清 0.

有同学好奇为什么图 2 中写的不是 execvp 而是 execve?

其实,exec 系列函数,只有 execve 才是真正的系统调用接口,其它的 5 个函数都是标准 C 函数库中的函数,这 5 个函数最终都调用了 execve 这个函数。具体调用流程看图 3。


这里写图片描述
图3 exec 系列函数调用链

3. exec + fork 双剑合璧

很多时候,你并不想像第 1 节中的代码那样,一旦 exec,你原来的程序就等于废了。而且,第 1 节的做法甚至没什么太大意义 ,还不如直接在 shell 里运行 ls -l 来的痛快,何必要再写一次程序 。

有了 fork 的出现,exec 才真正体现出它的强大。咱们完全可以把 exec 放到子进程中去。不多说了,看下面的实验。

  • 代码
// forkandexec.c
#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main() {
  char* argv[] = {"ls", "-l", NULL};
  pid_t pid = fork();

  if (pid > 0) {
    printf("I'm father\n");
  }
  else if (pid == 0) {
    printf("I'm child\n");
    if (execvp("ls", argv) == -1) {
      perror("exec");
      return 1;
    }   
  }
  else {
    perror("fork");
    return 0;
  }
  return 0;  
}
  • 编译
$ gcc forkandexec.c -o forkandexec
  • 运行
$ ./forkandexec
  • 结果
allen@allen-virtual-machine:~/learninglinux/process/exec$ ./forkandexec 
I'm father
allen@allen-virtual-machine:~/learninglinux/process/exec$ I'm child
总用量 36
-rwxrwxr-x 1 allen allen 7380 1219 20:19 a.out
-rwxrwxr-x 1 allen allen 7512 1219 21:55 forkandexec
-rw-rw-r-- 1 allen allen  353 1219 21:55 forkandexec.c
-rwxrwxr-x 1 allen allen 7432 1219 20:45 myexec
-rw-rw-r-- 1 allen allen  193 1219 20:45 myexec.c
-rw-rw-r-- 1 allen allen  132 1219 20:19 test.c
  • 结果分析

从上面的执行结果来看,父进程仍然可以正常运行不受影响,子进程也照常执行了 ls 命令。没毛病。

4. 总结

  • 理解 exec 系列函数干了什么
  • 知道进程空间的内部结构(图2)
  • 知道 exec 系列函数修改了进程空间的哪个位置
  • 掌握 fork 与 exec 系列函数联合用法
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页