UNIX再学习 -- exec 函数族

我们在讲,文件I/O的时候,简单提到过 exec 函数,讲到 vfork 的时候,也有用到。下面我们来详细介绍下它。

参看:UNIX再学习 -- 文件I/O 

参看:UNIX再学习 -- 函数 fork 和 vfork

一、exec 函数族概述

与 fork 或 vfork 函数不同,exec 函数不是创建调用进程的子进程,而是创建一个新的进程取代调用进程自身。新进程会用自己的全部地址空间,覆盖调用进程的地址空间,但进程的 PID 保持不变。exec 只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

exec 不是一个函数而是一堆函数,一般称为 exec 函数族。它们的功能是一样的,用法也很相近,只是参数的形式和数量略有不同。

exec函数族的作用:根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件

#include <unistd.h>
extern char **environ;
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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
7 个函数返回值:若出错,返回 -1;若成功,不返回

这些函数之间的第一个区别:

4 个函数取路径名作为参数,2 个函数则取文件名作为参数,最后一个取文件描述符作为参数。当指定 file 作为参数时:如果 file 中包含 /,则就将其视为路径名。否则就按 PATH 环境变量,在它所指定的各目录中搜索可执行文件。

环境变量,我们之前专门讲过,参看:UNIX再学习 -- 环境变量

PATH 变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔。例如,下面 name = value 环境字符串指定在 4 个目录中进程搜索。

PATH=/bin:/usr/bin:/usr/local/bin:.

最后的路径前缀 . 表示当前目录。(零长前缀也表示当前目录。在 value 的开始处可用 : 表示,在行中间则要用 :: 表示,在行尾以 : 表示)。

第二个区别与参数表的传递有关(l 表示表 (list), v 表示矢量(vector))。

函数 execl、execlp 和 execle 要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数以空指针结尾。对于另外 4 个函数 (execv、execvp、execve、fexecve),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这 4 个函数的参数

第三个区别与向新程序传递环境表相关

以 e 结尾的 3 个函数(execle 和 execve 和 fexecve)可以传递一个指向字符串指针数组的指针。其他四个函数则使用调用进程中的 environ 变量为新程序复制现存的环境。


这些函数,它们的函数名都是在 exec 后面加上一到两个字符后缀,不同的字符后缀代表不同的含义。

-l:  即 list ,新进程的命令行参数以字符指针列表 (const char *arge, ...) 的形式传入,列表以空指针结束。

-p: 即 path,若第一个参数中不包含“/”,则将其视为文件名,并根据 PATH 环境变量搜索该文件。

-e:  即 environment,新进程的环境变量以字符指针数组 (cahr *const envp[]) 的形式传入,数组以空指针结束,不指定环境变量则从调用进程复制。 

-v:  即 vector,新进程的命令行参数以字符指针数组 (char *const argv[]) 的形式传入,数组以空指针结束。


在很多 UNIX 实现中,这 7 个函数中只有 execve 是内核的系统调用另外 6 个只是库函数,它们最终都要调用该系统调用。这 7 个函数之间的关系如下图:

在这种安排中,库函数 execlp 和 execvp 使用 PATH 环境变量,查找第一个包含名为 filename 的可执行文件的路径名前缀。fexecve 库函数使用 /proc 把文件描述符参数转换成路径名,execve 用该路径名去执行程序。

到此,将 exec 函数族 7 个函数的区别和关系,简单讲了一下。下面我们就一一介绍:

二、execl 函数

#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
若出错,返回 -1, 若成功,不返回

1、函数解析

execl()其中后缀 "l" 代表 list 也就是参数列表的意思,第一参数 path 字符指针所指向要执行的文件路径 接下来的参数代表执行该文件时传递的参数列表:argv[0],argv[1]... 最后一个参数须用空指针NULL作结束

