《linux系统编程手册上册》读书笔记

734 阅读1小时+

第四章文件IO

1、文件描述符

2、I/O 模型的主要系统调用:打开文件、关闭文件、从文件中读数据、写数据

4.1文件描述符

文件描述符是一个整数指代打开的文件。每个进程的文件描述符都自成一套

4.2open函数:打开或创建一个文件

入参:path文件路径,flags位掩码指定了访问方式(只读、只写、读写等),mode位掩码指定了访问权限(可省略)

返回:文件描述符整数

4.3read函数

入参:文件描述符内存缓冲区指针,读取字节数

返回:成功读取的字节数

4.4write函数

入参:文件描述符,写入内容所在内存缓冲区指针,写入字节数

返回:写入的字节数(write写入成功不代表已经写入磁盘,只是写入文件系统的缓存,sync操作才会写入磁盘)

4.5close函数:释放文件描述符,供该进程继续使用

关闭一个打开的文件描述符,供进程重新使用。当一进程终止时,将自动关闭其已打开的所有文件描述符

4.6lseek函数:移动文件的偏移量

偏移量:调整read,write函数的操作开始位置,以字节为单位;打开一个文件偏移量默认指向文件开始;每次read,write调用将自动调整偏移量到已读或已写的下一个字节位置

文件空洞:当前文件偏移量超过了文件结尾,仍可以从该位置write,中间的部分不占用磁盘空间,这部分称为文件空洞;读取空洞将返回以 0(空字节)填充的缓冲区

返回:新的文件偏移量

第五章深入文件IO

1、原子操作的重要性及实现

2、文件描述符打开文件句柄文件 i-node 之间的关系

5.1原子操作和竞争条件

**系统调用都是原子的,**其间不会为其他进程或线程所中断

例子:使用 open()的标志位,来保证相关文件操作的原子性:

1、open的标志位O_EXEL + O_CREAT,保证检查文件是否已经存在创建文件的操作原子性(不允许两个进程同时创建一个文件)

2、open的标志位O_APPEND,保证文件偏移量移动数据写入的操作原子性

5.4文件描述符、打开文件句柄和文件 i-node 之间的关系

1)进程的文件描述符表:每个进程有一个文件描述符表;一条记录是一个文件描述符(文件描述符标志(亦即,close-on-exec 标志))

2)系统的打开文件表:操作系统全局唯一;一条记录是一个文件句柄,即一个文件被某个进程打开的动态信息文件偏移量、open方法的flags、引用的i-node文件指针等

3)系统的i-node表:操作系统全局唯一;一条记录是一个文件的i-node信息静态信息(文件类型、属性、大小、锁、访问权限等)

case分析:

1)同一个进程中,不同的文件描述符指向一个文件句柄:调用dup方法,复制文件描述符

2)不同的进程中,相同编号的文件描述符指向一个文件句柄:调用fork方法,父子进程继承文件描述符信息

3)两个不同的文件句柄指向一条i-node记录:同一个文件在一个进程或多个进程被多次打开

推论:共享一个文件句柄的文件描述符(这些文件描述符可能在一个进程,也可能在不同的进程),将共享一个文件偏移量,所以某个通过文件描述符的一次修改会影响另一个文件描述符

5.5dup函数,复制文件描述符

5.6 在文件特定偏移量处的 I/O:pread()和 pwrite()

pread()/pwrite调用等同于将如下调用纳入同一原子操作:lseek+read/write

5.7 分散输入和集中输出:readv()和 writev() 

原子的处理多个缓冲区的数据

第六章进程

进程由用户内存空间和一系列内核数据结构组成;重点关注用户内存空间的组成和实现(虚拟内存)

6.1进程和程序 

进程由用户内存空间和一系列内核数据结构组成

用户内存空间包含了程序代码及代码所使用的变量,而记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息等

6.2进程号和父进程号

每个进程都有一个进程号(PID),进程号是一个正数,用以唯一标识系统中的某个进程

新进程创建时,内核会按顺序将下一个可用的进程号分配给其使用。每当进程号达到 32767 的限制时,内核将重置进程号计数器

到1 号进程—init 进程,即所有进程的始祖

每个进程都有一个创建自己父进程

6.3 进程内存布局 

进程用户内存空间组成**:**

文本段:包含了进程运行的程序机器语言指令。文本段具有只读属性,因为多个进程可同时运行同一程序,所以又将文本段设为可共享

初始化数据段:包含显式初始化的全局变量和静态变量

未初始化数据段:包含了未进行显式初始化的全局变量和静态变量。程序启动之前,系统将本段内所有内存初始化0

栈:系统会为每个当前调用的函数分配一个栈帧。栈帧中存储函数的局部变量、实参和寄存器副本

堆:可在运行时动态进行内存分配的一块区域。malloc系统调用

6.4 虚拟内存管理

虚拟内存管理技术利用了大多数程序的一个典型特征**:访问局部性原理**

虚拟内存的实现

1)虚拟页物理页帧:将每个程序逻辑上需要使用的(虚拟)内存切割成小型的、固定大小的。相应地将物理内存划分成一系列与虚存页尺寸相同的页帧

2)按需加载:利用访问局部性原理,每个程序仅有部分页需要驻留在物理内存页帧中,程序未使用的页拷贝保存在交换区内(磁盘中),仅在需要时才会载入物理内存。因此每个进程的虚拟逻辑内存可以远大于物理内存。若进程欲访问的页面目前并未驻留在物理内存中,将会发生页面错误,内核即刻挂起进程的执行,同时从磁盘中将该页面载入内存

3)页表:内核需要为每个进程维护一张页表,来描述每页在进程虚拟地址空间的位置和物理内存页帧或交换区(磁盘)的位置映射关系;因为页表中的每个条目要么在内存中,要么驻留在磁盘上

虚拟内存的优点

1)进程与进程、进程与内核相互隔离

2)适当情况下,两个或者更多进程能够共享内存

3)因为需要驻留在内存中的仅是进程的一部分,所以程序的加载和运行都很快,且虚拟内存大小能够超出物理内存容量

6.5 栈和栈帧

栈帧包括如下信息

1)函数实参和局部变量:变量在调用函数时自动创建的,函数返回时将自动销毁这些变量(因为栈帧会被释放)

2)函数调用的链接信息:每个函数都会用到一些 CPU 寄存器,比如程序计数器(指向下一条将要执行的机器语言指令)。每当一函数调用另一函数时,会在被调用函数的栈帧保存这些寄存器的副本,以便函数返回时能为函数调用者将寄存器恢复原状

栈帧的实现与函数调用过程:

每个栈帧有两个指针寄存器,寄存器%ebp指向该栈帧的最底部,寄存器%esp指向该栈帧的最顶部。esp可以动态增长减小(push,pop),随函数调用而增,随函数返回而减。%ebp帧指针是不移动的

假设函数P调用函数Q,则Q的参数放在P的栈帧中,P的返回地址(即当程序从Q返回时应该继续执行的地方)也被压入P栈中,形成P的栈帧的末尾

Q的栈帧创建后首先要保存P的寄存器副本(如P的%ebp,待返回P时恢复),返回值通过寄存器返回

www.cnblogs.com/zlcxbb/p/57…

6.6 命令行参数(argc, argv)

存储在进程的用户内存空间,供main函数使用

6.7 环境列表

进程都有与其相关的称之为环境列表的字符串数组,其中每个字符串都以键值对形式定义。常将列表中的名称称为环境变量

新进程在创建之时,会继承其父进程的环境副本。这是一种原始的进程间通信方式,提供了将信息从父进程传递给子进程的方法。子进程创建后,父、子进程均可更改各自的环境变量,且这些变更对对方而言不再可见

