[APUE]进程控制(上)
一、进程标识
进程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);
返回: 调用进程的进程ID
pid_t getppid(void);
返回: 调用进程的父进程ID
uid_t getuid(void);
返回: 调用进程的实际用户ID
uid_t geteuid(void);
返回: 调用进程的有效用户ID
gid_t getgid(void);
返回: 调用进程的实际组ID
gid_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 = ;
char buf[] = "a write to stdout\n"; int main(void)
{
int var;
pid_t pid; var = ;
if (write(STDOUT_FILENO, buf, sizeof(buf) - ) != sizeof(buf) - ) {
fprintf(stderr, "write error");
} printf("before fork \n"); if ((pid = fork()) < ) {
fprintf(stderr, "fork error");
} else if (pid == ) {
glob++;
var++;
} else {
sleep();
} printf("pid=%d,glob=%d,var=%d\n", gitpid(), glob, var); return ;
}
编译运行上面的进程:
可以看到当我们将输出重定向到temp.out文件后多出个before fork的输出。write函数是不带缓存的。因为在fork之前调用write,所以其数据写到标准输出一次。但是标准IO是带缓存的。如果标准输出连到终端设备,则它是行缓存,否则它是全缓存。当以交互方式运行该程序时,只得到printf输出的行一次,其原因是标准输出缓存由新行符刷新。当我们将printf("before fork \n");后的换行符去掉之后即printf("before fork");来验证这一点,修改之后输出结果是:
可以看到before fork打印了两次,这说明因为我们去掉了换行符所以标准输出流的行缓存不会被flush。
但是当将标注输出重新定向到一个文件时,却得到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函数
进程有三种正常终止法和两种异常终止法。
(1) 正常终止:
(a) 在main函数内执行return语句,这相当于调用exit。
(b) 调用exit函数。
(c) 调用_exit函数。
(2) 异常终止:
(a) 调用abort。它产生SIGABRT信号,所以是下一种异常终止的特例。
(b) 当进程接收到某个信号时。进程本身(例如调用abort函数)、其他进程和内核都能产生传送到某一进程的信号。例如:进程越出其地址空间访问存储单元,或者除以0,内核都会为该进程产生相应的信号。
对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于e x i t和_ e x i t,这是依靠传递给它们的退出状态( exit status)参数来实现的。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态( termination status) 。在任意一种情况下,该终止进程的父进程都能用 w a i t或w a i t p i d函数(在下一节说明)取得其终止状态。(退出状态是传给exit/_exit的参数,或main返回值。在最后调用_exit时内核将其退出状态转为终止状态,如果子进程正常终止那父进程可以获取子进程的退出状态)。
一定是一个父进程生成一个子进程。上面又说明了子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,则将如何呢 ?其回答是对于其父进程已经终止的所有进程,它们的父进程都改变为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 领养的进程。
[APUE]进程控制(上)的更多相关文章
- [APUE]进程控制(中)
一.wait和waitpid函数 当一个进程正常或异常终止时会向父进程发送SIGCHLD信号.对于这种信号系统默认会忽略.调用wait/waidpid的进程可能会: 阻塞(如果其子进程都还在运行); ...
- [APUE]进程关系(上)
一.终端登录 1. 4.3+BSD终端登录 系统管理员创建一个通常名为/etc/ttys的文件,其中,每个终端设备有一行,每一行说明设备名和传到getty程序的参数,这些参数说明了终端的波特率.当系统 ...
- [APUE] 进程控制
APUE 一书的第八章学习笔记. 进程标识 大家都知道使用 PID 来标识的. 系统中的一些特殊进程: PID = 0: 调度进程,也称为交换进程 (Swapper) PID = 1: init 进程 ...
- [APUE]进程控制(下)
一.更改用户ID和组ID 可以用setuid设置实际用户ID和有效用户ID.可以用setgid函数设置实际组ID和有效组ID. #include <sys/types.h> #includ ...
- (六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制
. . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...
- APUE(8)---进程控制(1)
一.进程标识 每个进程都有一个非负整型标识的唯一进程ID.因为进程ID标识符总是唯一的,常将其用做其他标识符的一部分以保证其唯一性.进程ID虽然是唯一的, 但是却是可以复用的.ID为0的进程通常是调度 ...
- 进程控制(Note for apue and csapp)
1. Introduction We now turn to the process control provided by the UNIX System. This includes the cr ...
- 《UNIX环境高级编程》(APUE) 笔记第八章 - 进程控制
8 - 进程控制 Github 地址 1. 进程标识 每个进程都有一个非负整型表示的 唯一进程 ID .进程 ID 是可复用的(延迟复用算法). ID 为 \(0\) 的进程通常是调度进程,常常被称为 ...
- apue学习笔记(第八章 进程控制)
本章介绍UNIX系统的进程控制,包括创建新进程.执行程序和进程终止. 进程标识 每一个进程都有一个非负整数表示的唯一进程ID,除了进程ID,每个进程还有一些其他标识符.下列函数返回这些标识符 #inc ...
随机推荐
- Virtual Box配置CentOS7网络(图文教程)
之前很多次安装CentOS7虚拟机,每次配置网络在网上找教程,今天总结一下,全图文配置,方便以后查看. Virtual Box可选的网络接入方式包括: NAT 网络地址转换模式(NAT,Network ...
- UWP中新加的数据绑定方式x:Bind分析总结
UWP中新加的数据绑定方式x:Bind分析总结 0x00 UWP中的x:Bind 由之前有过WPF开发经验,所以在学习UWP的时候直接省略了XAML.数据绑定等几个看着十分眼熟的主题.学习过程中倒是也 ...
- 传播正能量——做一个快乐的程序员
引子 今天在博客园看到施瓦小辛格的文章我们搞开发的为什么会感觉到累,顿时有感而发.自己本来不擅长写文章,更不擅长写这种非技术性的文章,但是在思绪喷薄之际,还是止不住有很多话要说.针对从客观上说&quo ...
- 前端学Markdown
前面的话 我个人理解,Markdown就是一个富文本编辑器语言,类似于sass对于css的功能,Markdown也可以叫做HTML预处理器,只不过它是一门轻量级的标记语言,可以更简单的实现HTML ...
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(64)-WebApi与Unity注入
系列目录 前言: 有时候我们系统需要开放数据给手机App端或其他移动设备,不得不说Asp.net WebApi是目前首选 本节记录Asp.net MVC WebApi怎么利用Unity注入.系列开头已 ...
- H5坦克大战之【玩家控制坦克移动】
自从威少砍下45+11+11的大号三双之后,网上出现了各种各样的神级段子,有一条是这样的: 威少:Hey,哥们,最近过得咋样! 浓眉:对方开启了好友验证,请先添加对方为好友 威少:...... JRS ...
- Spring中Bean的实例化
Spring中Bean的实例化 在介绍Bean的三种实例化的方式之前,我们首先需要介绍一下什么是Bean,以及Bean的配置方式. 如果 ...
- 【Java每日一题】20170103
20161230问题解析请点击今日问题下方的"[Java每日一题]20170103"查看(问题解析在公众号首发,公众号ID:weknow619) package Jan2017; ...
- join Linq
List<Publisher> Publishers = new List<Publisher>(); Publisher publish1 = new Publisher() ...
- 跟着老男孩教育学Python开发【第二篇】:Python基本数据类型
运算符 设定:a=10,b=20 . 算数运算 2.比较运算 3.赋值运算 4.逻辑运算 5.成员运算 基本数据类型 1.数字 int(整型) 在32位机器上,整数的位数为32位,取值范围为-2**3 ...