本系列文章节选自本人所著《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,在子进程中执行用户指定的命令程序。