fork 和 exec 的区别

Posted

技术标签:

【中文标题】fork 和 exec 的区别【英文标题】:Differences between fork and exec 【发布时间】:2010-12-11 19:40:42 【问题描述】:

forkexec 有什么区别?

【问题讨论】:

对 fork、exec 和其他进程控制函数的详细总结在 yolinux.com/TUTORIALS/ForkExecProcesses.html @Justin,因为我们希望 SO 成为 解决编程问题的地方。 @Polaris878:哦,现在可以了! :D so fork 基本上是克隆 :O 这个教程也很有帮助! devconnected.com/understanding-processes-on-linux 【参考方案1】:

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

fork 调用基本上复制了当前进程,几乎在各个方面都相同。并非所有内容都被复制(例如,某些实现中的资源限制),但我们的想法是创建尽可能接近的副本。

新进程(子进程)获得不同的进程 ID(PID),并以旧进程(父进程)的 PI​​D 作为其父进程 PID(PPID)。因为这两个进程现在运行完全相同的代码,所以它们可以通过fork 的返回码来判断哪个是哪个——子进程得到 0,父进程得到子进程的 PID。当然,这就是全部,假设 fork 调用有效 - 如果没有,则不会创建子节点并且父节点会收到错误代码。

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

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

但它们不需要一起使用。例如,如果程序同时包含父代码和子代码(您需要小心您所做的事情,每个实现都可能有限制),则程序本身 fork 而没有 execing 是完全可以接受的。这在守护进程中被大量使用(现在仍然如此),这些守护进程只是侦听 TCP 端口和 fork 自身的副本以在父进程返回侦听时处理特定请求。

类似地,知道自己已完成并只想运行另一个程序的程序不需要为子代forkexecwait。他们可以直接将孩子加载到他们的进程空间中。

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

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

请注意,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

【讨论】:

【参考方案2】:

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

【讨论】:

还有pid &lt; 0fork()调用失败的情况 这根本没有让我大吃一惊 :-) 每次使用共享库或 DLL 时都会发生一段由两个进程执行的代码。【参考方案3】:

我认为"Advanced Unix Programming" by Marc Rochkind 的一些概念有助于理解fork()/exec() 的不同角色,尤其是对于习惯于Windows CreateProcess() 模型的人来说:

程序是保存在磁盘上的常规文件中的指令和数据的集合。 (从 1.1.2 程序、进程和线程开始)

.

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

.

如果不完全理解进程和程序之间的区别,就不可能理解 exec 或 fork 系统调用。如果这些条款对您来说是新的,您可能需要返回并查看第 1.1.2 节。如果您现在准备好继续,我们将用一句话总结区别:进程是一个执行环境,由指令、用户数据和系统数据段以及运行时获取的许多其他资源组成,而程序是包含用于初始化进程的指令和用户数据段的指令和数据的文件。 (来自 5.3 exec 系统调用)

一旦了解了程序和进程的区别,fork()exec() 函数的行为可以概括为:

fork() 创建当前进程的副本 exec()用另一个程序替换当前进程中的程序

(这本质上是paxdiablo's much more detailed answer 的简化版“傻瓜版”)

【讨论】:

【参考方案4】:

Fork 创建调用进程的副本。 一般遵循结构

int cpid = fork( );

if (cpid = = 0) 


  //child code

  exit(0);



//parent code

wait(cpid);

// end

(子进程文本(代码)、数据、栈与调用进程相同) 子进程执行 if 块中的代码。

EXEC 用新进程的代码、数据、堆栈替换当前进程。 一般遵循结构

int cpid = fork( );

if (cpid = = 0) 
   
  //child code

  exec(foo);

  exit(0);    


//parent code

wait(cpid);

// end

(在 exec 调用 unix 内核后清除子进程文本、数据、堆栈并填充 foo 进程相关的文本/数据) 因此子进程具有不同的代码(foo的代码与父进程不同)

【讨论】:

这与问题有点无关,但是如果子进程恰好先完成它的代码,上面的代码不会导致竞争条件吗?在这种情况下,父进程会一直等待子进程自行终止,对吧? @stdout:回答你的问题有点晚了,但我认为不会出现竞争条件。当一个进程在父进程等待它之前退出时,它会进入僵尸状态(它已经死了,但仍然有些徘徊)。闲逛的位基本上是退出代码,以便父母最终可以wait 并接收该信息。到那时,僵尸完全消失了。而且,如果父进程先消失,init 进程会继承子进程并最终获得退出进程(当子进程退出时)。【参考方案5】:

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

【讨论】:

第 7 行提到 exec() 函数创建子进程。真的是这样吗,因为 fork() 已经创建了子进程,而 exec() 调用只是替换了刚刚创建的新进程【参考方案6】:

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()ing 之前声明了一个计数器变量,设置为零。在 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() 函数以将其代码替换为日期程序。如果代码存储在名为 exec1.c 的文件中,则执行它会产生以下输出:

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

程序输出行 ―Ready to exec() 。 . . ‖ 并在调用 execvp() 函数后,将其代码替换为日期程序。请注意,行 ― 。 . .是否有效”不显示,因为此时代码已被替换。相反,我们看到执行“date -u”的输出。

【讨论】:

【参考方案7】:

fork() 创建当前进程的副本,并在新子进程中从 fork() 调用之后开始执行。在 fork() 之后,它们是相同的,除了 fork() 函数的返回值。 (更多详细信息请参见 RTFM。)这两个进程可以进一步分歧,一个进程无法干扰另一个进程,除非可能通过任何共享文件句柄。

exec() 用新进程替换当前进程。它与 fork() 无关,只是当想要启动不同的子进程而不是替换当前子进程时,exec() 通常会跟随 fork()。

【讨论】:

【参考方案8】:

fork():

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

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

    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;

【讨论】:

【参考方案9】:

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

对于许多命令,shell forks 和子进程 execs与名称相关的命令将命令行中剩余的单词视为命令的参数。

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

【讨论】:

以上是关于fork 和 exec 的区别的主要内容,如果未能解决你的问题,请参考以下文章

linux中fork,source和exec的区别

linux中fork, source和exec的区别

fork() 和exec() 的区别

在传统的 Linux fork-exec 中使用 _exit() 和 exit() 有啥区别?

shell中sh, exec, source, fork, ./的区别

fork exec source的区别