第7章内存分配

堆:用户自定义内存空间;malloc+free

7.1 在堆上分配内存

进程可以通过增加堆的大小来分配内存,堆的当前内存边界称为program break

1)malloc( )函数:在堆上分配参数 size 字节大小的内存,并返回指向新分配内存起始位置处的指针; malloc()的返回类型为 void*,因而可以将其赋给任意类型的 C 指针。

若无法分配内存(或许是因为已经抵达 program break 所能达到的地址上限),则 malloc()

返回 NULL并设置 errno 以返回错误信息

2)**free()函数:**释放 ptr 参数所指向的内存块,该参数应该是之前由 malloc()或其他其他堆内存分配函数之一所返回的地址。

一般情况下,free()并不降低 program break 的位置,而是将这块内存填加到空闲内存列表

中,因为被释放的内存块通常会位于堆的中间,而非堆的顶部,此外最大限度地减少了程序必须执行的 sbrk()调用次数

尽管当进程终止时,其占用的所有内存都会返还给操作系统,但最好能够在程序中显式释放所有的已分配内存

3)空闲内存块、使用中内存卡、空闲内存列表(malloc和free的原理)

malloc()首先会扫描空闲内存块列表,以求找到尺寸大于或等于要求的一块空闲内存(例如,first-fit 或 best-fito)如是一块较大的内存,那么将对其进行分割,把较小的那块空闲内存块保留在空闲列表中。

如果在空闲内存列表中找不到足够大的空闲内存块,那么 malloc()会调用 sbrk()以分配更****多的内存。为减少对调用次数,malloc()并未只是严格按所需字节数来分配内存,而是以更大幅度来增加 program break

空闲内存列表是一个双向链表,指向前一个空闲块的指针和指向后一个空闲块的指针放在内存块本身中。

空闲内存块由内存块大小,两个指针和剩余空间组成**;被分配且使用的块由**内存块大小和剩余空间组成

4)要避免内存使用,应该遵守以下规则

分配一块内存后,不要改变这块内存范围外的任何内容

释放同一块已分配内存超过一次是错误的

在编写需要长时间运行的程序(例如,shell 或网络守护进程)时,如果需要反复分配内存,那么应当确保释放所有已使用完毕的内存。否则会造成**“内存泄漏”**。 

第9章进程凭证

1、实际用户 ID:RUID代表此创建进程的用户不变

有效用户 ID:EUID代表此进程所拥有的权限可变,当访问的文件Set-User-ID 位被置上时,EUID变化为FUID;也可以将EUID置回SUID或RUID)。EUID 为 0 的进程属特权级进程,进程是否是特权root由EUID决定

保存的 set-user-ID:SUID保存了EUID的副本,当EUID改变后,如果想回到以前的EUID,此时SUID将发挥作用。执行set-user-ID 程序会将EUID的新值复制到SUID

文件系统用户 ID:文件创建者进程ID,FUID

文件的Set-User-ID 位(结合15-4节):创建进程时RUID,EUID,SUID相同,只有当访问的文件Set-User-ID 位被置上时,将EUID改为FUID以获得更多权限

blog.chinaunix.net/uid-2683388…

2、API

**setuid()**系列

getuid()系列

9.1 实际用户 ID 和实际组 ID(RUID)

登录shell 从**/etc/passwd 文件中读取相应用户密码记录,置为其实际用户 ID和实际组 ID**。当创建新进程时,将从其父进程中继承这些 ID

实际 ID 定义了进程所属,仅root用户可以修改,一般不会变化

9.2 有效用户 ID 和有效组 ID (EUID)

EUID决定授予进程的权限。进程是否是root由EUID决定

当访问的文件Set-User-ID 位被置上时,EUID变化为FUID;也可以将EUID置回SUID或RUID

变化途径: 其一是使用 9.7 节中所讨论的系统调用,其二是执行 set-user-ID 和 set-group-ID 程序。

9.3set-user-ID 程序和的Set-User-ID 和 Set-Group-ID 位

set-user-ID 程序会将进程的EUID 置为文件的FUID,从而获得常规情况下并不具有的权限。set-group-ID 程序对进程有效组 ID 实现类似任务

文件拥有两个特别的权限位 set-user-ID 位和 set-group-ID 位。可使用 chmod 命令来设置这些权限位

例如,如果一个可执行文件的FUID为 root(超级用户),且为此程序设置了 set-user-ID 权限位,那么当运行该程序时,进程无需登入root也会取得超级用户权限

9.4 保存 set-user-ID 和保存 set-group-ID(SUID)

保存 set-user-ID 和保存 set-group-ID 的值由对应的有效 ID 复制而来。无论正在执行的文件是否设置了 set-user-ID 或 set-group-ID 权限位,复制都将进行

举例说明上述操作的效果,假设某进程的实际用户 ID、有效用户 ID 和保存 set-user-ID 均

为 1000,当其执行了 root 用户(用户 ID 为 0)拥有的 set-user-ID 程序后,进程的实际用户 ID、有效用户 ID 和保存 set-user-ID 分别变为1000,0,0

9.5 文件系统用户 ID 和组 ID(FUID)

创建新文件时,将该进程的有效ID作为该文件的FUID

9.7 获取和修改进程凭证 

1、获取实际和有效 ID 

系统调用 getuid()和 getgid()分别返回调用进程的实际用户 ID 和组 ID

2、修改有效 ID 

非特权进程调用 setuid()时,仅能将有效用户 ID修改成相应的实际用户 ID 或保存 set-user-ID(两者相等)

特权进程以一个非 0 参数调用 setuid()时,其实际用户 ID、有效用户 ID 和保存 set-user-ID均被置为 uid 参数所指定的值。这一操作是单向的,一旦特权进程以此方式修改了其 ID,那么所有特权都将丢失

第13章文件IO缓冲

系统 I/O 调用(即内核)和标准 C 语言库 I/O 函数(即 stdio 函数)在操作磁盘文件时会对数据进行缓冲

13.1 文件 I/O 的内核缓冲:缓冲区高速缓存

read()和 write()系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区内核缓冲区高速缓存之间复制数据

在后续某个时刻,内核会将其缓冲区中的数据写入(刷新至**)磁盘**。如果在此期间,另一进程试图读取该文件的这几个字节,那么内核将自动从缓冲区高速缓存中提供这些数据,而不是从磁盘中读取(否则会读取过期的内容)

意在使 read()和 write()调用的操作更为快速,因为它们不需要等待(缓慢的)磁盘操作

13.2 控制用户(stdio 库)缓冲区

C 语言函数库的 I/O 函数(比如,fprintf()、fscanf()、fgets()、fputs()、fputc()、fgetc())

调用 setvbuf()函数,可以控制 stdio 库使用缓冲的形式:

_IONBF,不对 I/O 进行缓冲。每个 stdio 库函数将立即调用 write()或者 read()

_IOLBF,采用行缓冲 I/O。指代终端设备的流默认属于这一类型。对于输出流,在输出一个换行符(除非缓冲区已经填满)前将缓冲数据。对于输入流,每次读取一行数据。

_IOFBF,采用全缓冲 I/O。单次读、写数据(通过 read()或 write()系统调用)的**大小与缓冲区相同。**指代磁盘的流默认采用此模式

无论当前采用何种缓冲区模式,在任何时候,都可以使用 fflush()库函数强制将 stdio 输出流中的数据(即通过 write())刷新到内核缓冲区

13.3 控制内核缓冲区

