请解释exec()函数及其族

时间:2010-11-17 13:39:31

标签: c unix

exec()函数及其系列是什么?为什么使用此功能以及它如何工作?

请任何人解释这些功能。

7 个答案:

答案 0 :(得分:222)

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

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

第一个是fork(),它会创建一个包含当前程序副本的全新流程,包括其状态。这些过程之间存在一些差异,使他们能够找出哪个是父母,哪个是孩子。

第二个是exec(),它用一个全新的程序取代当前流程中的程序。

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


要在上面添加更多细节:

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

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

新进程(称为子进程)获取不同的进程ID(PID),并将旧进程的PID(父进程)作为其父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(),它使用了他们称之为copy-on-write的内容。这是在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

答案 1 :(得分:28)

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

  • l:参数作为字符串列表传递给main()
  • v:arguments作为字符串数组传递给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 page

man 3 exec  # if you are running a UNIX system

答案 2 :(得分:16)

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

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

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

要启动新流程,请使用fork系统调用。要在不替换原始程序的情况下执行程序,您需要fork,然后exec

答案 3 :(得分:7)

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

exec将当前进程转换为另一个程序。如果你曾经看过神秘博士,那就像他再生一样 - 他的旧身体被一个新的身体所取代。

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

这样做的原因是将正在运行 程序分成两步像这样你可以在两个步骤之间做一些事情。最常见的做法是确保新程序将某些文件作为特定文件描述符打开。 (请记住,文件描述符与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 */

来自命令shell。

答案 4 :(得分:5)

  

什么是exec函数及其系列。

exec函数系列是用于执行文件的所有函数,例如execlexeclpexecleexecv和{{1它们都是execvp的前端,并提供了不同的调用方法。

  

为什么使用这个功能

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

  

以及它是如何运作的。

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

有关详细信息:see this link

答案 5 :(得分:4)

exec(3,3p)函数将当前进程替换为。也就是说,当前进程停止,而另一个进程运行,接管原始程序的一些资源。

答案 6 :(得分:4)

当进程使用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()调用的组合。