[APUE]进程控制(上)

2017-01-07 08:36:32来源:cnblogs.com作者:orlion人点击

一、进程标识

  进程ID 0是调度进程,常常被称为交换进程(swapper)。该进程并不执行任何磁盘上的程序--它是内核的一部分,因此也被称为系统进程。进程ID 1是init进程,在自举(bootstrapping)过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负责在内核自举后启动一个UNIX系统。init通常读与系统有关的初始化(/etc/rc*文件),并将系统引导到一个状态(例如多用户)。init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。
  在某些UNIX的虚存实现中,进程ID 2是页精灵进程(pagedaemon)。此进程负责支持虚存系统的请页操作。与交换进程一样,页精灵进程也是内核进程。
除了进程ID,每个进程还有其他标识符。下列函数可以返回这些标识符:

#include <sys/types.h>#include <unistd.h>pid_t getpid(void);返回: 调用进程的进程IDpid_t getppid(void);返回: 调用进程的父进程IDuid_t getuid(void);返回: 调用进程的实际用户IDuid_t geteuid(void);返回: 调用进程的有效用户IDgid_t getgid(void);返回: 调用进程的实际组IDgid_t getegid(void);返回: 调用进程的有效阻ID

这些函数都没有出错返回

二、fork函数

  一个进程调用fork函数是UNIX内核创建一个新进程的唯一方法(除了交换进程、init进程和页精灵进程)

#include <sys/types.h>#include <unistd.h>pid_t fork(void);返回: 子进程中为0,父进程中为子进程的进程ID,出错为-1.

  子进程和父进程继续执行fork之后的指令。子进程是父进程的复制品。例如,进程获得父进程数据空间、堆和栈的复制品。但是这些都是进程拥有的拷贝不是与父进程共享。如果正文段是只读的,则父、子进程共享正文段。
  现在很多实现并不做一个父进程数据段和堆的完全拷贝,因为在fork之后经常跟随着exec。作为替代,使用了在**写时复制(Copy On Write, COW)**的技术。这些区域由父、子进程共享,而且内核将它们的存取权限改成只读的。如果有进程试图修改这些区域,则内核为有关部分(典型的是虚存系统中的"页"),做一个拷贝。

#include <sys/types.h>#include <stdio.h>#inlcude <unistd.h>int glob = 6;char buf[] = "a write to stdout/n";int main(void){    int var;    pid_t pid;        var = 88;    if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) {        fprintf(stderr, "write error");    }        printf("before fork /n");        if ((pid = fork()) < 0) {        fprintf(stderr, "fork error");    } else if (pid == 0) {        glob++;        var++;    } else {        sleep(2);    }        printf("pid=%d,glob=%d,var=%d/n", gitpid(), glob, var);        return 0;}

  

  编译运行上面的进程:
 

  可以看到当我们将输出重定向到temp.out文件后多出个before fork的输出。write函数是不带缓存的。因为在fork之前调用write,所以其数据写到标准输出一次。但是标准IO是带缓存的。如果标准输出连到终端设备,则它是行缓存,否则它是全缓存。当以交互方式运行该程序时,只得到printf输出的行一次,其原因是标准输出缓存由新行符刷新。但是当将标注输出重新定向到一个文件时,却得到printf输出行两次。其原因是,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓存中,然后在父进程数据空间复制到子进程的过程中时,该缓存数据也被复制到了子进程中。于是那时父、子进程各自有了带该行内容的缓存。在exit之前的第二个printf将其数据添加到现存的缓冲中。当每个进程终止时,缓存中的内容将被写到相应文件中。
  文件共享   对于上面的程序需要注意:在重定向父进程的标准输出时也重定向了子进程的标准输出。fork的一个特性是所有由父进程打开的文件描述符都被复制到子进程中。父、子进程每个相同的打开文件描述符共享一个文件表项。
  这种共享文件的方式使父子进程对同一文件使用了一个文件位移量。对于以下情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父、子进程都向标准输出执行写操作。如果父进程使其标准输出重定向(很可能是由shell实现的),那么子进程写到该标准输出时,他将更新与父进程共享的该文件的位移量。在我们所考虑的例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写入到标准输出上,并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共享同一文件位移量,这种形式的交互就很难实现。[为了理解这一点可参看APUE3.10节]

 


  如果父、子进程写到同一文件描述符文件,但又没有任何形式的同步(例如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的文件描述符是在fork之前打开的)。
  在fork之后处理文件描述符有两种常见的情况:
  (1) 父进程等待子进程完成。这种情况下,父进程无需对其描述符做任何处理。
  (2) 父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。

  除了打开文件之外,很多父进程的其他性质也会由子进程继承:

  • 实际用户ID、实际组ID、有效用户ID、有效组ID。
  • 添加组ID。
  • 进程组ID。
  • 对话期ID。
  • 控制终端。
  • 设置-用户-ID标志和设置-组-ID标志。
  • 当前工作目录。
  • 根目录。
  • 文件方式创建屏蔽字。(umask)
  • 信号屏蔽和排列。
  • 对任一打开文件描述符的在执行时关闭标志。
  • 环境。
  • 连接的共享存储段。

  父、子进程之间的区别是:

  • fork的返回值。
  • 进程ID
  • 不同的父进程iD。
  • 子进程的tms_utime,tms_stime,tms_cutime以及tms_ustime设置为0。
  • 父进程设置的锁,子进程不继承。
  • 子进程的未决告警被清除。
  • 子进程的未决信号集设置被清除。

