Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1814569
  • 博文数量: 438
  • 博客积分: 9799
  • 博客等级: 中将
  • 技术积分: 6092
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-25 17:25
文章分类

全部博文(438)

文章存档

2019年(1)

2013年(8)

2012年(429)

分类: 系统运维

2012-03-29 13:32:30

我们在8.3节提到fork的一个用法是创建一个新进程(子进程)然后调用某个exec函数然执行另一个程序。当一个进程调用某个exec函数时,这个进 程被新程序完全取代,而新程序开始执行它的main函数。在调用exec时进程的ID并没有发生变化,因为没有一个新的进程被创建;exec只是把当前的 进程--它的代码、数据、堆和栈--替换为从硬盘而来的全新的程序。


有6个不同的exec函数,但是我们经常简单地说“exec函数”,它表示我们可以使用这6个函数中的任一个。这6个函数使用UNIX系统的原始进程控制 丰满了起来。通过fork,我们可以创建新的进程,而使用exec函数,我们可以启动新的程序。exit函数和wait函数处理了终止和终止的等待。这些 我们仅需的原始进程控制。我们将用这些原始操作在后面各节中建立更多的函数,比如popen和system。



  1. #include <unistd.h>

  2. int execl(const char *pathname, const char *arg0, ... /* (char *) 0 */ );

  3. int execv(const char *pathname, char * const argv[]);

  4. int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );

  5. int execve(const char *pathname, char *const argv[], char *const envp[] );

  6. int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );

  7. int execvp(const char *filename, char *const argv[]);

  8. 六个函数错误都返回-1,成功不返回。


这些函数的第一个区别是前四个接受一个路径名参数,而后两个接受一个文件名参数。当一个文件名参数被指定时:

1、如果filname包含一个斜杠,那它这被作为一个路径名;

2、否则,可执行程序会在PATH环境变量指定的目录里查找。


PATH变量包含一个路径的列表,被称为路径前缀,它们由冒号分隔。例如,下面的name=value环境字符串
PATH=/bin:/usr/bin:/usr/local/bin/:.


指定了四个用于查找的路径。最后一个路径前缀指定了当前的目录。(一个零长度的前缀同样表示当前目录。它可以通过value的开头的冒号、一行中的两个冒号、或value结尾的冒号来表示。)


基于一些安全性的原因,绝不要把当前目录包含在查找路径里。


如果execlp或execvp使用某个路径前缀找到了一个可执行文件,但文件不是由链接器产生的机器执行文件,那么函数会假定这个程序是一个外壳脚本并尝试调用/bin/sh来执行它。


下一个区别是参数列表的传递(l表示列表而v表示矢量)。函数execl、execlp和execle要求新程序的每个命令行参数都由分隔的参数指定。我 们用一个空指针标记参数的末尾。对于其它三个函数(execv、execvp、和execve),我们必须创建一个参数指针的数组,然后把这个数组的地址 传给这三个函数。


在使用ISO C原型前,为execl、execle和execlp展示命令行参数的通常作法是:char *arg0, char *arg1, ..., char *argn, (char *)0。这指明了最后的命令行参数后面是一人空指针。如果这个空指针由常量0指定,我们必须显式把它转换成一个指针,如果我们不这样做的话,它会被解释为 一个整型参数。如果一个整型的尺寸和char *的尺寸不同,那么exec函数的真实参数将会出错。


最后的区别是给新程序的环境变量的传递。这两个以一个e结尾的函数(execle和execve)允许我们传递一个环境字符串指针数组的指针。然而,另外 四个函数在调用进程里使用environ变量来为进程序拷贝已有的环境。(回想下我们在7.9节里的环境字符串的讨论。我们提到过如果系统支持如 setenv和putenv的函数的话,我们可以改变当前环境和任何后续的子进程的环境,但是我们不会影响父进程的环境。)

通常,一个进程允许它的环境被 传播给它的子进程,但在一些情况下,一个进程想为一个子进程指定一个特殊的一间。后者的一个例子是当一个登录外壳被初始化时的login程序。通 常,login只用几个定义的变量创建一个指定的环境,并让我们通过外壳启动文件,当我们登录时在环境里加入变量。


在使用ISO C原型前,execle的参数为:char *pathname, char *arg0, ..., char *argn, (char*)0, char *envp[]。


这指名了最后的参数是环境字符串的字符指针的数组的地址。ISO C原型没有这样显示,因为所有的命令行参数、空指针和envp指针都用省略号显示。


这6个函数的参数很维记住。函数名里的字母某种程序上帮助记忆。字母p表示函数接受一个文件名参数并使用PATH环境变量来查找可执行文件。字母l表示函 数接受一个参数列表并和表示接受一个argv[]矢量的字母v互斥。最后,字母e表示函数接受一个envp[]数组而不是使用当前环境。下表显示了这6个 函数的区别:

6个exec函数的区别
函数 pathname filename 参数列表 argv[] environ envp[]
execl *   *   *  
execlp   * *   *  
execle *   *     *
execv *     * *  
execvp   *   * *  
execve *     *   *
名字里的字母   p l v   e


