lienhua34
2014-10-07

在文档“进程控制三部曲”中,我们提到 fork 函数创建子进程之后,通常都会调用 exec 函数来执行一个新程序。调用 exec 函数之后,该进程就将执行的程序替换为新的程序,而新的程序则从 main 函数开始执行。

UNIX 提供了 6 种不同的 exec 函数供我们使用。它们的原型如下所示,

#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(cosnt char *filename, char *const argv[]);
6个函数的返回值:若出错则返回-1,若成功则没有返回值

可能很多人会觉得这六个函数太难记了。但是,我们仔细观察会发现,这六个函数的命名是有一些规律的。

• 含有 l 和 v 的 exec 函数的参数表传递方式是不同的。

• 含有 e 结尾的 exec 函数会传递一个环境变量列表。

• 含有 p 结尾的 exec 函数取的是新程序的文件名作为参数,而其他exec 函数取的是新程序的路径。

1 exec 函数的参数表传递方式

exec 函数给新程序传递参数表方式的不同可以通过 exec 函数名称来体现。含有 l(l 表示 list)的 exec 函数(execl、execle 和 execlp)将新程序的参数表以列表的方式传递,要求每个命令行参数作为一个单独的参数,最后空指针结尾。含有 v(v 表示 vector)的 exec 函数(execv、execve 和execvp)将新程序的参数表构造成一个数组进行传递。下面我们来看一个例子,我们有一个程序 echoargs.c,其输出所有的命令行参数。

#include <stdio.h>
#include <stdlib.h> int
main(int argc, char *argv[])
{
int i;
for (i = ; i < argc; i++) {
printf("arg%d: %s\n", i, argv[i]);
}
exit();
}

编译该程序,生成 echoargs 文件,

lienhua34:demo$ gcc -o echoargs echoargs.c

然后在我们的 execdemo.c 中分别以两种不同参数表传递方式来调用execl 和 execv 函数,

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h> char echoargsPath[] = "/home/lienhua34/program/c/apue/ch08/demo/echoargs";
char *myargs[] = {"echoargs", "This", "is", "a", "demo" }; int
main(void)
{
pid_t pid; if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
printf("Transmits arguments by vector\n");
if (execv(echoargsPath, myargs) < ) {
printf("execv error: %s\n", strerror(errno));
exit(-);
}
}
wait(NULL); if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
printf("Transmits arguments by list\n");
if (execl(echoargsPath, myargs[], myargs[],
myargs[], myargs[], "another", myargs[], (char *)) < ) {
printf("execv error: %s\n", strerror(errno));
exit(-);
}
}
wait(NULL); exit();
}

编译该程序,生成并执行文件 execdemo,

lienhua34:demo$ gcc -o execdemo execdemo.c
lienhua34:demo$ ./execdemo
Transmits arguments by vector
arg1: This
arg2: is
arg3: a
arg4: demo
Transmits arguments by list
arg1: This
arg2: is
arg3: a
arg4: another
arg5: demo

观察上面的程序 execdemo.c,我们看到传递的参数表中 myargs[0] 等于“echoargs”。这是因为在 C 语言中 main 函数的命令行参数第一个默认都是要该执行程序的文件名。所以,我们这里调用 exec 函数传递参数表是第一个参数也要设置为要执行的新程序文件名。

需要特别说明一点,在调用 execl、execle 和 execlp 函数传递参数表,在最后一个命令行参数之后跟着一个空指针。如果要用常量 0 来表示空指针,则必须要将它转换为一个字符指针,否则它默认会是整型参数。如果一个整型数的长度和 char * 的长度不同,那么 exec 函数在执行时的实际参数将会出错。

2 带有环境变量列表的 exec 函数

带有 e 结尾的 exec 函数(execle 和 execve)可以传递一个指向环境字符串指针数组的指针。其他四个 exec 函数则使用调用进程中的 environ 变量(关于 environ 变量可以参考文档“进程环境变量”)为新进程复制现有的环境。

