您的位置:首页 > 编程语言

UNIX环境高级编程-第8章- 进程控制 - 二

2014-12-22 16:18 387 查看

8.10 exec函数

exec 替换进程映像

        在进程的创建上 UNIX 采用了一个独特的方法,它将进程创建与加载一个新进程映象分离,这样可以方便对两种操作进行管理。当创建了一个进程之后,通常可以用 exec 系列的函数将子进程替换成新的进程映象。当然,exec 系列的函数也可以将当前进程替换掉。

exec 序列函数

        fork 函数创建一个子进程时,几乎复制了父进程的全部内容。exec 序列函数可以在一个在进程中启动另一个程序的执行,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程 ID 未改变( exec 序列函数不创建新进程),其他全部被新的程序替换了。

        在 Linux 中使用 exec 序列函数主要有两种情况:

1、当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用 exec 序列函数中的任意一个函数让自己重生。

2、如果一个进程想执行另一个程序,那么它就可以调用 fork 函数新建一个进程,然后调用 exec 序列函数中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。

/* exec 序列函数 */

/*
* 函数功能:把当前进程替换为一个新的进程,新进程与原进程ID相同;
* 返回值:若出错则返回-1,若成功则不返回;
* 函数原型:
*/
#include <unistd.h>
int execl(const char *pathname, const char *arg, ...);
int execv(const char *pathnam, char *const argv[]);
int execle(const char *pathname, const char *arg, ... , char *const envp[]);
int execve(const char *pathnam, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg, ...);
int execvp(const char *filename, char *const argv[]);

        这6个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式、参数传递方式及环境变量这几个方面进行比较。

1、查找方式:前4个函数的查找方式都是完整的文件目录路径 pathname ,而最后两个函数(也就是以p结尾的两个函数)可以只给出文件名filename,系统就会自动按照环境变量“$PATH”
所指定的路径进行查找。

2、参数传递方式:exec 序列函数的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第5位字母来区分的,字母为 “l”(list)(参数可变,类似printf函数)的表示逐个列举参数的方式,其语法为 const char *arg;字母为 “v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为 char *const
argv[]。读者可以观察 execl()、execle()、execlp() 的语法与 execv()、execve()、execvp() 的区别。这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL结束。

3、 环境变量:exec 序列函数可以默认系统的环境变量,也可以传入指定的环境变量。这里以 “e”(environment)结尾的两个函数 execle() 和 execve() 就可以在 envp[] 中指定当前进程所使用的环境变量。

    表 1 exec 序列函数的总结
前4位 统一为:exec
第5位 l:参数传递为逐个列举方式 execl、execle、execlp
v:参数传递为构造指针数组方式 execv、execve、execvp
第6位 e:可传递新进程环境变量 execle、execve
p:可执行文件查找方式为文件名 execlp、execvp
        事实上,这6个函数中真正的系统调用只有 execve(),其他5个都是 C
库函数,它们最终都会调用execve() 这个系统调用。在使用 exec 序列函数时,一定要加上错误判断语句。exec 很容易执行失败,其中最常见的原因有:

1、找不到文件或路径,此时 errno 被设置为 ENOENT。

2、 数组 argv 和 envp 忘记用NULL结束,此时,errno 被设置为 EFAUL。

3、没有对应可执行文件的运行权限,此时errno 被设置为EACCES。

/* 执行exec函数,下面属性是不发生变化的: 
*进程ID和父进程ID(pid, ppid)
*实际用户ID和实际组ID(ruid, rgid)
*附加组ID(sgid)
*会话ID
*控制终端
*闹钟余留时间
*当前工作目录
*根目录
*umask
*文件锁
*进程信号屏蔽
*未处理信号
*资源限制
*进程时间
*/
/*而下面属性是发生变化的:
*文件描述符如果存在close-on-exec标记的话,那么打开的文件描述符会被关闭。
*如果可执行程序文件存在SUID和SGID位的话,那么有效用户ID和组ID(euid, egid)会发生变化
*/
         exec 序列函数的关系如下图所示:



测试程序:

#include "apue.h"
#include <unistd.h>
#include <sys/wait.h>

char *argv[] = {"echo", "STATUS=testing", "\tHellow linux","\tgood bye",NULL};
char *argve[] = {"env",NULL};
char *env[] = {"USER=unknown", "PATH=/tmp",NULL};
int main()
{
pid_t pid;

if((pid = fork()) < 0) /* fork error */
err_sys("fork error.");
else if(0 == pid) /* in child process */
{
if(execl("/bin/ls","ls","-a", "-l", (char *)0) == -1)
err_sys("execl error.\n");
// exit(0);
}

if(waitpid(pid, NULL, 0) < 0)
err_sys("wait error.");
if((pid = fork()) < 0) /* fork error */
err_sys("fork error.");
else if(0 == pid) /* in child process */
{
if(execlp("ls","ls","-a", "-l", (char *)0) == -1)
err_sys("execlp error.\n");
// exit(0);
}

if(waitpid(pid, NULL, 0) < 0)
err_sys("wait error.");
if((pid = fork()) < 0) /* fork error */
err_sys("fork error.");
else if(0 == pid) /* in child process */
{
if(execvp("echo", argv) == -1)
err_sys("execvp error.\n");
}

if(waitpid(pid, NULL, 0) < 0)
err_sys("wait error.");
if((pid = fork()) < 0) /* fork error */
err_sys("fork error.");
else if(0 == pid) /* in child process */
{
if(execve("/usr/bin/env", argve, env) == -1)
err_sys("execve error.\n");
}
exit(0);
}

输出结果:
[root@www chapter_8]# ./a.out
总计 56
drwxr-xr-x 2 root root 4096 12-22 14:28 .
drwxr-xr-x 10 root root 4096 12-21 14:28 ..
-rw-r--r-- 1 root root 1523 12-22 14:24 8-10.c
-rw-r--r-- 1 root root 233 12-21 14:36 8-1.c
-rw-r--r-- 1 root root 724 12-21 14:57 8-2.c
-rw-r--r-- 1 root root 989 12-21 15:45 8-3.c
-rw-r--r-- 1 root root 434 12-21 15:52 8-4.c
-rw-r--r-- 1 root root 725 12-21 16:03 8-5.c
-rw-r--r-- 1 root root 1534 12-21 17:06 8-6.c
-rw-r--r-- 1 root root 1240 12-22 13:41 8-7.c
-rw-r--r-- 1 root root 580 12-22 13:48 8-8.c
-rw-r--r-- 1 root root 386 12-22 14:03 8-9.c
-rwxr-xr-x 1 root root 7794 12-22 14:28 a.out
总计 56
drwxr-xr-x 2 root root 4096 12-22 14:28 .
drwxr-xr-x 10 root root 4096 12-21 14:28 ..
-rw-r--r-- 1 root root 1523 12-22 14:24 8-10.c
-rw-r--r-- 1 root root 233 12-21 14:36 8-1.c
-rw-r--r-- 1 root root 724 12-21 14:57 8-2.c
-rw-r--r-- 1 root root 989 12-21 15:45 8-3.c
-rw-r--r-- 1 root root 434 12-21 15:52 8-4.c
-rw-r--r-- 1 root root 725 12-21 16:03 8-5.c
-rw-r--r-- 1 root root 1534 12-21 17:06 8-6.c
-rw-r--r-- 1 root root 1240 12-22 13:41 8-7.c
-rw-r--r-- 1 root root 580 12-22 13:48 8-8.c
-rw-r--r-- 1 root root 386 12-22 14:03 8-9.c
-rwxr-xr-x 1 root root 7794 12-22 14:28 a.out
STATUS=testing Hellow linux good bye
USER=unknown
PATH=/tmp
[root@www chapter_8]#

8.12 解释器文件

解释器文件是文本文件,而解析器是可执行的二进制文件。解析器是由解析器文件的第一行指定的,其格式如下:

#! pathname [optional-argument]

//pathename 是解释器的绝对路径;
//optional-argument 是传递给解析器的参数

  内核调用 exec 函数的进程实际执行的并不是该解释器文件,而是该解释器文件的第一行中 pathname 所指定的解释器。注意以下区别:

解释器文件:以#!开头的文本文件。