每个系统都有参数列表和环境列表总尺寸的限制。从2.5.2节可以看到,这个限制由ARG_MAX给定。这个值在POSIX.1系统上必须至少为4096 字节。当使用外壳的文件名扩展我来产生一个文件名列表时,我们有时会碰到这个限制。例如,在一些系统上,命令grep getrlimit /usr/share/man/*/*可能产生一个这种形式的外壳错误:Argument list too long。


历史上,在系统V的早期实现上里的限制是5120字节。早期BSD系统有20480字节的限制。当前系统的限制高得多了。


为了解决参数列表尺寸的限制,我们可以使用xargs命令来分解长参数列表。为了查找所有在我们系统上man页里的getrlimit的出现, 我们可以使用
find /usr/share/man -type f -print | xargs grep getrlimit


然而,如果我们系统上的man页被压缩,我们可以尝试
find /usr/share/man -type f -print | xargs bzgrep getrlimit


我们在find命令里使用-type f选项来限制列表只包含普通文件,因为grep命令不会在目录里想找pattern,而我们要避免不必要的错误信息。


我们也提到进程ID在一个exec后不会改变,但是新的程序从调用进程继承了额外的属性:


1、进程ID和父进程ID;


2、真实用户ID和真实组ID;


3、补充组ID;


4、进程组ID;


5、会话ID;


6、控制终端;


7、闹钟响的剩余时间;


8、当前工作目录;


9、根目录;


10、文件模式创建掩码;


11、文件锁;


12、进程信号掩码;


13、资源限制;


14、tms_utime、tms_stime、tms_cutime和tms_cstime的值。


对打开文件的处理取决于每个操作符的close-on-exec标志。回想下第三章,我们在3.14节提过FD_CLOEXEC标志,它让一个进程里每个 打开的描述符都有一个close-on-exec标志。如果这个标志被设置的话,一个exec会关闭这个描述符。否则,在exec时描述符会保持打开。默 认行为是在exec时保持描述符打开,除非我们用fcntl来设置close-on-exec标志。


POSIX.1规定打开的目录流(回想下4.21节的opendir函数)在exec时必须关闭。这通常由opendir通过调用fcntl设置对应于打开的目录的流的文件描述符的close-on-exec标志来完成。


注意真实用户ID和真实组ID在exec时保持不变,但是有效ID可以改变,取决于被执行的程序的设置用户ID和调用组ID位的状态。如果新程序的设置组 ID位被设置,那么有效用户ID就变为程序文件属主ID。否则,有效组ID不变(它没有被设为真实用户ID)。组ID的处理方式一样。


在许多UNIX系统实现里,这6个函数里只有一个execve是内核的系统调用。其它5个只是最终调用这个系统调用的库函数。我们可以想像这6个函数之间的关系:


execlp把参数列表转换为数组,传递给execvp。同样,execl的列表转换为数组传给execv,execle的列表转换为数组传给 execve(系统调用)。execvp通过各个PATH前缀找到程序的位置,交给execv,而execv使用environ调用execve(系统调 用)。


下面的代码的展示了exec函数:



  1. #include <sys/wait.h>

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

  3. int
  4. main(void)
  5. {
  6.     pid_t pid;

  7.     if ((pid = fork()) < 0) {
  8.         printf("fork error\n");
  9.         exit(1);
  10.     } else if (pid == 0) { /* specify pathname, specify environment */
  11.         if (execle("/home/tommy/bin/echoall", "echoall", "myarg1", "MY ARG2", (char*)0, env_init) < 0) {
  12.             printf("execle error\n");
  13.             exit(1);
  14.         }
  15.     }

  16.     if (waitpid(pid, NULL, 0) < 0) {
  17.         printf("wait error\n");
  18.         exit(1);
  19.     }

  20.     if ((pid = fork()) < 0) {
  21.         printf("fork error\n");
  22.         exit(1);
  23.     } else if (pid == 0) { /* specify filename, inherit environment */
  24.         if (execlp("echoall", "echoall", "only 1 arg", (char*)0) < 0) {
  25.             printf("execlp error");
  26.             exit(1);
  27.         }
  28.     }

  29.     exit(0);
  30. }

我们首先调用execle,它需要一个路径名和一个指定的环境。下一个调用是execlp,它使用一个文件名并传入调用者的环境给新的程序。execlp 调用工作的唯一原因是目录/home/tommy/bin是其中一个当前路径前缀。还要注意我们把第一个参数,新程序的argv[0],设置为路径名的文 件名部分。一些外壳把这个参数设置为完整的路径名。这只是一个协议。我们可以把arg[0]设置成任何我们喜欢的字符串。login命令在它执行外壳时就 是这样做的。在执行外壳前,login在arg[0]前加个一个连接符作为前缀,来指定这个程序是作为一个登录外壳被调用的。一个登录外壳将会执行启动 profile命令,而非登录的外壳不会。


程序echoall被前面的程序执行了两次。它是一个普通的程序,打印它所有的命令行参数和它的整个环境列表。下面是echoall的代码:



  1. #include <stdio.h>

  2. int
  3. main(int argc, char *argv[])
  4. {
  5.     int i;
  6.     char **ptr;
  7.     extern char **environ;

  8.     for (i = 0; i < argc; i++) /* echo all command-line args */
  9.         printf("argv[%d]: %s\n", i, argv[i]);

  10.     for (ptr = environ; *ptr != 0; ptr++) /* and all env strings */
  11.         printf("%s\n", *ptr);

  12.     exit(0);
  13. }

运行结果:
$ ./a.out
argv[0]: echoall
argv[1]: myarg1
argv[2]: MY ARG2
USER=unknown
PATH=/tmp
$ argv[0]: echoall
argv[1]: only 1 arg
TERM=xterm
SHELL=/bin/bash
USER=tommy
PATH=/usr/lib/lightdm/lightdm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/tommy/bin
………… 中间省略了N行。
XAUTHORITY=/home/tommy/.Xauthority
_=./a.out
阅读(830) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~