请解释exec()函数及其家族


98

什么是exec()功能及其家族?为什么要使用此功能,其功能如何?

请任何人解释这些功能。


4
尝试再次阅读史蒂文斯,并弄清您不了解的内容。
vlabrecque,2010年

Answers:


245

简而言之,在UNIX中,您具有进程和程序的概念。进程是程序在其中执行的环境。

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

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

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

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


要在上面添加更多细节:

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

fork()调用使当前进程的近重复,几乎在每一个方式(不完全相同一切被复制,例如,在一些实现资源限制,但这个想法是尽可能接近的副本尽可能创造)。仅有一个进程调用, fork()但有两个进程从该调用返回-听起来很奇怪,但确实非常优雅

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

因为这两个进程现在正在运行完全相同的代码,所以它们需要能够分辨出哪个是哪个- fork()提供此信息的返回码-子代为0,父代为子代的PID(如果fork()失败,则为否。子级已创建,父级获得错误代码)。

这样,父级就可以知道子级的PID,并可以与其进行通信,杀死它,等待它等等(子级始终可以通过调用来找到其父级进程getppid())。

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

因此,通常按顺序使用fork()exec()来使新程序作为当前进程的子进程运行。当您尝试运行诸如findshell forks之类的程序时,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在上下文中这里是指任何人。

下图说明了使用shell通过命令列出目录的典型fork/exec操作:bashls

+--------+
| 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

12
谢谢您这么详尽的解释:)
Faizan

2
感谢您对find程序的shell参考。正是我需要了解的内容。
用户

为什么exec该实用程序用于重定向当前进程的IO?在没有参数的情况下运行exec的“空”情况如何用于此约定?

@Ray,我一直认为它是自然的扩展。如果你想exec为手段,在这个过程中与另一取代当前的计划(壳),则不能指定其他程序以能取代它只是意味着你想要取代它。
paxdiablo 2015年

我明白你的意思是“自然延伸”是指“有机增长”。似乎应该添加重定向以支持程序替换功能,并且我可以看到这种行为在exec没有程序的情况下仍然存在。但这在这种情况下有点奇怪,因为重定向到一个新程序(实际上会被exec废弃的程序)的原始用处消失了,并且您具有一个有用的工件,可以重定向当前程序(没有被exec废弃或启动)以任何方式-而是

36

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

  • l:参数作为字符串列表传递给main()
  • v:参数作为字符串数组传递给main()
  • p:搜索新运行程序的路径
  • 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 3 exec  # if you are running a UNIX system

1
有趣的是,您错过execve()了由POSIX定义的列表,并添加了POSIX未定义的列表execvpe()(主要是出于历史先例;它完成了功能集)。否则,将为家庭命名惯例提供有用的解释,这是paxdiablo的有用辅助工具,答案将解释有关功能运作的更多信息”。
乔纳森·莱夫勒

而且,为了您的辩护,我发现execvpe()(et al)的Linux手册页没有列出execve();它有自己的单独的手册页(至少在Ubuntu 16.04 LTS上如此)–区别在于其他exec()系列功能在第3节(函数)execve()中列出,而在第2节(系统调用)中列出。基本上,该系列中的所有其他功能都是通过调用来实现的execve()
乔纳森·莱夫勒

18

exec系列函数使你的程序执行不同的程序,取代旧的程序它运行。即,如果您致电

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

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

要启动新进程,将使用fork系统调用。要执行程序而不替换原始程序,您需要fork依次按exec


谢谢,那真的很有帮助。我当前正在做一个项目,要求我们使用exec(),您的描述巩固了我的理解。
TwilightSparkleTheGeek

7

什么是exec函数及其家族。

exec函数族是用于执行文件中的所有功能,如execlexeclpexecleexecv,和execvp。他们是所有的前端execve,并提供调用它的不同方法。

为什么使用此功能

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

以及它是如何工作的。

它们的工作方式是用您启动的过程映像覆盖当前过程映像。它们用启动的新进程替换(结束)当前正在运行的进程(称为exec命令的进程)。

有关更多详细信息:请参阅此链接


7

exec经常与结合使用fork,我看到您也问过这个问题,因此我将在此基础上进行讨论。

exec将当前进程转换为另一个程序。如果您曾经看过Who医生,那就像他再生时一样-他的旧身体被新的身体替代。

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

这是做这种方式的原因是,通过分离运行 一个 新的 程序分为两个步骤,这样你可以做两个步骤之间的一些事情。最常见的做法是确保新程序已打开某些文件作为某些文件描述符。(请记住,文件描述符与并不相同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

从命令外壳。


5

当流程使用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()之后,父级和子级都共享父级的内存地址空间。在linux中,这些地址空间分为多个页面。仅当子代写入父存储页面之一时,才会为该子代创建该页面的副本。这也称为在写时复制(仅在子项写入父页面时才复制父页面)。

让我们通过一个例子来理解写时复制。

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等。将与这些命令关联的程序代码包含在一个程序中,或者在需要时让子程序将这些程序加载到内存中是否合适?

这完全取决于您的用例。您有一个提供给定输入x的Web服务器,该输入x将2 ^ x返回给客户端。对于每个请求,Web服务器都会创建一个新的子代,并要求其进行计算。您是否将编写一个单独的程序来计算该值并使用exec()?还是只在父程序中编写计算代码?

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


4

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


6
不完全的。它替换当前的进程映像与一个新的进程映像。该进程是具有相同pid,相同环境和相同文件描述符表的相同进程。更改的是整个虚拟内存和CPU状态。
JeremyP 2010年

@JeremyP“相同的文件描述符”在这里很重要,它说明了shell中重定向的工作方式!我对执行程序覆盖所有内容时重定向如何工作感到困惑!谢谢
FUD
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.