016 UNIX再学习 -- exec 函数族

                                          
                                                                                   
                                                                                
                                           

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

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

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

一、exec 函数族概述

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

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

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


   
   
  1. #include <unistd.h>
  2. extern char **environ;
  3. int execl(const char *path, const char *arg, ...);
  4. int execlp(const char *file, const char *arg, ...);
  5. int execle(const char *path, const char *arg, ..., char *const envp[]);
  6. int execv(const char *path, char *const argv[]);
  7. int execvp(const char *file, char *const argv[]);
  8. int execve(const char *path, char *const argv[], char *const envp[]);
  9. int fexecve(int fd, char *const argv[], char *const envp[]);
  10. 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 函数


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

1、函数解析

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

2、示例说明


    
    
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main (void) 
  6.     printf ( "父进程开始执行\n"); 
  7.  
  8.     pid_t pid = vfork (); 
  9.     if (pid == -1
  10.         perror ( "vfork"), exit ( 1); 
  11.     if (pid == 0
  12.     { 
  13.         printf ( "子进程开始执行\n"); 
  14.         if (execl ( "/bin/ls", "ls", "-l", NULL) == -1
  15.             perror ( "execl"), _exit ( 1); 
  16.     } 
  17.     sleep ( 1); 
  18.     printf ( "父进程执行结束\n"); 
  19.     return 0
  20. 输出结果: 
  21. 父进程开始执行 
  22. 子进程开始执行 
  23. 总用量 16 
  24. -rwxr-xr-x 1 root root 7380 Apr 20 10: 22 a.out 
  25. -rw-r--r-- 1 root root  383 Apr 20 10: 22 test.c 
  26. -rw-r--r-- 1 root root  151 Apr 20 09: 56 test.c~ 
  27. 父进程执行结束 

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()后边的代码也就不会执行了。举例说明:

     
     
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main (void) 
  6.     printf ( "父进程开始执行\n"); 
  7.  
  8.     pid_t pid = vfork (); 
  9.     if (pid == -1
  10.         perror ( "vfork"), exit ( 1); 
  11.     if (pid == 0
  12.     { 
  13.         printf ( "子进程开始执行\n"); 
  14.         if (execl ( "/bin/ls", "ls", "-l", 0) == -1
  15.             perror ( "execlp"), _exit ( 1); 
  16.     } 
  17.     sleep ( 1); 
  18.     printf ( "父进程执行结束\n"); 
  19.     return 0
  20. 编译:
  21. 警告: 函数调用中缺少哨兵 [-Wformat]

三、execlp 函数


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

1、函数解析

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

2、示例说明


    
    
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main (void) 
  6.     printf ( "父进程开始执行\n"); 
  7.  
  8.     pid_t pid = vfork (); 
  9.     if (pid == -1
  10.         perror ( "vfork"), exit ( 1); 
  11.     if (pid == 0
  12.     { 
  13.         printf ( "子进程开始执行\n"); 
  14.         if (execlp ( "ls", "ls", "-l", NULL) == -1
  15.             perror ( "execlp"), _exit ( 1); 
  16.     } 
  17.     sleep ( 1); 
  18.     printf ( "父进程执行结束\n"); 
  19.     return 0
  20. 输出结果:
  21. 父进程开始执行
  22. 子进程开始执行
  23. 总用量 16
  24. -rwxr-xr-x 1 root root 7381 Apr 26 15: 02 a.out
  25. -rw-r--r-- 1 root root  480 Apr 26 15: 02 test.c
  26. -rw-r--r-- 1 root root  846 Apr 25 17: 01 test.c~
  27. 父进程执行结束

3、示例解析

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

四、execle 函数


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

1、函数解析

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

2、示例说明


     
     
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main (void) 
  6.     printf ( "父进程开始执行\n"); 
  7.  
  8.     pid_t pid = vfork (); 
  9.     if (pid == -1
  10.         perror ( "vfork"), exit ( 1); 
  11.     if (pid == 0
  12.     { 
  13.         printf ( "子进程开始执行\n"); 
  14.         if (execle ( "/bin/ls", "ls", "-l", NULL, NULL) == -1
  15.             perror ( "execle"), _exit ( 1); 
  16.     } 
  17.     sleep ( 1); 
  18.     printf ( "父进程执行结束\n"); 
  19.     return 0
  20. 输出结果:
  21. 父进程开始执行
  22. 子进程开始执行
  23. total 16
  24. -rwxr-xr-x 1 root root 7381 Apr 26 15: 32 a.out
  25. -rw-r--r-- 1 root root  491 Apr 26 15: 32 test.c
  26. -rw-r--r-- 1 root root  488 Apr 26 15: 13 test.c~
  27. 父进程执行结束

3、示例解析

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

五、execv 函数


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

1、函数解析

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

2、示例说明


     
     
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main (void) 
  6.   char *arg[] = { "ls", "-l", NULL};
  7.     printf ( "父进程开始执行\n"); 
  8.  
  9.     pid_t pid = vfork (); 
  10.     if (pid == -1
  11.         perror ( "vfork"), exit ( 1); 
  12.     if (pid == 0
  13.     { 
  14.         printf ( "子进程开始执行\n"); 
  15.         if (execv ( "/bin/ls", arg) == -1
  16.             perror ( "execv"), _exit ( 1); 
  17.     } 
  18.     sleep ( 1); 
  19.     printf ( "父进程执行结束\n"); 
  20.     return 0
  21. 输出结果:
  22. 父进程开始执行
  23. 子进程开始执行
  24. 总用量 16
  25. -rwxr-xr-x 1 root root 7380 Apr 26 15: 48 a.out
  26. -rw-r--r-- 1 root root  505 Apr 26 15: 48 test.c
  27. -rw-r--r-- 1 root root  488 Apr 26 15: 13 test.c~
  28. 父进程执行结束

3、示例解析

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

六、execvp 函数


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

1、函数解析

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

2、示例说明


     
     
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main (void) 
  6.   char *arg[] = { "ls", "-l", NULL};
  7.     printf ( "父进程开始执行\n"); 
  8.  
  9.     pid_t pid = vfork (); 
  10.     if (pid == -1
  11.         perror ( "vfork"), exit ( 1); 
  12.     if (pid == 0
  13.     { 
  14.         printf ( "子进程开始执行\n"); 
  15.         if (execvp ( "ls", arg) == -1
  16.             perror ( "execvp"), _exit ( 1); 
  17.     } 
  18.     sleep ( 1); 
  19.     printf ( "父进程执行结束\n"); 
  20.     return 0
  21. 输出结果:
  22. 父进程开始执行
  23. 子进程开始执行
  24. 总用量 16
  25. -rwxr-xr-x 1 root root 7381 Apr 26 15: 54 a.out
  26. -rw-r--r-- 1 root root  502 Apr 26 15: 54 test.c
  27. -rw-r--r-- 1 root root  488 Apr 26 15: 13 test.c~
  28. 父进程执行结束

3、示例解析

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

七、execve 函数


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

1、函数解析

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

2、示例说明


    
    
  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main (void) 
  6.   char *arg[] = { "ls", "-l", NULL};
  7.     printf ( "父进程开始执行\n"); 
  8.  
  9.     pid_t pid = vfork (); 
  10.     if (pid == -1
  11.         perror ( "vfork"), exit ( 1); 
  12.     if (pid == 0
  13.     { 
  14.         printf ( "子进程开始执行\n"); 
  15.         if (execve ( "/bin/ls", arg, NULL) == -1
  16.             perror ( "execve"), _exit ( 1); 
  17.     } 
  18.     sleep ( 1); 
  19.     printf ( "父进程执行结束\n"); 
  20.     return 0
  21. 输出结果:
  22. 父进程开始执行
  23. 子进程开始执行
  24. total 16
  25. -rwxr-xr-x 1 root root 7381 Apr 26 16: 12 a.out
  26. -rw-r--r-- 1 root root  513 Apr 26 16: 12 test.c
  27. -rw-r--r-- 1 root root  488 Apr 26 15: 13 test.c~
  28. 父进程执行结束

3、示例解析

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

八、fexecve 函数 


    
    
  1. #include <unistd.h>
  2. int fexecve(int fd, char *const argv[], char *const envp[]);
  3. 若出错,返回 -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文件,并重命名


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

或使用 -exec


    
    
  1. root@zslf- virtual-machine:/mnt/test # ls 
  2. abc.txt  def.txt  ef.jpg  hh.jpg  images.tar.gz  sd.jpg 
  3. root@zslf- virtual-machine:/mnt/test # find ./ -name "*.jpg" -exec cp {} {}.old \;  
  4. root@zslf- virtual-machine:/mnt/test # ls 
  5. 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。

                                   
                                   
               
                   
  •                                                
  •                                                     点赞                         1                        
  •                        
  •                                                     收藏
  •                        
  •                                                     分享
  •                                                                                                                        
  •                                                        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值