1)fsync()系统调用将使缓冲数据和与打开文件描述符 fd 相关的所有元数据都刷新到磁盘上

sync()系统调用会使包含更新文件信息的所有内核缓冲区(即数据块、指针块、元数据等)刷新到磁盘上 

元数据:诸如文件属主、属组、文件权限、文件大小、文件(硬)链接数量,表明文件最近访问、修改以及元数据发生变化的时间戳,指向文件数据块的指针

2)open()函数如指定 O_SYNC 标志,则会使所有后续输出同步,每个 write()调用会自动将文件数据和元数据刷新到磁盘上

13.4 I/O 缓冲小结

1)stdio 库用户数据传递到 stdio 缓冲区,该缓冲区位于用户态内存区;缓冲区填满时(或者调用fflush函数强制刷用户态缓冲),stdio 库会调用 write()系统调用,将数据传递到内核高速缓冲区(位于内核态内存区);可以通过setvbuf设置特定模式,禁用 stdio 库的缓冲

2)write等系统调用将数据传输到内核IO缓冲;合适的时候(或者sync,fsync系统调用),内核发起磁盘操作,将数据传递到磁盘;可以通过open系统调用的O_SYNC模式,禁用内核IO缓冲

第14章系统编程概念

文件系统:ext2是 Linux 上使用最为广泛的文件系统

14.1 设备专用文件(设备文件)

设备划分为以下两种类型:

字符型设备:基于每个字符来处理数据。终端和键盘都属于字符型设备。

块设备:每次处理一块数据。块的大小取决于设备类型,但通常为 512 字节的倍数。

磁盘和磁带设备都属于块设备

14.2 磁盘

1)组成:

磁盘驱动器由一个或多个高速旋转的盘片组成。

磁盘表面信息物理上存储于称为磁道(track)一组同心圆上。

磁道自身又被划分为若干扇区,每个扇区则包含一系列物理块

物理块的容量一般为 512 字节(或 512 的倍数),代表了驱动器可读/写的最小信息单元

2)读写过程

首先,磁头要移动到相应磁道 (寻道时间);然后,在相应扇区旋转到磁头下之前,驱动器必须一直等待**(旋转延迟**);最后,还要从所请求的块上传输数据(传输时间)

3)可将每块磁盘划分为一个或多个(不重叠的)分区

分区类型包括:文件系统、数据区、交换区

14.3文件系统

ext2是 Linux 上使用最为广泛的文件系统

由14.2知,一个文件系统挂载在一个磁盘分区上。文件系统中,用来分配空间的基本单位是逻辑块,一个逻辑块对应若干个连续的磁盘物理块。文件系统由以下几种块组成:

引导块:引导块不为文件系统所用,只是包含用来引导操作系统的信息

超级块:紧随引导块之后的一个独立块,包含与文件系统有关的参数信息,其中包括: i 节点表容量;文件系统中逻辑块的大小;文件系统的大小等

i节点表:文件系统中的每个文件或目录在i节点表中都对应着唯一一条记录,登记了关乎文件的各种信息

数据块:文件系统的大部分空间都用于存放数据

14.4 i节点

文件的 i 节点号ls –li 命令所显示的第一列

i 节点所维护的信息包括:文件类型、所属用户ID、三类用户权限、文件大小、实际上使用磁盘块个数及指针等

文件系统在**存储文件时,数据块不一定连续,**内核在 i 节点内维护有一组指针。无需连续存储文件块,使得文件系统对磁盘空间的利用更为高效。

每个 i 节点包含 15 个指针。其中的前 12 个指针是一级指针,直接指向块(直接寻址),还有两个二级指针和一个三级指针**(间接寻址**)。间接寻址的目的在于该系统在维持 i 节点结构大小固定的同时,支持大型文件;而占绝对多数的小文件而言,这种设计满足了对文件数据块的快速访问即通过 i 节点的直接指针访问

14.5 虚拟文件系统(VFS)

Linux 所支持的各种文件系统,其实现细节均不相同。举例来说,这些差异包括文件块的分配方式,以及目录的组织方式

虚拟文件系统是一种内核特性,通过为文件系统操作创建抽象层来解决上述问题,定义了一套通用接口

所有与文件交互的程序都会按照这一接口来进行操,程序只需理解 VFS 接口,而无需过问具体文件系统的实现细节

14.8挂载

c.biancheng.net/view/2859.h…

在 Linux 看来,任何硬件设备也都是文件,它们各有自己的一套文件系统(文件目录结构)。在 Linux 系统中使用这些硬件设备时,只有将Linux本身的文件目录与硬件设备的文件目录合二为一,硬件设备才能为我们所用。合二为一的过程称为“挂载”。

挂载,指的就是将设备文件中的顶级目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。
纠正一个误区,并不是根目录下任何一个目录都可以作为挂载点,由于挂载操作会使得原有目录中文件被隐藏,因此根目录以及系统原有目录都不要作为挂载点,会造成系统异常甚至崩溃,挂载点最好是新建的空目录

第15章文件属性

文件的各种属性(文件元数据

系统调用 stat(), 返回stat结构

15.1 获取文件信息:stat() 

利用系统调用 stat()等,获取与文件有关的信息,其中大部分提取自文****件 i 节点

系统调用 stat()无需对其所操作的文件本身拥有任何权限,但针对指定pathname 的父目录要有执行(搜索)权限

stat()会在缓冲区中返回一个由 statbuf 指向的 **stat 结构:**i节点号、文件所有权、指向文件的(硬)链接数、文件类型及权限等

15.2 文件时间戳

stat 结构的 st_atime、st_mtime 和 st_ctime 字段分别记录了对文件的上次访问时间、上次修改时间,以及文件状态(即文件 i 节点内信息)上次发生变更的时间

15.3 文件属主

文件都有一个属主ID和属组ID,即FUID,FGID(见第9章)。文件创建时,FUID取自创建文件的进程有效用户 ID(EUID)

只有特权级进程才能使用 chown()改变文件的FUID,FGID;****非特权级进程可使用 chown()将自己所拥有文件的FGID**** 改为其所从属的任一属组的 ID,前提是进程的有效用户ID 与文件的用户 ID 相匹配

如果文件组的属主或属组发生了改变, set-user-ID 和 set-group-ID 权限位也会随之关****闭

15.4 文件权限

1)stat 结构中 st_mod 字段的低 12 位定义了文件权限

其中的前 3 位为专用位,分别是 set-user-ID 位、set-group-ID 位(参考第9章)和 sticky 位( U、G、T位)

其余 9 位则构成了定义权限的掩码,分别授予访问文件的类用户。

文件权限掩码分为 3 类:

Owner(亦称为 user):授予文件属主的权限

Group:授予文件属组成员用户的权限。

Other:授予其他用户的权限。

可为每一类用户授予的3种权限

Read:可阅读文件的内容

Write:可更改文件的内容

Execute:可以执行文件(亦即,文件是程序或脚本)

头文件<sys/stat.h>定义了可与 stat 结构中 st_mode 相与(&)的常量,用于检查特定权限

位置位与否。

2)目录与文件拥有相同的权限方案,只是对 3 种权限的含义另有所指:

Read:可列出(比如,通过 ls 命令)目录之下的内容(即目录下的文件名)

Write:可在目录内创建、删除文件。注意,要删除文件,对文件本身无需有任何权限

