分类: LINUX
2011-05-03 11:04:25
/* fork_test.c */ #include #inlcude main() { pid_t pid; /*此时仅有一个进程*/ pid=fork(); /*此时已经有两个进程在同时运行*/ if(pid<0) printf("error in fork!"); else if(pid==0) printf("I am the child process, my process ID is %d\n",getpid()); else printf("I am the parent process, my process ID is %d\n",getpid()); } |
$gcc fork_test.c -o fork_test $./fork_test I am the parent process, my process ID is 1991 I am the child process, my process ID is 1992 |
两个进程中,原先就存在的那个被称作“父进程”,新出现的那个被称作“子进程”。父子进程的区别除了进程标志符(process ID)不同外,变量pid的值也不相同,pid存放的是fork的返回值。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
fork出错可能有两种原因:(1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。(2)系统内存不足,这时errno的值被设置为ENOMEM。(关于errno的意义,请参考本系列的第一篇文章。)
二、linux进程控制-wait()
#include #include pid_t wait(int *status) |
pid = wait(NULL); |
下面就让我们用一个例子来实战应用一下wait调用:
/* wait1.c */ #include #include #include #include main() { pid_t pc,pr; pc=fork(); if(pc<0) /* 如果出错 */ printf("error ocurred!\n"); else if(pc==0){ /* 如果是子进程 */ printf("This is child process with pid of %d\n",getpid()); sleep(10); /* 睡眠10秒钟 */ } else{ /* 如果是父进程 */ pr=wait(NULL); /* 在这里等待 */ printf("I catched a child process with pid of %d\n"),pr); } exit(0); } |
编译并运行:
$ cc wait1.c -o wait1 $ ./wait1 This is child process with pid of 1508 I catched a child process with pid of 1508 |
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
1,WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数--指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)
2, WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
下面通过例子来实战一下我们刚刚学到的内容:
/* wait2.c */ #include #include #include main() { int status; pid_t pc,pr; pc=fork(); if(pc<0) /* 如果出错 */ printf("error ocurred!\n"); else if(pc==0){ /* 子进程 */ printf("This is child process with pid of %d.\n",getpid()); exit(3); /* 子进程返回3 */ } else{ /* 父进程 */ pr=wait(&status); if(WIFEXITED(status)){ /* 如果WIFEXITED返回非零值 */ printf("the child process %d exit normally.\n",pr); printf("the return code is %d.\n",WEXITSTATUS(status)); }else /* 如果WIFEXITED返回零 */ printf("the child process %d exit abnormally.\n",pr); } } |
$ cc wait2.c -o wait2 $ ./wait2 This is child process with pid of 1538. the child process 1538 exit normally. the return code is 3. |
当然,处理进程退出状态的宏并不止这两个,但它们当中的绝大部分在平时的编程中很少用到,就也不在这里浪费篇幅介绍了,有兴趣的读者可以自己参阅Linux man pages去了解它们的用法。
有时候,父进程要求子进程的运算结果进行下一步的运算,或者子进程的功能是为父进程提供了下一步执行的先决条件(如:子进程建立文件,而父进程写入数据),此时父进程就必须在某一个位置停下来,等待子进程运行结束,而如果父进程不等待而直接执行下去的话,可以想见,会出现极大的混乱。这种情况称为进程之间的同步,更准确地说,这是进程同步的一种特例。进程同步就是要协调好2个以上的进程,使之以安排好地次序依次执行。解决进程同步问题有更通用的方法,我们将在以后介绍,但对于我们假设的这种情况,则完全可以用wait系统调用简单的予以解决。请看下面这段程序:
#include #include main() { pid_t pc, pr; int status; pc=fork(); if(pc<0) printf("Error occured on forking.\n"); else if(pc==0){ /* 子进程的工作 */ exit(0); }else{ /* 父进程的工作 */ pr=wait(&status); /* 利用子进程的结果 */ } } |
#include #include pid_t waitpid(pid_t pid,int *status,int options) |
从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。
options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED); |
ret=waitpid(-1,NULL,0); |
而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。
看到这里,聪明的读者可能已经看出端倪了--wait不就是经过包装的waitpid吗?没错,察看<内核源码目录>/include/unistd.h文件349-352行就会发现以下程序段:
static inline pid_t wait(int * wait_stat) { return waitpid(-1,wait_stat,0); } |
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
/* waitpid.c */ #include #include #include main() { pid_t pc, pr; pc=fork(); if(pc<0) /* 如果fork出错 */ printf("Error occured on forking.\n"); else if(pc==0){ /* 如果是子进程 */ sleep(10); /* 睡眠10秒 */ exit(0); } /* 如果是父进程 */ do{ pr=waitpid(pc, NULL, WNOHANG); /* 使用了WNOHANG参数,waitpid不会在这里等待 */ if(pr==0){ /* 如果没有收集到子进程 */ printf("No child exited\n"); sleep(1); } }while(pr==0); /* 没有收集到子进程,就回去继续尝试 */ if(pr==pc) printf("successfully get child %d\n", pr); else printf("some error occured\n"); } |
编译并运行:
$ cc waitpid.c -o waitpid $ ./waitpid No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited successfully get child 1526 |
父进程经过10次失败的尝试之后,终于收集到了退出的子进程。
因为这只是一个例子程序,不便写得太复杂,所以我们就让父进程和子进程分别睡眠了10秒钟和1秒钟,代表它们分别作了10秒钟和1秒钟的工作。父子进程都有工作要做,父进程利用工作的简短间歇察看子进程的是否退出,如退出就收集它。
三、linux进程控制-exit()
/* exit_test1.c */ #include main() { printf("this process will exit!\n"); exit(0); printf("never be displayed!\n"); } |
$gcc exit_test1.c -o exit_test1 $./exit_test1 this process will exit! |
作为系统调用而言,_exit和exit是一对孪生兄弟。
这时随便一个懂得C语言并且头脑清醒的人都会说,_exit和exit没有任何区别,但我们还要讲一下这两者之间的区别,这种区别主要体现在它们在函数库中的定义。_exit在Linux函数库中的原型是:
#include void _exit(int status); |
从图中可以看出,_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理I/O缓冲”一项。
在Linux 的标准函数库中,有一套称作“高级I/O”的函数,我们熟知的printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(buffered I/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束符EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。
请看以下例程:
/* exit2.c */ #include main() { printf("output begin\n"); printf("content in buffer"); exit(0); } |
编译并运行:
$gcc exit2.c -o exit2 $./exit2 output begin content in buffer /* _exit1.c */ #include main() { printf("output begin\n"); printf("content in buffer"); _exit(0); } |
编译并运行:
$gcc _exit1.c -o _exit1#include int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]); |
程序里调用了2个Linux常用的系统命令,echo和env。echo会把后面跟的命令行参数原封不动的打印出来,env用来列出所有环境变量。
由于各个子进程执行的顺序无法控制,所以有可能出现一个比较混乱的输出--各子进程打印的结果交杂在一起,而不是严格按照程序中列出的次序。
编译并运行: