Linux中的fork与exec系列函数分析
fork 和 exec 的使用体现了 UNIX 的精髓,它提供了一种非常简单的方式来启动新的任务。注意这里“任务”一词的使用,我刻意避免使用“进程”或“程序”这两个术语:
进程是“执行引擎”,是操作系统中能够运行程序的实体;
程序是用于执行相同任务的特定代码片段。
你可以在不同的进程中运行同一个程序(例如交互式 shell)。
fork()的功能
考虑到这一点,fork 调用基本上会复制当前进程,几乎在各个方面都完全相同。并非所有内容都会被复制(例如,某些实现中的资源限制),但其目的是创建尽可能接近的副本。
fork将创建新进程(也叫子进程),子进程会获得不同的进程 ID (PID),并将创建它的进程(父进程)的 PID 作为其父进程 PID (PPID)。虽然两个进程运行的是同一个程序,但是它们可以通过 fork 的返回值来区分——子进程返回 0,而父进程返回子进程的 PID。当然,这一切都建立在 fork 调用成功的前提上——如果失败,则不会创建子进程,父进程会返回错误码。
exec()的功能
exec 调用本质上是用一个新程序替换进程中整个当前程序的方法。它会将新程序加载到当前进程空间,并从入口点运行。
因此,fork 和 exec 通常按顺序使用,以使新程序作为当前进程的子进程运行。每当你尝试运行类似 find 的程序时,Shell 通常会执行此操作——Shell 会 fork,然后子进程将 find 程序加载到内存中,设置所有命令行参数、标准 I/O 等等。
一些 UNIX 实现对 fork 进行了优化,使用了所谓的“写时复制”机制。这是一种技巧,可以延迟 fork 过程中对进程空间的复制,直到程序尝试更改该空间中的某些内容。这对于只使用 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 | to finish V
| +--------+ | | pid=22 |
| | ppid=7 |
| | ls |
V +--------+
+--------+ || pid=7 | | exits
| ppid=4 | <---------------+
| bash |
+--------+
|
| continues
V
评论