Execute:可访问目录中的文件。(cd命令

访问文件时,需要拥有对路径名所列所有目录的执行权限。例如,想读取文件/home/mtk/x,

则需拥有对目录/、/home 以及/home/mtk 的执行权限(还要有对文件 x 自身的读权限)

拥有对目录的读权限无可执行权限,用户只是能查看目录中的文件列表不能访问文件;拥有对目录的可执行权限,而无读权限,只要知道目录内文件的名称,仍可其进行访问,但不能列出目录下内容;要想在目录中添加或删除文件,需要同时拥有对该目录的执行和写权限

3)权限检查顺序:

进程的有效用户 ID文件的用户 ID(属主)相同,内核会根据文件的属主权限,授予进程相应的访问权限。

进程的有效组 ID 或任一附属组 ID 与文件的组 ID(属组)相匹配,内核会根据文件的属组权限,授予进程对文件的相应访问权限。

若以上三点皆不满足,内核会根据文件的 other(其他)权限,授予进程相应权限。

内核会依次执行针对属主、属组以及其他用户的权限检查,只要匹配上述检查规则之一便会停止检查

4)作用于目录时,sticky 权限位限制删除位的作用。为目录设置该位,则表明仅当非特权进程具有对目录的写权限,且为文件或目录的属主时,才能对目录下的文件进行删除操作

5)umask

umask 是一种进程属性,当进程新建文件或目录时,该属性用于指明应屏蔽哪些权限位

大多数 shell 的初始化文件会将 umask 默认置为八进制值 022 (----w--w-)。其含义为对于

同组或其他用户,应总是屏蔽写权限

6)更改文件权限:chmod()

要想更改文件权限,进程要么具有特权级别,要么需进程的EUID与文件所属ID 相匹配

第18章目录与链接

fd table->open file table->i node table->dirctory table

i-node 中并不包含文件的名称。相反,对文件的命名利用的是目录条目,而目录的内容则是列

出文件名和 i-node 编号之间对应关系的一个表格,表格中的成员就是目录条目。也将这些目录条目称作(硬)链接。

www.jianshu.com/p/d1f7922b6…

创建文件的四个主要操作如下:

  1. 存储文件属性:内核在i-node表中找一个空的i-node
  2. 存储文件数据:根据文件大小分配合适数量的空磁盘块(数据块)
  3. 记录分配情况:在第1步的i-node节点上记录了第2步分配的磁盘序列(维护两者映射关系)
  4. 添加文件名到目录:以<i-node_index, filename>的形式在目录中建立文件名和实际的物理块号的关联。

18.1 目录和(硬)链接

1)目录与普通文件的区别:

目录在其 i-node表对应的条目中,会将目录标记为一种不同的文件类型

目录的内容是一个表格,该目录下每一个文件对应一条记录文件条目,包括文件名i-node 编号)

2)硬链接:

在相同或者不同目录创建不同文件名的文件(条目),每个均指向相同的 i-node 节点

利用 ln 命令为一个业已存在的文件创建新的硬链接;ls–li 命令所示内容的第三列为对 i-node 链接的计数

若移除其中一个硬链接,另一硬链接以及文件本身将继续存在

rm 命令从目录列表中删除一文件名,将相应 i-node的链接计数减一(只删除硬链接文件);若链接计数因此而降为 0,则还将释放该文件名所指代的 i-node 和数据块(真正删除文件内容)。

硬链接的不足有二,均可用软链接来加以规避:

 i-node 编号的唯一性仅在一个文件系统之内才能得到保障,所以硬链接必须与其指代的文件驻留在同一文件系统中; 不能为目录创建硬链接,从而避免出现令诸多系统程序陷于混乱的链接环路。

18.2 符号(软)链接

软链接又称符号链接,是一种特殊的文件类型,只是其数据内容是另一文件的名称

符号链接是由 ln–s 命令创建的

文件的i-node链接计数不包括软链接,如果移除了符号链接所指向的文件,符号链接本身还将继续存在,但无法进行解引用,也将此类链接称之为悬空链接

符号链接之间可能会形成环路,Linux 内核规定最小解引用次数:8 次。

18.3 创建和移除(硬)链接:link()和 unlink() 

link()和 unlink()系统调用分别创建和移除硬链接,两者都不会对符号链接进行解引用操作,unlink()系统调用移除一个链接(删除一个文件名),且如果此链接是指向文件的最后一个链接,那么还将移除文件本身。

但是,因为仅当关闭所有文件描述符时,方可删除一个已打开的文件(参见图 5-2),所以当移除指向文件的最后一个链接时,如果仍有进程持有指代该文件的打开文件描述符,那么在关闭所有此类描述符之前,系统实际上将不会删除该文件(i-node表中的条目及相关数据块)。

18.4 更改文件名:rename()  

rename()调用仅操作目录条目,而不移动文件数据。改名既不影响指向该文件的其他硬链接,也不影响持有该文件打开描述符的任何进程,因为这些文件描述符指向的是打开文件描述,(在调用 open()之后)与文件名并无关系。

18.5 使用符号链接:symlink()和 readlink()  

18.6 创建和移除目录:mkdir()和 rmdir() 

mkdir的mode 参数指定了新目录的权限。mkdir()系统调用所创建的仅仅是路径名中的最后一部分。换言之,mkdir("aaa/bbb/ccc",mode)仅当目录 aaa 和 aaa/bbb 已经存在的情况下才会成功

要使 rmdir()调用成功,则要删除的目录必须为空

18.7 移除一个文件或目录:remove() 

如果 pathname 是一文件,那么 remove()去调用 unlink();如果 pathname 为一目录,那么

remove()去调用 rmdir()

与 unlink()、rmdir()一样,remove()不对符号链接进行解引用操作

第20章信号:基本概念

gityuan.com/2015/12/20/…

Linux系统共定义了64种信号,分为两大类:可靠信号与不可靠信号

在进程task_struct结构体中有一个进程的等待信号集的成员变量 struct sigpending pending。每个信号在进程中注册都会把信号值加入进程的等待信号集(非实时信号不可重复注册,所以可能会丢失)

20.1 概念和概述

1)信号事件发生时对进程的通知机制,也称之为软件中断。

一个(具有合适权限的)进程能够向另一进程(或自己)发送信号,信号的这一用法可作进程间通信的原始形式;但是大多数发往进程的信号源于内核

2)引发内核为进程产生信号的各类事件:

硬件发生异常,诸如,被 0 除;用户键入了能够产生信号的终端特殊字符如中断字符(通常是 Control-C)

3)信号分为两大类:

标准信号,即内核向进程通知事件,Linux 中标准信号编号范围 1~31;也称为不可靠信号,不支持排队,信号可能会丢失

**实时信号,**也称为可靠信号,支持排队, 信号不会丢失

4)信号的状态(产生、等待、到达、阻塞):

信号产生后,会于稍后被传递给某一进程。在产生到达期间,信号处于等待状态

如果内核接下来要调度的进程是要接受信号的进程,等待信号会马上送达

有时需要确保一段代码不为传递来的信号所中断,可以将信号添加到进程的信号掩码中,阻塞该组信号的到达。如果信号被阻塞,将保持等待状态直至稍后对其解除阻塞(从信号掩码中移除)

5)信号到达后,进程视具体信号执行如下默认操作之一:

忽略信号:内核将信号丢弃,信号对进程没有产生任何影响

终止(杀死)进程:这有时是指进程异常终止,而不是进程因调用 exit()而发生的正常终止

产生核心转储文件,同时进程终止:核心转储文件包含对进程虚拟内存的镜像,可将其加载到调试器中以检查进程终止时的状态

执行信号处理器程序(信号处理器程序是由程序员编写的函数,用于为响应传递来的信号而执行适当任务)

20.3 改变信号处置:signal() 

blog.csdn.net/weibo123012…

