分类: LINUX
2012-01-15 20:43:18
++++++APUE读书笔记-08进程控制(07)++++++
10、exec
================================================
当调用exec函数的时候,进程会完全被exec所执行的可执行文件所对应的新的程序所替代,从新程序的main函数开始执行(exec后面的语句已经无法再执行到了)。注意使用exec并没有创建进程,所以进程的PID并没有改变,exec仅仅替换当前进程的text,data,heap,和stack段为磁盘中新程序相应的段。
有6中exec函数,这些函数构成进程控制的基本要素,我们一般就把这些exec函数统称为exec函数。使用fork,我们可以创建一个进程;使用exec,我们可以初始化一个程序;使用exit和wait,我们可以指定和获取退出进程的退出码(以及同步等)。这是我们进行进程控制的基本函数,使用这些函数我们可以构造出一些其他的函数,例如popen,system等。
#include
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 []);
(1) 这些函数的第一个不同的地方就是,前四个函数将pathname作为参数,后两个函数把filename作为参数。
当指定filename的参数的时候,
a)如果filename包含了一个slash,那么就把它当作pathname。
b)否则,会在PATH回敬变量中指定的目录搜索filename对应的可执行文件。
PATH环境变量包含一系列以冒号分割的目录,也就是路径前缀。例如,
PATH=/bin:/usr/bin:/usr/local/bin/:.
这个name=value形式的环境变量name是PATH,value指定了四个搜索目录,最后一个"."代表当前目录(实际一个0长度的前缀也表示当前目录,指定的时候可以使用一个冒号在value的开始,或者两个冒号构成一列,或者一个在value结尾的冒号。)
一般不要包含当前目录,因为这样会有安全隐患。
如果execlp或者execvp使用路径前缀找到了一个可执行文件,但是这个文件和机器链接的时候的可执行文件格式不一样,那么这个函数假设文件是一个shell脚本,然后尝试调用/bin/sh,一这个文件名作为参数,来运行这个脚本。
(2)另外一个不同的地方是参数列表的传递方式(l表示list,v表示vector)。
函数execl,execlp,和execle要求每个传递到这个程序的命令行参数是一个独立的参数,我们把这些参数的结尾添加一个null指针。对于其他的三个函数(execv,execvp,execve),我们需要建立一个指针数组,其指针指向相应参数,然后这个数组的地址作为这三个函数的参数。
在使用ISO C声明之前,正常对函数execl,execle,和execlp展示的命令行参数形式是:
char *arg0, char *arg1, ..., char *argn, (char *)0
这个标准要求命令行的最后一个参数是一个null 指针。如果值是0那么我们要把它显示地转换成char*类型的,否则会被当做一个int参数来看代。如果整数的大小和char*这个标准要求命令行的最后一个参数是一个null 指针。如果int变量的大小和char的大小不一样,那么实际传递给exec函数的参数会发生错误。
(3)最后一个不同的地方就是对新程序的环境变量的传递。
execle和execve函数允许我们传递一个指向环境变量字符串的一个指针数组。另外的四个函数,使用调用进程的environ变量来为新程序拷贝一份已经存在的环境变量(回顾前面的关于环境变量的内容可知,如果我们系统支持setenv和putenv函数,那么我们可以在不影响父进程的情况下来修改子进程的环境变量)。一般来说,一个进程允许它的环境变量被传播到它的子进程,但是在一个时候,进程需要对一个子进程指定特定的环境变量,例如login程序初始化login shell的时候。一般,login创建一个指定的仅定义了几个变量环境变量列表然后让我们通过shell的启动文件在我们登录的时候添加环境变量。
在使用ISO C声明之前,对execle的相应(环境变量列表)参数的展示形式是:
char *pathname, char *arg0, ..., char *argn, (char *)0, char *envp[]
由这个标准可知最后一个参数是环境变量指针列表的数组地址。 ISO C的声明就没有这样来表示,因为所有的命令行参数,空指针,以及环境变量列表指针使用(...)来进行表示了。
这六个exec函数的参数很难记忆,但是考虑一下函数名字可以帮助我们记忆。字母p表示函数会使用PATH来接受一个filename参数,进而寻找可执行文件;字母l表示函数会接受一个参数列表,它和字母v相对;字母v表示一个argv[](元素为参数指针的指针数组);字母e表示函数接受一个envp[]数组(是指向环境变量列表的指针数组),和使用当前的环境变量相对。下面通过一个表格列出了所有exec函数的区别。
六种exec函数的区别
+-----------------------------------------------------------------------------+
| Function | pathname | filename | Arg | argv[] | environ | envp[] |
| | | | list | | | |
|-----------------+----------+----------+---------+--------+---------+--------|
| execl | • | | • | | • | |
|-----------------+----------+----------+---------+--------+---------+--------|
| execlp | | • | • | | • | |
|-----------------+----------+----------+---------+--------+---------+--------|
| execle | • | | • | | | • |
|-----------------+----------+----------+---------+--------+---------+--------|
| execv | • | | | • | • | |
|-----------------+----------+----------+---------+--------+---------+--------|
| execvp | | • | | • | • | |
|-----------------+----------+----------+---------+--------+---------+--------|
| execve | • | | | • | | • |
|-----------------+----------+----------+---------+--------+---------+--------|
| (letter in | | p | l | v | | e |
| name) | | | | | | |
+-----------------------------------------------------------------------------+
每个系统的参数列表和环境变量列表的长度是有限制的,在第2章已经说明,限制由ARG_MAX给出。在POSIX.1中,这个值必须至少是4096字节。有时候我们在敲入文件名的时候会遇到这个限制,例如:
grep getrlimit /usr/share/man/*/*
可能会提示shell错误,说是参数过长.
以前,在System V上面这个参数限制是5120字节。旧版本的BSD系统的限制是20480字节,当前系统的限制更高。
为了避免遇到参数列表大小的限制,我们使用xargs命令把长的参数列表分割,例如上面的命令就可以写成:
find /usr/share/man -type f -print | xargs grep getrlimit
如果我们系统上面的man pages是压缩的,我们可以运行:
find /usr/share/man -type f -print | xargs bzgrep getrlimit
这里,使用了type -f选项是为了限定查找的文件类型是正常文件,因为grep无法在命令中进行搜索,我们需要避免一些不必要的错误。
除了exec调用进程的进程ID不会改变之外,新程序还有一些其他的属性也会被继承过来,具体参见参考资料。
新程序对打开的文件的处理取决于相应文件描述番号的close-on-exec标志。每个打开的文件描述符号有一个close-on-exec标志,如果标志被设置了,那么文件描述符在调用exec的时候会被关闭,否则执行exec的时候也会保持打开的状态。默认这个标志是没有设置的也就是说文件描述符号在执行exec的时候是打开的状态,除非我们使用了fcntl来设置这个close-on-exec标志。
POSIX.1规定打开的目录流(参见opendir)在执行exec的时候被关闭。这一点是通过opendir函数调用fcntl设置打开的目录流中的相应文件描述符号的close-on-exec标志来做到的。
需要注意的是调用exec之后,real user ID和real group ID是保持不变的,但是effective ID却是变化的,这取决于相应的要执行的文件的set-user-ID和set-group-ID位。如果这个位被设置了,那么effective user ID就变成了这个程序文件相应的属主ID了。否则,effective 就不变(不会设置成real user ID).group ID的情况处理类似。
六个exec函数之间的关系:
在许多unix系统中,这6个函数中只有一个(execve),是内核中的系统调用。其他的5个都是库函数,是调用这个系统调用来实现的。我们在参考资料中给出了这些函数的调用关系。
图形在这里就省略了,图形大概描述的情况就是:
execlp--(build argv)-->execvp--(尝试每一个path前缀)-->execv--(使用环境变量,执行系统调用)-->execve。
execl--(build argv)-->execv--(使用环境变量,执行系统调用)-->execve。
exece--(build argv,执行系统调用)-->execve。
上面的规律是:
先l变v(如果有的话,通过build argv)
再去p(如果有的话,通过尝试PATH前缀)
再用e(如果有的话,通过使用环境变量)
举例:
书中给出例子如下:
...
if (execle("/home/sar/bin/echoall", "echoall", "myarg1",
"MY ARG2", (char *)0, env_init) < 0)
...
if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0)
这里,我们先使用execle,它需要被传递一个路径名称以及一个环境变量数组指针;然后我们第二个方法是execlp,它只需要被传递一个文件名称,因为它会在PATH环境变量里面寻找这个文件名称的路径前缀。
需要注意的是,这里我们设置的arg0的地方,它实际上可以是任何字符串(例如可以是程序名称,也可以是路径名称),有些shell执行可执行程序的时候,把这个args[0]设置成为了全路径.login会将args[0]前面加上一个破折号前缀,表示是登陆的shell。我这ps之后看到login如下,不知道是不是说的这个:
root 2683 0.0 0.2 2984 1156 tty1 Ss+ 08:30 0:00 /bin/login --
参考: