exec族函数是Linux进程控制原语中非常重要的一部分,往往和fork/vfork配合使用。

当一个进程调用一种exec函数时,该进程执行的程序完全替换未新程序,而新程序则从其main函数开始执行(原程序后续部分不会再执行了)。但是调用exec并不创建新进程(不同于fork和vfork),所以调用exec前后进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈。

下面是exec族的函数:

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...
               /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...
               /* (char  *) NULL */);
int execle(const char *path, const char *arg, ...
               /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[],
                  char *const envp[]);
int execvp(const char *file, char *const argv[]);

函数失败返回-1,成功则不返回。

这个家族挺大的,挺难记忆的。我们可以稍微总结一下特征:

特点1. 以p结尾的两个函数(execlp,execvp)取文件名作为第一个参数,其它几个取路径名作为参数。这里的文件名可以带绝对路径,也可以不带。这样的话,程序将会取PATH环境变量里面找可执行文件。而其它几个不以p结尾只能使用绝对路径。

程序1:使用不以p结尾的,且不使用路径名作为参数:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    printf ( "begin ...n" );
    /* 使用不带p版本的 */
    if (execl("ls", "ls", "-rt", NULL) == -1)   
    {
        perror("execl error");
    }
    printf ( "end ...n" );

    return 0;
}

执行结果:

allan@NYC:~/test$ ./exec_test 
begin ...
execl error: No such file or directory
end ...

程序2:将程序1改为使用以p结尾的

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    printf ( "begin ...n" );
    if (execlp("ls", "ls", "-rt", NULL) == -1)
    {
        perror("execl error");
    }
    printf ( "end ...n" );

    return 0;
}

执行结果:

allan@NYC:~/test$ ./exec_test 
begin ...
apue.c  bug.c  test1  test1.out  test.dat  file.c  file  c-7.c  c-7  bug_exp  exec_test.c  exec_test

可以看到,程序2可以正常执行。要注意的是,后面的printf并没有执行到。因为exec替换了原有的进程映像。

所以,对于以p结尾的这两个exec函数,我们使用的时候,可以不用指定可执行文件(如果不是ld产生的二进制文件,exec就会尝试用/bin/sh取执行,即把它当做一个shell脚本取执行)的路径,让系统取PATH里面查找。当然也可以指定,但如果指定的话,就一定要指定对,因为系统只会去指定的路径查找,不会再去PATH指定的路径找了。举个例子,ls名字的完整路径是/bin/ls,那么我们可以写成execlp("ls", "ls", "-rt", NULL) ,或者execlp("/bin/ls", "ls", "-rt", NULL),但不能写成execlp("/usr/bin/ls", "ls", "-rt", NULL)。而对于不带p后缀的,第一个参数只能写成"/bin/ls"。即,带不带p决定exec族函数的第一个参数如何写——如果带p,可以不用写正确的绝对路径;如果不带,就必须写正确的绝对路径。

特点2. 参数中的l(list)和v(vector)决定其它参数如何传递(l和v是互斥的,只会包含其中之一)简单说就是,包含l的(execl,execlv,execlp),将要执行的可执行文件的参数直接写到exec函数参数里面;包含v的(execv,execve,execvp),必须先将要执行的可执行文件的参数写到一个字符串数组里面,然后把这个数组作为exec的参数。需要注意的是,包含l的,最后一个参数必须是(char *)0,当然,就是C里面的NULL。

程序2就是包含l的例子,我们举一个包含v的例子,程序3:包含v的

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

char* argv[] = { "ls", "-rt" };

int main()
{
    printf ( "begin ...n" );
    if (execv("/bin/ls", argv) == -1)
    {
        perror("exec error");
    }
    printf ( "end ...n" );

    return 0;
}

执行结果:

allan@NYC:~/test$ ./exec_test 
begin ...
apue.c  bug.c  test1  test1.out  test.dat  file.c  file  c-7.c  c-7  bug_exp  exec_test.c  exec_test

特点3:以e结尾的exec函数(execle,execve)可以(选择性的)传递一个指针数组,该指针数组是一些环境变量的定义。其它的不以e结尾的则使用调用进程总的environ变量(一个系统预定义的全局变量)为新程序复制现有的环境。

下面我们举一个例子来看一下以e结尾exec函数和其它的区别:

程序4:echoall程序,打印命令行参数以及环境变量,在后面的程序中会使用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    char            **ptr;
    extern  char    ** environ;

    for (int i = 0; i < argc; i++)
    {
        printf ( "argv[%d]: %sn", i, argv[i] );
    }

    for (ptr = environ; *ptr != 0; ptr++)
    {
        printf ( "%sn", *ptr );
    }

    exit(0);
}

程序5:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

char    *env_init[] = { "USER=known", "PATH=/tmp", NULL };

int main()
{
    pid_t   pid;

    if ((pid = fork()) < 0)
    {
        printf ( "fork errorn" );
        exit(1);
    }
    else if (pid == 0)
    {
        if (execle("/home/allan/test/echoall", "echoall", "myarg1", "MY_ARG2", NULL, env_init) < 0)
        {
            perror ( "execle error" );
            exit(2);
        }
    }

    if (waitpid(pid, NULL, 0) < 0)
    {
        perror("wait error");
        exit(3);
    }

    if ((pid = fork()) < 0)
    {
        printf ( "fork error 2n" );
        exit(4);
    }
    else if (pid == 0)
    {
        if (execlp("/home/allan/test/echoall", "echoall", "only 1 arg", NULL) < 0)
        {
            perror("execlp error");
            exit(5);
        }
    }

    exit(0);
}

程序5运行结果:

allan@NYC:~/test$ ./exec_test 
argv[0]: echoall
argv[1]: myarg1
argv[2]: MY_ARG2
USER=known
PATH=/tmp
allan@NYC:~/test$ argv[0]: echoall
argv[1]: only 1 arg
XDG_VTNR=7
LC_PAPER=zh_CN.UTF-8
/* 中间省略 */
GTK_IM_MODULE=fcitx
LESSCLOSE=/usr/bin/lesspipe %s %s
LC_TIME=zh_CN.UTF-8
LC_NAME=zh_CN.UTF-8
XAUTHORITY=/home/allan/.Xauthority
_=./exec_test

可以看到,对于带e的,使用了我们指定的环境变量,不带e的继承了父进程的环境变量(即系统默认的环境变量)。再强调一点:对于带e的exec函数,我们也可以不传递最后一个envp这个参数,这个和传一个空的envp指针数组是一样的,只是编译的时候会报一个WARNING。当然,这样做的后果是我们新的进程映像就没有任何环境变量了。所以,如果不是要为新的进程指定某个特定的环境(变量),就不要使用带e的exec函数。

最后,我们再说两个知识点:

  1. 虽然exec族有很多函数,但具体的实现中往往只有execve是内核的系统调用,其它的都是库函数,它们最终都要调用execve系统调用。
  2. exec族函数中都有一个arg0参数,习惯上指定为要执行的文件名,但其实没有什么用。程序并不使用这个参数。
    看下面两个例子:

程序6:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    if (execlp("ls", "abc", NULL) < 0)
    {
        perror("execle error");
        exit(1);
    }

    exit(0);
}

程序7:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

char *argv[] = { "abc" , NULL};

int main()
{
    if (execvp("ls", argv) < 0)
    {
        perror("execle error");
        exit(1);
    }

    exit(0);
}

上面程序6和程序7中的arg0参数我们都指定为了"abc",但它们都可以正常运行。当然,我们还是遵从习惯,指定为可执行文件名比较好。

至此,exec家族就算介绍完了。