signal()函数第一个参数 sig,标识希望处理的信号编号,第二个参数是20.4节的信号处理器函数(的地址),该函数无返回值(void),并接收一个整型参数 ;SIG_DFL、SIG_IGN、SIG_ERR 是信号处理器函数的三个默认值)

signal返回值是先前的信号处理函数指针,如果有错误则返回SIG_ERR

20.4 信号处理器简介

信号处理器程序是当指定信号传递给进程时将会调用的一个函数(也就是signal函数的第二个入参) ,内核会将引发调用的信号编号作为一个整型参数传递给处理器函数

调用信号处理器程序,可能会随时打断主程序流程, 当处理器函数返回时,主程序会在处理器打断的位置恢复执行

20.5 发送信号:kill()

一个进程能够使用 kill()系统调用向另一进程发送信号 。

1)kill函数的pid 参数标识目标进程,sig 则指定了要发送的信号

如果pid小于等于0,将不代表进程号,而会有特殊含义:

pid 等于 0,那么会发送信号给与调用进程同组的每个进程,包括调用进程自身

pid 小于−1,那么会向组 ID 等于该 pid 绝对值的进程组内所有下属进程发送信号

pid 等于−1,那么会发给所有该进程有权发送的所有进程(除去 init和自身)。如果特权级进程发起这一调用,那么会发送信号给系统中的所有进程(除去 init和自身)。显而易见,有时也将这种信号发送方式称之为广播信号

2)进程要发送信号给另一进程,还需要适当的权限,其权限规则如下:

特权级进程可以向任何进程发送信号;

root 用户和组运行的 init 进程不接受自定义信号处理器函数,以防止系统停止;

chuang,那么非特权进程也可以向另一进程发送信号。(将目标进程有效用户 ID 排除在检查范围之外,这一举措的作用在于防止用户某甲向用户某乙的进程发送信号,而某乙的进程正在执行的 set-user-ID 程序又属于用户某甲)

20.9 信号集 

多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t. 

sigemptyset()函数初始化一个未包含任何成员的信号集。sigfillset()函数则初始化一个信号 集,使其包含所有信号

使用 sigemptyset()或者 sigfillset()来初始化信号集

 20.10 信号掩码(阻塞信号传递) 

 内核会为每个进程维护一个信号掩码,代表当前传递遭到阻塞的一组信号(信号集)。将遭阻塞的信号发送给某进程直至从进程信号掩码中移除该信号

sigprocmask()系统调用,既可从信号掩码中添加或者移除信号,又可获取现有掩码

其中,how 参数指定了给信号掩码带来的变化(如将 set 指向信号集内的指定信号添加到信号掩码中,将 set 指向信号集中的信号从信号掩码中移除即取消屏蔽),set,oldset参数为两个信号集(20.9节)指针。如果只获取信号掩码而又对其不作改动,那么可将 set 参数指定为空

如果sigprocmask****解除了对某个等待信号的屏蔽,那么会进程之前接收到且被阻塞的信号会立刻传递给进程

20.11 处于等待状态的信号

如果某进程接受了一个该进程正在阻塞的信号(sigprocmask()系统调用),那么会将该信号填加到进程的等待信号集中。当(且如果)之后解除了对该信号的锁定时,会随之将信号传递给此进程

为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()。sigpending()系统调用为调用进程返回处于等待状态的信号集

20.12 不对信号进行排队处理

等待信号集是一个掩码,仅表明一个信号是否发生,而未表明其发生的次数

对于标准信号,如果在阻塞状态下产生多次,那么会将该信号记录在等待信号集中,并在稍后仅传递一次,系统会对标准信号进行排队处理

标准信号和实时信号之间的差异之一在于( 22.8 节),对实时信号进行了排队处理

20.14 等待信号:pause() 

借助于 pause(),进程可暂停执行,直至信号到达为止

第21章信号:信号处理器函数

21.1 设计信号处理器函数

1)原则:信号处理器函数设计得越简单越好,降低引发竞争条件的风险

两种常见设计

信号处理器函数设置全局性标志变量后退出,主程序对此标志进行周期性检查,一旦置位随即采取相应动作;

信号处理器函数执行某种类型的清理动作,接着终止进程或跳回到主程序中的预定位置

2)可重入函数:同一个进程的多条线程可以同时安全地调用某一函数,那么该函数就是可重入的。如果一个函数只访问自己的局部变量或参数,则可重入更新全局变量或静态数据结构的函数可能是不可重入

malloc()函数族(更新内存空闲块时线程不安全)以及使用它们的其他库函数都是不可重入的。stdio 函数库成员(printf()、scanf()等)都是不可重入的,它们为缓冲区 I/O 更新内部数据结构时线程不安全

3)异步信号安全函数:如果某一函数是可重入的,或者信号处理器函数无法将其中断时,则称该函数是异步信号安全的。当信号处理器函数调用时,异步信号安全函数是安全的(所以信号处理器函数必须是****异步信号安全函数

printf不是异步信号安全的原因:诸如printf之类的标准库函数内部实现有个互斥锁,在信号处理程序中调用printf可能要出现死锁:当前逻辑执行流调用printf正好取得锁时,信号发生调用信号处理函数,如果信号处理函数也要执行打印printf,同样要获取缓冲区控制锁,但是锁已经被自己先前lock了

blog.csdn.net/littlehedge…

4)**信号处理器函数必须是****异步信号安全函数。**因此,编写信号处理器函数有如下两种选择

确保信号处理器函数代码本身可重入的,且只调用异步信号安全的函数

确保主程序对不安全函数的调用不为信号处理器函数所中断,这有些困难,所以通常采用第一种方法

21.2终止信号处理器函数的其他方法

除了返回之外,信号处理器函数的终止还存在多种其他方法,其中包括:调用_exit(),发

送信号来终止进程(kill()、raise()或 abort()),或者执行非本地跳转

21.5 系统调用的中断和重启

如果信号处理器函数中断了阻塞的系统调用,系统调用会产生 EINTR 错误。利用这种特性,就可以为阻塞的系统调用设置一个定时器。如果需要恢复系统调用,可以添加代码来重新启动这些系统调用

第24章进程的创建

24.1 fork()、exit()、wait()以及 execve()的简介

 fork()父进程创建子进程。子进程复制父进程栈、数据段、堆和执行文本段的拷贝,互不干扰

 exit终止一进程,将进程占用的所有资源(内存、文件描述符等)归还内核参数 status 为一整型变量,表示进程的退出状态。

系统调用 wait的目的有二:

一,如果子进程尚未调用 exit()终止,那么 wait()会挂起父进程直至子进程终止;

二,父进程获取子进程的终止状态,是通过 wait()的 status 参数返回的

系统调用 **execve(pathname,argv,envp)**加载一个新程序(路径名为 pathname,参数列表为 argv,环境变量列表为 envp)到当前进程的内存,丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序

24.2 创建新进程:fork()

1)完成对fork调用后将存在两个进程,且每个进程都会从 fork()的返回处继续执行

 两个进程将执行相同的程序文本段,但却各自拥有不同的栈段、数据段以及堆段拷贝。每个进程修改各自的栈数据、以及堆段中的变量不影响另一进程

程序代码则可通过 fork()的返回值来区分父、子进程。在父进程中,fork()将返回新创建子进程的进程 ID。而 fork()在子进程中则返回 0

调用 fork()之后,系统将率先调度哪个进程是无法确定的

2)父、子进程间的文件共享

