进程概览
1、 exit和_exit区别
头文件不同:
exit #include <stdlib.h> ANSI C定义
_exit #include <unistd.h> POSIX.1定义
执行操作不同:
Exit 先执行一些清除处理(调用atexit的处理函数以及关闭IO流),然后进入内核
_exit则直接进入内核
2、 C程序的存储空间
<1>正文段
也就是程序代码段,也就是CPU执行的机器指令部分,一般是共享以及只读的。
<2>数据段
包括初始化数据段和非初始化数据段,一般是全局变量,前者赋了初值,后者没有赋初值(由内核初始化为0)
<3>栈
自动变量以及每次函数调用时所需保存的信息都存放在此段中。
<4>堆
进行动态存储分配。
正文段从0地址开始,然后是数据段(初始化数据段和非初始化数据段),然后是堆,然后是未用的虚地址空间,然后是栈,最后是命令行参数和环境变量。
3、 自动变量、寄存器变量和易失变量, 静态变量,全局变量
(1) 静态变量和全局变量
在函数内或者函数外使用static定义的变量,后者称为在本文件内使用的全局变量;
函数外没有用static定义的变量,可以在多个文件使用的全局变量(别的文件需要使用extern)
(2) 定义在函数内的静态变量和自动变量
静态变量存在数据段,可能为初始化数据段或者是非初始化数据段,为固定的内存地址;
而自动变量存在栈中,每次函数调用的时候地址都不一样;
(3) 寄存器变量和其他所有变量
寄存器变量用register定义,存在CPU寄存器中;
其他所有变量存储在内存中,访问起来比访问寄存器要慢。
(4) 易失变量
是用volatile定义的变量,强制从内存中访问此变量。
未进行优化,所有变量存在内存中;
进行优化,所有变量存在寄存器中。
Volatile和register相当于是限制变量存储的位置,前者是内存中,后者是CPU的寄存器中,既可以定义全局变量,也可以用来定义局部变量。
4、 Setjump和longjump
(1)与goto比较
Goto是在函数内部进行跳转,而jump是在函数之间进行跳转
(2)使用场景
在很深函数嵌套时,由于对每个函数的返回值进行判断显得很麻烦,直接采用jump将会使逻辑变得简单
(3)具体使用
使用步骤如下:
ü 声明全局jmp_buf buf;
ü 在目标点ret = setjump(buf);
ü 在返回点longjump(buf, ret ),导致setjump返回,返回值为ret
注意事项:存放在内存中的变量在setjump目标点,将具备longjump时的值;
而存放在寄存器中的变量在setjump目标点,将恢复为调用setjump时的值。
5、 Fork理解
Fork一次调用,两次返回。子进程中返回0,父进程中返回子进程ID号。
子进程和父进程继续执行fork之后的指令(不同),子进程是父进程的复制品(包括父进程的数据段、堆和栈,当然也包括正文段)。注意是copy,而不是共享。如果正文段是只读,则可以是共享的。
写时拷贝,copy-on-write,fork时将数据段和堆栈设为共享只读,只有当进程试图修改这些区域时,才拷贝其中的内存页面。
Int glob = 6; Int main() { Int var=88; Pid_t pid; Printf( “before fork\n” ); If( pid = fork() < 0 ) Printf( “fork error”); Else if( pid == 0 ) { Glob++; //child Var++; } Else Sleep(2); // parent Printf( “pid = %d, glob = %d, var = %d\n”, getpid(), glob, var ); Exit(0); } |
父子进程只有上面两段不一样,其他的代码段都是一样的。
#./a.out Before fork Pid= 430, glob=7, var =89 //child Pid=429, glob=6, var=88 // parent |
#./a.out > log #Cat log Before fork Pid= 430, glob=7, var =89 //child Before fork Pid=429, glob=6, var=88 // parent |
标准IO函数是带缓存的,如果标准输出连接的是终端设备,则是行缓存的,换行符能刷新缓存;如果标准输出连接的是文件,则是全缓存的。
当标准输出重定向到稳健log时,则变成了全缓存,当执行fork时,”before fork”在父进程的数据段里,复制父进程时,也将这块缓存复制了过来,于是父子进程均包含了带该行内容的缓存,当父子进程退出时,这行内容均输出了。
父子进程对每个相同的打开的描述符共享一个文件表项。同时进行操作会造成混乱,一般有如下两种方式:
(1) 父进程等待子进程完成。当子进程结束后,共享描述符的文件位移量已做了相应更新。
(2) 父子进程各自执行不同的程序段。Fork之后,父子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。
Fork的应用场景有如下两种:
(1) 父进程希望复制自己,使父子进程执行不同的代码段。在网络服务进程中很常见,当请求到达时,父进程调用fork,使子进程处理请求,父进程继续等待下一个服务请求。
(2) 一个程序要执行另外一个程序,则在子进程fork返回之后,立即调用exec。
6、 Fork与exec连用以及spawn
Exec族函数,一旦调用,则调用者进程就死亡了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段和堆栈段,唯一留下来的就是进程号,对系统而言,还是同一个进程,不过已经是另外一个程序了。
如果程序想启动另外一个程序,而自己又想继续运行,则可以组合调用fork和exec。在windows系统中spawn函数具备类似的功能。
7、 Fork 和 vfork
Vfork用于创建一个新进程,而该新进程的目的是为了exec一个新程序。
Vfork和fork同样是创建一个子进程,但是vfork并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,于是也就不会访问此地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。
Int glob = 6; Int main() { Int var=88; Pid_t pid; Printf( “before fork\n” ); If( pid = vfork() < 0 ) Printf( “fork error”); Else if( pid == 0 ) { Glob++; //child Var++; _exit(0); } Printf( “pid = %d, glob = %d, var = %d\n”, getpid(), glob, var ); Exit(0); } |
#./a.out Before fork Pid= 430, glob=7, var =89 |
因为vfork产生的子进程是在父进程的地址空间(内存)中运行的,所以改变glob和var,则是直接改变父进程中的值。
如果将_exit(0)变成exit(0),则由于exit关闭了标准输出、标准输入和错误输出,则第二行将不会输出。
8、 进程结束时返回状态的解析
程序退出有两种情况。
正常终止:return、exit、_exit
异常终止:abort(SIGABRT),信号终止(来自进程本身例如SIGABRT信号,以及其它进程或者内核传送的信号)
对于任何一种终止情形,都希望终止进程能够通知其父进程它是如何终止的。对于正常终止,终止进程通过传递退出状态来实现的;对于异常终止,内核产生一个指示其异常终止原因的终止状态。
对于任何一种终止情形,父进程都可以通过wait或waitpid函数来取得子进程的终止状态。
注意退出状态最终还是会转换成终止状态的。
#include <sys/types.h> #include <sys/wait.h> Pid_t wait( int *status); Pid_t waitpid( pid_t pid, int *status, int options); 成功返回终止子进程的ID,出错返回-1 |
两个函数的区别:
<1>在一个子进程终止之前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞
<2>waitpid并不等待第一个终止的子进程,有选项可以指定控制它所等待的进程。
两个函数通过status返回子进程的终止状态,这个返回的整型值,某些位表示退出状态,某些位指示终止状态的信号值,有一位指示是否产生了一个core文件等。
WIFEXITED(status):如果为真,则是正常终止,通过WEXITSTATUS(status)返回退出状态的值; WIFSIGNALED(status):如果为真,则为异常终止,通过WTERMSIG(status)返回信号值,通过WCOREDUMP(status)为真,表示产生了core文件; 通过WIFSTOPPED(status)为真,表示子进程被暂停了,通过WSTOPSIG(status)返回信号值。 |
Void pr_exit( int status ) { If(WIFEXITED(status) ) Printf( “normal termination, exit status is %d\n”, WEXITSTATUS(status) ); Else if(WIFSIGNALED(status)) Printf( “abnormal termination, signal number is %d”, WTERMSIG(status) ); Else if(WIFSTOPPED(status)) Printf( “child process is stopped,signal number is %d\n”, WSTOPSIG(status) ); } Int main() { Pid_t pid; Int status; If( pid = fork() < 0 ) Printf(“fork error”) Else if ( pid == 0 ) Exit(7); If( pid != wait(&status) ) Printf(“wait error”); Pr_exit(status); If( pid = fork() < 0 ) Printf(“fork error”) Else if ( pid == 0 ) Abort(); // generate SIGABRT If( pid != wait(&status) ) Printf(“wait error”); Pr_exit(status); If( pid = fork() < 0 ) Printf(“fork error”) Else if ( pid == 0 ) Status/=0;; //generate SIGFPE If( pid != wait(&status) ) Printf(“wait error”); Pr_exit(status); } |
输出为:
normal termination, exit status is 7 abnormal termination, signal number is 6 abnormal termination, signal number is 8 |
9、 僵尸进程详细问答
² 父进程在子进程之前终止,会发生什么情况?
当父进程终止时,将其子进程的父进程改变为init进程,叫由init进程领养。过程为:当一个进程终止时,内核会逐个检查所有活动进程,以判断它是否是终止进程的子进程,如果是,则将该进程的父进程ID设置为1。这样就保证了每个进程都有一个父进程。
² 子进程在父进程之前终止,会发生什么情况?
进程终止后,系统中仍然保存着一定量的信息(包括进程ID、该进程的终止状态、以及该进程使用的CPU时间总量,以及打开的文件句柄,所使用的内存等)。
(1) 只有当终止进程的父进程通过调用wait或waitpid时,才可以获得相应的信息以及释放相应的资源。
(2) 如果父进程没有调用wait或waitpid来对终止的子进程进行善后处理,则这个以已经终止的子进程将变为僵死进程。
² 一个由init进程领养的进程终止时会发生什么情况?
Init程序被设计成:只要有子进程终止,就会调用wait或waitpid函数来取得其终止状态。这样保证了系统中不会出现很多僵死进程。
² Init的子进程包括哪些?
包括由init直接产生的子进程,以及由init来领养的进程。
² 僵尸进程是否是进程生命周期中必定经历的状态?
是的。特点是:终止了,但未被父进程进行善后处理,占用进程表中的一个表项,多了,将会导致fork失败。当进程终止时,会向父进程发送SIGCHLD信号。
² 僵尸进程是否一定有父进程?
是的。如果在此进程终止之前,父进程终止了,则会过继给init进程,init进程为其父进程;如果此进程终止之后,父进程在对其进程善后处理之前,父进程终止了,僵尸进程在这一时刻,将会变成孤儿进程(没有父进程,此状态的进程存在时间短),随后会过继给init进程,init进程为其父进程。
² 避免僵尸进程方法一:显示忽略SIGCHLD信号
父进程接收到此信号时,代表有子进程终止了。显示忽略此信号,也是种善后处理方式,当然不能获取子进程的终止状态了。
signal(SIGCHLD,SIG_IGN); |
² 避免僵尸进程方法二:安装SIGCHLD信号处理句柄
在信号处理函数中,可以选择调用wait或waitpid函数来获取子进程的退出状态。
² 避免僵尸进程方法三:在父进程中调用wait或waitpid
这样会造成父进程阻塞。
² 避免僵尸进程方法四:fork两次
如果一个进程fork一个子进程,但不要求它等待子进程终止,也不希望子进程终止后处理僵尸状态直到父进程结束(僵尸的子进程会过继给init)。
通过fork两次实现这种要求。A进程fork一次,产生一个子进程,在子进程中再fork一次产生第二个子进程。在第一个子进程中,调用exit,第一个子进程终止,A进程对其进行善后处理;此时第二个子进程的父进程(第一个子进程)终止了,则将其过继给init进程,则第二个子进程永远不会成为僵尸进程了。
Int main() { Pid_t pid; Int status; If( pid = fork() < 0 ) Printf( “parent fork error” ) Else if( 0 == pid ) { If( pid = fork() < 0 ) Printf( “first child fork error” ); Else if( 0 == pid ) { Printf( “second child, parent is %d”, getppid() ); Real_main(); } Else { Sleep(2); Printf( “exit first child %d”, getpid() ); Exit(0); } } If( pid != Wait(&status) ) Printf( “parent wait error” ); Exit(0); } |
此方法可以作为模板,将fork出来的多个子进程交由init来托管,父进程则不用管理多个子进程了。
² 如何清除已经产生的僵尸进程?
僵尸进程是无法通过kill -9来清除的,除非kill掉其父进程,这样僵尸进程将会过继给init进程,由init进程对其进行善后处理工作。
10、 竞态条件
定义:当多个进程企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则认为发生了竞态条件。
常发生场景:fork之后,父进程还是子进程优先运行,无法预知,取决于系统负载以及内核的调度算法。
例如fork两次的例子,第二个子进程打印出其父进程ID。
如果第二个子进程在其父进程(第一个子进程)之前运行,则打印出来的是第一个子进程的ID;
如果第二个子进程在其父进程之后运行,则打印出来的是init的ID 1。
即使在系统中调用sleep,这也不能保证,如果系统负担很重,那么在第二个子进程从sleep返回时,可能第一个子进程还没有获得机会运行,这样第二个子进程获取的父进程ID,仍然是第一个子进程的ID。
如何通过polling的方式来判断其父进程已经终止了?
While( getppid() != 1 ) Sleep(1); //父进程终止了,子进程会过继给init,getppid返回1 |
为了避免竞态条件和polling,通过信号机制实现父子进程间通知的作用。包括TELL_WAIT,TELL_CHILD,TELL_PARENT,WAIT_CHILD,WAIT_PARENT。(实现原理是两个自定义信号SIG_USER1和SIG_USER2,以及信号屏蔽字集)。
Int main() { Pid_t pid; Int status; TELL_WAIT(); If( pid = fork() < 0 ) Printf( “parent fork error” ) Else if( 0 == pid ) { If( pid = fork() < 0 ) Printf( “first child fork error” ); Else if( 0 == pid ) { WAIT_PARENT(); Printf( “second child, parent is %d”, getppid() ); Real_main(); } Else { Sleep(2); Printf( “exit first child %d”, getpid() ); Exit(0); } } If( pid != Wait(&status) ) Printf( “parent wait error” ); TELL_CHILD(); Exit(0); } |
无法应用到fork两次的场景,因为A进程无法知道第二个子进程的ID,而第一个子进程在exit后也无法再给其子进程(第二个子进程)发送信号了。
Int main() { Int status; Pid_t pid; TELL_WAIT(); If( pid = fork() < 0 ) Printf( “fork eror” ); Else If( pid == 0 ) { WAIT_PARENT(); Printf( “child printf” ); } Printf( “parent printg” ); TELL_CHILD(pid); If( pid != wait(&status) ) Printf( “wait error” ); } |
此程序保证先执行父进程,然后在执行子进程。