《UNIX环境高级编程——APUE》
概念:消息的通知机制
解释:涉及到IO通知机制;
同步,就是发起调用后,被调用者处理消息,必须等处理完才直接返回结果,没处理完之前是不返回的,调用者主动等待结果;
eg. 我去银行办理业务, 选择排队等,排到头了就办理。
异步,就是发起调用后,被调用者直接返回,但是并没有返回结果,等处理完消息后,通过状态、通知或者回调函数来通知调用者,调用者被动接收结果。
eg. 我去银行办理业务*,* 取一个小纸条上面有我的号码*,* 等到排到我这一号时由柜台的人通知我轮到我去办理业务。
概念:程序等待调用结果时的状态
解释:涉及到CPU线程调度;
阻塞:就是调用结果返回之前,该执行线程会被挂起,不释放 CPU 执行权,线程不能做其它事情,只能等待,只有等到调用结果返回了,才能接着往下执行;
eg. 上面的那个例子, 不论是排队还是使用号码等待通知,如果在这个等待的过程中,等待者除了等待消息之外不能做其它的事情, 那么该机制就是阻塞的。
非阻塞:就是在没有获取调用结果时,不是一直等待,线程可以往下执行,如果是同步的,通过轮询的方式检查有没有调用结果返回,如果是异步的,会通知回调。
eg. 在银行办理这些业务的时候一边打打电话发发短信一边等待,这样的状态就是非阻塞的。
用 fork 创建新进程;
用 exec 可以初始执行新的程序;
exit 函数 和 wait 函数处理终止和等待终止。
exit(0) 正常退出
exit(1) 非正常退出
进程组与会话:
进程组是一组相关进程的集合,会话是一組相关进程组的集合。进程都有父进程, 父进程也有父进程, 这就形成了一个以init进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组、会话。进程组和会话在进程之前形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。
这样说来,一个进程会有如下ID:
PID:进程的唯一标识。对于多线程的进程而言所有线程调用getpid()函数会返回相同值。
PGID:进程组ID。每个进程都会有进程组ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组ID。
SID:会话ID。每个进程也都有会话ID。默认情况下,新创建的进程会继承父进程的会话ID
会话:
由于Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话。
一个会话包含多个进程组,但是只能有一个前台进程组。每个会话都有一个会话首领 (leader),即创建会话的进程 sys_setsid() / setsid() 调用能创建一个会话。但必须注意的是,只有当前进程不是进程组组长时,才能创建一个新的会话。调用 setsid 之后,该进程成为会话的 leader 。
一个会话只能有一个控制终端。这通常是登录到其上的终端设备(在终端登录情况下)或者伪终端设备在网络登录情况下。建立与控制终端连接的会话被称为控制进程一个会话中的几个进程组可以分为前台进程组与后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组与任意多个后台进程组。
前台进程 与 后台进程:
用户在 shell 中可以同时执行多个命令。对于耗时很久的命令(如编译大型工程),用户不必傻傻等待命令运行完毕才执行下一个命令。用户在执行命令时,可以在命令的结尾添加 “&” 符号,表示将命令放入后台执行。这样该命令对应的进程组即为后台进程组。
在任意时刻,可能同时存在多个后台进程组,但是不管什么时候都只能有一个前台进程组。只有在前台进程组中进程才能在控制终端读取输入。当用户在终端输入信号生成终端字符(如ctrl+c、ctrl+z、ctr+\等)时,对应的信号只会发送给前台进程组。
shell 中可以存在多个进程组,无论是前台进程组还是后台进程组,它们或多或少存在一定的联系,为了更好地控制这些进程组(或者称为作业),系统引入了会话的概念。会话的意义在于将很多的工作囊括在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他的工作则放在后台执行。
终端控制:
会话的领头进程打开一个终端后,该终端就会成为该会话的控制终端(SVR4/linux),与控制终端建立连接的会话领头进程成为控制进程(session leader)。一个会话只能有一个控制终端,产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程,终端上的连接断开时(比如网络断开或Modem断开),挂起信号将发送到控制进程(session leader)。
综上:
进程属于一个进程组,进程组属于一个会话,会话可能有也可能没有控制终端。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。
进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组 标识符。类似的,每个会话也有对应一个领头进程。同一会话中的进程通过该会话的领头进程和一个终端相连,该终端作为这个会话的控制终端。
一个会话只有一个控制终端,而一个控制终端只能控制一个会话。用户通过控制终端,可以向控制终端所控制的会话中的进程发送键盘信号。
同一个会话中只能有一个前台进程组,属于前台进程组的进程可以从控制终端获得输入,而其他进程均是后台进程,可能分属于不同的后台进程组。
当我们打开多个终端窗口时,实际上就创建了多个终端会话。每个会话都有自己的前台工作和后台工作
在计算机上启动Linux时,内核装入并启动init程序。然后init程序装载硬盘和启动终端程序。登录终端程序时,它启动命令行界面Shell。在计算机上启动Linux之后,init程序监视任何关闭计算机的信号,如不间断电源(UPS)发生的电源故障信号和重新启动命令。init是Linux系统操作中不可缺少的程序之一。所谓的init进程,它是一个由内核启动的用户级进程。内核自行启动(已经被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,就通过启动一个用户级程序init的方式,完成引导进程。所以,init始终是第一个进程(其进程编号始终为1)。
内核会在过去曾使用过init的几个地方查找它,它的正确位置(对Linux系统来说)是/sbin/init。如果内核找不到init,它就会试着运行/bin/sh,如果运行失败,系统的启动也会失败。
System V和BSD
Unix操作系统在操作风格上主要分为System V和BSD(目前一般采用BSD的第4个版本SVR4),前者的代表的操作系统有Solaris操作系统,在Solaris1.X之前,Solaris采用的是BSD风格,2.x之后才投奔System V阵营。后者的代表的操作系统有FreeBSD。
System V它最初由AT&T开发,曾经也被称为AT&T System V,是Unix操作系统众多版本中的一支。在1983年第一次发布,一共发行了4个System V的主要版本,System V Release4,或者称为SVR4,是最成功的版本,该版本有些风格成为一些UNIX共同特性的源头,如下表格的初始化脚本/etc/init.d。用来控制系统的启动和关闭。
BSD(Berkeley Software Distribution,伯克利软件套件)是Unix的衍生系统,1970年代由伯克利加州大学(Uni Versity of California, Berkeley)开创。BSD用来代表由此派生出的各种套件集合。
Poxis和System V
System V的概念如上所述。Posix是Portable Operating System Interface(可移植性操作系统接口)的简称,是一个电气与电子工程学会即IEEE开发的一系列标准,目的是为运行在不同操作系统的应用程序提供统一的接口,实现者是不同的操作系统内核。
将这两个名词放在一起讨论的一般是在Linux的进程间通信中,如在信号量编程中,有Posix信号量和System V信号量。它们都可以用于进程或者线程间的同步。然而, Posix信号量是基于内存的,即信号量值是放在共享内存中的,它使与文件系统中的路径名对应的名字来标识。当我们谈论“Posix 信号量”时,所指的是单个计数信号量。在Linux操作系统中,Posix信号量(共享内存、消息队列)可以通过ipcs命令查看。Posix信号量多用于进程间通信。
System v信号量测试基于内核的,它放在内核里面,要使用System V信号量需要进入内核态,所以在多线程编程中一般不建议使用System V信号量,因为线程相对于进程是轻量级的,从操作系统的调度开销角度看,如果使用System V信号量会使得每次调用都要进入内核态,丧失了线程的轻量优势。当我们讨论“System v信号量”时,所指的是计数信号量集。
• UNIX 诞生于 20 世纪 60 年代末,
Windows 诞生于 20 世纪 80 年代中期,
Linux 诞生于 20 世纪 90 年代初
(后来的 Windows 和 Linux 都参考了 UNIX)
• UNIX大多与硬件配套(UNIX操作系统),Linux 可运行在多种硬件平台上。
• UNIX收费(贝尔实验室),Linux 免费开源
Linux系统主要由以下4部分构成:
Linux内核
内核主要负责以下四种功能:
系统内存管理
内核不断地在交换空间(swap space)和实际物理内存之间交换虚拟内存中的内容。
2.软件程序管理(进程管理)
3.硬件设备管理
通过驱动程序实现硬件设备与应用程序之间的通信。在Linux系统中加入驱动程序代码的方式有以下两种: 1.编译进内核的设备驱动代码
2.可插入内核的设备驱动代码
4.文件系统管理
ext,ext2,ext3,ext4,minix,nfs,ntfs,XFS等。
GNU工具
GNU(GNU is not Unix的缩写),是一套为Unix系统管理员设计的一套类似于Unix的环境。
Linux 系统和 GNU 工具的结合体称为 Linux系统,也叫做 GNU/Linux系统。 核心 GNU 工具(coreutils)包括以下三部分:
1.用以处理文件的工具
2.用于处理文本的工具
3.用于管理进程的工具
还包括 shell,例如 bash shell。
图形化桌面环境
X Window软件包:直接和 PC 上的显卡和显示器打交道的底层程序,可以产生图形化显示环境。 其中最流行的软件包时 x.org。 桌面环境:KDE、GNOME、Unity(Ubuntu特有)等
应用程序
进程
进程:程序的执行实例被称为进程(process)。
进程ID: UNIX系统确保每个进程都有一个唯一的数字标识符(是一个非负整数)。
进程控制:有 3 个用于进程控制的主要函数: folk 、exec 和 waitpid。
线程
线程:某一时刻执行的一组机器指令。
线程ID:线程也有自己的ID标识符,只在它所属的进程内起作用。一个线程中的线程ID在另一个进程中没有意义。
信号
信号(signal): 用于通知进程发生了某种情况。
没讲啥
文件 I/O函数主要包括:打开文件、读文件、写文件 操作。
用到的函数有:open、 read、 write、 lseed、 close。
1) UNIX系统shell把
文件描述符 0 与进程的标准输入关联;
文件描述符 1 与标准输出关联;
文件描述符 2 与标准错误关联;
一个套接字端点表示为一个文件描述符(16.5)。
在
O_RDONLY 只读打开, 一般定义为 0
O_WRONLY 只写打开, 一般定义为 1
O_RDWR 读、写打开, 一般定义为2
#include
int creat(const char *path, mode_t mode);
//返回值:成功,返回为只写打开的文件描述符。出错,返回-1
//path 创建的文件名, mode 权限位
//eg.
#include
#include
#include
#include
#define PERM 0755
int main(void)
{
static char filename[] = “file.txt”; //会创建一个名为 file的txt文件
int fd;
fd = creat(filename,PERM);
if(fd < 0)
printf("[%s] create fail !!!\n",filename);
else
printf("[%s] open success !\n",filename); //输出 open success!
exit(0);
}
4.1 Linux 系统中采用三位十进制数表示权限,如0755, 0644.
ABCD A- 0, 表示十进制 B-用户 C-组用户 D-其他用户
— -> 0 (no excute , no write ,no read) --x -> 1 excute, (no write, no read) -w- -> 2 write -wx -> 3 write, excute r-- -> 4 read r-x -> 5 read, excute rw- -> 6 read, write , rwx -> 7 read, write , excute
0755-> 即用户具有读/写/执行权限,组用户和 其它用户具有读写权限; 0644->即用户具有读写权限, 组用户和 其它用户具有只读权限;
一般赋予目录0755权限,文件0644权限。
头文件:
#include
//功能:关闭一个已经打开的文件
原型
int close(int fd)
//参数说明: fd:是需要关闭的文件描述符
返回值
//成功:返回0;
//失败:返回-1,并设置errno
可以调用 lseek 显示地为一个打开文件设置偏移量。
从打开文件中读数据。
像打开文件写数据
1.函数 pread 和 pwrite
这两种扩展允许原子性地定位并执行 I/O
这两个函数可用来复制一个现有的文件描述符。
文件描述符:
Linux 中一切皆文件。Linux 会给每一个文件分配一个编号(ID),这个编号就是一个整数,也就是文件描述符。
延迟写(delay write):我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。
这三个函数是为了保证磁盘上实际文件系统与缓冲区中内容的一致性。
它可以改变已经打开文件的属性
ioct1 函数一直是 I/O 操作的杂物箱。终端I/O是使用 ioct1最多的地方。
其目录项是名为0、1、2等的文件。
打开文件 /dev/fd/n 等效于复制描述符 n(假定描述符 n 是打开的)。
功能:返回与此命名文件有关的信息结构。
• 普通文件(regular file)
• 目录文件(directory file)
• 块特殊文件(block special file)
• 字符特殊文件(character special file)
• FIFO
• 套接字(socket)
• 符号链接(symbolic link)
每个文件有 9 个访问权限位
S_IROTH 其他读
S_IWOTH 其他写
S_IXOTH 其他执行
这两个函数是按实际用户 ID 和实际组 ID 进行访问权限测试的。
#include
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
函数 truncate 和ftruncate 可以将一个现有文件长度截断为 length 长
任何一个文件可以有多个目录指向其 i 节点。
函数link、linkat 可以创建一个指向现有文件的链接。
#include
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
函数 unlink 可以删除一个现有的目录项。
#include
int unlink(const char *pathname);
int unlink(int fd, const char *pathname, int flag);
函数 unlinkat 可以类似于 rmdir 一样删除目录
函数 remove 可以解除对一个文件或目录的链接。
文件或目录可以用 rename 函数 或者renameat函数进行重命名。
可以用 symlink 或 symlinkat函数创建一个符号链接
futimens 和 utimensat函数可以指定纳秒级精度的时间戳。
用 mkdir 和 mkdirat 函数创建目录
用 rmdir 函数删除目录。
对某个目录具有访问的任一用户都可以读该目录,但是为了防止文件系统产生混乱,只有内核才能写目录。
进程调用 chdir 和 fchdir 函数可以更改当前工作目录。
• 对于所有的 I/O函数,都是围绕文件描述符的;
对于标准的 I/O库,它们的操作是围绕 流(stream)进行的。
• 流的定向(stream’s orientation)决定了所读、写的字符是单字节还是多字节。
有两个函数可以改变流的定向: freopen 、 fwide;
=> freopen函数可以清除一个流的定向;
fwide 函数可用于设置流的定向。
分别是: STDIN_FILENO 、STDOUT_FILENO、STDERR_FILENO
标准 I/O库提供缓冲的目的是尽可能减少使用 read 和write调用的次数。标准I/O库提供了三种类型的缓冲:
• 全缓冲:在填满标准I/O缓冲区后才进行实际I/O操作。
• 行缓冲:当再输入和输出中遇到换行符时,标准I/O库执行I/O操作。
• 不带缓冲:标准I/O库不对字符进行缓冲存储。
与以下三个函数有关: fopen、 freopen、 fdopen。
• fopen 函数打开路径名为 pathname 的一个指定的文件;
• fropen 函数在一个指定的流上打开一个指定的文件,若该流已经打开,则先关闭该流;
• fdopen 函数取一个已有的文件描述符,并使一个标准的I/O流与该描述符相结合。
6、读和写流
一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对其进行读、写操作。
• 每次一个字符的I/O;
• 每次一行的I/O;
• 直接I/O。
7、每行一次 I/O
有两个函数指定了缓冲区的地址,读入的行将送入其中:fgets、gets函数。
• fgets 必须指定缓冲的长度 n
• gets 函数是一个不推荐的函数。
问题:调用子在使用gets时不能指定缓冲区的长度。
8、标准 I/O 的效率
9、二进制 I/O
Linux 提供了两个函数以执行二进制I/O操作: fread 函数 和 fwrite函数。
函数功能是 返回读或写的对象数。
问题:它只能用于读在同一系统上已写的数据。
10、定位流
有三种方法定位 I/O流:
• ftell 和 fseek 函数。
• ftello 和 fseeko 函数
• fgetpos 和 fsetpos 函数。
11、格式化 I/O
• 格式化输出:
由5个printf函数来处理:
o printf:将格式化数据写到标准输出
o fprintf:写至指定的流;
o dprintf:写至指定的文件描述符;
o sprintf:将格式化的字符送入数组 buf 中。
o snprintf:能够解决缓冲区溢出问题。
• 格式化输入:
由3个 scanf函数来处理:
o scanf 族用于分析输入字符串,并将字符序列转换成指定类型的变量。
12、实现细节
13、临时文件
ISO C标准I/O库提供了两个函数以帮助创建临时文件。
• tmpnam 函数产生一个与现有文件名不同的一个有效路径名字字符串。
• tmpfile 函数创建一个临时二进制文件(类型 wb+),在关闭该文件或程序结束时将自动删除这种文件。
14、内存流
我们已经看到,标准 I/O库把数据缓存在内存中,因此每次一字符和每次一行的 I/O更有效。有的I/O都是通过在缓冲区主存之间来回传送字节来完成的。
15、标准 I/O的替代软件
第 6 章- 系统数据文件和信息
2、口令文件
某些系统提供了 vipw 命令,允许管理员使用该命令编辑口令文件。vipw命令串行化地更改口令文件。
POSIX.1定义了两个获取口令文件项的函数。在给出用户登录名或数值用户ID后,这两个函数就能查看相关项目:getpwuid函数、getpwnam函数。
getpwuid: 由 ls(1)程序使用,它将i节点中的数字用户ID映射为用户登录名。在键入登录名后,getpwnam函数由login(1)程序使用。
3、阴影口令
为使别人难以获得原始资料(加密口令),现在,某些系统将加密口令存放在另一个通常称为阴影口令(shadow password)的文件中。该文件至少要包含用户名和加密口令。
4、组文件
5、附属组 ID
1983年左右,4.2 BSD( 伯克利软件套件 )引入了附属组ID的概念。这样不仅可以属于口令文件记录项中组ID所对应的组,也可以属于多至15个另外的组。
6、实现区别
7、其它数据文件
8、登陆账户记录
大多数时候UNIX系统都提供下列两个数据文件:
utmp文件记录当前登陆到系统的各个用户;
wtmp文件跟踪各个登陆和注销事件。
9、系统标识
POSIX.1(可移植操作系统接口)定义了uname 函数,它返回与主机和操作系统有关的信息。
10、时间和日期历程
第7章-进程环境
1、引言
本章将学习:
当程序执行时,其main函数是如何被调用的; 02
命令行参数是如何传递给新程序的; 04
典型的存储空间布局是什么样式; 06
如何分配另外的存储空间; 08
进程如何使用环境变量 09
进程的各种不同终止方式; 03
longjmp 和 setjmp 函数以及它们与栈的交互作用。 10
2、 main函数
main 函数的原型是:
int main( int argc, char *argv[] )
其中 argc 是命令行参数的数目; argv是指向参数的各个指针所构成的数组( 存放命令行每个字符串的首地址)。
在调用 mian 函数前先调用一个特殊的 启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由 连接编辑器设置的,而连接编辑器则是由 C 编译器调用。
3、进程终止
有8种方式使进程终止(termination):
5种正常终止:
• 从 main 返回;
• 调用 exit;
• 调用 _exit 或 _Exit;
• 最后一个线程从其启动历程返回 (11.5节)
• 从最后一个线程调用 pthread_exit; (11.5节)
3种异常终止:
• 调用 abort (10.17节)
• 接到一个信号 (10.2节)
• 最后一个线程对取消请求作出响应(11.5节、12.7节)
3.1- 退出函数
有 3 个函数可用于正常终止一个程序:
• _exit 和 _Exie 会立即进入内核;
• exit 则先执行一些清理处理,然后返回内核。
这3个退出函数都带一个整形参数,称为 终止状态。
若 main 的返回类型是整形, 并且 main 执行到最后一条语句时返回(隐式返回),那么该进程的终止状态是 0。
于是: main函数中的 return 0; 等价于 exit(0);
3.2- 函数 atexit
按照国际标准化组织的规定,一个进程可以登记多至32个终止处理程序(exit handler)的函数,这些函数将由 exit 自动调用,系统同时调用 atexit 函数来登记这些函数。
4、命令行参数
当执行一个程序时,调用 exec 的进程可将命令行参数传递给该新程序。
5、环境表
每个程序都接收到一张环境表。
与参数表一样,环境表也是一个字符指针,其中每个指针包含一个以 null 结束的 C 字符串的地址。
全局变量 环境指针( environ) 则包含了该指针数组的地址:
extern char **environ;
6、 C程序的存储空间布局
历史沿袭至今,C程序一直由下列几部分组成:
• 正文段;
• 初始化数据段;
• 未初始化数据段;
• 栈;
• 堆。
7、共享库
共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例进程的一个副本。
8、存储空间分配
国际标准组织 说明了 3 个用于存储空间动态分配的函数:
• malloc : 分配指定字节数的存储区;
• calloc : 为指定数量指定长度的对象分配存储空间;
• realloc : 增加或减少以前分配区的长度。
9、环境变量
环境字符串的形式是:
name = value
国际标准化组织 C 定义了一个函数 getenv, 可以用其取环境变量值,但是该标准又称环境的内容是由实现定义的。
10、函数 setjmp 和 longjmp
在 C 中,goto 语句不能跨越函数,而 setjmp 和 longjmp 函数可以。
这两个函数对于处理发生在很深层嵌套函数调用中的出错情况是非常有用的。
11、函数 getrlimit 和setrlimit
函数 getrlimit 和 setrlimit 可以进行查询和更改资源限制。
对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针。
12、小结
本章说明了:
• 一个进程是如何启动和终止的;
• 如何向其传递参数表和环境;
• C 程序的典型存储空间布局;
• 一个进程如何动态地分配和释放存储空间;
• setjmp 和 longjmp 函数,他们提供了一种在进程内非局部转移的方法。
第 8 章-进程控制
1、引言
本章介绍 UNIX 系统的进程控制,包括:创建新进程、执行程序和进程终止。
还将说明进程属性的各种 ID ——实际、有效和保存的用户 ID 和组ID ,以及它们如何受到进程控制原语的影响。
2、进程标识
每个进程都有一个非负整数表示的唯一进程 ID 。P.S. 虽然进程 ID 是唯一的,但是当一个进程终止后,其进程 ID 就称为复用的候选者。
系统中有一些专用进程 ID:
• ID 为 0 的进程通常是调度进程,也被称为 交换进程(swapper) ,它并不执行任何磁盘上的程序,因此也被称为系统进程。
• ID 为 1 通常是 init 进程,次进程负责在自举内核后启动一个 UNIX 系统。
• ID 为 2 是页守护进程( page daemon),此进程负责支持虚拟存储器系统的分页操作。
3、函数 fork
fork() 系统调用通过复制一个现有进程来创建一个全新的进程(进程的另外一个名字叫做任务)。
由 fork 创建的新进程被称为 子进程(child process)
fork 函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0 ,而父进程的返回值则是新建子进程的进程 ID。
子进程是父进程的副本。eg. 子进程获得父进程数据空间、堆和栈的副本,父进程和子进程并不共享这些存储空间。
4、函数 vfork
vfork 函数的调用序列和返回值与 fork 相同,但 两者 的语义不同。
vfork 函数用于创建一个新进程,而该新进程的目的是执行一个新程序。但是 vfork 和 fork还是有 2 点不同的(书 p .187)
5、函数 exit
详细介绍了 5 中正常终止及 3 种异常终止的方式。
终止函数: exit 、 _exit、 _Exit,它们终止后,会给父进程传递退出状态( exit status )的参数。
僵死进程: 一个已经终止、但是其父进程尚未对其进行善后处理 (获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程。
6、 函数 wait 和 waitpid
当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。
一个进程有多个子进程,只要有一个子进程终止, wait 就返回。
大家知道,当用fork启动一个新的子进程的时候,子进程就有了新的生命周期,并将在其自己的地址空间内独立运行。但有的时候,我们希望知道某一个自己创建的子进程何时结束,从而方便父进程做一些处理动作。同样的,在用 ptrace 去 attach 一个进程滞后,那个被 attach 的进程某种意义上说可以算作那个 attach 它进程的子进程,这种情况下,有时候就想知道被调试的进程何时停止运行。
以上两种情况下,都可以使用 Linux 中的 waitpid() 函数做到。先来看看waitpid函数的定义:
如果在调用waitpid()函数时,当指定等待的子进程已经停止运行或结束了,则waitpid()会立即返回;但是如果子进程还没有停止运行或结束,则调用waitpid()函数的父进程则会被阻塞,暂停运行。
调用 wait 或 waitpid 的进程可能会放生什么?
• 如果其所有子进程都还在运行,则阻塞;
• 如果一个子进程已终止,正等待父进程获得其终止状态,则取得该子进程的终止状态立即返回;
• 如果它没有任何子进程,则立即出错返回。
• waitpod 函数提供了 wait 函数没有提供的 3 个功能:
7、函数 waitid
UNIX 包含了另一个取得进程终止状态的函数—— waitid , 此函数类似于 waitpid,但提供了更多的灵活性。
waitid 函数允许一个进程指定要等待的子进程。
8、函数 wait3 和 wait4
大多数 UNIX 系统实现提供了另外两个函数 wait3 和 wait4。
相较于函数 wait 、 waitpid 、waitid 所提供的功能要多一个,这与附加参数有关。该参数允许内核返回由终止进程及其所有子进程使用的资源概况,包括: CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。
9、竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争关系( race condition )。
10、函数 exec
在 “3-函数 fork” 中提到:用 fork 函数创建新的子进程后,子进程往往要调用一种 exec 函数以执行另一个程序。
当进程 调用一种 exec 函数时,该进程执行的程序完全替换为 新程序,而新程序是从其 main 函数开始执行。
因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变。 exec 只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
11、更改用户 ID 和 更改组 ID
在 UNIX 系统中,特权(如能改变当前日期的表示法)以及访问控制(如能否读、写一个特定文件),是基于用户 ID 和 组 ID 的。
当程序需要增加特权,或需要访问当前不允许访问的资源时,我们需要更换自己的用户 ID 或 组 ID,使得新 ID 具有合适的特权或访问权限。当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户 ID 或组 ID,新 ID 不具有相应特权或访问这些资源的能力。
12、解释器文件
解释器文件是一种文本文件。
13、函数 system
ISO C定义了 system函数,它总共有 3 种返回值。
相较于 fork 、 exec, system的优点为: system进行了所需的各种出错处理以及各种信号处理。
14、 进程会计
大多数 UNIX 系统提供了一个选项以进行 进程会计( process accounting )处理。
启用该选项后,每当进程结束时内核就写一个会计记录。一般进程会计包括: 命令名、所使用的 CPU 时间总量、用户 ID 和 组 ID 、启动时间等。
15、用户标识
任一进程都可以得到其实际用户 ID 和有效用户 ID 及组 ID。
但是,有时候我们希望找到运行该程序用户的登陆名。系统通常记录用户登陆时使用的名字,然后用 getlogin 函数可以获取此登录名。
如果调用此函数的进程没有连接到用户登陆时所用的终端,则函数失败。通常称这些进程为 守护进程( daemon )
16、 进程调度
调度策略和调度优先级是有内核确定的。
进程可以通过调整 友好值 选择以更优先级运行(通过调整友好值降低它对 CPU 的占有,因此该进程是有好的)。只有特权进程允许提高调度权限。
友好值越小,优先级越高(越靠前嘛!)。
进程可以通过 nice 函数获取或更改它的友好值。使用这个函数,进程只能影响自己的友好值,不能影响任何其它进程的友好值。
17、进程时间
在 书 p16中说明了可以度量的 3 个时间:墙上时钟时间、用户 CPU时间 和 系统 CPU时间。
任一进程都可以调用 times 函数获得它自己以及终止子进程的上述值。
18、小结
UNIX 必须掌握的几个函数: fork、exec系列、 _exit、wait、waitpid。
• fork:用来创建新的进程;
• exec:用 fork 函数创建新的子进程后,子进程往往要调用一种 exec 函数才能执行另一个程序;
• _exit:给父进程传递退出状态( exit status )的参数;
• wait、waitpid:我们希望知道某一个自己创建的子进程何时结束;
• 本章说明了 system函数和进程会计,这也使我们进一步了解所有这些进程控制函数。
system进行了所需的各种出错处理以及各种信号处理
• 本章还说明了 exec函数的另一种变体:解释器文件及它们的工作方式。
• 对各种不同的用户 ID 和组 ID(实际、有效 和 保存的)的理解,对编写安全的设置用户 ID 程序是至关重要的。
在 UNIX 系统中,特权(如能改变当前日期的表示法)以及访问控制(如能否读、写一个特定文件),是基于用户 ID 和 组 ID 的。
当程序需要增加特权,或需要访问当前不允许访问的资源时,我们需要更换自己的用户 ID 或 组 ID,使得新 ID 具有合适的特权或访问权限。当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户 ID 或组 ID,新 ID 不具有相应特权或访问这些资源的能力。
第 9 章-进程关系
1、引言
本章将详细地说明进程组 以及 POSIX.1(可一只操作系统接口)引入的会话的概念。还将介绍登陆 shell(登录时所调用的)和所有从登陆 shell 启动的进程之间的关系。
2、终端登陆
系统经由内核中的终端设备驱动程序。
4 种终端登陆方式:
• BSD 终端登陆 ( Berkeley Software Distribution 伯克利软件套件)
• Mac OS X 终端登陆
• Linux 终端登陆
• Solaris 终端登录
3、网络登陆
通过串行终端登录至系统和经由网络登陆至系统两者之间主要(物理上的)区别是:网络登录时,在终端和计算机之间的连接不再是点到点的。在网络登陆情况下, login仅仅是一种可用的服务,这与其它网络服务(如 FTP 或 SMTP)的性质相同。
在 9.2 的终端登录中, init 知道哪些终端设备可用来进行登陆,并为每个设备生成一个 getty 进程。但是,对网络登陆情况则有所不同,所有登陆都经由内核的网络接口驱动程序(如 以太网驱动程序 )。
为使同一个软件既能处理终端设备,又能处理网络登陆,系统使用了一种称为 伪终端(pseudo terminal )的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作。
有以下四种登陆方式:
• BSD 网络登陆
• Mac OS X 网络登陆
• Linux 网络登录
• Solaris 网络登陆
4、进程组
每个进程除了有一个进程 ID 之外,还属于一个进程组。进程组是一个或多个进程的集合。
每个进程组有一个唯一的进程组 ID,进程组 ID 类似于进程 ID ——它是一个正整数,并可存放在 pid_t 数据类型中。
进程调用 setpgid 函数 可以加入一个现有的进程组或者创建一个新进程组。
#include
int setpgid( pid_t pid, pid_t pgid);
//返回值:若成功,返回0; 若出错,返回 -1
setpgid 函数 将 pid 进程的进程组 ID 设置为 pgid。
一个进程只能为它自己或它的子进程设置进程组 ID。 在它的子进程调用了 exec 后,它就不再改该子进程的进程组 ID。
5、会话
会话( session ) 是一个或多个进程组的集合。
通常是由 shell 的管道将几个进程编成一组的。
进程调用 setsid 函数建立一个新会话。
#include
pid_t setsid(void);
//返回值:若成功,返回进程组 ID; 若出错,返回 -1
• 如果该调用进程已经是一个进程组的组长,则此函数返回出错(有具体解决方法);
• 如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。(具体会发生 3 件事)
6、控制终端
会话 和 进程组还有一些其它特性:
• 一个会话可以有一个控制终端( controlling terminal )。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登陆情况下)。
• 建立与控制终端连接的会话首进程被称为 控制进程( controlling process )
• 一个会话的几个进程组可被分成一个 前台进程组( foreground process group )以及一个或多个 后台进程组( background process group )。
• 如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组称为后台进程组。
7、函数 tcgetpgrp、 tcsetpgrp 和 tcgetsid
有时候需要有一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能知道将终端输入和终端产生的信号发往何处。
函数 tcgetpgrp 返回前台进程组 ID,它与在 fd 上打开的终端相关联。
如果进程有一个控制终端,则该进程可以调用 tcsetpgrp 将前台进程组 ID 设置为 pgrpid。
大多数应用程序并不直接调用这两个函数。他们通常由 作业控制 shell调用。
需要管理控制终端的应用程序可以调用 tcgetsid 函数识别出控制终端的会话首选进程的 会话ID (它等价于会话首进程的进程组 ID)
8、作业控制
作业控制是 BSD 在 1980年左右增加的一个新特性。它允许在一个终端是启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业再后台运行。
一个作业只是几个进程的集合,通常是一个进程管道。
作业控制要求一下 3 种形式的支持:
第 10 章- 信号
1、引言
本章先对信号机制进行综述,并说明每种信号的一般用法。
然后分析早期实现的问题。
在分析存在的问题之后再说明解决这些问题的方法,这种安排有助于加深对改进机制的理解。
2、信号概念
定义:
信号更多的是通知事件的发生。信号产生之后第一时间也不是直接处理而是先存储下来。
流程:
信号的产生—>信号的注册—>信号的阻塞(不处理)—>信号的注销—>信号的处理。
分类:
① 不可靠信号(非实时信号):1~31
② 可靠信号(实时信号):34~64
Linux下有62种信号,使用kill -l 命令查看
每个信号都有一个名字。这些名字都以 3 个字符 SIG 开头。
e.g. SIGABRT 是夭折信号,当进程调用 about 函数时产生这种信号;
SIGALRM 是闹钟信号,由 alarm 函数设置的定时器超时后将产生此信号;
不存在编号为 0 的信号,因为(10.9节)kill 函数对信号编号 0 有特殊的应用。
3、函数 signal
UNIX 系统信号机制最简单的接口是 signal 函数。
当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为捕捉该信号,称此函数为信号处理程序( signal handler )或信号捕捉函数( signal-catching function )。
signal 函数原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值( void )。
程序启动
当执行一个程序时,所有信号的状态都是系统默认或忽略。
进程创建
当一个进程调用 fork 时,其子进程继承父进程的信号处理方式。
4、不可靠的信号
不可靠在这里指的是:信号可能会丢失。
5、终端的系统调用
早期 UNIX 系统的一个特性是: 如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被终端不再继续执行。
该系统调回返回出错,其 error 设置为 EINTR。
6、可重入函数
7、 SIGCLD语义
8、可靠信号术语和语义
每个进程都有一个 信号屏蔽字( signal mask ),它规定了当前要阻塞递送到该进程的信号集。
对于每种可能的信号,该屏蔽字中都有一位与之对应,对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用 sigprocmask(10.2节)来检测和更改其当前信号屏蔽字。
9、函数 kill 和 raise
kill 函数将信号发送给进程或进程组; raise 函数则允许进程向自身发送信号。
如前所述,进程将信号发送给其它进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际用户 ID 或有效用户 ID 必须等于接受者的实际用户 ID 或有效用户 ID。
10、函数 alarm 和 pause
使用 alarm 函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生 SIGALRM 信号。
每个进程只能有一个闹钟时间。
pause 函数使调用者进程挂起直至捕捉到一个信号。
只有执行了一个信号处理程序并从其返回时, pause才返回。在这种情况下, pause 返回 -1,errno设置为 EINTR。
11、信号集
数据集 用来告诉内核不允许发生该信号集中的信号。
函数 sigemptyset 初始化由 set 指向的信号集,清除其中所有信号。 函数 sigfillset 初始化由 set 指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用 sigemptyset 或 sigfillset 一次。这是因为 C 编译器程序将 不赋初值的 外部变量和静态变量都初始化为 0。
12、 函数 sigprocmask
在(10.8)中提及一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。调用函数 sigprocmask 可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。
13、函数 sigpending
sigpending 函数返回一信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。 该信号集通过 set 参数返回。
14、函数 sigaction
sigaction 函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。此函数取代了 UNIX 早起版本使用的 signal 函数。
15、 函数 sigsetjmp 和 siglongjmp
16、函数 sigsuspend
更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的代码临界区。
17、 函数 abort
abort 函数的功能是使 程序异常终止。
该函数将 SIGABRT信号发送给调用进程(进程不应忽略此信号)。 ISO C 规定,调用 abort将向主机环境递送一个未成功终止的通知,其方法是调用 raise(SIGABRT) 函数。
18、函数 system
POSIX.1 要求 system 忽略 SIGINT 和 SIGQUIT ,阻塞 SIGCHLD。
system 的返回值
注意 system 的返回值,它是 shell 的终止状态,但 shell 的终止状态并不总是执行命令字符串进程的终止状态。
19、函数 sleep、nanosleep 和 clock_nanosleep
这本书中很多例子中都已经使用了 sleep 函数。
nanosleep 函数与 sleep函数类似,但是它提供了纳秒级的精度。这个函数挂起调用进程,知道要求的时间已经超时或者某个信号中断了该函数。
20、函数 sigqueue
除了对信号排队以外,这些扩展允许应用程序在递交信号时传递更多的信息。这些信息嵌入在 siginfo 结构中。除了系统提供的信息,应用程序还可以想信号处理程序传递整数或者指向包含更多信息的缓冲区指针。
sigqueue 函数只能吧信号发送给单个进程,可以使用 value 参数想信号处理程序传递整数和指针值,除此之外, sigqueue 函数与 kill 函数类似。
21、作业控制信号
POSIX.1 认为有以下 6 个与作业控制有关:
除了 SIGCHLD 以外,大多数应用程序并不处理这些信息,交互式 shell 则通常会处理这些信号的所有工作。
当我们通知 shell 在前台或后台回复运行一个作业时, shell 向该作业中的所有进程发送 SIGCONT 信号。
22、信号名和编号
可以使用 psignal 函数可移植地打印与信号编号对应的字符串。
如果在 sigaction 信号处理程序中有 siginfo 结构,可以使用 psiginfo 函数打印信号信息。
23、小结
本章 说明了早起信号实现的问题以及它们是如何显现出来的。
然后介绍了 POSIX.1 的可靠信号概念,以及所有相关的函数。
在此基础上提供了 about、system 和 sleep 函数的 POSIX.1实现。
最后以观察分析作业控制信号以及信号名和信号编号之间的转换结束。
第 11 章-线程
1、引言
本章了解如何使用多个控制线程( 简单说就是线程 )在单进程中执行多个任务。一个进程中的所有线程都可以访问该进程的组成部件,如文件描述符和内存。
最后本章将讨论目前可用的同步机制,防止多个线程在共享资源时出现不一致的问题。
有时,也可以将应用程序设计成使用多线程的(11章),从而避免使用非阻塞 I/O(14章)。
2、线程概念
典型的 UNIX 进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。有了多个控制线程以后,在程序设计时就可以把进程设计成在某一时刻能够不止做一件事,每个线程处理各自独立的任务。
每个线程都包含有表示执行环境所必须的信息,其中包括进程中标识线程的 线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、 errno变量(1.7节)以及线程私有数据(12.6节)。
一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。
3、进程标识
就像每个进程有一个进程 ID 一样,每个线程也有一个线程 ID。进程 ID 在整个系统中是唯一的,但 线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。
线程 ID 是用 pthread_t数据类型来表示的。 用结构表示 pthread_t 数据类型的后果是不能用一种可以只的方式打印该数据类型的值。在程序调试过程中打印线程 ID 有时是非常有用的。
线程可以通过调用 pthread_self函数获得自身的线程 ID。
pthread_self函数可以与 pthread_equal 函数一起使用。例如,主线程可能把工作任务放在一个队列中,用线程 ID 来控制每个工作线程处理哪些作业。
4、 线程创建
在传统 UNIX 进程模式中,每个进程只有一个控制线程。
新增的线程可以通过调用 pthread_create 函数创建(注,它不是系统默认的库,需要在编译的时候加上 -lpthread )。
线程创建时并不能保证哪个线程会先运行:是新创建的线程,还是调用线程。
5、进程终止
如果进程中的任意线程调用了 exit、 _Exit 或者 _exit,那么整个进程就会终止。与此相类似,如果默认的动作是终止进程,那么,发送到线程的信息就会终止整个进程。
单个线程可以通过 3 种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流:
•
•
• 线程调用 pthread_exit
进程中的其它线程也可以通过调用 函数 pthread_join 访问到其它线程指针。 pthread_join 函数可以等待指定的线程终止,但并不获取线程的终止状态。
当一个线程通过调用 函数pthread_exit 退出或者简单地从启动历程中返回时,进程中的其它线程可以通过调用 pthread_join 函数获得该线程的退出状态。
线程可以通过调用 pthread_cancel 函数来请求取消同一进程中的其它线程。【注】该函数仅仅是提出请求。
线程可以安排它退出时需要调用的函数,这与进程在退出时可以用 atexit函数 安排退出是类似的。这样的函数称为 线程清理处理程序( thread cleanup handler )。一个线程可以建立多个清理处理程序。
6、线程同步
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。
当一个线程可以修改的变量,其它线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。
在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。
为了解决这个问题,线程不得不使用 锁 ,同一时间只允许一个线程访问该变量。
两个或多个线程视图在同一时间修改同一变量时,也需要进行同步。考虑变量增量操作的情况,增量操作通常分解为以下 3 步:
1.
2. 。
3. 。
11.6.1- 互斥量
可以使用 pthread 的互斥忌口来保护数据,确保同一时间只有一个线程访问数据。
互斥量( mutex )从本质上说是一把锁,在访问共享资源前对互斥量进行 设置(加锁),在访问完成之后释放(解锁)互斥量。(通过休眠使进程阻塞)
互斥变量是用 pthread_mutex_t 数据类型表示的。在使用互斥变量以前,必须首先对它进行初始化。
11.6.2- 避免死锁
⼀般情况下,如果同⼀个线程先后两次调⽤lock,在第⼆次调⽤时,由于锁已经被占⽤,该线程会挂起等待别的线程释放锁,然⽽锁正是被⾃⼰占⽤着的,该线程又被挂起⽽没有机会释放锁,因此 就永远处于挂起等待状态了,这叫做死锁(Deadlock)。
另一种情况:线程A获 得了锁1,线程B获得了锁2,这时线程A调⽤lock试图获得锁2,结果是需要挂起等待线程B释放 锁2,⽽这时线程B也调⽤lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都 永远处于挂起状态了。
有时候,应用程序的结构使得对互斥量进行排序是很困难的。如果涉及了太多的锁和数据结构,可用的函数并不能把它转换成简单的层次,那么就需要采用另外到底方法。
在这汇总情况下,可以先释放占有的锁,然后过一点时间再试。这种情况可以使用 pthread_mutex_trylock接口 避免死锁。
11.6.3- 函数 pthread_mutex_timelock
当线程试图获取一个已加锁的互斥量时, pthread_mutex_timelock互斥量 原语允许绑定线程阻塞时间。 该接口和 pthread_mutex_trylock接口 是基本等价的,但是在达到超时时间值时, pthread_mutex_timelock 不会对互斥量进行加锁,而是返回错误码 ETIMEDOUT。
11.6.4- 读写锁
读写锁( reader-write lock )与互斥量类似,不过读写锁允许更高的并行性。
读写锁非常适合于对数据结构读的次数远大于写的情况。
读写锁也叫共享互斥锁( shared-exclusive lock )。当读写锁是读模式锁定时,就可以说城市共享模式锁住的。当它是写模式锁住的时候,就可以说成是以互斥锁模式锁定的。
读写锁可以有 3 种状态:
• 读模式下加锁状态;
• 写模式下加锁状态;
• 不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
11.6.5- 带有超时的读写锁
带有超时的读写锁加锁函数可以使应用程序在获取读写锁时避免陷入永久阻塞状态。
这两个函数分别是: pthread_rwlock_timedrdlock 和 pthread_rwlock_timedwelock。
11.6.6- 条件变量
条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会和的场所。
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待待定的条件发生。
在使用条件变量之前,必须先对它进行初始化。由 pthread_cond_t 数据类型表示的条件变量可以用两种方式进行初始化。 在释放条件变量底层的内存空间之前,可以使用 pthread_cond_destroy 函数对条件进行反初始化( deinitialize )。
11.6.7- 自旋锁
自旋锁与互斥量基本类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。
自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多成本。
只有一个属性是自旋锁所特有的:那就是 进程共享属性 支持线程进程共享同步。
当自旋锁用在非抢占式内核中时是非常有用的:除了提供互斥机制以外,它们会阻塞终端,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自旋锁( 把中断想成是另一种抢占 )。
11.6.8- 屏障
屏障( barrier )是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,知道所有的合作线程都到达某一点,然后从该点继续执行。
屏障对象的概念很广,它们允许任意数量的线程等待,知道所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。
可以使用 pthread_barrier_init 函数 对屏障进行初始化。
一旦达到屏障计数值,而且线程处于非阻塞状态,屏障就可以被重用。
11.7 小结
本章介绍了线程的概念,主要讨论了 5 个基本的同步机制:
第 12 章- 线程控制
1、引言
本章将介绍控制线程行为方面的内容:
• 线程属性;
• 同步原语属性;
• 同一进程中的多个线程之间如何保持数据的私有性;
• 基于进程系统调用如何与线程进行交互。
2、线程限制
线程限制的使用时为了增强应用程序在不同的操作系统实现之间的可移植性。
3、线程属性
ptherd接口允许我们通过设置每个对象关联的不同属性来细调线程和同步对象的行为。通常,管理这些属性的函数都遵循相同的模式:
第 13 章- 守护进程
1、引言
守护进程常常用作服务器进程。
守护进程( daemon )是生存期长的一种进程。 它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的。
2、守护进程的特征
大多数守护进程都以超级用户( root )特权运行。所有的守护进程都没有控制终端,其终端名设置为问号。大多数用户层守护进程都是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程。
3、编程规则
在编写守护进程程序时需要遵循一些基本规则,以防止产生不必要的交互作用。
• 首先要做的是调用 umask 将文件模式创建屏蔽字设置为一个已知值(通常是0);
• 调用 fork,然后使父进程 exit;
• 调用 setsid 创建一个新会话;
• 将当前工作目录更改为根目录;
• 关闭不再需要的文件描述符;
• 某些守护进程打开 /dev/null 使其具有文件描述符 0、1 和 2 ,这样,任何一个试图读标准输入、写标准输出或标准错误的库历程都不会产生任何效果。
4、出错记录
守护进程存在的一个问题是如何处理出错消息。 因为它本就不应该有控制终端,所以不能只是简单地写到标准错误上。
有以下 3 种产生日志消息的方法:
• 内核例程可以调用 log 函数;
• 大多数用户进程(守护进程 )调用 syslog(3)函数来产生日志消息。
• 无论一个用户进程是在此主机上,还是在通过 TCP/IP网络连接到此主机的其它主机上,都可以将日志消息发向 UDP端口 514。
5、单实例守护进程
为了正常运作,某些守护进程会实现为,在任一时刻只运行该守护进程的一个副本。
如果守护进程需要访问一个设备,而该设备驱动程序有时会阻止想要多次打开 /dev 目录下相应设备节点的尝试。这就限制了在一个时刻只能运行守护进程的一个副本。
文件 和 记录锁机制为一种方法提供了基础,该方法保证一个守护进程只有一个副本在运行。
文件和记录锁提供了一种方便的互斥机制。
守护进程的每个副本都将试图创建一个文件,并将其进程 ID 写到该文件中。
6、守护进程的惯例
在 UNIX 系统中,守护进程遵循下列通用惯例:
• 若守护进程使用锁文件,那么该文件通常存储在 /var/run 目录中;
• 若守护进程支持配置选项,那么配置文件通常存放在 /etc 目录中;
• 守护进程可用命令行启动,但通常它们是由系统初始化脚本之一( /etc/rc* 或 /etc/init.d/* )启动的;
• 若一个守护进程有一个配置文件,那么当该守护进程启动时会读该文件,但在此之后一般就不会再查看它。
7、客户进程-服务器进程 模型
守护进程常常用作服务器进程。
一般而言, 服务器进程 等待 客户进程 与其通信,提出某种类型的服务要求。有的通信是单向的,有的通信是双向的。
8、小结
在大多数 UNIX 系统中,守护进程是一直运行的。
为了初始化我们自己的进程,使之作为守护进程运行,需要一些审慎的思索并理解第 9 章 说明的进程之间的关系。
本章还讨论了守护进程记录 出错消息 的几种方法。
第 14 章- 高级 I/O
1、引言
本章会讲到:
• 非阻塞 I/O;
• 记录锁;
• I/O多路转换( select 和 poll 函数 );
• 异步 I/O;
• readv 和 writev 函数;
• 存储映射 I/O( mmap );
2、非阻塞 I/O
10.5节中曾将系统调用分成两类:“低速”系统调用 和 其它。 低速系统调用是可能会使进程永远阻塞的一类系统调用。
非阻塞 I/O 使我们可以发出 open、read 和 write这样的 I/O 操作,并使这些操作不会永远阻塞。
有时,也可以将应用程序设计成使用多线程的(11章),从而避免使用非阻塞 I/O。如若我们能在其它线程中继续进行,则可以允许单个线程在 I/O 调用中阻塞。
3、记录锁
商用 UNIX 系统提供了记录锁机制。
记录锁( record locking )的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其它进程修改同一文件区。(“记录”这个词并不准确,因为 UNIX 系统内核没有使用文件记录这种说法。 一个更合适的术语是 “字节范围锁”( byte-range locking ),因为它锁定的只是文件中的一个区域 )。
14.3.1- 历史
早起 UNIX 是没有 记录锁的。
14.3.2- fcnt1 记录锁
主要分为两种类型的锁: 共享读锁( l_type 为 L_RDLCK )和独占性写锁( L_WRLCK )。基本规则是:任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程有一把独占写锁。
14.3.3- 锁的隐含继承和释放
关于记录锁的自动继承和释放有 3 条规则:
• 锁与进程和文件两者相关联;
• 由 fork 产生的子进程不继承父进程所设置的锁;
• 在执行 exec后,新程序可以继承原执行程序的锁。
14.3.4- FreeBSD 实现
14.3.5- 在文件尾端加锁
14.3.6- 建议性锁 和 强制性锁
考虑数据库访问例程库。
如果该库中所有函数都以一致的方法处理记录锁,则称使用这些函数访问数据库的进程集为 合作进程( cooperating process )。如果这些函数是唯一地用来访问数据库的函数,那么它们使用建议性锁是可行的。但是建议性锁并不能阻止对数据库文件有写权限的任何其它进程写这个数据库文件。
4、 I/O 多路转接
当从一个描述符读,然后又写到另一个描述符时,可以在下列形式的循环中使用阻塞 I/O: … 这种形式的阻塞 I/O 到处可见。但是如果必须从两个描述符读,又将如何呢?
• 法1- 将一个进程变成两个进程(用 fork),每个进程处理一条数据通路。如果使用两个进程,则可使每个进程都执行阻塞 read。
• 法2- 我们可以不使用 两个进程,而是用一个进程中的两个线程。 虽然避免了终止的复杂性,但却要求处理两个线程之间的同步。
• 法3- 仍旧使用一个进程执行该程序,但使用非阻塞 I/O读取数据。
• 法4- 还有一种技术成为 异步I/O( asynchronous I/O )。
• 法5- 一种比较好的技术是使用 I/O多路转接( I/O multiplexing )。为了使用这种技术,先构造一张我们感兴趣的描述符(通常不止一个)的列表,然后调用一个函数,知道这些描述符中的一个已准备好进行 I/O时,该函数才返回。
14.4.1- 函数 select 和 pselect
select函数使我们可以执行 I/O多路转接。
pselect 是 select函数的变体。
14.4.2- 函数 poll
poll 函数类似于 select,但是程序员接口有所不同。 虽然 poll 函数是 System V 引入进来支持 STREAMS 子系统的,但是 poll函数可用于任何类型的文件描述符。
5、异步 I/O
14.5.1- System V 异步 I/O
14.5.2- BSD 异步 I/O
14.5.3- POSIX 异步 I/O
6、 函数 readv 和 writev
readv 和 writev 函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为 散步读( scatter read )和聚集写( gather write )
7、函数 readn 和 writen
函数 readn 和 writen 的功能分别是读、写指定的 N 字节数据,并处理返回值可能小于要求值的情况。这两个函数只是按需多次调用 read 和 wtire直至读、写了 N 字节数据。
8、存储映射 I/O
存储映射 I/O( memory-mapped I/O )能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样就可以在不使用 read 和 write 的情况下执行 I/O。
9、小结
本章描述了很多高级 I/O 功能:
• 非阻塞 I/O——发一个 I/O操作,不使其阻塞;
• 记录锁
• I/O多路转接—— select 和 poll 函数
• readv 和 writev 函数
• 存储映射 I/O(mmap)
第 15 章- 进程间通信
1、引言
进程间通信( InterProcess Communication, IPC )。
本章讨论经典的 IPC: 管道(pipe)、FIFO、消息队列、信号量、共享存储。
2、管道
管道是 UNIX 系统 IPC 的最古老的形式,一般是半双工的(即数据只能在一个方向时流动),现在某些系统也提供全双工管道。
每当在管道中键入一个命令序列, 让 shell 执行时, shell 都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相连接。
管道通常用 pipe 函数创建。
fstat 函数对管道的每一端都返回一个 FIFO(也称命名管道) 类型的文件描述符。
3、函数 popen 和 pclose
常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据。
为此,标准 I/O 库提供了两个函数 popen 和 pclose : 创建一个管道, fork 一个子进程,关闭未使用的管道端,执行一个 shell 运行命令,然后等待命令终止。
4、协同进程
UNIX 系统过滤程序从标准输入读取数据,向标准输出写数据。几个过滤程序通常在 shell 管道中线性连接。当一个过滤程序即产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了 协同进程( coprocess )。
协同进程通常在 shell 的后台运行,其标准输入和标准输出通过管道连接到另一个程序。
popen 只提供连接到另一个进程的标准输入或标准输出的一个单向管道;
而协同进程则有连接到另一个进程的两个单项管道:一个接到其标准输入,另一个则来自其标准输出。
5、FIFO
FIFO 有时被称为 命名管道。
未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还有有一个共同的创建了它们的祖先进程。
但是通过 FIFO,可以用于非线性连接,不相关的进程也能交换数据!
FIFO 有以下两种用途:
12、小结
本章详细说明了进程间通信的多种形式: 管道、命名管道( FIFO )、通常称为 XSI IPC 的 3 种形式的 IPC( 消息队列、信号量 和 共享存储 ),以及 POSIX 提供的替代信号量机制。
给出了一下建议:
• 要学会使用管道 和 FIFO;
• 要尽可能避免使用消息队列以及信号量;
• 应该考虑全双工管道和记录锁;
• 共享存储仍然有它的用途, 虽然通过 mmap 函数也可能提供通项的功能。
第 16 章- 网络 IPC: 套接字
进程间通信( InterProcess Communication, IPC )。
1、引言
本章将考察不同计算机(通过网络相连)上的进程相互通信的机制:网络进程间通信( network IPC ).
在本章中,将描述套接字网络进程间通信接口,进程用改接口能够和其它进程通信,无论它们是在同一台计算机上还是不同的计算机上。本章仅是一个套接字 API 的概述。
2、 套接字描述符
套接字是通信端点的抽象。
套接字描述符在 UNIX 系统中被当做是一种文件描述符。
为创建一个套接字,调用 socket 函数。
对于数据报( SOCK_DGRAM )接口,两个对等进程之间通信时不需要逻辑连接。只需要向对等进程所使用的套接字送出一个报文。因此 数据报提供了一个无连接的服务。
另一方面,字节流( SOCK_STREAM )要求在交换数据之前,在本地套接字和通信的对等进程的套接字之间建立一个逻辑连接。
流控制传输协议( Stream Control Transmission Protocol, SCTP )提供了因特网域上的顺序数据包服务。
调用 socket 与调用 open 相类似。在两种情况下,均可获得用于 I/O 的文件描述符。当不再需要该文件描述符时,调用 close 来关闭对文件或套接字的访问,并且释放该描述符以便重新使用。
套接字通信是双向的。 可以采用 shutdown 函数来禁止一个套接字的 I/O。
3、寻址
进程标识由两部分组成:
• 计算机的网络地址;
• 计算机上用 端口号 表示的服务。
16.3.1- 字节序
字节序 是一个处理器架构特性,用于指示像整数这样的大数据类型内部的字节如何排序。
如果处理器架构支持 大端( big-endian )字节序:那么最大字节地址出现在最低有效字节( Least Significant Byte, LSB )上。
小端( little-endian )字节序:最低有效字节包含最小字节地址。
【注】不管字节如何排序,自最高有效字节(Most Significant Byte, MSB)总在左边,最低有效字节总是在右边。
网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会被字节序所混淆。 TCP/IP 协议栈使用大端字节序。对于TCP/IP,地址用网络字节序来表示,所以应用程序有时需要在处理器的字节序与网络字节序之间转换他们。
16.3.2- 地址格式
一个地址标识一个特定通信域的套接字端点,地址格式与这个特定的通信域相关。
16.3.3- 地址查询
16.3.4- 将套接字与地址关联
对于服务器,需要给一个接受客户端请求的服务器套接字关联上一个众所周知的地址。
客户端应有一种方法来发现连接服务器所需要的地址,最简单的方法就是服务器保留一个地址并且注册在 /etc/services 或者某个名字服务中。
4、建立连接
如果要处理一个面向连接的 网络服务( SOCK_STREAM 或 SOCK_SEQPACKET ),那么在开始叫唤数据以前,需要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之间建立一个连接。 使用 connect 函数 来建立连接。
5、数据传输
既然一个套接字端点表示为一个文件描述符,那么只要建立连接,就可以使用 read 和 write来通过套接字通信。
在套接字描述符上使用 read 和 write 是非常有意义的,因为这意味着可以将套接字描述符传递给那些原先为处理本地文件而设计的函数。 而且还可以安排将套接字描述符传递给子进程,而该子进程执行的程序并不了解套接字。
6、套接字选项
套接字机制提供了两个套接字选项接口来控制套接字行为:
• 一个接口用来设置选项;
• 另一个接口可以查询选项的状态。
可以获取 or 设以下 3 种选项:
• 通用选项,工作在所有套接字类型上;
• 在套接字层次管理的选项,但是依赖于下层协议的支持;
• 特定于某协议的选项,每个协议独有的。
可以使用 setsockopt 函数来设置套接字选项。
7、带外数据
带外数据( out-of-band data )是一些通信协议所支持的可选功能,与普通数据相比,它允许更好优先级的数据传输。带外数据先行传输,即使传输队列已经有数据。
TCP 支持带外数据,但是 UDP 不支持。 套接字口对带外数据的支持很大程度上受 TCP 带外数据具体实现的影响。
TCP 将带外数据成为 紧急数据( urgent data )。 TCP 仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。
8、非阻塞和异步 I/O
9、小结
本章讨论了:
• 套接字端点如何命名,在连接服务器时,如何发现所用的地址。
• 给出了采用无连接的(即基于数据报的)套接字和面向连接的套接字的客户端和服务器的实例;
• 讨论了 异步和 阻塞的套接字 I/O;
• 用于管理套接字选项的接口。
第 17 章-高级进程间通信
1、引言
本章将会:
• 介绍一种高级 IPC(进程间通信)——UNIX 域套接字机制,并说明它的应用方法。这种形式的 IPC 可以在同一计算机系统上运行的两个进程之间传送打开文件描述符。
• 服务进程可以使它们打开的文件描述符与指定的名字相关联,同一系统上运行的客户进程可以使用这些名字与服务器进程汇聚。
• 还会了解到操作系统如何为每一个客户进程提供一个独用的 IPC 通道。
2、 UNIX域套接字
UNIX 域套接字用于在同一台计算机上运行的进程之间的通信。
虽然因特网网域套接字可以用于同一目的,但 UNIX 域套接字的效率更高。
UNIX 域套接字提供流和数据报两种接口,UNIX 域套接字更像是套接字和管道的混合。
3、唯一连接
服务器进程可以使用标准 blind、listen 和 accept函数,为客户进程安排一个唯一 UNIX 域连接。客户进程使用 connect 与服务器进程联系。在服务器进程接受了 connect 请求后,在服务器进程和客户进程之间就存在了 唯一连接。
4、传送文件描述符
传送文件描述符可以使一个进程(通常是服务器进程)能够处理打开一个文件所要做的一切操作(包括将网络名翻译为网络地址、拨号调制调节器、协商文件锁等)以及向调用进程送回一个描述符,该描述符可被用于以后的所有 I/O 函数。
5、 打开服务器进程第 1 版
6、 打开服务器进程第 2 版
7、小结
如何在两个进程之间传送文件描述符,以及服务器进程如何接受来自客户进程的唯一连接。
了解了如何用它们来实现一个全双工的管道以及如何利用它们来适应 14.4 节的 I/O多路转接函数以间接地用于 XSI 消息队列中。
第 18 章- 终端 I/O