执行 fork()时,子进程会获得父进程所有文件描述符的副本(类似于dup()),这也意味着父、子进程中对应的描述符均指向相同的打开文件句柄(5.4节)。打开文件句柄包含有当前文件偏移量(以及文件状态标志等),如果子进程更新了文件偏移量,那么这种改变也会影响到父进程中相应的描述符。

父子进程间共享打开文件属性的妙用:假设父子进程同时写入一文件,共享文件偏移量会确保二者不会覆盖彼此的输出内容

如果不需要这种对文件描述符的共享方式,那么在设计应用程序时可以令父、子进程使用不同的文件描述符;或各自立即关闭不再使用的描述符;如果进程之一执行了 exec(),那么 27.4 节所描述的执行时关闭功能close-on-exec)也会很有用处。

3)子进程复制父进程时的优化

共享同一代码段: 内核(Kernel)将每一进程的代码段标记为只读,从而使进程无法修改自身代码。这样,父、子进程可共享同一代码段

写时复制(copy-on-write):对于父进程数据段、堆段和栈段中的各页,内核采用写时复制(copy-on-write)技术来处理。调用 fork()之后,内核会捕获所有父进程或子进程针对这些页面的修改企图,并为将要修改的页面创建拷贝。系统将新的页面拷贝分配给遭内核捕获的进程,还会对子进程的相应页表项做适当调整。从这一刻起,父、子进程可以分别修改各自的页拷贝,不再相互影响

4)控制进程的内存需求

通过将 **fork()与 wait()**组合使用,可以控制一个进程的内存需求

24.3 系统调用 vfork() 

vfork()因为如下两个特性而更具效率,这也是其与 fork()的区别所在。

无需为子进程复制虚拟内存页或页表。相反,子进程共享父进程的内存,直至其成功

**执行了 exec()**或是调用_exit()退出;

子进程调用 exec()或_exit()之前,将暂停执行父进程

在使用时,一般应立即在vfork()之后调用exec()。除非速度绝对重要的场合,新程序应当舍 vfork()而取 fork()。

24.4 fork()之后的竞争条件

调用 fork()后,无法确定父、子进程间谁将率先访问 CPU。若确需保证某一特定执行顺序,则必须采用某种同步技术

第25章进程的终止 

25.1 进程的终止:_exit()和 exit()

1)进程有两种终止方式

异常终止,由信号的接收引发,该信号的默认动作为终止当前进程

正常终止,进程使用_exit()系统调用,_exit()的 status 参数定义了进程的终止状态,**父进程可调用 wait()**以获取该状态(26.1节)

2)库函数 exit(int status),会执行的动作如下:

调用退出处理程序(通过 atexit()和 on_exit()注册的函数)(见 25.3 节);

刷新 stdio 流缓冲区

使用由 status 提供的值执行_exit()系统调用

3)main函数执行 return n 等同于执行对 exit(n)的调用, main()的运行时会将main()的返回值作为 exit()的参数。

25.2 进程终止的细节

释放所有打开文件描述符、持有的任何文件锁等一切进程相关的资源

25.3 退出处理程序

应用程序需要在进程终止时自动执行一些操作,所以需要使用退出处理程序

退出处理程序是一个由程序设计者提供的函数,可于进程生命周期的任意时点注册,并在该进程调用 exit()正常终止时自动执行;如果程序直接调用_exit()或因信号而异常终止,则不会调用退出处理程序。

函数的执行顺序与注册顺序相反。一旦有任一退出处理程序无法返回,无论因为调用了_exit()还是进程因收到信号而终止,那么就不会再调用剩余的处理程序

通过 fork()创建的子进程会继承父进程注册的退出处理函数。而进程调用 exec()时,会移除所有已注册的退出处理程序

 atexit()或 on_exit()注册退出处理程序的两种库函数。由 atexit()注册的退出处理程序会受到两种限制:其一,退出处理程序在执行时无法获知传递给 exit()的状态status;其二,无法给**退出处理程序指定参数;**on_exit()解决了这两个问题

25.4 fork()、stdio 缓冲区以及_exit()之间的交互

stdio 缓冲区是在进程的用户空间内存中维护的,通过 fork()创建子进程时会复制这些缓冲区。当标准输出定向到终端时,默认行缓冲,所以会立即显示函数 printf()输出;当标准输出重定向到文件时,默认块缓冲,调用 fork()时,printf()输出仍在父进程的 stdio 缓冲区中,并随子进程的创建而产生一份副本。父、子进程调用 exit()时会刷新各自的 stdio 缓冲区,从而导致重复的输出结果

两种解法方案:

调用 fork()之前使用函数 fflush()来刷新 stdio 缓冲区;

在创建子进程的应用中,典型情况下仅有一个进程(一般为父进程)应通过调用 exit()终止,而其他进程应调用_exit()终止,从而确保只有一个进程调用退出处理程序并刷新 stdio 缓冲区

第26章监控子进程 

系统调用 wait()(及其变体)以及信号 SIGCHLD

26.1 等待子进程

1)系统调用 wait():

如果调用时无子进程终止,调用将一直阻塞,直至某个子进程终止

如果调用时已有子进程终止,wait()则立即返回

参数 status 所指向的缓冲区中返回该子进程的终止状态,终止子进程的 ID 作为 wait()的结果返回,出错时,wait()返回-1,可能的错误原因之一是所有子进程都已经终止

2)如何等待多个子进程:

父进程**循环调用 wait()**来监控这些子进程的终止。而直到 wait()返回-1 时才会退出循环

另一种退出循环的方法是当记录终止子进程数量的变量 numDead 与创建的子进程数目相同时,也会退出循环。

3)wait的不足:

如果创建多个子进程,使用 wait()将无法等待某个特定子进程的完成,只能按顺序等待下一个子进程的终止;

如果没有子进程退出,wait()总是保持阻塞。有时候会希望执行非阻塞的等待:是否有子进程退出,立判可知;

无法返回因信号而停止的子进程信息(25.1节,正常退出的子进程才会把status写入,从而父进程通过wait获取到)。

waitpid:优化这些不足,返回值以及参数 status 的意义相同,参数 pid 用来表示需要等待的具体子进程,参数 options 是一个位掩码增加了一项选择属性(如WNOHANG,如果参数 pid 所指定的子进程并未发生状态改变,则立即返回不会阻塞,亦即 poll轮询

4)wait的入参指针status

该状态表明子进程是正常终止(带有表示成功或失败的退出状态);还是异常中止,因收到

某个信号而停止;还是因收到 SIGCONT 信号而恢复执行

26.2 孤儿进程与僵尸进程

****孤儿进程:父进程先终止,它的子进程就成了孤儿进程,孤儿进程的父进程就自动变成init进程

僵尸进程:

父进程执行 wait()之前,其子进程就已经终止,系统仍然允许其父进程在之后的某一时刻去执行 wait(),以确定该子进程是如何终止的。内核通过将子进程转为僵尸进程,释放子进程的大部分资源,只保留内核进程表中的一条记录,其中包含了子进程ID、终止状态、资源使用数据等信息。 

父进程执行 wait()后,由于不再需要子进程所剩余的最后信息,故而内核将删除僵尸进****程。如果父进程未执行 wait()随即退出,那么 init 进程将接管子进程并自动调用 wait(),从而从系统中移除僵尸进程

在设计长生命周期的父进程时应执行 wait()方法,以确保系统总是能够清理那些死去的子进程,避免使其成为长寿僵尸占用过多内核资源

26.3 SIGCHLD 信号

由26.2知,父进程应使用 wait()来防止僵尸子进程的累积,但是反复调用非阻塞的 wait会造成 CPU 资源的浪费

为了规避这些问题,捕获终止子进程一般方法是为信号 SIGCHLD 设置信号处理程序:

