一、什么是exec函数

  • 用fork函数创建子进程后,子进程如果想要执行另一个程序,往往要调用exec函数以执行另一个程序
  • exec函数执行的特点:
  • exec​把当前进程映像替换成新的程序文件​,该进程完全由新程序代换,而且新程序从其main函数开始执行
  • 因为调用exec并不创建新进程,​所以前后的进程ID并未改变​。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段
  • 如果exec函数执行成功,则原程序中的​exec之后的代码都不会执行
  • exec函数​不会关闭原程序打开的文件描述符​,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性(见socket函数的type参数

二、exec函数

  • 有7种不同的exec函数可供使用,它们常常被统称为exec函数,我们可以使用这些中的其中一个,这些exec函数使得UNIX进程控制原语更加完善。用fork可以创建新进程,用exec可以执行新的程序
  • exit函数和两个wait函数处理终止和等待终止。这些是我们需要的基本的进程控制原语。在后面各节中将使用这些原语构造另外一些如popen和system之类的函数
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /*(char *)0*/);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, .../*(char *)0, char *const envp[]*/);
int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);

int fexecve(int fd, char *const argv[], char *const envp[]);
  • 参数:​前四个函数取路径名作为参数,后两个函数则取文件名作为参数,最后一个函数取文件描述符作为参数
  • 这几个函数分别使用pathname或filename或打开的文件描述符fd来打开一个新的进程
  • 返回值:​出错返回-1并errno;成功不返回(一般情况下,exec函数是不返回的,除非出错)


当指定filename作为参数时:

  • 如果filename中包含/,则就将filename作为一个路径名来打开一个程序
  • 否则就从PATH环境变量的相关目录中搜寻​可执行文件(​有很多出于安全性方面的考虑,要求在搜索路径中决不要包括当前目录​



这7个exec函数的参数很难记忆。函数名中的字符会给我们一些帮助:

  • 字母p表示该函数取filename作为参数,并且用PATH环境变量寻找可执行文件
  • 字母l表示该函数取一个参数表,它与字母v互斥
  • v表示该函数取一个argv[]矢量
  • 母e表示该函数取envp[]数组,而不使用当前环境

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_c函数



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

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_c函数_02

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_c函数_03


三、不同exec函数的区别

区别总结:

  • ①待执行的程序文件是由文件名还是由路径名决定
  • ②新程序的参数是一一列出还是由一个指针数组来引用
  • ③把调用进程的环境传递给新程序还是给新程序重新制定新的环境


execlp、execvp:

  • 如果execlp和execvp中的任意一个使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑程序产生的机器可执行代码文件,则就认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入



第二个区别:新进程的参数表是通过逐个参数传递还是通过一个指针数组传递(l表示列表list),v表示矢量(vector)

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



在使用ISOC原型之前,对execl,execle和execlp三个函数表示命令行参数的一般方法是:

char *arg0, char *arg1, ..., char *argn, (char *)0

  • 这种语法显示地说明了最后一个命令行参数之后跟了一个空指针。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则它将被解释为整型参数。如果一个整型数的长度与char*的长度不同,exec函数实际参数就将出错



最后一个区别与向新程序传递环境表相关:

  • 以 e结尾的3个函数(execle和execve、fexecve) 可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现存的环境。(回忆7 . 9节及表7-2中对环境字符串的讨论。其中曾提及如果系统支持setenv和putenv这样的函数,则可更改当前环境和后面生成的子进程的环境,但不能影响父进程的环境)
  • 通常,一个进程允许将其环境传播给其子进程, 但有时也有这种情况,进程想要为子进程指定一个确定的环境。例如,在初始化一个新登录的shell时,login程序创建一个只定义少数几个变量的特殊环境,而在我们登录时, 可以通过shell启动文件,将其他变量加到环境中

在使用ISO C 原型之前, execle的参数是:

char *pathname, char *arg0, ..., char *argn, (char *)0, char *envp[]

  • 从中可见,最后一个参数是指向环境字符串的各字符指针构成的数组的指针。而在ISO C原型中,所有命令行参数,包括空指针,envp指针都用省略号(…)表示


四、参数表和环境表的限制

  • 概念:​每个系统对参数表和环境表的总长度都有一个限制
  • 案例​:在表2-7中,这种限制是ARG_MAX。 在POSIX.1系统中,此值至少是4096字节。当使用shell的文件名扩充功能产生一个文件名表时, 可能会受到此值的限制。


  • 例如,命令:

grep getrlimit /usr/share/man/*/*

  • 在某些系统上可能产生下列形式的shell错误:Argument list too long
  • 备注:由于历史原因,System V中此限制是5120字节。早期BSD系统的此限制是20480字节


  • xargs命令:
  • 为了摆脱对参数表长度的限制。我们可以使用xargs命令,将长参数表断分为几部分。为了寻找在我们所用系统手册页中的getrlimit,我们可以使用:​find /usr/share/man -type f -print | xargs grep getrlimit
  • 如果所用的系统手册是压缩过的,则可使用:​find /usr/share/man -type f -print | xargs bzgrep getrlimit
  • 对于find命令,我们使用-type f,以限制输出列表质只包含普通文件。这样的原因是:
  • grep命令不能在目录中进行模式搜索,我们也想避免不必要的出错消息

五、新程序的特点

在执行exec后,进程ID没有改变。但新程序从调用进程继承了的下列属性:

  • 进程ID和父进程ID
  • 实际用户ID和实际组ID
  • 附属组ID
  • 进程组ID
  • 对话ID
  • 控制终端
  • 闹钟尚余留的时间
  • 当前工作目录
  • 根目录
  • 文件模式创建屏蔽字
  • 文件锁
  • 进程信号屏蔽
  • 未处理信号
  • 资源限制
  • nice值(遵循XSI的系统)
  • tms_utime、tms_stime、tms_cutime以及tms_ustime值

六、执行时关闭(close-on-exec)

  • 对打开文件的处理与每个描述符的执行时关闭(close-on-exec)标志值有关
  • 进程中每个打开描述符都有一个执行时关闭标志。若设置了此标志,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开
  • POSIX.1明确要求在exec时关闭打开目录流(见opendir函数)。这通常是由opendir函数实现的,它调用fcntl函数为对应于打开目录流的描述符设置exec关闭标志

七、用户ID与实际组ID

  • 注意,在exec前后实际用户ID和实际组ID保持不变
  • 而有效ID是否改变则取决于所执行程序的文件的设置用户ID位和设置组ID位是否设置
  • 如果新程序的设置用户ID位已设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。对组ID的处理方式与此相同

八、演示案例1

#include <sys/wait.h>
char *env_init[] ={"USER=unknown", "PATH=/tmp", NULL };

int main(void)
{
pid_t pid;

if ((pid = fork()) < 0) {
perror("fork error");
}
else if (pid == 0) {
if (execle("/home/sar/bin/echoall", "echoall", "myarg1","MY ARG2", (char *)0, env_init) < 0)
perror("execle error");
}

if (waitpid(pid, NULL, 0) < 0)
perror("wait error");

if ((pid = fork()) < 0) {
perror("fork error");
}
else if (pid == 0) {
if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0)
perror("execlp error");
}
exit(0);
}
//图8-16

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_exec函数_04

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_exec函数_05

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

1for (i = 0; i < argc; i++)
printf("argv[%d]: %s\n", i, argv[i]);

for (ptr = environ; *ptr != 0; ptr++)
printf("%s\n", *ptr);
exit(0);
}
//图8-17

运行结果

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_c函数_06APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_exec函数_07

九、演示案例2

  • exec程序使用execl函数打开一个newcode进程
  • 父进程调用execl前cat读取text.txt文件的内容,然后子进程调用execl函数打开newcode进程向text.txt文件中写入一个字符串。之后子进程退出,父进程再读取一个text.txt文件的内容
//exec程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
int fd,status;
pid_t pid;
if((fd=open("text.txt",O_RDWR|O_APPEND|O_CREAT,0644))==-1){
perror("open");
exit(1);
}

printf("before execl cat text.txt:\n");
system("cat text.txt");
fflush(stdout);//刷新标准输出

if((pid=fork())==-1){
perror("fork");
exit(2);
}
else if(pid==0){
char buf[10];
sprintf(buf,"%d",fd);
//参数2,3为传递给newcode程序的参数
if(execl("./newcode","newcode",buf,(char*)0)==-1){
perror("execl");
exit(3);
}
}
else{
wait(&status);
printf("after execl cat text.txt:\n");
system("cat text.txt");
//因为text.txt中有换行符,所以不需要fflush()
}

exit(0);
}
//newcode程序
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char *argv[])
{
int fd=atoi(argv[1]);
char buff[]="newcode write\n";
int n=strlen(buff);
if(write(fd,buff,strlen(buff))!=n){
perror("write");
exit(1);
}
close(fd);
exit(0);
}

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_描述符_08

提示:

  • 如果想要使用execv函数,可以更改exec程序的下面这部分代码

APUE编程:101---进程管理(exec、execv、execle、execve、execlp、execvp、fexecve函数)_#include_09