fork和exec有什么区别?


当前回答

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

fork调用基本上复制了当前进程,几乎在所有方面都是一样的。并不是所有的内容都被复制(例如,在某些实现中存在资源限制),但其思想是创建尽可能接近的副本。

新进程(子进程)获得不同的进程ID (PID),并将旧进程(父进程)的PID作为其父PID (PPID)。因为两个进程现在正在运行完全相同的代码,它们可以通过fork的返回代码来区分哪个是哪个——子进程得到0,父进程得到子进程的PID。当然,这只是假设fork调用有效的情况——如果不行,就不会创建子进程,父进程会得到一个错误代码。

exec调用基本上是用一个新程序替换整个当前进程的一种方法。它将程序加载到当前进程空间中,并从入口点运行它。

因此,fork和exec通常按顺序使用,以使新程序作为当前进程的子程序运行。每当您试图运行find之类的程序时,shell通常都会这样做——shell会fork,然后子进程将find程序加载到内存中,设置所有命令行参数、标准I/O等等。

但它们不需要一起使用。例如,如果程序包含父代码和子代码,则完全可以在不执行的情况下对程序进行fork(您需要小心,每个实现都可能有限制)。这在守护进程中被大量使用(现在仍然如此),这些守护进程只是在TCP端口上侦听,并生成自己的副本来处理特定的请求,而父进程则返回侦听。

类似地,知道自己已经完成,只想运行另一个程序的程序不需要fork、exec然后等待子程序。它们可以直接将子进程加载到进程空间中。

一些UNIX实现有一个优化的分叉,它使用所谓的写时复制。这是一种延迟fork中进程空间复制的技巧,直到程序试图更改该空间中的某些内容。这对于那些只使用fork而不使用exec的程序很有用,因为它们不需要复制整个进程空间。

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

注意,这里有一整个exec调用家族(execl, execle, execve等等),但这里上下文中的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

其他回答

理解fork()和exec()概念的主要例子是shell,用户通常在登录到系统后执行命令解释器程序。shell将命令行的第一个单词解释为命令名

对于许多命令,shell fork和子进程执行与名称相关的命令,将命令行上的剩余单词作为命令的参数。

shell允许三种类型的命令。首先,命令可以是 包含由源代码编译产生的目标代码的可执行文件(例如C程序)。其次,命令可以是一个可执行文件 包含shell命令行序列。最后,命令可以是一个内部shell命令。(而不是一个可执行文件ex->cd,ls等)

Fork()创建当前进程的副本,在Fork()调用之后的新子进程中执行。在fork()之后,除了fork()函数的返回值外,它们是相同的。(详情请参阅RTFM。)这两个进程可以进一步分离,其中一个进程不能干扰另一个进程,除非可能通过任何共享的文件句柄。

Exec()将当前进程替换为新进程。它与fork()无关,只是当需要启动不同的子进程,而不是替换当前进程时,exec()通常会在fork()之后执行。

它们一起使用来创建一个新的子进程。首先,调用fork创建当前进程(子进程)的副本。然后,在子进程内部调用exec,用新进程“替换”父进程的副本。

这个过程是这样的:

child = fork();  //Fork returns a PID for the parent process, or 0 for the child, or -1 for Fail

if (child < 0) {
    std::cout << "Failed to fork GUI process...Exiting" << std::endl;
    exit (-1);
} else if (child == 0) {       // This is the Child Process
    // Call one of the "exec" functions to create the child process
    execvp (argv[0], const_cast<char**>(argv));
} else {                       // This is the Parent Process
    //Continue executing parent process
}

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

fork调用基本上复制了当前进程,几乎在所有方面都是一样的。并不是所有的内容都被复制(例如,在某些实现中存在资源限制),但其思想是创建尽可能接近的副本。

新进程(子进程)获得不同的进程ID (PID),并将旧进程(父进程)的PID作为其父PID (PPID)。因为两个进程现在正在运行完全相同的代码,它们可以通过fork的返回代码来区分哪个是哪个——子进程得到0,父进程得到子进程的PID。当然,这只是假设fork调用有效的情况——如果不行,就不会创建子进程,父进程会得到一个错误代码。

exec调用基本上是用一个新程序替换整个当前进程的一种方法。它将程序加载到当前进程空间中,并从入口点运行它。

因此,fork和exec通常按顺序使用,以使新程序作为当前进程的子程序运行。每当您试图运行find之类的程序时,shell通常都会这样做——shell会fork,然后子进程将find程序加载到内存中,设置所有命令行参数、标准I/O等等。

但它们不需要一起使用。例如,如果程序包含父代码和子代码,则完全可以在不执行的情况下对程序进行fork(您需要小心,每个实现都可能有限制)。这在守护进程中被大量使用(现在仍然如此),这些守护进程只是在TCP端口上侦听,并生成自己的副本来处理特定的请求,而父进程则返回侦听。

类似地,知道自己已经完成,只想运行另一个程序的程序不需要fork、exec然后等待子程序。它们可以直接将子进程加载到进程空间中。

一些UNIX实现有一个优化的分叉,它使用所谓的写时复制。这是一种延迟fork中进程空间复制的技巧,直到程序试图更改该空间中的某些内容。这对于那些只使用fork而不使用exec的程序很有用,因为它们不需要复制整个进程空间。

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

注意,这里有一整个exec调用家族(execl, execle, execve等等),但这里上下文中的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

Fork()将当前进程拆分为两个进程。换句话说,你的线性程序突然变成了两个单独的程序,运行着一段代码:

 int pid = fork();

 if (pid == 0)
 {
     printf("I'm the child");
 }
 else
 {
     printf("I'm the parent, my child is %i", pid);
     // here we can kill the child, but that's not very parently of us
 }

这可能会让你大吃一惊。现在,两个进程执行了一段状态几乎相同的代码。子进程继承刚刚创建它的进程的所有代码和内存,包括从fork()调用刚刚停止的地方开始。唯一的区别是fork()返回的代码告诉你你是父类还是子类。如果你是父进程,返回值是子进程的id。

Exec更容易掌握一些,您只需告诉Exec使用目标可执行文件执行一个进程,并且不需要两个进程运行相同的代码或继承相同的状态。就像@Steve Hawkins说的,exec可以在你fork后在当前进程中执行目标可执行文件。