无论一个子进程于何时终止,都会向其父进程发送 SIGCHLD 信号。对该信号的默认处理是将其忽略,不过也可以安装信号处理程序来捕获它。

SIGCHILD 信号处理程序正在为一个终止的子进程运行时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一个。(20.10 节和 20.12 节)解决方案是:在 SIGCHLD 处理程序内部循环以 WNOHANG 标志(26.1节)来调waitpid直至暂时再无其他终止的子进程需要处理为止

为保障可移植性,应在创建任何子进程之前就设置好 SIGCHLD 处理程序**,防止处理程序执行前就有子进程退出**(此时子进程SIGCHLD 的信号会丢失)

第27章程序的执行

27.1 执行新程序:execve() 

系统调用 execve()可以将新程序加载到某一进程的内存空间。在这一操作过程中,将丢弃旧有程序,而进程的栈、数据以及堆段会被新程序的相应部件所替换,新程序会从 main()函数处开始执行

execve()

参数 pathname 包含准备载入当前进程空间的**新程序(如编译后的二进制文件、脚本)**的路径名

参数 argv 则指定了传递给新进程的命令行参数

参数 envp 指定了新程序的环境列表

调用 execve()之后,因为同一进程依然存在,所以进程 ID 仍保持不变。对 execve()的成功调用将永不返回

27.3 解释器和脚本

解释器:能够读取并执行命令脚本文件)的程序。(编译器则是将输入源代码译为可在真实或虚拟机器上执行的机器语言)

脚本(一种特殊的文件,文件内容是命令)必须满足下面两点要求:必须赋予脚本文件可执行权限;文件的起始行必须指定运行脚本解释器的路径名(/bin/sh)

脚本执行的过程:程序/进程调用 execve()来运行脚本,execve()检测到传入的文件以两字节序列“#!”开始,就会析取该行的剩余部分(路径名以及参数),然后按参数列表来调用执行解释器程序

27.4 文件描述符与 exec()

默认情况下,由 exec()的调用程序所打开的所有文件描述符在 exec()的执行过程中会保持打开状态,且在新程序中依然有效

内核为每个文件描述符提供了执行时关闭标志close-on-exec)。如果设置了这一标志,那么在成功执行 exec()时,会自动关闭该文件描述符,如果调用 exec()失败,文件描述符则会保持打开状态

27.6 执行 shell 命令:system()

程序可通过调用 system()函数来执行任意的 shell 命令(效率低)

使用 system()运行命令需要创建至少两个进程。一个用于运行 shell,另外一个或多个则用于 shell 所执行的命令(执行每个命令都会调用一次 exec())。如果对效率或者速度有所要求,最好还是直接调用 fork()和 exec()来执行既定程序。

27.7 system()的实现

使用 fork()来创建一个子进程,将shell 命令作为参数来调用 execl();为了收集 system()所创建的子进程状态,还以指定的子进程 ID 调用了waitpid()(使用 wait()并不合适,因为 wait()等待的是任一子进程)

第28章详述进程创建和程序执行  

28.2 系统调用 clone() 

clone() 与 fork()不同的是,克隆生成的子进程继续运行时不以调用处为起点,转而去调用以参数 func 所指定的函数,func 又称为子函数

clone() 在进程创建期间对步骤的控制更为精准,主要用于线程库的实现。由于clone()有损于程序的可移植性,故而应避免在应用程序中直接使用

28.4 exec()和 fork()对进程属性的影响

尽管 vfork()要快于 fork(),但较之于子进程随后调用 exec()所耗费的时间,二者间的时间差异也就微不足道了

第29章线程:介绍 

29.1 概述

同一进程(程序)中的所有线程均会独立执行相同程序共享相同的堆变量,但每个线程都配有用来存放局部变量的私有栈。同一进程中的线程还共享一干其他属性,包括进程 ID、打开的文件描述符、信号处置、当前工作目录以及资源限制

在多处理器环境下,多个线程可以同时并行。如果一线程因等待 I/O 操作而遭阻塞,那么其他线程依然可以继续运行

线程相对进程的优点

进程间通信困难,除去只读代码段外,父子进程并未共享内存,因此必须采用进程间通信,较为繁琐。而线程之间能够方便、快速地共享信息,将数据复制到共享(全局或堆)变量中即可,不过要注意线程安全问题

创建线程比创建进程通常要 10 倍,调用 fork()来创建进程的代价相对较高

29.3 创建线程

启动程序时,产生的进程只有单条线程,称之为初始或主线程

pthread_create()负责创建一条新线程。新线程参数start函数 处开始执行。调用 pthread_create()的线程会继续执行该调用之后的语句(原理如28.2节clone调用),将经强制转换的整型数作为线程 start 函数的返回值时,必须小心谨慎,防止与取消线程(见第 32 章)时的返回值 PTHREAD_CANCELED冲突导致信息误解

应用程序无从确定系统接着会调度哪一个线程来使用 CPU 资源,如果对执行顺序确有强制要求,那么就必须采用第 30 章所描述的同步技术

29.4 终止线程

1)终止线程的运行的方式:

线程 start 函数执行 return 语句并返回指定值;

线程调用 pthread_exit()

调用 **pthread_cancel()**取消线程;

任意线程调用了 exit(),或者主线程执行了 return 语句(在 main()函数中),导致进程中的所有线程立即终止

2)pthread_exit()函数将终止调用线程,且其返回值可由另一线程通过调用 **pthread_join()**来获取

29.5 线程 ID

在 Linux 的线程实现中,线程 ID 在所有进程中都是唯一

29.6 连接(joining)已终止的线程

函数 pthread_join()等待由 thread 标识的**线程终止,并获取返回值,这称之为线程连接。**pthread_join()只能连接特定线程 ID

若线程并未分离(见 29.7 节),则必须使用 ptherad_join()来进行连接。如果未连接,那么线程终止时将产生僵尸线程,与僵尸进程的概念相类似

如向 pthread_join()传入一个之前已然连接过的线程 ID,将会导致无法预知的行为(不能重复join

pthread_join()执行的功能类似于针对进程的 waitpid()调用,不过二者之间存在差别

1)线程之间的关系是对等的,进程中的任意线程均可以调用 pthread_join()与该进程的任何其他线程连接起来,与进程间的层次关系不同(只有父进程能wait子进程)

2)无法“连接任意线程”,也只能以阻塞方式进行连接

29.7 线程的分离

有时候并不关心线程的返回状态只是希望系统在线程终止时能够自动清理并移除之。则可以调用 pthread_detach()并向 thread 参数传入指定线程标识符,将该线程处于分离状态,无需join也不会成为僵尸线程

一旦线程处于分离状态,就**不能再使用 pthread_join()来获取其状态,也无法使其重返“**可连接”状态

其他线程调用了 exit(),或是主线程执行 return 语句时,即便遭到分离的线程也还是会受到影

第30章线程:线程同步

互斥量可以防止多个线程同时修改共享变量

条件变量则是允许线程相互通知共享变量的状态发生了变化

30.1 保护对共享变量的访问:互斥量

1)必须确保多个线程不会同时修改同一共享变量,或者某一线程不会读取正由其他线程修改的共享变量。多线程只允许同时读,不允许同时写和边读边写

临界区是指访问某一共享资源(共享变量)的代码片段,并且这段代码的执行应为原子操作

2)使用互斥量来保证对任意共享资源的原子访问

加锁:任何时候至多只有一个线程可以锁定该互斥量。多个线程等待解锁后争夺锁,无法判断究竟哪个线程将获取锁

解锁:只有所有者才能给互斥量解锁

3)pthread_mutex_t 类型:

函数 pthread_mutex_lock()可以锁定某一互斥量。如果互斥量当前处于未锁定状态,该调用将锁定互斥量并立即返回。如果其他线程已经锁定了这一互斥量,那么lock()调用会一直堵塞直至该互斥量被解锁,所有调用lock的线程将争夺互斥量,只有一个线程将锁定互斥量并返回

函数 **pthread_mutex_unlock()**则可以将一个互斥量解锁

三个使用原则:

同一线程不应对同一互斥量加锁两次;

线程不应对不为自己所拥有的互斥量解锁;

线程不应对一尚未锁定的互斥量做解锁动作

4)线程对互斥量的持有时间应尽可能,以减小影响其他线程的并发执行

5)每个线程都成功地锁住一个互斥量,接着试图对已为另一线程锁定的互斥量加锁,两个线程将无限期地等待下去发生死锁

要避免此类死锁问题,最简单的方法是所有线程以相同顺序对该组互斥量进行锁定;另一种方案的使用频率较低,就是尝试一下,然后释放已经占有的锁

30.2 通知状态的改变:条件变量

1)条件变量允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待这一通知。条件变量总是要与一个互斥量****搭配使用

2)条件变量的主要操作是发送信号(signal)和等待(wait)

发送信号:通知其他处于等待状态的线程,某个共享变量的状态已经改变

等待:线程收到通知前一直处于阻塞状态

3)条件变量的数据类型是 pthread_count_t

函数 pthread_cond_signal()、pthread_cond_broadcast():由参数 cond 所指定的条件变量而发送信号

pthread_cond_wait()函数将阻塞一线程,直至收到条件变量 cond 的通知(两个入参,条件变量cond和互斥量)

4)函数 pthread_cond_signal()和 pthread_cond_broadcast()之间的差别在于,pthread_cond_signal()函数只保证唤醒至少一条遭到阻塞的线程; pthread_cond_broadcast()则会唤醒所有遭阻塞的线程。

pthread_cond_signal()会更为高效,因为所有线程被唤醒后都会执行互斥量的检查,然而只有一个线程拿到加锁权

5)条件变量总是要与一个互斥量相关,将互斥量通过函数参数传递给pthread_cond_wait()

pthread_cond_wait执行步骤(前两部是原子操作,因为要保证调用线程进入阻塞之前其他线程不能获取到该互斥量,从而无法就该条件变量发出信号):

解锁互斥量;unlock

堵塞调用线程直至另一线程通过条件变量发出信号;wait

收到条件变量 cond 发出信号后,争夺锁定互斥量;lock

6)生产者消费者模型

互斥量+条件变量+共享变量

生产者:lock,do sth,unlock, signal

signal不能放unlock前,否则不释放锁唤醒消费者必失败,损耗性能

消费者:lock,while check wait,do sth,unlock

消费者必须由一个 while循环,而不是 if 语句,来控制对 pthread_cond_wait()的调用。这是因为,当代码从 pthread_cond_wait()返回时,并不能确定共享变量的状态,所以应该立即重新检查判断条件,在条件不满足的情况下继续休眠等待

**7)**示例程序:连接任意已终止线程

第31章线程:线程安全和每线程存储

线程特有数据和线程局部存储:在无需改变函数接口定义的情况下保障不安全函数的线程安全

31.1 线程安全(再论可重入性)

若函数可同时供多个线程安全调用,则称之为线程安全函数;反之,如果函数不是线程安全的,则不能并发调用

导致线程不安全的典型原因:使用了在所有线程之间共享的全局或静态变量

解决方案:将共享变量与互斥量关联起来;避免使用全局或静态变量

31.2 一次性初始化

库函数可以通过函数 pthread_once()实现一次性初始化,即不管创建了多少线程,有些初始化动作只能发生一次

参数 :once_control和 init 调用者定义函数。对该函数的首次调用修改once_control 所指向的内容,以便对其后续调用不会再次执行 init

常常将 Pthread_once()和线程特有数据结合使用

31.3 线程特有数据

线程特有数据技术,可以无需修改函数接口实现已有函数的线程安全。较之于可重入函数,采用线程特有数据的函数效率可能要略低一些

blog.csdn.net/cywosp/arti…

原理:

一个线程内部各个函数调用都能访问、但其它线程不能访问的变量,这就是线程特有数据

针对某个变量,每个线程数据创建一个**键(即每个线程针对这个变量有一份独立拷贝),**线程内部各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。(图31-3)

线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。

调用函数pthread_key_create()可创建此“键”,且只需在首个调用该函数的线程中创建一次,函数pthread_once()的使用正是出于这一目的;函数会为每个调用者线程创建线程特有数据块,每个线程只分配一次,且只会在线程初次调用此函数时分配

第32章线程:线程取消 

32.1 取消一个线程

pthread_cancel()向由 thread 指定的线程发送一个取消请求。发出取消请求后,函数 pthread_cancel()当即返回不会等待目标线程的退出

32.2 取消状态及类型

函数 pthread_setcancelstate()和 pthread_setcanceltype()设定取消类型

PTHREAD_CANCEL_DISABLE:线程不可取消

PTHREAD_CANCEL_ENABLE:线程可以取消。这是新建线程取消性状态的默认值

PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时点(也许是立即取消,但不一定)取消线程

PTHREAD_CANCEL_DEFERED:取消请求保持挂起状态,直至到达取消点才取消线程(32-3节)

32.3 取消点

取消点即是对由实现定义的一组函数之一加以调用,若将线程的取消类型置为PTHREAD_CANCEL_DEFERED,仅当线程运行抵达某个取消点时,取消请求才会起作用,该线程终止

如果该线程尚未分离,那么为防止其变为僵尸线程,必须由其他线程对其进行连接。连接之后,当线程终止后,返回至函数 pthread**_join()中第二个参数的将是PTHREAD_CANCELED**

32.5 清理函数

线程可以设置一个或多个清理函数,当线程遭取消时会自动运行这些函数,在线程终止之前可执行诸如修改全局变量,解锁互斥量等动作

每个线程都可以拥有一个清理函数栈。当线程遭取消时,会沿该栈自顶向下依次执行清理函数,首先会执行最近设置的函数。

函数 pthread_cleanup_push()和 pthread_cleanup_pop()分别负责向调用线程的清理函数栈添加和移除清理函数。

线程如在执行时收到取消调用,当运行到**取消点(如sleep,pthread_cond_wait等)**时,自动执行之前pthread_cleanup_push传入的清理函数

一般来说,如果线程顺利执行完这段代码而未遭取消,那么就不再需要清理,此时则调用 pthread_cleanup_pop()。参数 execute为零清理函数栈中移除最顶层的函数,如果清除的同时还要执行清理函数,execute传非零

所以,每个对 pthread_cleanup_push()的调用都会伴随着对 pthread_cleanup_pop()的调用,在代码中一一对应

32.6 异步取消

线程的取消类型为PTHREAD_CANCEL_ASYNCHRONOUS

异步取消的问题在于,尽管清理函数依然会得以执行,但处理函数却无从得知线程的具体状态

作为一般性原则,可异步取消的线程不应该分配任何资源,也不能获取互斥量或锁

第33章线程:更多细节

不要将线程与信号混合使用,只要可能多线程应用程序的设计应该避免使用信号

33.1 线程栈

创建线程时,每个线程都有一个属于自己的线程栈,且大小固定。在 Linux/x86-32 架构上,

除主线程外的所有线程,其栈的缺省大小均为 2MB。