2、示例说明

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
int main (void)  
{  
    printf ("父进程开始执行\n");  
  
    pid_t pid = vfork ();  
    if (pid == -1)  
        perror ("vfork"), exit (1);  
    if (pid == 0)  
    {  
        printf ("子进程开始执行\n");  
        if (execl ("/bin/ls", "ls", "-l", NULL) == -1)  
            perror ("execl"), _exit (1);  
    }  
    sleep (1);  
    printf ("父进程执行结束\n");  
    return 0;  
}  
输出结果:  
父进程开始执行  
子进程开始执行  
总用量 16  
-rwxr-xr-x 1 root root 7380 Apr 20 10:22 a.out  
-rw-r--r-- 1 root root  383 Apr 20 10:22 test.c  
-rw-r--r-- 1 root root  151 Apr 20 09:56 test.c~  
父进程执行结束  

3、示例解析

上例为 vfork 函数的典型用法。在所创建的子进程里直接调用 exec 函数启动另外一个进程取代其自身,这比调用 fork 函数完成同样的工作要快得多。
if (execl ("/bin/ls", "ls", "-l", NULL) == -1)  解释:
/bin/ls  为 ls 指令文件路径;ls -l 为执行 ls 指令和选项;NULL 最后一个参数;失败返回 -1.
有时也会看到如下的写法:
if (execl ("ls", "ls", "-l", (char *)0) == -1)  
将第三个参数写为 (char*)0 ,我们之前讲过空指针,参看:C语言再学习 -- NUL和NULL的区别
如果用常数 0 来表示一个空指针,则必须将它强制转换为一个字符指针,否则它将解释为整形参数,如果一个整形数的长度与 char * 的长度不同,那么 exec 函数的实际参数就将出错。如果函数调用成功,进程自己的执行代码就会变成加载程序的代码,execl()后边的代码也就不会执行了。举例说明:
#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
int main (void)  
{  
    printf ("父进程开始执行\n");  
  
    pid_t pid = vfork ();  
    if (pid == -1)  
        perror ("vfork"), exit (1);  
    if (pid == 0)  
    {  
        printf ("子进程开始执行\n");  
        if (execl ("/bin/ls", "ls", "-l", 0) == -1)  
            perror ("execlp"), _exit (1);  
    }  
    sleep (1);  
    printf ("父进程执行结束\n");  
    return 0;  
}  
编译:
警告: 函数调用中缺少哨兵 [-Wformat]

三、execlp 函数

#include <unistd.h>
int execlp(const char *file, const char *arg, ...);
若出错,返回 -1, 若成功,不返回

1、函数解析

execlp()会从 PATH 环境变量所指的目录中查找符合参数 file 的文件名,找到后便执行该文件,然后将第二个以后的
参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。

2、示例说明

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
int main (void)  
{  
    printf ("父进程开始执行\n");  
  
    pid_t pid = vfork ();  
    if (pid == -1)  
        perror ("vfork"), exit (1);  
    if (pid == 0)  
    {  
        printf ("子进程开始执行\n");  
        if (execlp ("ls", "ls", "-l", NULL) == -1)  
            perror ("execlp"), _exit (1);  
    }  
    sleep (1);  
    printf ("父进程执行结束\n");  
    return 0;  
}  
输出结果:
父进程开始执行
子进程开始执行
总用量 16
-rwxr-xr-x 1 root root 7381 Apr 26 15:02 a.out
-rw-r--r-- 1 root root  480 Apr 26 15:02 test.c
-rw-r--r-- 1 root root  846 Apr 25 17:01 test.c~
父进程执行结束

3、示例解析

开始就讲到,当指定 file 作为参数时,如果 file 中包含 /,则就将其视为路径名。否则就按 PATH 环境变量,在它所指定的各目录中搜索可执行文件 这也就是 execlp 和 execl 的区别了。

四、execle 函数

#include <unistd.h>
int execle(const char *path, const char *arg, ..., char * const envp[]);
若出错,返回 -1, 若成功,不返回

1、函数解析

execl是用来执行参数 path 字符串所代表的文件路径,并为新程序复制最后一个参数所指示的环境变量。接下来的参数代表执行该文件时传递过去的argv(0)、argv[1]……,最后一个参数必须用空指针(NULL)作结束。

2、示例说明

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
int main (void)  
{  
    printf ("父进程开始执行\n");  
  
    pid_t pid = vfork ();  
    if (pid == -1)  
        perror ("vfork"), exit (1);  
    if (pid == 0)  
    {  
        printf ("子进程开始执行\n");  
        if (execle ("/bin/ls", "ls", "-l", NULL, NULL) == -1)  
            perror ("execle"), _exit (1);  
    }  
    sleep (1);  
    printf ("父进程执行结束\n");  
    return 0;  
}  
输出结果:
父进程开始执行
子进程开始执行
total 16
-rwxr-xr-x 1 root root 7381 Apr 26 15:32 a.out
-rw-r--r-- 1 root root  491 Apr 26 15:32 test.c
-rw-r--r-- 1 root root  488 Apr 26 15:13 test.c~
父进程执行结束

3、示例解析

倒数第二个参数为,传递一个指向字符串指针数组的指针。

五、execv 函数

#include <unistd.h>
int execv(const char *path, char *const argv[]);
若出错,返回 -1, 若成功,不返回

1、函数解析

execv() 用来执行参数 path 字符串所代表的文件路径,与 execl() 不同的地方在于 execve() 只需两个参数,第二个参数利用数组指针来传递给执行文件

2、示例说明

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
int main (void)  
{  
	char *arg[] = {"ls", "-l", NULL};
    printf ("父进程开始执行\n");  
  
    pid_t pid = vfork ();  
    if (pid == -1)  
        perror ("vfork"), exit (1);  
    if (pid == 0)  
    {  
        printf ("子进程开始执行\n");  
        if (execv ("/bin/ls", arg) == -1)  
            perror ("execv"), _exit (1);  
    }  
    sleep (1);  
    printf ("父进程执行结束\n");  
    return 0;  
}  
输出结果:
父进程开始执行
子进程开始执行
总用量 16
-rwxr-xr-x 1 root root 7380 Apr 26 15:48 a.out
-rw-r--r-- 1 root root  505 Apr 26 15:48 test.c
-rw-r--r-- 1 root root  488 Apr 26 15:13 test.c~
父进程执行结束

3、示例解析

arg 是一个以 NULL 结尾的字符串数组的指针。

六、execvp 函数

#include <unistd.h>
int execvp(const char *file, char *const argv[]);
若出错,返回 -1, 若成功,不返回

1、函数解析

execvp()会从 PATH 环境变量所指的目录中查找符合参数 file 的文件名,找到后便执行该文件,然后将第二个参数argv 传给该欲执行的文件。

2、示例说明

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
int main (void)  
{  
	char *arg[] = {"ls", "-l", NULL};
    printf ("父进程开始执行\n");  
  
    pid_t pid = vfork ();  
    if (pid == -1)  
        perror ("vfork"), exit (1);  
    if (pid == 0)  
    {  
        printf ("子进程开始执行\n");  
        if (execvp ("ls", arg) == -1)  
            perror ("execvp"), _exit (1);  
    }  
    sleep (1);  
    printf ("父进程执行结束\n");  
    return 0;  
}  
输出结果:
父进程开始执行
子进程开始执行
总用量 16
-rwxr-xr-x 1 root root 7381 Apr 26 15:54 a.out
-rw-r--r-- 1 root root  502 Apr 26 15:54 test.c
-rw-r--r-- 1 root root  488 Apr 26 15:13 test.c~
父进程执行结束

3、示例解析

当指定 file 作为参数时,如果 file 中包含 /,则就将其视为路径名。否则就按 PATH 环境变量,在它所指定的各目录中搜索可执行文件这也就是 execvp 和 execv 的区别了。
execlp 或 execvp 使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件是一个 shell 脚本,于是试着调用 /bin/sh,并以该 file 作为 shell 的输入。

七、execve 函数

#include <unistd.h>
int execvpe(const char *file, char *const argv[], char *const envp[]);
若出错,返回 -1, 若成功,不返回

1、函数解析

execve() 用来执行参数 file 字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。