解释器:解释器文件第一行pathname所指定的程序。

  下面用exec 函数调用解释器文件例程:注意:以下文件的工作目录为/tmp

  首先创建解释器echoarg:解释器echoarg由echoarg.c文件编译而成,echoarg.c文件如下所示,实现功能是显示执行程序的参数列表:

#include "apue.h"
/*
* 函数功能:显示执行程序的参数列表;
*/
int main(int argc, char *argv[])
{
int i = 0;
for(i=0;i<argc;i++)
printf("argv[%d]: %s\n",i,argv[i]);
exit(0);
}

  接着定义解释器文件testintererp(/tmp是工作路径,echoarg是解释器):这里需要注意,我们创建该文件时,是没有执行权限的,必须更改使其具有可执行权限,最简单的更改就是:
chmod 777 testintererp  

 

#! /tmp/echoarg arg  

  最后创建包含exec函数调用的文件test.c,并编译成可执行文件test:

#include "apue.h"
#include <sys/wait.h>

int main(void)
{
pid_t pid;

if((pid = fork()) < 0)
err_sys("fork error");
else if(0 == pid)
{
if(execl("/tmp/testintererp","testintererp","myarg1","myarg2",(char *)0) < 0)
err_sys("execl error");
}
if(waitpid(pid,NULL,0) < 0)
err_sys("wait error");
exit(0);
}

  执行test文件之后输出结果为:
argv[0]: /tmp/echoarg  

argv[1]: arg  

argv[2]: /tmp/testintererp  

argv[3]: myarg1  

argv[4]: myarg2  

8.13 system函数

system 函数能够执行函数中参数的命令,实现如下:

/* system 函数 */
#include <stdlib.h>
int system(const char *cmdstring);

首先我们看下该函数的具体实现:
/* system 函数的实现 */
#include "apue.h"
#include <sys/wait.h>

int system(const char *cmdstring)
{
pid_t pid;
int status;

if(NULL == cmdstring)
return -1;
if((pid = fork()) < 0)
status = -1;
else if(0 == pid)
{
execl("/bin/sh", "sh", "-c","cmdstring", (char *)0);
_exit(127);
}
else
{
while(waitpid(pid,&status,0) < 0)
{
if(errno != EINTR)
{
status = -1;
break;
}
}
}
return status;
}
  从该函数的实现我们可以知道,在现有进程中调用 fork 函数创建新的子进程,在 fork 所返回的子进程中调用 execl 函数执行参数字符命令,在 fork 所返回父进程中调用 waitpid 函数等待进程终止;因此有下面的三个返回值:

1、如果 fork 失败或者 waitpid 返回值除 EINTR 之外的出错,则system 返回-1,而且 errno 中设置了错误类型值;

2、如果 exec 失败(表示不能执行shell),其返回值如同 shell 执行 exit(127)一样;

3、否则所有三个函数(fork、exec、waitpid)都执行成功,并且system的返回值是 shell 的终止状态;

测试程序:

 #include <sys/wait.h>
#include "apue.h"

void pr_exit(int status)
{
if(WIFEXITED(status))
printf("normal termination, exit status = %d\n",WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n",WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "(corefile generated)" : " ");
#else
" ");
#endif

else if(WIFSTOPPED(status))
printf("child stoped, signal number = %d\n",WSTOPSIG(status));
}

int main(void)
{
int status;

if((status = system("date")) < 0)
err_sys("system error");
pr_exit(status);

if((status = system("nosuchcommand")) < 0)
err_sys("system error");
pr_exit(status);

if((status = system("stat;who; exit 44")) < 0)
err_sys("system error");
pr_exit(status);

exit(0);
}
返回值:

[root@www chapter_8]# ./a.out
2014年 12月 22日 星期一 15:23:28 CST
normal termination, exit status = 0
sh: nosuchcommand: command not found
normal termination, exit status = 127
stat: 缺少操作数
请尝试执行“stat --help”来获取更多信息。
root :0 2014-12-22 13:13
root pts/1 2014-12-22 13:13(:0.0)
normal termination, exit status = 44
[root@www chapter_8]#

测试程序2:
#include <sys/wait.h>
#include "apue.h"

void pr_exit(int status)
{
if(WIFEXITED(status))
printf("normal termination, exit status = %d\n",WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n",WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "(corefile generated)" : " ");
#else
" ");
#endif

else if(WIFSTOPPED(status))
printf("child stoped, signal number = %d\n",WSTOPSIG(status));
}

int main(int argc, char *argv[])
{
int status;
if(argc != 2)
{
printf("usage: a.out <cmdstring>\n");
exit(1);
}
status = system(argv[1]);
pr_exit(status);

exit(0);
}

以上程序可以根据自己需要输入要执行的字符命令参数。假设编译成可执行文件a.out,例如输入./ a.out ls,输出如下:
[root@www chapter_8]# ./a.out ls
8-10.c 8-11.c~ 8-13.c 8-2.c 8-4.c 8-6.c 8-8.c a.out
8-11.c 8-12.c 8-1.c 8-3.c 8-5.c 8-7.c 8-9.c
normal termination, exit status = 0

8.16 进程时间

        进程时间(注意区别于文件的三个时间)有墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可以调用 times 函数以获得它自己以及终止子进程的上述值。

/* 进程时间 */

/*
* 函数功能:获取进程的时间:墙上时钟时间、用户CPU时间和系统CPU时间;
* 返回值:若成功则返回流逝的墙上时钟时间(单位:时钟滴答数),若出错则返回-1;
* 函数原型:
*/

#include <sys/times.h>
clock_t times(struct tms *buf);

/*
* struct tms 结构如下:
*/
struct tms{

clock_t tms_utime; /* user CPU time */
clock_t tms_stime; /* system CPU time */
clock_t tms_cutime; /* user CPU time, terminated children */
clock_t tms_cstime; /* system CPU time, terminated children */
};

      在进程时间结构 tms 中并没有包含墙上时钟时间的测量值,times 函数返回值可作为墙上时钟时间的测量值,其取值是两次times 返回值的差值。由此函数返回的 clock_t 值都用_SC_CLK_TCK(由 sysconf函数返回的每秒时钟滴答数)变换成秒数。
测试程序:

#include <sys/times.h>
#include <time.h>
#include <wait.h>
#include "apue.h"

static void pr_times(clock_t, struct tms *, struct tms *);
static void do_cmd(char *);

void pr_exit(int status);
int main(int argc, char *argv[])
{
int i;
for(i=1; i<argc; i++){
do_cmd(argv[i]);
}
exit(0);
}

static void do_cmd(char* cmd)
{
struct tms tmsstart, tmsend;
clock_t start, end;
int status;
printf("\ncommand: %s\n", cmd);
if((start=times(&tmsstart)) == -1)
err_sys("times error");

if((status = system(cmd)) < 0)
err_sys("system error");

if((end = times(&tmsend)) == -1)
err_sys("times error");

pr_times(end-start, &tmsstart, &tmsend);
pr_exit(status);
}

static void pr_times(clock_t real, struct tms* tmsstart, struct tms* tmsend)
{
static long clktck = 0;
if((clktck = sysconf(_SC_CLK_TCK)) < 0)
err_sys("sysconf error");
printf("real: %7.2f\n",real/(double)clktck);
printf("user: %7.2f\n",(tmsend->tms_utime - tmsstart->tms_utime)/(double)clktck);
printf("sys: %7.2f\n",(tmsend->tms_stime - tmsstart->tms_stime)/(double)clktck);
printf("child user: %7.2f\n",(tmsend->tms_cutime - tmsstart->tms_cutime)/(double)clktck);
printf("child sys: %7.2f\n",(tmsend->tms_cstime - tmsstart->tms_cstime)/(double)clktck);
}
void pr_exit(int status)
{
if(WIFEXITED(status))
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "(core file generated)" : " ");
#else
" ");
#endif

else if(WIFSTOPPED(status))
printf("child stoped, signal number = %d\n", WSTOPSIG(status));
}

输出结果:
[root@www chapter_8]# ./a.out date

command: date
2014年 12月 22日 星期一 16:09:07 CST
real: 0.01
user: 0.00
sys: 0.00
child user: 0.00
child sys: 0.00
normal termination, exit status = 0

8.17 小结

对于UNIX 环境中的高级编程而言,完整地了解 UNIX 的进程控制是非常重要的。其中必须熟练掌握的只有几个函数——fork 、exec族、_exit、wait和waitpid。很多应用程序都使用这些原语。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: