如果 fork()是程序员唯一可使用的建立进程的手段,那么 Linux 的性能会受很大影响。因为 fork()只能建立相同程序的副本。幸运的是,Linux 还提供了系统调用 exec 系列,它可以用于新程序的运行。exec 系列中的系统调用都完成相同的功能,它们把一个新程序装入调用进程的内存空间,来改变调用进程的执行代码,从而形成新进程。如果 exec 调用成功,调用进程将被覆盖,然后从新程序的入口开始执行。这样就产生了一个新的进程,但是它的进程标识符与调用进程相同。这就是说,exec 没有建立一个与调用进程并发的新进程,而是用新进程取代了原来的进程。所以,对 exec 调用成功后,没有任何数据返回,这与 fork()不同。下面给出了 exec 系列调用在 Linux 系统库中 unistd.h 中的函数声明:
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[]);
为了使事情简单明了,我们将着重讨论 exec 系列中的一个系统调用,即 execl()。execl()调用的参数均为字符型指针,第一个参数 path 给出了被执行的程序所在的文件名,它必须是一个有效的路径名,文件本身也必须含有一个真正的可执行程序。但是不能用 exec()l 来运行一个 shell 命令组成的文件。系统只要检查文件的开头两个字节,就可以知到该文件是否为程序文件(程序文件的开头两个字节是系统规定的专用值)。第二个以及用省略号表示的其它参数一起组成了该程序执行时的参数表。按照 Linux 的惯例,参数表的第一项是不带路径的程序文件名。被调用的程序可以访问这个参数表,它们相当于 shell 下的命令行参数。实际上,shell 本身对命令的调用也是用 exec 调用来实现的。由于参数的个数是任意的,所以必须用一个 null 指针来标记参数表的结尾。下面给出一个使用 execl 调用来运行目录列表程序 ls 的例子:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(void) { printf("executing ls -l!\n"); execl("/bin/ls","ls","-l",NULL); perror("----------\n"); return 0; }
调用前那一部分给出了 execl()即将执行之前时的进程情况,调用后那一部分给出了被改变进程的情况,它现在运行 ls 程序。程序计数器 PC 指向 ls 的第一行,表明 execl()导致从新程序的入口开始执行。请注意,程序在 execl()调用后紧跟着一个对库例行程序 perror()的无条件调用。这是因为,如果调用程序还存在,并且 execl()调用返回,那么肯定是 execl()调用出错了。这时,execl()和其它 exec 调用总是返回-1。这也就是说,只要 execl()和其它 exec 调用成功,就肯定清除了调用程序而代之以新的程序。