【问题标题】:Please explain the exec() function and its family请解释一下 exec() 函数及其家族
【发布时间】:2011-05-11 10:24:34
【问题描述】:

exec() 函数及其家族是什么?为什么要使用这个函数,它是如何工作的?

请任何人解释这些功能。

【问题讨论】:

  • 试着再读一遍史蒂文斯,澄清你不明白的地方。

标签: c unix execl


【解决方案1】:

exec(3,3p) 函数用另一个替换当前进程。也就是说,当前进程停止,然后另一个进程运行,接管了原始程序拥有的一些资源。

【讨论】:

  • 不完全。它用新的进程映像替换当前进程 image。进程是同一个进程,同一个pid,同一个环境,同一个文件描述符表。改变的是整个虚拟内存和 CPU 状态。
  • @JeremyP “相同的文件描述符”在这里很重要,它解释了重定向在 shell 中的工作原理!如果 exec 覆盖所有内容,我对重定向如何工作感到困惑!谢谢
【解决方案2】:

exec 系列函数使您的进程执行不同的程序,替换它正在运行的旧程序。即,如果您致电

execl("/bin/ls", "ls", NULL);

然后使用调用execl 的进程的进程ID、当前工作目录和用户/组(访问权限)执行ls 程序。之后,原来的程序就不再运行了。

要启动一个新进程,使用fork 系统调用。要执行一个程序而不替换原来的,你需要fork,然后exec

【讨论】:

  • 谢谢你真的很有帮助。我目前正在做一个要求我们使用 exec() 的项目,您的描述巩固了我的理解。
【解决方案3】:

什么是 exec 函数及其家族。

exec函数族是所有用于执行文件的函数,例如execlexeclpexecleexecvexecvp。它们都是execve和提供不同的调用方法。

为什么要用这个函数

当您想要执行(启动)文件(程序)时使用执行函数。

它是如何工作的。

它们的工作原理是用您启动的进程映像覆盖当前进程映像。它们用已启动的新进程替换(通过结束)当前正在运行的进程(调用 exec 命令的进程)。

更多详情:see this link