下面我们来看一个例子。我们有一个 echoenv.c 文件,其输出进程的所有环境变量,其代码如下所示,

#include <stdlib.h>
#include <stdio.h> int
main(void)
{
extern char **environ;
char **ptr; for (ptr = environ; *ptr != NULL; ptr++) {
printf("%s\n", *ptr);
}
exit();
}

编译该文件,生成并执行 echoenv 文件,

lienhua34:demo$ gcc -o echoenv echoenv.c
lienhua34:demo$ ./echoenv
MANPATH=/usr/local/texlive//texmf-dist/doc/man:
/usr/local/texlive//texmf-dist/doc/man:
SSH_AGENT_PID=
...
XAUTHORITY=/home/lienhua34/.Xauthority
_=./echoenv

下面 execedemo.c 文件中,我们分别使用 execv 函数和 execve 函数调用 echoenv 文件,然后查看两者打印的环境变量列表有什么区别。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h> char echoenvPath[] = "/home/lienhua34/program/c/apue/ch08/demo/echoenv";
char *myEnvs[] = { "env1=foo", "env2=bar" }; int
main(void)
{
pid_t pid; if (putenv("newenv=add in parent process") < ) {
printf("putenv error: %s\n", strerror(errno));
exit(-);
} if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
printf("Call echoenv by execv\n");
if (execv(echoenvPath, NULL) < ) {
printf("execv error: %s\n", strerror(errno));
exit(-);
}
}
wait(NULL); if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
printf("\nCall echoenv by execve\n");
if (execve(echoenvPath, NULL, myEnvs) < ) {
printf("execve error: %s\n", strerror(errno));
exit(-);
}
}
wait(NULL); exit();
}

execedemo.c

编译该程序,生成并执行文件 execedemo,

lienhua34:demo$ gcc -o execedemo execedemo.c
lienhua34:demo$ ./execedemo
Call echoenv by execv
MANPATH=/usr/local/texlive//texmf-dist/doc/man:
/usr/local/texlive//texmf-dist/doc/man:
SSH_AGENT_PID=
...
XAUTHORITY=/home/lienhua34/.Xauthority
_=./execedemo
newenv=add in parent process
Call echoenv by execve
env1=foo
env2=bar

从上面的运行结果,我们看到调用 execv 函数时父进程会将其设置的环境变量(newenv=add in parent process)也传递给了子进程。而调用execve 函数时,子进程的环境变量列表只有 execve 函数传递的 myEnvs 列表。

3 取文件名作为参数的 exec 函数

含有 p 作为结尾的两个 exec 函数(execlp 和 execvp)传递的是新程序的文件名,而其它四个传递的是路径名。这两个 exec 函数传递的 filename参数按照 PATH 环境变量,在指定的各个目录中寻找可执行文件。

PATH 环境变量包含一张目录表(称为路径前缀),目录之间用冒号(:)分割。例如,
    PATH=/bin:/usr/bin:/usr/local/bin:.
指定了四个目录项,最后一个路径前缀是当前目录。(零长前缀也表示当前目录,在 value 的开始处可用: 表示,在行中间则要用:: 表示,在行尾则以:表示。)

我们来看一个例子。我们有一个 execvpdemo.c 文件,在该文件的目录下有一个 echoargs 可执行文件,该执行文件输出命令行参数。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
char *myargs[] = {"echoargs", "hello" };
int
main(void)
{
pid_t pid;
char *pathEnvVal; if ((pathEnvVal = getenv("PATH")) == NULL) {
printf("putenv error: %s\n", strerror(errno));
exit(-);
}
printf("PATH=%s\n", pathEnvVal); if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
if (execvp("echoargs", myargs) < ) {
printf("execve error: %s\n", strerror(errno));
exit(-);
}
}
wait(NULL); if (setenv("PATH", "/usr/bin:.", ) < ) {
printf("setenv error: %s\n", strerror(errno));
exit(-);
}
printf("PATH=/usr/bin:.\n"); if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
if (execvp("echoargs", myargs) < ) {
printf("execve error: %s\n", strerror(errno));
exit(-);
}
}
wait(NULL); exit();
}

execvpdemo.c

编译该程序,生成并执行文件 execvpdemo,

lienhua34:demo$ gcc -o execvpdemo execvpdemo.c
lienhua34:demo$ ./execvpdemo
PATH=/usr/local/texlive//bin/i386-linux:/usr/local/texlive//bin/i386-linux:/usr/lib/lightdm/lightdm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
execve error: No such file or directory
PATH=/usr/bin:.
arg1: hello

从上面的运行结果,我们看到在原始的 PATH 环境变量中没有包含当前目录,于是调用 execvp 函数失败,提示找不到 echoargs 文件。而当将PATH 环境变量设置为“/usr/bin:.”之后,调用 execvp 函数能够正常执行echoargs 文件。

4 exec 函数的其他性质

在调用 exec 函数新程序之后,新进程会保留 exec 之前进程的大多数特征。但是,有几个需要特殊说明一下:有效用户 ID、有效组 ID 和文件描述符。

在执行 exec 前后进程的实际用户 ID 和实际组 ID 保持不变,而有效用户 ID 和有效组 ID 是否改变跟要执行的程序文件的设置用户 ID 和设置组 ID 位有关。如果要执行的程序文件的设置用户 ID 位没有设置,则有效用户 ID 不变;否则,执行 exec 之后,进程的有效用户 ID 将被设置为要执行的程序文件的所有者。(对于有效组 ID 的情况类似。)

在文档“fcntl 函数访问已打开文件的性质”中,我们提到打开的文件描述符都有一个执行时关闭标志(close-on-exec),该标志跟调用 exec 时是否要关闭该文件描述符有关。若某个打开的文件描述符设置了执行时关闭标志,则在执行 exec 函数时会将该描述符关闭,否则该描述符保持不变。文件描述符的执行时关闭标志默认是不设置的。

POSIX.1 明确要求在执行 exec 时关闭打开的目录流。这通常是由opendir 函数实现的,它调用 fcntl 函数为对应打开目录流的文件描述符设置执行时关闭标志。

(done)

UNIX环境编程学习笔记(20)——进程管理之exec 函数族的更多相关文章

  1. UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习

    lienhua342014-10-07 在“进程控制三部曲”中,我们学习到了 fork 是三部曲的第一部,用于创建一个新进程.但是关于 fork 的更深入的一些的东西我们还没有涉及到,例如,fork ...

  2. UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数

    lienhua342014-10-12 当一个进程正常或者异常终止时,内核就向其父进程发送 SIGCHLD信号.父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用的函数(信号处理程序).对于这 ...

  3. UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲

    lienhua342014-10-05 1 进程控制三部曲概述 UNIX 系统提供了 fork.exec.exit 和 wait 等基本的进程控制原语.通过这些进程控制原语,我们即可完成对进程创建.执 ...

  4. UNIX环境编程学习笔记(17)——进程管理之进程的几个基本概念

    lienhua342014-10-05 1 main 函数是如何被调用的? 在编译 C 程序时,C 编译器调用链接器在生成的目标可执行程序文件中,设置一个特殊的启动例程为程序的起始地址.当内核执行 C ...

  5. UNIX环境编程学习笔记(22)——进程管理之system 函数执行命令行字符串

    lienhua342014-10-15 ISO C 定义了 system 函数,用于在程序中执行一个命令字符串.其声明如下, #include <stdlib.h> int system( ...

  6. UNIX环境编程学习笔记(16)——进程管理之进程环境变量

    lienhua342014-10-03 1 环境表和环境指针 在每个进程启动时,都会接到一张环境表.环境表是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符串的地址.全局变量env ...

  7. UNIX环境编程学习笔记(15)——进程管理之进程终止

    lienhua342014-10-02 1 进程的终止方式 进程的终止方式有 8 种,其中 5 种为正常终止,它们是 1. 从 main 返回. 2. 调用 exit. 3. 调用_exit 或_Ex ...

  8. UNIX环境编程学习笔记(7)——文件I/O之文件访问权限与进程访问控制

    lienhua342014-09-02 1 文件的设置用户 ID位 和设置组 ID位 与进程相关联的 ID 如下表所示, 表 1: 与进程相关联的用户 ID 和组 ID 实际用户 ID 我们实际上是谁 ...

  9. UNIX环境编程学习笔记(24)——信号处理进阶学习之信号集和进程信号屏蔽字

    lienhua342014-11-03 1 信号传递过程 信号源为目标进程产生了一个信号,然后由内核来决定是否要将该信号传递给目标进程.从信号产生到传递给目标进程的流程图如图 1 所示, 图 1: 信 ...

随机推荐

  1. C# if为false仍然进入方法体,==和qeual结果不一致

    场景: 代码: if( e.Key == Key.Tab) { // ... } 断点调试:结果为false,进入方法体   ??? 更改为: if(Key.Tab.Equals(e.key)) { ...

  2. sed——linux下对文本当控制操作(替换,追加)

    sed——linux下对文本当控制操作(替换,追加) 2011-12-12 19:27:17 分类: LINUX Linux下sed命令 1. Sed简介 sed 是一种在线编辑器,它一次处理一行内容 ...

  3. Java学习笔记:具体解释传值和传引用

    传值和传引用 When you're passing primitives into a method ,you get a distinct copy of the primitive. When ...

  4. windows 中 到底是用的哪个java.exe??? 删除了PATH变量的Java设置还是可以运行java.exe windows/system32

    到底是用的哪个java.exe??? 删除了PATH变量的Java设置还是可以运行java.exe JavaWindowsCC++C#  当电脑里面有多个jdk或是jre时,在cmd里面到底是用的哪个 ...

  5. PCL特征点与配准(1)

    关于输入一个具体的物体的点云,从场景中找出与该物体点云相匹配的,这种方法可以用来抓取指定的物体等等,具体的代码的解释如下,需要用到的一些基础的知识,在之前的博客中都有提及,其中用到的一些方法可以翻阅前 ...

  6. Java设计模式(11)外观模式(Facade模式)

    外观模式(Facade)的定义:为子系统中的一组接口提供一个一致的界面. Facade一个典型应用就是数据库JDBC的应用,如下例对数据库的操作: public class DBCompare { C ...

  7. 解决java.lang.IllegalStateException: The application’s PagerAdapter changed the adapter’s content

    A界面中有viewpager的动态加载,从界面A跳到界面B,再finish掉B返回A时报出此异常. java.lang.IllegalStateException: The application's ...

  8. 自然语言交流系统 phxnet团队 创新实训 项目博客 (十一)

    神经网络的计算过程 神经网络结构如下图所示,最左边的是输入层,最右边的是输出层,中间是多个隐含层,隐含层和输出层的每个神经节点,都是由上一层节点乘以其权重累加得到,标上“+1”的圆圈为截距项b,对输入 ...

  9. tornado 添加请求头进行允许跨域

    什么是跨域? 这个例子是csdn找的, 声明下哈 什么是跨域? 跨域,指的是浏览器不能执行其他网站的脚本.它是由浏览器的同源策略造成的,是浏览器施加的安全限制. 所谓同源是指,域名,协议,端口均相同, ...

  10. 【转】【WPF】WPF中MeasureOverride ArrangeOverride 的理解

    1. Measure Arrange这两个方法是UIElement的方法 MeasureOverride ArrangeOverride这两个方法是FrameworkElement的方法,Framew ...