在日常的编程过程中,是否注意到exit(0)这个语句,在我们初学者的理解中,他的作用可能是结束程序,但是,在学习了计算机系统基础这门课之后,我们发现,它的作用并非只是简单的结束一个正在运行中的程序,在讲真正的作用之前我们需要先知道一个概念——进程。
进程:一个执行中程序的实例。
exit(0)的作用其实是结束一个进程,在这一点上return 0有和它同样的作用。
既然可以结束进程,我们是否可以人为的创建进程呢?这个时候我们就需要fork函数的帮助了。
#include <unistd.h> int fork(void);fork函数的作用就是在原来的父进程上建立一个子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户及虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈,子进程还获得与父进程任何打开文件描述相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID(Process ID进程编号)。
fork函数非常有趣,fork函数有一个特性:调用一次,返回两次。怎么理解呢?两次返回一次在父进程,一次在子进程,在父进程中,fork返回子进程的PID。在子进程中,fork返回0。因为紫禁城的PID总是非零,所以用返回值可以表明是父进程还是子进程在执行。我们可以来看一个例子来加深理解。
在Linux系统下运行如下代码: 例一:
#include <stdio.h> #include <unistd.h> int main(){ int r=fork(); if(r==0){ printf("子进程PID:%d\n",getpid()); printf("child:hello\n"); }else{ printf("父进程PID:%d\n",getpid()); printf("parent:hello\n"); } return 0; }结果如下:
父进程PID:2677 parent:hello 子进程PID:2678 child:hello
例二:
#include <stdio.h> #include <unistd.h> int main() { int i=1; int pid; pid = fork(); if(pid == 0) //返回子进程 { printf("child: %d\n", ++i); } else { printf("parent pid: %d\n", --i);//父进程中返回子进程的pid } }结果如下:
parent:0 child:2
这两个例子展现了fork函数的一些特性;
调用一次,返回两次。很显然,我们只调用了一次fork,但是返回了两次,一次是执行完父进程,一次是执行完子进程的时候,对于只创建了一个子进程的程序还是很好理解的,但是一旦创建多个子进程,就会令人迷惑。并发执行。父进程和子进程是并发运行的独立进程。内行人能够以任何方式交替执行它们的逻辑控制流中的指令、在我们的系统上运行这个程序是,父进程先完成它的printf语句,然后是子进程。。然而在另一个系统上可能正好相反。一般而言作为程序员,我们绝不能对不同进程中指令的交替执行做任何假设。相同但是独立的地址空间。两个进程的地址空间都是相同的。每个进程有相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值,以及相同的代码。因此在例二中,当fork返回时,本地变量i在父进程和子进程都为1,然而父进程和子进程是两个独立的进程,它们都有自己的私有的地址空间。后面,父进程和子进程对i所作的任何改变都是独立的,不会反映在另外的一个进程的内存中。共享文件。当运行这两个示例程序时,我们可以发现父进程和子进程都将它们的输出结果显示在了屏幕上,这是因为,当父进程调用fork时stdout文件是打开的,并指向屏幕,所以子进程执行时继承了父进程的这个文件,它的输出也指向屏幕。如果只是一个子进程还以理解,但是如果一个父进程创建了好几个子进程就会迷惑我们。 例三:
#include <stdio.h> #include <unistd.h> int main() { fork(); fork(); printf("hello\n"); return 0; }结果如下:
hello hello hello hello
对于上面例三我们可以使用进程图来帮助理解。 以上就是我目前对fork函数全部理解,如果在后面的学习中发现与现在的理解有出入会及时更改或者有新的内容会及时补充。
理解了fork函数的主要用法后,对于进程的执行我们还需要了解其他的相关知识
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程一直处于一种已终止的状态,直到被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。一个以终止的但尚未被回收的进程被称为僵死进程。
如果出于某种原因父进程还未回收子进程就终止了,此时子进程就成了一个孤儿进程,就像Java中的类都是继承与Object类一样,在进程中同样存在着这样一个老大哥,他负责去收养那些无家可归的子进程,该进程就是pid为1的init进程,当发现孤儿进程内核会直接安排init进程将其接管。为了尽可能避免产生僵死进程与孤儿进程,可以通过调用waitpid函数赖等待他的子进程终止或者停止。
#include<sys/types.h> #include<sys/wait.h> pid_t waitpid(pid_t pid,int * statusp,int options); //返回:如果成功,则为子进程的pid,如果WNOHANG,贼为0,如果其他错误,则为-11.pid参数判定等待集合的成员:
如果pid>0,那么等待集合就是一个单独的子进程(pid=该子进程的pid),它的进程ID就等于pid。如果pid=-1,那么等待集合就是由父进程所有的子进程组成的。2.options参数修改默认行为
WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回0.默认的行为是挂起调用进程,直到有子进程终止。WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或被停止。返回的pid为导致返回的已终止或被停止子进程的pid。默认的行为是指返回已终止的子进程。WCONTINUED:挂起调用进程的执行,知道等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。(SIGCONT信号简单来说就是起死回生的作用,让已终止的进程重新执行)以上选项可以用或运算组合,如:WNOHANG | WUNTRACED:立即返回,如果等待集合中的子进程都没有被停止或终止,则返回值为0;如果有一个停止或终止,则返回值为该子进程的pid。3.statusp检查已回收子进程的退出状态 如果statusp参数是非空的,那么waitpid就会在status中放上关于导致返回的子进程的状态信息,status是statusp指向的值。wait.h头文件定义了解释status参数的几个宏:
WIFEXCITED(status):如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。WEXITSTATUS(status):返回一个正常终止的子进程的退出状态。只有在WIFEXCITED()返回为真时,才会定义这个状态。WIFSIGNALED(status):如果子进程是因为一个未被捕获的信号终止的,那么就返回真。WTERMSIG(status):返回导致子进程终止的信号的编号。只有在WIFSIGNALED()返回为真时,才定义这个状态。WIFSTOPPED(status):如果引起返回的子进程当前是停止的,那么返回为真。WSTOPSIG(status):返回引起子进程停止的信号的编号。只有在WIFSTOPPED()返回为真时,才定义这个状态。WIFCONTINUED(status):如果子进程收到SIGCONT信号重新启动,则返回真。4.错误条件 若调用进程没有子进程,那么waitpid返回-1。
wait函数是waitpid函数的简化版
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int * statusp); //返回:如果成功,则为子进程的pid,如果出错,则为-1.在了解了fork、waitpid、wait函数的用法后,我们已经初步的理解了如何创建进程以及回收进程,下面通过例题加以巩固:
void fork0() { if (fork() == 0) { printf("Hello from child\n"); } else { printf("Hello from parent\n"); } }运行结果: 进程图: 这里别的没有什么好说明的,但是为何在输出子进程运行结果之前会先输出命令提示符呢?原因就在于,父进程并没有回收子进程就先return了,返回shell,所以命令提示符和子进程就成了两个互不相干的进程,并发执行,所以才会出现子进程前有命令提示符的情况,那如果我们在父进程结束前用waitpid或wait语句回收子进程再结束,会是什么样的结果呢? 修改后的代码如下:
void fork0() { int child_status; if (fork() == 0) { printf("Hello from child\n"); } else { printf("Hello from parent\n"); wait(&child_status); } }运行结果: 进程图:
void fork1() { int x = 1; pid_t pid = fork(); if (pid == 0) { printf("Child has x = %d\n", ++x); } else { printf("Parent has x = %d\n", --x); } printf("Bye from process %d with x = %d\n", getpid(), x); }运行结果: 进程图: 3.
void fork2() { printf("L0\n"); fork(); printf("L1\n"); fork(); printf("Bye\n"); }运行结果: 进程图: 4.
void fork4() { printf("L0\n"); if (fork() != 0) { printf("L1\n"); if (fork() != 0) { printf("L2\n"); } } printf("Bye\n"); }运行结果: 进程图: 5.
void cleanup(void) { printf("Cleaning up\n"); } void fork6() { atexit(cleanup); fork(); exit(0); }atexit函数解析详见:https://blog.csdn.net/baidu_37964071/article/details/80238224
运行结果: 进程图: 6.
void fork7() { if (fork() == 0) { /* Child */ printf("Terminating Child, PID = %d\n", getpid()); exit(0); } else { printf("Running Parent, PID = %d\n", getpid()); while (1) ; /* Infinite loop */ } }运行结果: 进程图; 7.
void fork8() { if (fork() == 0) { /* Child */ printf("Running Child, PID = %d\n", getpid()); while (1) ; /* Infinite loop */ } else { printf("Terminating Parent, PID = %d\n", getpid()); exit(0); } }运行结果: 进程图:
对于上面的例6、7,分别在父进程和子进程中设置死循环,使得两个进程无法结束。例6父进程死循环,不会返回shell,没有人来回收子进程,使得子进程变为了孤儿进程,所以也无法输入下一条指令,只能ctrl+z挂起父进程或者ctrl+c强制结束,才能输入下一条指令。例7子进程死循环,父进程没有回收子进程就直接结束了,返回shell,不用挂起或强制结束就可以输入下一条指。
void fork9() { int child_status; if (fork() == 0) { printf("HC: hello from child\n"); exit(0); } else { printf("HP: hello from parent\n"); wait(&child_status); printf("CT: child has terminated\n"); } printf("Bye\n"); }运行结果: 进程图:
由进程图可知,父进程的两个printf中间加了一个wait函数,所以父进程会在执行一个printf语句之后等待子进程结束后再直行第二个printf。
#define N 5 void fork10() { pid_t pid[N]; int i, child_status; for (i = 0; i < N; i++) if ((pid[i] = fork()) == 0) { exit(100+i); /* Child */ } for (i = 0; i < N; i++) { /* Parent */ pid_t wpid = wait(&child_status); if (WIFEXITED(child_status)) printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status)); else printf("Child %d terminate abnormally\n", wpid); } }运行结果:
exit函数与return语句用法与区别详见:https://blog.csdn.net/Rex_WUST/article/details/88372481
在本程序fork阶段,每fork一次,创建的子进程就会直接异常退出,所以只有父进程在创建子进程。但是并没有规定等待的顺序,所以在每一台电脑上每一次所运行的结果几乎都是不一样的。
void fork11() { pid_t pid[N]; int i; int child_status; for (i = 0; i < N; i++) if ((pid[i] = fork()) == 0) exit(100+i); /* Child */ for (i = N-1; i >= 0; i--) { pid_t wpid = waitpid(pid[i], &child_status, 0); if (WIFEXITED(child_status)) printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status)); else printf("Child %d terminate abnormally\n", wpid); } }运行结果:
在本程序fork阶段,每fork一次,创建的子进程就会直接异常退出,所以只有父进程在创建子进程。后面的循环从N-1到0表示从后往前按顺序等待子进程结束,之前我们已经讲解过了waitpid函数的用法,在这里就是等待所有子进程结束,以及下面的两个宏也在上面讲过了这里就不深入解释了,请读者自行理解。
void fork12() { pid_t pid[N]; int i; int child_status; for (i = 0; i < N; i++) if ((pid[i] = fork()) == 0) { /* Child: Infinite Loop */ while(1) ; } for (i = 0; i < N; i++) { printf("Killing process %d\n", pid[i]); kill(pid[i], SIGINT); } for (i = 0; i < N; i++) { pid_t wpid = wait(&child_status); if (WIFEXITED(child_status)) printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status)); else printf("Child %d terminated abnormally\n", wpid); } }运行结果:
本例中使用了信号,通过kill函数发送SIGINT信号使得子进程强制中断,因为子进程非正常退出,所以WIFEXITED(child_status)返回false,进而执行else后面的语句。进程图也可参考例10。
这里补充一下kill函数与SIGINT信号。 进程通过调用kill函数发送信号给其他进程(包括他们自己)
#include <sys/types.h> #include <signal.h> int kill(pid_t pid,int sig); //返回:若成功则为0,若错误则为-1。如果pid大于零,那么kill函数发送信号好吗sig给进程pid。如果pid等于零,那么kill发送信号sig给调用进程所在进程组中的每个进程,包括调用进程自己。如果pid小于零,kill发送信号sig给进程组|pid|(pid的绝对值)中的每一个进程。 关于进程组详见:https://blog.csdn.net/qq_40927789/article/details/80035174 信号SIGINT默认行为终止,如果进程在前台运行时,你在键盘上同时按下ctrl键和C键,那么内核就会发送一个SIGNINT信号给这个前台进程组中的每一个进程,将这些进程终止。
void int_handler(int sig) { printf("Process %d received signal %d\n", getpid(), sig); exit(0); } void fork13() { pid_t pid[N]; int i; int child_status; signal(SIGINT, int_handler); for (i = 0; i < N; i++) if ((pid[i] = fork()) == 0) { /* Child: Infinite Loop */ while(1) ; } for (i = 0; i < N; i++) { printf("Killing process %d\n", pid[i]); kill(pid[i], SIGINT); } for (i = 0; i < N; i++) { pid_t wpid = wait(&child_status); if (WIFEXITED(child_status)) printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status)); else printf("Child %d terminated abnormally\n", wpid); } }运行结果:
这里是修改信号默认行为的例子,原本SIGINT信号就是来自键盘的中断,也就是ctrl+c,在这里通过signal函数修改了该信号的默认行为,使得其在中断之前,先输出所终止的子进程的pid和SIGINT的信号编号,再调用exit函数正常退出。
int ccount = 0; void child_handler(int sig) { int child_status; pid_t pid = wait(&child_status); ccount--; printf("Received SIGCHLD signal %d for process %d\n", sig, pid); fflush(stdout); } void fork14() { pid_t pid[N]; int i; ccount = N; signal(SIGCHLD, child_handler); for (i = 0; i < N; i++) { if ((pid[i] = fork()) == 0) { sleep(1); exit(0); /* Child: Exit */ } } while (ccount > 0) ; }运行结果:
挂起信号不排队,所以会导致最后输出的结果非常乱而且具有随机性。
void child_handler2(int sig) { int child_status; pid_t pid; while ((pid = wait(&child_status)) > 0) { ccount--; printf("Received signal %d from process %d\n", sig, pid); fflush(stdout); } } void fork15() { pid_t pid[N]; int i; ccount = N; signal(SIGCHLD, child_handler2); for (i = 0; i < N; i++) if ((pid[i] = fork()) == 0) { sleep(1); exit(0); /* Child: Exit */ } while (ccount > 0) { pause(); } }运行结果:
此处的信号默认行为被修改了,但是与上一例不同,此处信号的处理变味了等待多个子进程。即使不排队也能正常输出并结束。
补充一下pause函数: 作用:使调用进程进入挂起状态,直到接收到信号且信号函数成功返回 pause函数才会返回 返回值:始终返回-1
void fork16() { if (fork() == 0) { printf("Child1: pid=%d pgrp=%d\n", getpid(), getpgrp()); if (fork() == 0) printf("Child2: pid=%d pgrp=%d\n", getpid(), getpgrp()); while(1); } }运行结果: 关于进程组请参考: 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/kwinway/article/details/80139744
void fork17() { if (fork() == 0) { printf("Child: pid=%d pgrp=%d\n", getpid(), getpgrp()); } else { printf("Parent: pid=%d pgrp=%d\n", getpid(), getpgrp()); } while(1); }运行结果: 以上16个例子均来自博主的资源,可下载,以上进程图并不标准,应绘制仓促没有注意细节望体谅。部分代码运行结果不同比如回收子进程,每个系统,或者每一次运行都有可能不一样,是正常情况。
——————————进程学习笔记到此为止—————————————
