进程控制地字第1号系统调用——exec

来源:互联网 发布:bt天堂新域名 编辑:程序博客网 时间:2024/05/01 02:40

本系列文章节选自本人所著《Linux下C语言应用编程》

本系列文章,所需代码请从以下地址下载:

http://download.csdn.net/download/scyangzhu/5129027



当一个程序调用fork产生子进程,通常是为了让子进程去完成不同于父进程的某项任务,因此含有fork的程序,通常的编程模板如下:

if ((pid = fork()) == 0) {

       dosomething in child process;

       exit(0);

}

do something in parent process;

       这样的编程模板使得父、子进程各自执行同一个二进制文件中的不同代码段,完成不同的任务。这样的编程模板在大多数情况下都能胜任,但仔细观察这种编程模板,你会发现它要求程序员在编写源代码的时候,就要预先知道子进程要完成的任务是什么。这本不是什么过分的要求,但在某些情况下,这样的前提要求却得不到满足,最典型的例子就是Linux的基础应用程序 —— shell。你想一想,在编写shell的源代码期间,程序员是不可能知道当shell运行时,用户输入的命令是ls还是cp,难道你要在shell的源代码中使用if--elseif--else if--else if ……结构,并拷贝 ls、cp等等外部命令的源代码到shell源代码中吗?退一万步讲,即使这种弱智的处理方式被接受的话,你仍然会遇到无法解决的难题。想一想,如果用户自己编写了一个源程序,并将其编译为二进制程序test,然后再在shell命令提示符下输入./test,对于采用前述弱智方法编写的shell,它将情何以堪?

看来天字1号虽然很牛,但亦难以独木擎天,必要情况下,也需要地字1号予以协作,啊,伟大的团队精神!

1.1.1   exec的机制和用法

下面就详细介绍一下进程控制地字第1号系统调用——exec的机制和用法。

exec的机制:

在用fork函数创建子进程后,子进程往往要调用exec函数以执行另一个程序。

当子进程调用exec函数时,会将一个二进制可执行程序的全路径名作为参数传给exec,exec会用新程序代换子进程原来全部进程空间的内容,而新程序则从其main函数开始执行,这样子进程要完成的任务就变成了新程序要完成的任务了。

因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。进程还是那个进程,但实质内容已经完全改变。呵呵,这是不是和中国A股的借壳上市有异曲同工之妙?

顺便说一下,新程序的bss段清0这个操作,以及命令行参数和环境变量的指定,也是由exec完成的。

exec的用法:

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

pathname是新程序的二进制文件的全路径名,arg0是新程序的第1个命令行参数argv[0],之后是新程序的第2、3、4……个命令行参数,以(char*)0表示命令行参数的结束,envp是新程序的环境变量。

exec执行失败返回-1,成功将永不返回(想想为什么?)。哎,牛人就是有脾气,天字1号是调用1次,返回2次;地字1号,干脆就不返回了,你能奈我何?

1.1.2   exec的使用实例

echoall.c

  1 #include <stdio.h>

  2 #include <stdlib.h>

  3 #include <unistd.h>

  4

  5 int main(int argc, char*argv[])

  6 {

 7         int                     i;

 8         char            **ptr;

 9         extern char     **environ;

 10         for (i = 0; i < argc; i++)              /* echo all command-line args */

 11                 printf("argv[%d]:%s\n", i, argv[i]);

 12         for (ptr = environ; *ptr != 0;ptr++)   /* and all env strings */

 13                 printf("%s\n",*ptr);

 21 }

将此程序进行编译,生成二进制文件命名为echoall,放在当前目录下。很容易看出,此程序运行将打印进程的所有命令行参数和环境变量。

exec.c

  1 #include <stdio.h>

  2 #include <stdlib.h>

  3 #include <unistd.h>

  4 #include<sys/types.h>

  5

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

  7 int main(void)

  8{

 9         pid_t   pid;

 10

 11         if ( (pid = fork()) < 0)

 12                 { perror("forkerror"); exit(-1); }

 13         else if (pid == 0) {    /* specify pathname, specify environment */

 14                 if (execle("./echoall","echoall", "myarg1", "MY ARG2", (char *) 0,env_init) < 0)

 15                         { perror("execleerror"); exit(-2); }

 16         }

 17         if (waitpid(pid, NULL, 0) < 0)

 18                 { perror("waiterror"); exit(-3); }

 19

 20         if ( (pid = fork()) < 0)

 21                 { perror("forkerror"); exit(-1); }

 22         else if (pid == 0) {    /* specify filename, inherit environment */

 23                 if(execlp("./echoall", "echoall", "only 1 arg",(char *) 0) < 0)

 24                         { perror("execlperror"); exit(-2); }

 25         }

 26         exit(0);

 27 }

程序运行结果:

  1argv[0]: echoall

  2argv[1]: myarg1

  3argv[2]: MY ARG2

  4USER=unknown

  5PATH=/tmp

  6argv[0]: echoall

  7argv[1]: only 1 arg

  8ORBIT_SOCKETDIR=/tmp/orbit-dennis

  9SSH_AGENT_PID=1792

 10TERM=xterm

 11SHELL=/bin/bash

 12XDG_SESSION_COOKIE=0a13eccc45d521c3eb847f7b4bf75275-1320116445.669339

 13GTK_RC_FILES=/etc/gtk/gtkrc:/home/dennis/.gtkrc-1.2-gnome2

 14WINDOWID=62919986

 15GTK_MODULES=canberra-gtk-module

 16USER=dennis

.......

 

运行结果分析:

1-5行是第1个子进程14行运行新程序echoall的结果,其中:1-3行打印的是命令行参数;4、5行打印的是环境变量。

6行之后是第2个子进程23行运行新程序echoall的结果,其中:6、7行打印的是命令行参数;8行之后打印的是环境变量。之所以第2个子进程的环境变量那么多,是因为程序23行调用execlp时,没有给出环境变量参数,因此子进程就会继承父进程的全部环境变量。

 

1.1.3   exec与fork合作

终于到了可以让天、地1号双剑合璧,完成shell程序的时候了。

 

shellv2.c

  1 #include <stdio.h>

  2 #include <stdlib.h>

  3 #include <unistd.h>

  4 #include <fcntl.h>

  5 #include <errno.h>

  6 #include<sys/types.h>

  7 #include<sys/wait.h>

  8 #include <string.h>

  9 int parseargs(char *cmdline);

 10 char *cmdargv[20] = {0};

 11 int main(void)

 12{

 13        pid_t pid;

 14         char buf[100];

 15         int retval;

 16         printf("WoLaoDa# ");

 17         fflush(stdout);

 18         while (1) {

 19                 fgets(buf, 100, stdin);

 20                buf[strlen(buf) - 1] = '\0';

 21                 if ((pid =fork()) < 0) {

 22                        perror("fork");

 23                         exit(-1);

 24                 } else if (pid == 0) {

 25                         parseargs(buf);

 26                         execvp(cmdargv[0],cmdargv);

 27                         exit(0);

 28                 }

 29                 wait(&retval);

 30                 printf("WoLaoDa# ");

 31                 fflush(stdout);

 32         }

 33 }

 34

 35 int parseargs(char *cmdline)

 36 {

 37         char *head, *tail, *tmp;

 38         int i;

 39         head = tail = cmdline;

 40         for( ; *tail == ' '; tail++)

 41                 ;

 42         head = tail;

 43         for (i = 0; *tail != '\0'; i++) {

 44                 cmdargv[i] = head;

 45                 for( ;(*tail != ' ') && (*tail != '\0'); tail++)

 46                         ;

 47                 if (*tail == '\0')

 48                         continue;

 49                 *tail++ = '\0';

 50                 for( ; *tail == ' '; tail++)

 51                         ;

 52                 head = tail;

 53         }

 54        cmdargv[i] = '\0';

 55        return i;

 56 }

 

运行结果:



程序分析:

如果用户从键盘输入ls -l /tmp,那么main函数将调用parseargs,将字符串“ls -l /tmp”作为参数传入,parseargs执行结束后,全局指针数组cmdargv将被填充为:cmdargv[0] = “ls”,

cmdargv[1] = “-l”,cmdargv[2] = “/tmp”,cmdargv[3] = NULL。此全局数组cmdargv将被作为参数传递给exec函数

16行打印shell提示符,17行强制printf提交缓冲区内容,这是因为16行没有打印行缓冲的‘\n’

18行到32行的while循环,每循环一次处理一次用户输入命令,并调用exec来在子进程中运行用户输入的外部命令程序

19行从键盘获得用户输入命令,20行去掉fgets读取到的’\n’;

21行调用fork创建子进程后,父进程经24、29行后,将会阻塞,等待子进程结束。注意:29行必须有,这是因为如果没有29行,并且子进程运行时间很长的话,那么父进程打印的shell提示符将会被淹没在子进程的输出中,从而当子进程运行结束,用户需要第2次输入命令时,却见不到shell提示符,用户将会非常迷惑。所以必须调用wait等待子进程结束(此时:子进程也输出了全部内容)后,父进程再打印shell提示符。此外,如果父进程不调用wait的话,还会导致严重问题:结束执行的子进程将成为僵尸进程,随着shell不断的执行外部命令,系统中的僵尸进程将累积得越来越多,最终耗尽OS的资源,令系统崩溃。

30、31行是父进程打印shell提示符

子进程执行21、24行后,再执行25行,构造出正确的命令行参数数组cmdargv,然后以cmdargv为参数调用execvp,在子进程中执行用户指定的命令程序。


原创粉丝点击