fork和exec之间的区别

238

forkexec有什么区别?


5
一个好的、详细的fork、exec和其他进程控制函数的摘要可以在http://www.yolinux.com/TUTORIALS/ForkExecProcesses.html找到。 - Jonathan Fingland
10
@Justin,因为我们希望SO成为编程问题的首选解答平台。 - paxdiablo
所以 fork 基本上就是克隆 :O - Sebastian Hojas
这个教程也非常有帮助!https://devconnected.com/understanding-processes-on-linux/ - Victor
9个回答

422
使用forkexec函数体现了UNIX的精神,因为它提供了一种非常简单的方式来启动新进程。 fork调用基本上会创建一个当前进程的副本,几乎每个方面都是相同的。并不是所有的东西都被复制(例如,在某些实现中有资源限制),但其想法是创建尽可能接近的副本。
新进程(子进程)获得不同的进程ID(PID),并将旧进程(父进程)的PID作为其父PID(PPID)。由于两个进程现在运行完全相同的代码,它们可以通过fork的返回码来确定哪个进程是哪个 - 子进程获得0,父进程获得子进程的PID。当然,这一切都假设fork调用成功 - 如果没有,则不会创建子进程,并且父进程会收到一个错误代码。 exec调用是替换整个当前进程为新程序的一种方式。它将程序加载到当前进程空间中,并从入口点运行它。
因此,forkexec通常连续使用,以使新程序作为当前进程的子进程运行。 Shell通常在您尝试运行像find这样的程序时执行此操作- shell进行分叉,然后子进程将find程序加载到内存中,并设置所有命令行参数、标准I/O等等。
但是它们不需要一起使用。对于包含父代和子代码的程序(每个实现都可能有限制),如果例如程序可以自己fork而不需要使用exec,则完全可接受。这被用得非常多(现在仍然如此)用于简单侦听TCP端口并fork其自身的守护程序,以处理特定请求,而父级则返回侦听。
同样,已知它们已经完成且只想运行另一个程序的程序不需要forkexec,然后wait等待子进程。它们可以直接将子程序加载到其进程空间中。
一些UNIX实现具有优化的fork,其使用所谓的写时复制。这是一种技巧,可以延迟在fork中复制进程空间,直到程序尝试更改该空间中的某些内容。对于那些只使用fork而不是exec的程序来说,这很有用,因为它们不必复制整个进程空间。
如果在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

61

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 后使用,以在当前进程中执行目标可执行文件。


8
pid < 0fork() 调用失败时,也存在这种情况。 - Jonathan Fingland
5
这并不让我感到惊讶 :-) 当使用共享库或DLL时,一个代码片段被两个进程执行的情况每次都会发生。 - paxdiablo

38

我认为Marc Rochkind的《高级Unix编程》中的一些概念有助于理解fork()/exec()的不同角色,特别是对于习惯于Windows CreateProcess()模型的人:

程序是一组指令和数据,保存在磁盘上的普通文件中。(来自1.1.2程序、进程和线程)

.

为了运行一个程序,首先需要请求内核创建一个新的进程,它是程序执行的环境。(也来自1.1.2 程序、进程和线程)

.

如果你不完全理解进程和程序之间的区别,那么就无法理解exec或fork系统调用。如果这些术语对你来说是新的,你可能需要回顾第1.1.2节。现在,我们将在一句话中总结区别:进程是一个执行环境,包括指令、用户数据和系统数据段,以及在运行时获取的许多其他资源,而程序是一个包含指令和数据的文件,用于初始化进程的指令和用户数据段。(摘自5.3 exec系统调用)
一旦你理解了程序和进程之间的区别,fork()exec()函数的行为可以概括如下:
- fork()创建当前进程的副本。 - exec()用另一个程序替换当前进程中的程序。
(这本质上是paxdiablo更详细的答案的简化版'白痴'版本)

34

Fork会创建一个调用进程的副本,通常遵循以下结构enter image description here

int cpid = fork( );

if (cpid = = 0) 
{

  //child code

  exit(0);

}

//parent code

wait(cpid);

// end

(对于子进程的文本(代码),数据和堆栈与调用进程相同)子进程在if块中执行代码。

EXEC将当前进程替换为新进程的代码、数据和堆栈。通常遵循以下结构enter image description here

int cpid = fork( );

if (cpid = = 0) 
{   
  //child code

  exec(foo);

  exit(0);    
}

//parent code

wait(cpid);

// end

在执行调用后,Unix内核会清除子进程的文本、数据和堆栈,并填充与foo进程相关的文本/数据。因此,子进程具有不同的代码(不同于父进程的代码)


4
这与问题有些不相关,但是如果子进程恰好先完成了它的代码,这段代码会不会导致竞态条件呢?在这种情况下,父进程将一直等待子进程自行终止,是吗? - stdout
2
@stdout: 回答您的问题有点晚,但我认为不会发生竞争条件。当进程在父进程等待之前退出时,它会进入僵尸状态(已死亡但仍然存在)。挂起的部分基本上是退出代码,以便父进程最终可以“等待”并接收该信息。此时,僵尸将完全消失。如果父进程先消失,则“init”进程继承子进程并最终清除退出进程(当子进程退出时)。 - paxdiablo

7

它们一起用于创建新的子进程。首先,调用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
}

2
第7行提到exec()函数创建子进程。但实际上fork()已经创建了子进程,而exec()函数调用只是替换刚创建的新进程的程序。 - cbinder

7
fork()exec()的主要区别在于,fork()系统调用会创建当前运行程序的克隆。原始程序在fork()函数调用后继续执行下一行代码。克隆程序也会从下一行代码开始执行。 看下面这段代码,它来自http://timmurphy.org/2014/04/26/using-fork-in-cc-a-minimum-working-example/
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
    printf("--beginning of program\n");
    int counter = 0;
    pid_t pid = fork();
    if (pid == 0)
    {
        // child process
        int i = 0;
        for (; i < 5; ++i)
        {
            printf("child process: counter=%d\n", ++counter);
        }
    }
    else if (pid > 0)
    {
        // parent process
        int j = 0;
        for (; j < 5; ++j)
        {
            printf("parent process: counter=%d\n", ++counter);
        }
    }
    else
    {
        // fork failed
        printf("fork() failed!\n");
        return 1;
    }
    printf("--end of program--\n");
    return 0;
}

这个程序在fork()之前声明了一个计数器变量并将其设置为零。调用 fork 函数后,我们有两个进程同时运行,每个进程都会增加自己的计数器值。每个进程都会执行完毕并退出。由于这些进程并行运行,我们无法知道哪个进程会先完成。运行此程序将打印类似于下面所示的内容,但结果可能会因每次运行而异。
--beginning of program
parent process: counter=1
parent process: counter=2
parent process: counter=3
child process: counter=1
parent process: counter=4
child process: counter=2
parent process: counter=5
child process: counter=3
--end of program--
child process: counter=4
child process: counter=5
--end of program--
exec() 系列系统调用可以将进程当前正在执行的代码替换为另一段代码。该进程保留其 PID 但变成了一个新程序。例如,考虑以下代码:
#include <stdio.h> 
#include <unistd.h> 
main() {
 char program[80],*args[3];
 int i; 
printf("Ready to exec()...\n"); 
strcpy(program,"date"); 
args[0]="date"; 
args[1]="-u"; 
args[2]=NULL; 
i=execvp(program,args); 
printf("i=%d ... did it work?\n",i); 
} 

这个程序调用 execvp() 函数,将其代码替换为 date 程序。如果代码存储在名为 exec1.c 的文件中,则执行它会产生以下输出:

Ready to exec()... 
Tue Jul 15 20:17:53 UTC 2008 

程序输出了一行 “Ready to exec() . . .” 的信息,调用 execvp() 函数后,程序代码被 date 程序替换。需要注意的是,“. . . did it work” 这行信息没有被显示出来,因为此时代码已经被替换。取而代之的是执行 "date -u" 命令所输出的结果。

4

fork()函数会创建当前进程的一个副本,新的子进程从fork()调用后的位置开始执行。在fork()之后,这两个进程是相同的,除了fork()函数的返回值。(请参阅RTFM以获取更多细节。)然后,这两个进程可以进一步分叉,其中一个进程无法通过任何共享文件句柄干扰另一个进程。

exec()函数将当前进程替换为一个新进程。它与fork()没有关系,除非想要启动一个不同的子进程而不是替换当前进程,否则通常会在fork()之后紧跟着exec()。


2

enter image description herefork():

它创建一个正在运行的进程的副本。正在运行的进程称为父进程,新创建的进程称为子进程。区分两者的方法是查看返回值:

  1. fork()在父进程中返回子进程的进程标识符(pid)

  2. fork()在子进程中返回0。

exec()

它在进程内部启动一个新进程。它将一个新程序加载到当前进程中,替换现有的程序。

fork()+exec()

当启动一个新程序时,首先要fork(),创建一个新进程,然后exec()(即加载到内存并执行)它应该运行的程序二进制文件。

int main( void ) 
{
    int pid = fork();
    if ( pid == 0 ) 
    {
        execvp( "find", argv );
    }

    //Put the parent to sleep for 2 sec,let the child finished executing 
    wait( 2 );

    return 0;
}

0
了解fork()exec()概念的最佳示例是shell,这是用户在登录系统后通常执行的命令解释器程序。 shell将命令行的第一个单词解释为命令名称。
对于许多命令,shell forks并且子进程execs与名称相关联的命令,将命令行上剩余的单词视为命令的参数。 shell允许三种类型的命令。首先,命令可以是包含通过编译生成的目标代码的可执行文件(例如C程序)。其次,命令可以是包含一系列shell命令行的可执行文件。最后,命令可以是内部shell命令(而不是可执行文件,例如cdls等)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接