2、示例说明

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
int main (void)  
{  
	char *arg[] = {"ls", "-l", NULL};
    printf ("父进程开始执行\n");  
  
    pid_t pid = vfork ();  
    if (pid == -1)  
        perror ("vfork"), exit (1);  
    if (pid == 0)  
    {  
        printf ("子进程开始执行\n");  
        if (execve ("/bin/ls", arg, NULL) == -1)  
            perror ("execve"), _exit (1);  
    }  
    sleep (1);  
    printf ("父进程执行结束\n");  
    return 0;  
}  
输出结果:
父进程开始执行
子进程开始执行
total 16
-rwxr-xr-x 1 root root 7381 Apr 26 16:12 a.out
-rw-r--r-- 1 root root  513 Apr 26 16:12 test.c
-rw-r--r-- 1 root root  488 Apr 26 15:13 test.c~
父进程执行结束

3、示例解析

第一个参数为 file,最后一个参数为 NULL,这没什么讲的了。
重点是 7 个函数中只有 execve 是内核的系统调用,另外 6 个只是库函数,它们最终都要调用该系统调用。

八、fexecve 函数 

#include <unistd.h>
int fexecve(int fd, char *const argv[], char *const envp[]);
若出错,返回 -1, 若成功,不返回

1、函数解析

fexecve ( )执行与 execve  相同的任务,区别在于通过文件描述符 fd  指定要执行的文件,而不是通过路径名。 文件描述符 fd 必须以只读方式打开,并且调用方必须具有执行权限它所指的文件。
fexecve 库函数使用 /proc 把文件描述符参数转换成路径名,execve 用该路径名去执行程序。
暂时不讲,找不到相关示例。

九、find 和 xargs 命令相关内容 

每个系统对参数表和环境表的总长度都有一个限制。这种限制是由 ARG_MAX 给出的。在 POSIX.1 系统中,此值至少是 4096 字节。而我用的 Ubuntu 12.04 在该系统中默认为 2097152,以通过 getconf ARG_MAX 可查看系统当前设置的值。 参看:getconf命令及查看Linux32位和64位命令
当我们执行某些命令有时候会报“Argument list too long”错误,例如,当前目录文件很多时执行mv * 或rm *,该错误表示执行命令的参数太长,超过系统允许的最大值。
出现这种情况时,通常有两种解决办法:
1、更改命令执行方式。
例如执行rm * 时使用如下命令替换:find . -name "*" | xargs rm {}
2、修改 ARG_MAX 的大小  (了解,系统不对实现不了)
参看:lsattr 命令
(1)使用命令lsattr -El sys0 -a ncargs查看ncargs占有字节,输出结果:ncargs 512 ARG/ENV list size in 4K byte blocks True
(2)getconf ARG_MAX 查看ARG_MAX设置值大小,2097152
(3)调整ncargs占用字节:chdev -l sys0 -a ncargs=8 表示设置ncargs占用8字节,增加这个值就可以修改ARG_MAX参数的设置了

我们讲 find 和 xargs 命令时,也涉及到 exec 选项。
-exec [commend]        查找后执行命令的时候不询问用户,直接执行。

查找所有jpg文件,并重命名

root@zslf-virtual-machine:/mnt/test# ls  
abc.txt  def.txt  ef.jpg  hh.jpg  images.tar.gz  sd.jpg  
root@zslf-virtual-machine:/mnt/test# find ./ -name "*.jpg" -type f | xargs -i cp {} {}.old   
root@zslf-virtual-machine:/mnt/test# ls  
abc.txt  def.txt  ef.jpg  ef.jpg.old  hh.jpg  hh.jpg.old  images.tar.gz  sd.jpg  sd.jpg.old  

或使用 -exec

root@zslf-virtual-machine:/mnt/test# ls  
abc.txt  def.txt  ef.jpg  hh.jpg  images.tar.gz  sd.jpg  
root@zslf-virtual-machine:/mnt/test# find ./ -name "*.jpg" -exec cp {} {}.old \;   
root@zslf-virtual-machine:/mnt/test# ls  
abc.txt  def.txt  ef.jpg  ef.jpg.old  hh.jpg  hh.jpg.old  images.tar.gz  sd.jpg  sd.jpg.old  

十、最常见的错误

大家在平时的编程中,如果用到了exec函数族,一定记得要加错误判断语句。因为与其他系统调用比起来,exec很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:
找不到文件或路径,此时errno被设置为ENOENT;
数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;
没有对要执行文件的运行权限,此时errno被设置为EACCES。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聚优致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值