【讨论】:

    【解决方案4】:

    简单地说,在 UNIX 中,您有进程和程序的概念。进程是程序执行的环境。

    UNIX“执行模型”背后的简单想法是您可以执行两种操作。

    第一个是fork(),它创建了一个全新的进程,其中包含当前程序的副本(大部分),包括其状态。这两个进程之间存在一些差异,这使它们能够确定哪个是父进程,哪个是子进程。

    第二个是exec(),将当前进程中的程序替换为全新的程序。

    从这两个简单的操作,就可以构建出整个 UNIX 执行模型。


    在上面添加更多细节:

    fork()exec() 的使用体现了 UNIX 的精神,因为它提供了一种非常简单的方式来启动新进程。

    fork() 调用几乎复制了当前进程,几乎在所有方面都相同(并非所有内容都被复制,例如,在某些实现中,资源限制,但想法是创建尽可能接近的副本)。只有一个进程调用fork(),但两个进程从该调用返回 - 听起来很奇怪,但它真的很优雅

    新进程(称为子进程)获得不同的进程 ID (PID),并将旧进程(父进程)的 PI​​D 作为其父进程 PID (PPID)。

    因为两个进程现在运行完全相同的代码,它们需要能够分辨哪个是哪个 - fork() 的返回码提供了此信息 - 子进程获取 0,父进程获取子进程的 PID (如果fork() 失败,则不会创建子节点并且父节点会收到错误代码)。

    这样,父进程知道子进程的 PID 并可以与它通信、杀死它、等待它等等(子进程总是可以通过调用 getppid() 找到它的父进程)。

    exec() 调用将进程的整个当前内容替换为新程序。它将程序加载到当前进程空间并从入口点运行它。

    因此,fork()exec() 通常按顺序使用,以使新程序作为当前进程的子进程运行。每当您尝试运行像 find 这样的程序时,Shell 通常会执行此操作 - Shell 分叉,然后子进程将 find 程序加载到内存中,设置所有命令行参数、标准 I/O 等等。

    但它们不需要一起使用。程序调用fork() 而不跟随exec() 是完全可以接受的,例如,如果程序同时包含父代码和子代码(你需要小心你所做的事情,每个实现都可能有限制)。

    这在守护进程中被大量使用(现在仍然如此),这些守护进程只是侦听 TCP 端口并派生自己的副本以处理特定请求,而父进程则返回侦听。对于这种情况,程序同时包含父代码子代码。

    类似地,知道自己已完成并只想运行另一个程序的程序不需要为子代fork()exec()wait()/waitpid()。他们可以使用exec() 将孩子直接加载到他们当前的进程空间中。

    一些 UNIX 实现有一个优化的fork(),它使用他们所谓的写时复制。这是延迟fork() 中的进程空间复制的技巧,直到程序尝试更改该空间中的某些内容。这对于那些只使用fork() 而不是exec() 的程序很有用,因为它们不必复制整个进程空间。在 Linux 下,fork() 只复制页表和新的任务结构,exec() 将完成“分离”两个进程的内存的繁重工作。

    如果exec fork 之后调用(这是最常见的情况),这会导致写入进程空间,然后在修改之前将其复制给子进程是允许的。

    Linux 还有一个vfork(),甚至更加优化,它在两个进程之间共享一切。因此,孩子可以做的事情有一定的限制,父母会停下来,直到孩子打电话给exec()_exit()

    必须停止父进程(并且不允许子进程从当前函数返回),因为这两个进程甚至共享同一个堆栈。这对于 fork() 的经典用例来说效率稍高一些,紧接着是 exec()

    请注意,exec 调用的整个家族(execlexecleexecve 等等)但这里的上下文中的 exec 表示其中任何一个。

    下图说明了典型的fork/exec 操作,其中bash shell 用于通过ls 命令列出目录:

    +--------+
    | pid=7  |
    | ppid=4 |
    | bash   |
    +--------+
        |
        | calls fork
        V
    +--------+             +--------+
    | pid=7  |    forks    | pid=22 |
    | ppid=4 | ----------> | ppid=7 |
    | bash   |             | bash   |
    +--------+             +--------+
        |                      |
        | waits for pid 22     | calls exec to run ls
        |                      V
        |                  +--------+
        |                  | pid=22 |
        |                  | ppid=7 |
        |                  | ls     |
        V                  +--------+
    +--------+                 |
    | pid=7  |                 | exits
    | ppid=4 | <---------------+
    | bash   |
    +--------+
        |
        | continues
        V
    

    【讨论】:

    • 感谢您如此详尽的解释:)
    • 感谢 find 程序的 shell 参考。正是我需要理解的。
    • 为什么exec是用来重定向当前进程IO的工具?没有参数运行 exec 的“null”案例是如何用于这个约定的?
    • @Ray,我一直认为它是一种自然的扩展。如果您认为exec 是用另一个程序替换此进程中的当前程序(shell)的方法,那么 not 指定要替换它的其他程序可能只是意味着您 don '不想替换它。
    • 我明白你的意思,如果“自然延伸”是指类似于“有机增长”的东西。似乎已经添加了重定向以支持程序替换功能,并且我可以看到这种行为仍然存在于exec 在没有程序的情况下被调用的退化情况下。但在这种情况下有点奇怪,因为重定向新程序的原始有用性——一个实际上会得到executed 的程序——消失了,你有一个有用的工件,重定向当前程序——这不是execute 或以任何方式启动 - 代替。
    【解决方案5】:

    exec 经常与fork 连用,我看到你也问过这个问题,所以我会考虑到这一点。

    exec 将当前进程转换为另一个程序。如果你看过神秘博士,那么这就像他重生的时候——他的旧身体被新的身体取代了。

    您的程序和exec 发生这种情况的方式是操作系统内核检查大量资源以查看您作为程序参数(第一个参数)传递给exec 的文件是否可以由当前用户(进行exec 调用的进程的用户ID),如果是这样,它将用新进程的虚拟内存替换当前进程的虚拟内存映射并复制传递的argvenvp 数据在exec 调用这个新的虚拟内存映射的区域。这里也可能发生其他一些事情,但是为名为 exec 的程序打开的文件仍将为新程序打开,并且它们将共享相同的进程 ID,但名为 exec 的程序将停止(除非执行失败)。

    这样做的原因是通过将 running a new program 分成两个步骤像这样你可以在两个步骤之间做一些事情。最常见的做法是确保新程序将某些文件作为某些文件描述符打开。 (请记住,文件描述符与FILE * 不同,而是内核知道的int 值)。这样做你可以:

    int X = open("./output_file.txt", O_WRONLY);
    
    pid_t fk = fork();
    if (!fk) { /* in child */
        dup2(X, 1); /* fd 1 is standard output,
                       so this makes standard out refer to the same file as X  */
        close(X);
    
        /* I'm using execl here rather than exec because
           it's easier to type the arguments. */
        execl("/bin/echo", "/bin/echo", "hello world");
        _exit(127); /* should not get here */
    } else if (fk == -1) {
        /* An error happened and you should do something about it. */
        perror("fork"); /* print an error message */
    }
    close(X); /* The parent doesn't need this anymore */
    

    这样就完成了运行:

    /bin/echo "hello world" > ./output_file.txt
    

    从命令外壳。

    【讨论】:

      【解决方案6】:

      exec() 系列中的函数具有不同的行为:

      • l : 参数作为字符串列表传递给 main()
      • v : 参数作为字符串数组传递给 main()
      • p : path/s 搜索新的运行程序
      • e : 环境可以由调用者指定

      你可以混合它们,因此你有:

      • int execl(const char *path, const char *arg, ...);
      • int execlp(const char *file, const char *arg, ...);
      • int execle(const char *path, const char *arg, ..., char * const envp[]);
      • int execv(const char *path, char *const argv[]);
      • int execvp(const char *file, char *const argv[]);
      • int execvpe(const char *file, char *const argv[], char *const envp[]);

      对于所有这些,初始参数是要执行的文件的名称。

      更多信息请阅读exec(3) man page:

      man 3 exec  # if you are running a UNIX system
      

      【讨论】:

      • 有趣的是,您从 POSIX 定义的列表中错过了execve(),并且您添加了 POSIX 未定义的execvpe()(主要是出于历史先例的原因;它完成了集合的功能)。否则,对系列命名约定的有用解释 - 对 paxdiablo'a answer 的有用补充,它解释了有关函数工作的更多信息。
      • 而且,在你的辩护中,我看到execvpe()(等人)的Linux手册页没有列出execve();它有自己独立的手册页(至少在 Ubuntu 16.04 LTS 上)——不同之处在于其他 exec() 系列函数列在第 3 节(函数)中,而 execve() 列在第 2 节(系统调用)中。基本上,该系列中的所有其他功能都是通过调用execve() 来实现的。
      【解决方案7】:

      当一个进程使用 fork() 时,它会创建一个自身的副本,这个副本成为该进程的子进程。 fork() 是使用 linux 中的 clone() 系统调用实现的,它从内核返回两次。

      • 一个非零值(子进程 ID)返回给父进程。
      • 将零值返回给孩子。
      • 如果由于内存不足等问题导致子代未成功创建,则将 -1 返回到 fork()。

      让我们通过一个例子来理解这一点:

      pid = fork(); 
      // Both child and parent will now start execution from here.
      if(pid < 0) {
          //child was not created successfully
          return 1;
      }
      else if(pid == 0) {
          // This is the child process
          // Child process code goes here
      }
      else {
          // Parent process code goes here
      }
      printf("This is code common to parent and child");
      

      在示例中,我们假设 exec() 未在子进程中使用。

      但是父子节点在某些 PCB(进程控制块)属性上是不同的。它们是:

      1. PID - 子进程和父进程 ID 不同。
      2. 待处理信号 - 子代不继承父代的待处理信号。子进程创建时为空。
      3. 内存锁 - 子代不继承其父代的内存锁。内存锁是可以用来锁定一个内存区域,然后这个内存区域不能被交换到磁盘的锁。
      4. 记录锁 - 子级不继承其父级的记录锁。记录锁与文件块或整个文件相关联。
      5. 子进程资源利用率和消耗的 CPU 时间设置为零。
      6. 孩子也不会从父母那里继承计时器。

      但是孩子的记忆呢?是否为孩子创建了新的地址空间?

      没有答案。在 fork() 之后,parent 和 child 共享 parent 的内存地址空间。在linux中,这些地址空间被划分为多个页面。只有当孩子写入其中一个父内存页面时,才会为孩子创建该页面的副本。这也称为写时复制(仅在子页面写入时复制父页面)。

      让我们通过一个例子来理解copy on write。

      int x = 2;
      pid = fork();
      if(pid == 0) {
          x = 10;
          // child is changing the value of x or writing to a page
          // One of the parent stack page will contain this local               variable. That page will be duplicated for child and it will store the value 10 in x in duplicated page.  
      }
      else {
          x = 4;
      }
      

      但为什么需要写时复制?

      典型的进程创建是通过 fork()-exec() 组合进行的。我们先来了解一下 exec() 的作用。

      Exec() 函数组将子地址空间替换为新程序。一旦在子进程中调用 exec(),将为子进程创建一个与父进程完全不同的独立地址空间。

      如果没有与 fork() 关联的写入时复制机制,则会为子页面创建重复页面,并且所有数据都将被复制到子页面。分配新内存和复制数据是一个非常昂贵的过程(占用处理器的时间和其他系统资源)。我们还知道,在大多数情况下,孩子会调用 exec(),这会用新程序替换孩子的记忆。因此,如果没有写入时复制,我们所做的第一个副本将是一种浪费。

      pid = fork();
      if(pid == 0) {
          execlp("/bin/ls","ls",NULL);
          printf("will this line be printed"); // Think about it
          // A new memory space will be created for the child and that   memory will contain the "/bin/ls" program(text section), it's stack, data section and heap section
      else {
          wait(NULL);
          // parent is waiting for the child. Once child terminates, parent will get its exit status and can then continue
      }
      return 1; // Both child and parent will exit with status code 1.
      

      为什么父进程要等待子进程?

      1. 父级可以将任务分配给其子级并等待其完成任务。然后它可以进行一些其他的工作。
      2. 一旦子进程终止,与子进程相关的所有资源都将被释放,进程控制块除外。现在,孩子处于僵尸状态。使用wait(),父母可以查询孩子的状态,然后请求内核释放PCB。如果父母不使用等待,孩子将保持僵尸状态。

      为什么需要 exec() 系统调用?

      没有必要将 exec() 与 fork() 一起使用。如果子程序将执行的代码在与父程序关联的程序中,则不需要 exec()。

      但是想想孩子必须运行多个程序的情况。让我们以shell程序为例。它支持多个命令,如 find、mv、cp、date 等。是否可以将与这些命令相关的程序代码包含在一个程序中,或者在需要时让孩子将这些程序加载到内存中?

      这完全取决于您的用例。您有一个 Web 服务器,它给出了一个输入 x,它将 2^x 返回给客户端。对于每个请求,Web 服务器都会创建一个新的子节点并要求它进行计算。你会编写一个单独的程序来计算这个并使用 exec() 吗?还是直接在父程序里面写计算代码?

      通常,进程创建涉及 fork()、exec()、wait() 和 exit() 调用的组合。

      【讨论】:

        猜你喜欢
        • 2016-03-19
        • 1970-01-01
        • 1970-01-01
        • 2014-11-07
        • 2016-09-20
        • 1970-01-01
        • 1970-01-01
        • 2016-10-03
        • 1970-01-01
        相关资源
        最近更新 更多