三、vfork函数

  vfork函数的调用序列和返回值与fork相同,但两个函数的语义不同。
  vfork用于创建一个新进程,而该进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),所以就不会用到此地址空间。
  vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec/exit之后父进程才可能被调度运行。(如果在调用exec/exit之前子进程依赖于父进程的进一步动作,则会导致死锁。) 对于以下示例:

#include <sys/types.h>#include <stdio.h>#include <unistd.h>
int glob = 6;int main(void){ int var; pid_t pid; var = 88; printf("before vfork/n"); if ((pid = vfork()) < 0) { fprintf(stderr, "vfork error/n"); } else if (pid == 0) { glob++; var++; _exit(0); } printf("pid=%d,glob=%d,var=%d/n", getpid(), glob, var); return 0;}

  编译运行该程序:
 

  需要注意在上面的程序我们调用了_exit而不是exit。_exit并不执行IO缓存的刷新操作。如果是调用exit而不是_exit,则该程序的输出是:

  

(上图是APUE原书, 下图是我在centos上实验结果

  

 之所以结果不同是因为在linux中子进程关闭的是自己的, 虽然他们共享标准输入、标准输出、标准出错等 “打开的文件”, 子进程exit时,也不过是递减一个引用计数,不可能关闭父进程的,所以父进程还是有输出的。

)

  可见父进程的printf输出消失了。其原因是子进程调用了exit,它刷新开关闭了所有标准IO流,这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,所以所有受到影响的标准IO FILE对象都是在父进程中。当父进程调用prinf时,标准输出已经被关闭了,于是printf返回-1。

四、exit函数

 对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于e x i t和_ e x i t,这是依靠传递给它们的退出状态( exit status)参数来实现的。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态( termination status) 。在任意一种情况下,该终止进程的父进程都能用 w a i t或w a i t p i d函数(在下一节说明)取得其终止状态。

  一定是一个父进程生成一个子进程。上面又说明了子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,则将如何呢 ?其回答是对于其父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程 I D就更改为1 ( i n i t进程的I D )。这种处理方法保证了每个进程有一个父进程。

  另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子进程的终止状态呢?对此问题的回答是内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用 w a i t或waitpid 时,可以得到有关信息。这种信息至少包括进程I D、该进程的终止状态、以反该进程使用的 C P U时间总量。内核可以释放终止进程所使用的所有存储器,关闭其所有打开文件。在 U N I X术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程。

  最后一个要考虑的问题是:一个由i n i t进程领养的进程终止时会发生什么 ?它会不会变成一个僵死进程?对此问题的回答是“否” ,因为i n i t被编写成只要有一个子进程终止, i n i t就会调用一个w a i t函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。当提及“一个 i n i t的子进程”时,这指的是 i n i t直接产生的进程(例如,将在9 . 2节说明的g e t t y进程),或者是其父进程已终止,由init 领养的进程。

 

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台