UnixEnvAdvProgramm_Process

 

进程概览

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” );

}

此程序保证先执行父进程,然后在执行子进程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值