I/O | 文件系统 | 进程 | 线程 | 进程间通信 |
---|---|---|---|---|
一.文件I/O 三.标准I/O库 八.高级I/O |
二.文件和目录 | 四.进程环境 五.进程控制 |
六.线程 七.线程控制 |
九.进程间通信 十.信号 |
* [一.文件I/O](#一文件io) - [1.文件描述符](#1文件描述符) - [2.相关调用](#2相关调用) + [2.1 打开文件](#21-打开文件) + [2.2 创建文件](#22-创建文件) + [2.3 关闭文件](#23-关闭文件) + [2.4 定位读写位置](#24-定位读写位置) + [2.5 文件读](#25-文件读) + [2.6 文件写](#26-文件写) + [2.7 fcntl函数](#27-fcntl函数) - [3.进程间文件共享](#3进程间文件共享) - [4.原子操作](#4原子操作) - [5.数据同步](#5数据同步) * [二.文件和目录](#二文件和目录) - 前言:[文件系统](#文件系统) - [1.获取文件信息](#1获取文件信息)(stat、fstat、lstat、fstatat) - [2.文件类型](#2文件类型) - [3.用户ID和组ID](#3用户id和组id) + [3.1 进程与文件的用户ID及组ID](#31-进程与文件的用户id及组id) + [3.2 修改文件的所有者和组所有者](#32-修改文件的所有者和组所有者)(chown、fchown、fchownat、lchown) - [4.文件访问权限](#4文件访问权限) + [4.1 进程的文件访问权限](#41-进程的文件访问权限)(access、faccessat) + [4.2 新文件的文件访问权限](#42-新文件的文件访问权限)(umask) + [4.3 修改文件访问权限](#43-修改文件访问权限)(chmod、fchmod、fchmodat) - [5.新文件和目录的所有权](#5新文件和目录的所有权) - [6.粘着位](#6粘着位) - [7.文件长度](#7文件长度) + [7.1 文件中的空洞](#71-文件中的空洞) + [7.2 文件截断](#72-文件截断)(truncate、ftruncate) - [8.硬链接](#8硬链接) + [8.1 创建硬链接](#81-创建硬链接)(link、linkat) + [8.2 删除硬链接](#82-删除硬链接)(unlink、unlinkat) - [9.符号链接](#9符号链接) + [9.1 创建符号链接](#91-创建符号链接)(symlink、symlinkat) + [9.2 读取符号链接](#92-读取符号链接)(readlink、readlinkat) - [10.目录](#10目录) + [10.1 创建目录](#101-创建目录)(mkdir、mkdirat) + [10.2 删除目录](#102-删除目录)(rmdir) + [10.3 读目录](#103-读目录)(opendir、fdopendir、readdir、rewinddir、closedir、telldir、seekdir) + [10.4 更改当前目录](#104-更改当前目录)(chdir、fchdir) + [10.5 获取当前目录的绝对路径](#105-获取当前目录的绝对路径)(getcwd) - [11.重命名](#11重命名)(rename、renameat) - [12.文件的时间](#12文件的时间) + [12.1 更改文件的访问和修改时间](#121-更改文件的访问和修改时间)(futimens、utimensat、utimes) - [13.设备特殊文件](#13设备特殊文件) * [三.标准I/O库](#三标准io库) - [1.流](#1流) + [1.1 流的定向](#11-流的定向)(fwide) + [1.2 3个标准流](#12-3个标准流) - [2.FILE对象](#2file对象) - [3.缓冲](#3缓冲) + [3.1 3种缓冲类型](#31-3种缓冲类型)(setbuf、setvbuf) + [3.2 缓冲区冲洗](#32-缓冲区冲洗)(fflush) + [3.3 标准流与缓冲](#33-标准流与缓冲) - [4.相关调用](#4相关调用) + [4.1 打开流](#41-打开流)(fopen、freopen、fdopen) + [4.2 关闭流](#42-关闭流) + [4.3 读写流](#43-读写流) + [4.4 定位流](#44-定位流) + [4.5 格式化I/O](#45-格式化io) + [4.6 获取流相应的文件描述符](#46-获取流相应的文件描述符) + [4.7 创建临时文件](#47-创建临时文件) - [5.内存流](#5内存流) * [四.进程环境](#四进程环境) - [1.进程的启动与终止](#1进程的启动与终止) + [1.1 main函数](#11-main函数) + [1.2 进程终止的方式](#12-进程终止的方式) + [1.3 终止函数](#13-终止函数) + [1.4 终止状态](#14-终止状态) + [1.5 登记终止处理程序](#15-登记终止处理程序) - [2.环境表](#2环境表) + [2.1 获取环境变量](#21-获取环境变量) + [2.2 修改环境变量](#22-修改环境变量) - [3.C程序的存储空间布局](#3c程序的存储空间布局) - [4.共享库](#4共享库) - [5.进程堆空间的管理](#5进程堆空间的管理) - [6.进程资源限制](#6进程资源限制) - [7.Core Dump](#7core-dump)(外加) + [7.1 Core Dump的概念及用途](#71-core-dump的概念及用途) + [7.2 产生Core Dump](#72-产生core-dump) + [7.3 调试Core Dump](#73-调试core-dump) * [五.进程控制](#五进程控制) - [1.进程标识](#1进程标识) + [1.1 2个特殊进程](#11-2个特殊进程) + [1.2 进程相关的ID获取函数](#12-进程相关的id获取函数) - [2.进程的创建](#2进程的创建) + [2.1 fork](#21-fork) + [2.2 vfork](#22-vfork) - [3.进程的终止](#3进程的终止) + [3.1 子进程向父进程传递状态](#31-子进程向父进程传递状态) + [3.2 父子进程以不同顺序终止](#32-父子进程以不同顺序终止) - [4.竞争条件](#4竞争条件) - [5.exec函数](#5exec函数) - [6.更改用户ID和更改组ID](#6更改用户id和更改组id) - [7.system函数](#7system函数) - [8.用户标识](#8用户标识) - [9.进程调度](#9进程调度) - [10.进程时间](#10进程时间) * [六.线程](#六线程) - [1.相关函数](#1相关函数) + [pthread_create函数](#1pthread_create函数) + [pthread_join函数](#2pthread_join函数) + [pthread_self函数](#3pthread_self函数) + [pthread_detach函数](#4pthread_detach函数) + [pthread_exit函数](#5pthread_exit函数) + [pthread_equal函数](#6pthread_equal函数) + [pthread_cancel函数](#7pthread_cancel函数) + [pthread_cleanup_push和pthread_cleanup_pop函数](#8pthread_cleanup_push和pthread_cleanup_pop函数) - [2.线程同步](#2线程同步) + [2.1 互斥锁](#21-互斥锁) + [2.2 读写锁](#22-读写锁) + [2.3 条件变量](#23-条件变量) + [2.4 自旋锁](#24-自旋锁) + [2.5 屏障](#25-屏障) * [七.线程控制](#七线程控制) - [1.线程限制](#1线程限制) - [2.线程属性](#2线程属性) + [2.1 线程属性](#21-线程属性) + [2.2 取消选项](#22-取消选项) - [3.同步属性](#3同步属性) + [3.1 互斥锁属性](#31-互斥锁属性) + [3.2 读写锁属性](#32-读写锁属性) + [3.3 条件变量属性](#33-条件变量属性) + [3.4 屏障属性](#34-屏障属性) - [4.线程特定数据](#4线程特定数据) + [pthread_once和pthread_key_create函数](#1pthread_once和pthread_key_create函数) + [pthread_getspecific和pthread_setspecific函数](#2pthread_getspecific和pthread_setspecific函数) + [pthread_key_delete函数](#3pthread_key_delete函数) - [5.线程和信号](#5线程和信号) + [5.1 阻止信号发送](#51-阻止信号发送) + [5.2 等待信号](#52-等待信号) - [6.线程和fork](#6线程和fork) - [7.线程和I/O](#7线程和io) * [八.高级I/O](#八高级io) - [1.非阻塞I/O]( https://github.com/arkingc/note/blob/master/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/UNIX%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%8D%B71.md#%E4%B9%9D%E9%9D%9E%E9%98%BB%E5%A1%9E%E5%BC%8Fio) - [2.记录锁](#2记录锁) - [3.I/O复用]( https://github.com/arkingc/note/blob/master/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/UNIX%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%8D%B71.md#%E5%9B%9Bio%E5%A4%8D%E7%94%A8) - [4.异步I/O](#4异步io) + [4.1 AIO控制块](#41-aio控制块) + [4.2 异步读与异步写](#42-异步读与异步写) + [4.3 获取异步I/O的状态](#43-获取异步io的状态) + [4.4 获取异步I/O返回值](#44-获取异步io返回值) + [4.5 阻塞进程到异步I/O完成](#45-阻塞进程到异步io完成) + [4.6 取消异步I/O](#46-取消异步io) + [4.7 批量提交异步I/O请求](#47-批量提交异步io请求) + [4.8 异步I/O的数量限制](#48-异步io的数量限制) - [5.readv与writev]( https://github.com/arkingc/note/blob/master/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/UNIX%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%8D%B71.md#2readv%E5%92%8Cwritev%E5%87%BD%E6%95%B0) - [6.存储映射I/O](#6存储映射io) + [6.1 mmap建立映射](#61-mmap建立映射) + [6.2 mprotect修改映射区权限](#62-mprotect修改映射区权限) + [6.3 msync冲洗映射区](#63-msync冲洗映射区) + [6.4 munmap解除映射](#64-munmap解除映射) * [九.进程间通信](#十进程间通信) - [1.管道](#1管道) + [1.1 创建管道](#11-创建管道) + [1.2 管道的读写规则](#12-管道的读写规则) + [1.3 标准I/O库管道函数](#13-标准io库管道函数) - [2.协同进程](#2协同进程) - [3.FIFO](#3fifo) + [3.1 创建FIFO](#31-创建fifo) + [3.2 打开FIFO](#32-打开fifo) + [3.3 读写FIFO](#33-读写fifo) - [4.XSI IPC](#4xsi-ipc) + [5.消息队列](#5消息队列) * [5.1 与消息队列相关的结构](#51-与消息队列相关的结构) * [5.2 创建或打开消息队列](#52-创建或打开消息队列) * [5.3 操作消息队列](#53-操作消息队列) * [5.4 添加消息](#54-添加消息) * [5.5 获取消息](#55-获取消息) + [6.信号量](#6信号量) * [6.1 信号量的相关结构](#61-信号量的相关结构) * [6.2 获得信号量](#62-获得信号量) * [6.3 操作信号量](#63-操作信号量) + [7.共享存储](#7共享存储) * [7.1 共享存储的内核结构](#71-共享存储的内核结构) * [7.2 创建或获得共享存储](#72-创建或获得共享存储) * [7.3 操作共享存储](#73-操作共享存储) * [7.4 与共享存储段连接](#74-与共享存储段连接) * [7.5 与共享存储段分离](#75-与共享存储段分离) - [8.POSIX信号量](#8posix信号量) + [8.1 创建或获取命名信号量](#81-创建或获取命名信号量) + [8.2 关闭释放信号量](#82-关闭释放信号量) + [8.3 销毁命名信号量](#83-销毁命名信号量) + [8.4 调节信号量的值](#84-调节信号量的值) + [8.5 创建未命名信号量](#85-创建未命名信号量) + [8.6 销毁未命名信号量](#86-销毁未命名信号量) + [8.7 检索未命名信号量的值](#87-检索未命名信号量的值)
一.文件I/O
本章描述的函数经常被称为不带缓冲的I/O,不带缓冲值的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的I/O函数不是ISO C的组成部分,它们是POSIX.1和Single UNIX Specification的组成部分
1.文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负数。范围是0~OPEN_MAX-1。当进程创建时,默认为它打开了3个文件描述符,它们都链接向终端:
- 0: 标准输入
- 1: 标准输出
- 2: 标准错误输出
通常使用STDIN_FILENO
,STDOUT_FILENO
和 STDERR_FILENO
来替代这三个幻数,从而提高可读性。这三个常量位于
中
复制文件描述符
这些函数返回的新文件描述符与参数fd共享同一个文件表项:
2.相关调用
2.1 打开文件
**以上五个常量必须且只能指定一个。下面的常量是可选的(进行或运算)**:
* `O_APPEND`:每次写时都追加到文件的尾端
* `O_CLOEXEC`:将`FD_CLOEXEC`常量设置为文件描述符标志
* `O_CREAT`:若文件不存在则创建。使用此选项时,需要同时说明参数`mode`(指定该文件的访问权限)
* `O_DIRECTORY`:若`path`引用的不是目录,则出错
* `O_EXCL`:若同时指定了`O_CREAT`时,且文件已存在则出错。根据此可以测试一个文件是否存在。若不存在则创建此文件。这使得测试和创建两者成为一个原子操作
* `O_NOCTTY`:若`path`引用的是终端设备,则不将该设备分配作为此进程的控制终端
* `O_NOFOLLOW`:若`path`引用的是一个符号链接,则出错
* `O_NONBLOCK`:如果`path`引用的是一个`FIFO`、一个块特殊文件或者一个字符特殊文件,则文件本次打开操作和后续的 I/O 操作设为非阻塞模式
* `O_SYNC`:每次 `write` 等待物理 I/O 完成,包括由 `write` 操作引起的文件属性更新所需的 I/O
* `O_TRUNC`: 如果此文件存在,且为`O_WRONLY`或者`O_RDWR`成功打开,则将其长度截断为0
* `O_RSYNC`:使每一个`read`操作等待,直到所有对文件同一部分挂起的写操作都完成
* `O_DSYNC`:每次 `write` 等待物理 I/O 完成,但不包括由 `write` 操作引起的文件属性更新所需的 I/O
* `mode`:文件访问权限。文件访问权限常量在 `` 中定义,有下列九个:
* `S_IRUSR`:用户读
* `S_IWUSR`:用户写
* `S_IXUSR`:用户执行
* `S_IRGRP`:组读
* `S_IWGRP`:组写
* `S_IXGRP`:组执行
* `S_IROTH`:其他读
* `S_IWOTH`:其他写
* `S_IXOTH`:其他执行
对于openat
函数,被打开的文件名由fd
和path
共同决定:
- 如果
path
指定的是绝对路径,此时fd
被忽略。openat
等价于open
- 如果
path
指定的是相对路径名,则fd
是一个目录的文件描述符。被打开的文件的绝对路径由该fd
描述符对应的目录加上path
组合而成 - 如果
path
是一个相对路径名,而fd
是常量AT_FDCWD
,则path
相对于当前工作目录。被打开文件在当前工作目录中查找。
由 open/openat
返回的文件描述符一定是最小的未使用的描述符数字
2.2 创建文件
该函数等价于open(path,O_WRONLY|O_CREAT|O_TRUNC,mode)
creat
的存在一个不足是:它以只写方式打开创建的文件。如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用create
、close
,然后再调用open
,新版本open
出来后,可以以下列方式实现:
open(path,O_RDWR|O_CREAT|O_TRUNC,mode)
2.3 关闭文件
- 进程关闭一个文件会释放它加在该文件上的所有记录锁
- 当一个进程终止时,内核会自动关闭它所有的打开的文件
2.4 定位读写位置
每个打开的文件都有一个与其关联的“当前文件偏移量”。它通常是个非负整数,用于度量从文件开始处计算的字节数。通常读、写操作都从当前文件偏移量处开始,并且使偏移量增加所读写的字节数。注意:
- 打开一个文件时,除非指定
O_APPEND
选项,否则系统默认将该偏移量设为0 - 如果文件描述符指定的是一个管道、FIFO、或者网络套接字,则无法设定当前文件偏移量,则
lseek
将返回 -1 ,并且将errno
设置为ESPIPE
- 对于普通文件,其当前文件偏移量必须是非负值。但是某些设备运行负的偏移量出现。因此比较
lseek
的结果时,不能根据它小于0 就认为出错。要根据是否等于 -1 来判断是否出错 lseek
并不会引起任何 I/O 操作,lseek
仅仅将当前文件的偏移量记录在内核中- 当前文件偏移量可以大于文件的当前长度。此时对该文件的下一次写操作将家常该文件,并且在文件中构成一个空洞。空洞中的内容位于文件中但是没有被写过,其字节被读取时都被读为0(文件中的空洞并不要求在磁盘上占据存储区。具体处理方式与操作系统有关)
2.5 文件读
读操作从文件的当前偏移量开始,在成功返回之前,文件的当前偏移量会增加实际读到的字节数
有多种情况可能导致实际读到的字节数少于期望读到的字节数:
- 读普通文件时,在读到期望字节数之前到达了文件尾端
- 当从终端设备读时,通常一次最多读取一行(终端默认是行缓冲的)
- 当从网络读时,网络中的缓存机制可能造成返回值小于期望读到的字节数
- 当从管道或者
FIFO
读时,若管道包含的字节少于所需的数量,则read
只返回实际可用的字节数 - 当从某些面向记录的设备(如磁带)中读取时,一次最多返回一条记录
- 当一个信号造成中断,而已读了部分数据时
2.6 文件写
write
的返回值通常都是与nbytes
相同。否则表示出错(出错的一个常见原因是磁盘写满,或者超过了一个给定进行的文件长度限制)
对于普通文件,写操作从文件的当前偏移量处开始。如果打开文件时指定了O_APPEND
选项,则每次写操作之前,都会将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数
2.7 fcntl函数
fcntl可以改变已经打开文件的属性
3.进程间文件共享
- 内核为每个进程分配一个进程表项(所有进程表项构成进程表),进程表项中都有一个打开的文件描述符表。每个文件描述符占用一项,其内容为:
- 文件描述符标志
- 指向一个文件表项的指针
- 内核为每个打开的文件分配一个文件表项(所有的文件表项构成文件表)。每个文件表项的内容包括:
- 文件状态标志(读、写、添写、同步和阻塞等)
- 当前文件偏移量
- 指向该文件 v 结点表项的指针
- 每个打开的文件或者设备都有一个 v 结点结构。 v 结点结构的内容包括:
- 文件类型和对此文件进行各种操作函数的指针
- 对于大多数文件, v 结点还包含了该文件的 i 结点 这些信息都是在打开文件时从磁盘读入内存的。如 i 结点包含了文件的所有者、文件长度、指向文件实际数据在磁盘上所在位置的指针等等。 v 结点结构和 i 结点结构实际上代表了文件的实体(Linux没有使用v节点,而是使用了通用i节点结构。虽然两种实现有所不同,但在概念上,v节点与i节点是一样的,两者都指向文件系统特有的i节点结构)
- 这两个文件表项指向同一个 v 结点表项
- 进程 A 、B 各自的文件描述符表项分别指向这两个文件表项(因此每个进程都有自己的对该文件的当前偏移)
对文件的操作结果:
- 每次
write
之后,在文件表项中的当前文件偏移量即增加所写入的字节数- 若这导致当前文件偏移量超过当前文件长度,则修改 i 节点的当前文件长度,设为当前文件偏移量
- 如果用
O_APPEND
标志打开一个文件,则相应标志也设置到文件表项的文件状态标志中- 每次对具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先被置为 i 结点中的文件长度
- 若用
lseek
定位到文件当前的尾端,则文件表项中的当前文件偏移量设置为 i 结点中的文件长度lseek
函数只是修改文件表项中的当前文件偏移量,不进行任何 I/O 操作
4.原子操作
多个进程写同一文件时,可能产生预想不到的结果。为了避免这种情况,需要理解原子操作
多个进程同时追加写一个文件时,如果通过 lseek
到末尾然后 write
可能出现问题:因为 “先定位到文件尾端,然后写” 并不是一个原子操作,如果先执行两个进程的 lseek
,然后执行 write
,就会出现问题
UNIX提供了一种原子操作方法:通过 O_APPEND
选项打开文件。这样做使得内核在每次调用 write
执行写操作之前,都将进程的当前偏移量设置到该文件的末尾,于是就不需要执行lseek
定位操作
定位读和定位写
调用pread
相当于先调用lseek
再调用read
.但是调用pread
时,无法中断其定位和读操作,并且不更新当前文件偏移量
调用pwrite
相当于先调用lseek
再调用write
.但是调用pwrite
时,无法中断其定位和写操作,并且不更新当前文件偏移量
5.数据同步
UNIX操作系统在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘 I/O 都通过缓冲区进行。当我们想文件写入数据时,内核通常都先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式称为延迟写
以下2种情况会将缓冲区中的数据写回到磁盘:
- 当内核需要重用缓冲区来存放其他数据时,它会把所有延迟写的数据库写入磁盘
- 可以调用
sync
、fsync
或fdatasync
来显式的将所有延迟写的数据块写回磁盘
3个函数的区别:
sync
(update
守护进程会周期性(一般每隔30s)的调用sync
函数。命令sync
也会调用sync
函数):- 将所有修改过的块缓冲区排入写队列,然后返回
- 它并不等待实际写磁盘操作结束
fsync
:- 只对由
fd
指定的单个文件起作用 - 等待写磁盘操作结束才返回
- 只对由
fdatasync
:- 只对由
fd
指定的单个文件起作用,但是它只影响文件的数据部分(fsync
会同时更新文件的属性) - 等待写磁盘操作结束才返回
- 只对由
二.文件和目录
文件系统
可以把一个磁盘分成一个或多个分区。每个分区可用包含一个文件系统
inode是固定长度的记录项,包含有关文件的大部分信息
下图为在一个目录下创建一个目录testdir,注意testdir所在目录,以及新建目录testdir的inode链接数:
文件信息结构用一个结构体stat
表示,实际定义可能随具体实现有所不同,但基本形式是:
struct stat{
mode_t st_mode; /*文件模式字,包含有文件类型、ID和读写权限位信息*/
ino_t st_ino; /* inode号 */
dev_t st_dev; /* 设备号(文件系统) */
dev_t st_rdev; /* 特殊文件的设备号 */
nlink_t st_nlink; /* 链接数 */
uid_t st_uid; /* 所有者的用户ID */
gid_t st_gid; /* 组所有者的ID */
off_t st_size; /* 字节大小,用于一般文件 */
struct timespec st_atime; /* 最后一次访问时间 */
struct timespec st_mtime; /* 最后一次修改时间 */
struct timespec st_ctime; /* 最后一个文件状态改变的时间 */
blksize_t st_blksize; /* 磁盘块(block)大小 */
blkcnt_t st_blocks; /* 分配的磁盘块(block)数量 */
};
POSIX.1未要求
st_rdev
、st_blksize
和st_blocks
字段。Single UNIX Specification XSI扩展定义了这些字段
timespec
结构类型按照秒和纳秒定义了时间,至少包括下面2个字段:
time_t tv_sec;
long tv_nsec;
使用
stat
函数最多的地方可能就是ls -l
命令获得有关一个文件的所有信息
2.文件类型
- 普通(一般)文件
- 目录文件
- 块特殊文件:这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行
- 字符特殊文件:这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变(系统中的所有设备要么是字符特殊文件,要么是块特殊文件)
- FIFO:这种类型的文件用于进程间通信,有时也称为命名管道
- 套接字:这种类型的文件用于进程间的网络通信
- 符号链接:这种类型的文件指向另一个文件
文件类型信息包含在stat
结构的st_mode
成员中。可以使用下图的宏判断文件类型(传入st_mode
):
3.用户ID和组ID
3.1 进程与文件的用户ID及组ID
与一个进程相关联的ID有6个或更多,下图是用户ID和组ID
3.2 修改文件的所有者和组所有者
如果owner
或group
中的任意一个是-1,则对应的ID不变:
基于BSD的系统一直规定只有超级用户才能更改一个文件的所有者;System V则允许任一用户更改他们所拥有的文件的所有者
能否更改还与
_POSIX_CHOWN_RESTRICTED
常量是否对文件生效有关
4.文件访问权限
下图列出了文件访问权限的所有权限位,以及它们对普通文件和目录文件的作用:
S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR
S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP
S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH
4.1 进程的文件访问权限
文件stat
结构的st_mode
成员除了包含设置用户ID位和设置组ID位用以修改进程的有效用户ID和有效组ID,还包含9个文件访问权限的位,这些权限位指明了所有者、组和其他对于该文件的访问权限,可分为3类:
- 目录
- 执行权限:决定了我们能否通过该目录(即如果某个文件的路径包含该目录,那么要打开这个文件,就需要对这个目录具有执行权限)
- 读权限:允许读目录,获得在目录中所有文件名的列表
- 文件
- 读权限:决定了我们能否打开现有文件进行读操作
- 写权限:决定了我们能否打开现有文件进程写操作
创建和删除文件:必须对该文件所在的目录具有写权限和执行权限
进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,内核进行的测试如下:
- 若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分自由
- 若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问,否则拒绝访问
- 若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问,否则拒绝访问
- 若其他用户适当的访问权限位被设置,则允许访问,否则拒绝访问
按顺序执行这4步
- 若进程拥有此文件(第2步),则按用户访问权限批准或解决该进程对文件的访问——不查看组访问权限
- 若进程不拥有该文件。但进程属于某个适当的组,则按组访问权限批准或拒绝该进程对文件的访问——不查看其他用户的访问权限
如果进程希望按照其实际用户ID和实际组ID来测试其访问能力,那么可以使用下列函数(进程使用实际用户ID和实际组ID来测试其访问能力也分成4步,与上面相同):
这两个函数在下面2种情况下是相同的:
pathname
参数为绝对路径fd
参数取值为AT_FDCWD
,而pathname
参数为相对路径
4.2 新文件的文件访问权限
可以使用umask
函数为进程设置文件模式创建屏蔽字,并返回之前的值:
在进程创建一个新文件或新目录时,一定会使用文件模式创建屏蔽字。在文件模式创建屏蔽字为1的位,在文件mode中的相应位一定被关闭。因此,使用creat
创建文件时,指定的mode,屏蔽掉文件模式创建屏蔽字中为1的位,就是新创建文件的权限
子进程的文件模式创建屏蔽字不会影响父进程。因此,如果子进程修改了文件模式创建屏蔽字,父进程的文件模式创建屏蔽字不会改变(如shell在调用一个会修改文件创建屏蔽字的程序前后使用umask打印文件创建屏蔽字,不会发生改变)
下图为八进制的屏蔽位和含义:
修改文件的权限位的前提是:进程的有效用户ID必须等于文件的所有者ID,或者进程必须具有超级用户权限
ls -l列出的是文件内容最后修改的时间,这里介绍的函数修改文件权限是在修改inode,所以ls -l列出的时间不会发生变化
5.新文件和目录的所有权
- 新文件或新目录的用户ID设置为进程的有效用户ID
- 关于组ID,POSIX.1允许实现选择下列之一作为新文件或新目录的组ID
- 新文件或新目录的组ID可以是进程的有效组ID
- 新文件或新目录的组ID可以是它所在目录的组ID(Linux的
/var/mail
目录中就使用了这种方法)
6.粘着位
即S_ISVTX
- 在UNIX尚未使用请求分页式技术的早期版本中,
S_ISVTX
位被称为粘着位 - 后来的UNIX版本称它为保存正文位
用途
- 以前,如果一个可执行文件设置了该位,当程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区。这使得下次执行该程序时能较快地将其装载入内存(原因是:通常的UNIX文件系统中,文件的各数据块很可能是随机存放的,相比较而言,交换区是被作为一个连续文件来处理的)
- 现在,系统扩展了粘着位的使用范围,Single UNIX Specification允许针对目录设置粘着位。如果对一个目录设置了该位,只有满足下列2个情况,才能删除或重命名该目录下的文件:
- 对该目录具有写权限
- 满足下列条件之一
- 拥有此文件
- 拥有此目录
- 是超级用户
目录/tmp和/var/tmp就是设置粘着位的典型候选者
7.文件长度
stat
结构的st_size
成员表示以字节为单位的文件的长度。该字段只对普通文件、目录文件和符号链接有意义
- 对于普通文件,其文件长度可以是0,在读这种文件时,将得到文件结束指示
- 对于目录,文件长度通常是一个数(如16或512)的整倍数
- 对于符号链接,文件长度是文件名中的实际字节数(因为符号链接文件长度总是由
st_size
指示,所以它并不包含通常C语言用作名字结尾的null字节)
7.1 文件中的空洞
普通文件可以包含空洞。空洞是由所设置的偏移量超过文件尾端,并写入了某些数据后造成的
空洞并不占用实际磁盘空间,但是会参与文件大小的计算,在读文件时,读到空洞部分的内容是字节0
7.2 文件截断
可以在打开文件时使用O_TRUNC
标志将文件截断为0
下列函数可以截断文件:
8.硬链接
8.1 创建硬链接
对于linkat函数:
- 如果两个路径名中的任一个是相对路径,那么需要通过相对于对应的文件描述符进行计算
- 如果两个文件描述符中的任一个设置为
AT_FDCWD
,那么相应的路径名就通过当前目录进行计算
- 如果两个文件描述符中的任一个设置为
- 如果两个路径名中的任一个是绝对路径,那么相应的文件描述符参数就会被忽略
如果实现支持创建指向一个目录的硬链接,那么也仅限于超级用户才可以这样做
8.2 删除硬链接
要删除硬链接,必须对包含该目录项(硬链接)的目录具有写和执行权限
这两个函数会删除目录项,并将由pathname
所引用文件的链接计数减1
- 如果对该文件还有其它链接,则仍可通过其它链接访问该文件的数据
- 如果出错,则不对该文件做任何更改
何时删除实际文件?
- 当链接计数到达0时
- 并且没有进程打开该文件(如果有进程打开了该文件,那么会删除目录项,但是进程退出前不会删除实际文件)
关闭一个文件时,内核首先检查打开该文件的进程个数。如果这个数达到0,内核再去检查其链接数;如果链接数,如果也是0,那么就删除该文件的内容
unlink的这种特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用open或creat创建一个文件,然后立即调用unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时,该文件的内容才被删除
也可以用remove函数(c库函数)解除对一个文件或目录的链接:
9.符号链接
符号链接是对一个文件的间接指针。与硬链接直接指向文件的inode不同,符号链接是为了避开硬链接的一些限制:
- 硬链接通常要求链接和文件位于同一文件系统中
- 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)
对于符号链接以及它指向何种对象并无任何文件系统限制(符号链接一般用于将一个文件或整个目录结构移到系统中另一个位置)
下图指明当传入的文件名表示一个符号链接时,函数能否跟随符号链接处理其链接的目标文件:
用open打开文件时,如果传递给open函数的路径名指定了一个符号链接,那么open跟随此符号链接到达所指定的文件。若此符号链接所指向的文件并不存在,则open返回出错,表示它不能打开该文件
9.1 创建符号链接
9.2 读取符号链接
因为open函数跟随符号链接,所以需要一种方法打开该链接本身,并读该链接中的名字。下列函数提供了这种功能:
两个函数组合了open、read和close的所有操作。如果函数成功执行,则返回读入buf
的字节数。在buf
中返回的符号链接的内容不以null字节终止
10.目录
10.1 创建目录
对于目录通常至少要设置一个执行权限位,以允许访问该目录中的文件名
10.2 删除目录
10.3 读目录
对某个目录具有访问权限的任一用户都可以读该目录,但是,为了防止文件系统产生混乱,只有内核才能写目录
结构dirent
定义在
中,具体实现相关,至少包含下列2个成员:
struct dirent{
ino_t d_ino; /* inode号 */
char d_name[]; /* null-terminated文件名 */
...
};
d_name
的大小并没有指定,但是必须保证至少包含NAME_MAX
个字节(不包含终止null字节)
DIR
结构是一个内部结构,用以保存当前正在被读的目录的有关信息。作用类似于FILE
结构
10.4 更改当前目录
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)
用户登录到UNIX系统时,其当前工作目录通常是口令文件(/etc/passwd
)中该用户登录项的第6个字段——用户的起始目录
- 当前工作目录是进程的一个属性(所以如果调用
chdir
修改当前目录,只影响调用函数的进程本身) - 起始目录则是登录名的一个属性
下列2个函数可以更改当前工作目录:
10.5 获取当前目录的绝对路径
11.重命名
权限:如果newname
已经存在,则调用进程对它需要由写权限。另外,调用进程将删除oldname
目录项,并可能要创建newname
目录项,所以它需要对包含oldname
及包含newname
的目录具有写和执行权限
12.文件的时间
对每个文件维护3个时间字段,他们的意义如下图:
下列2个函数包含在POSIX.1中,可以指定纳秒级精度的时间戳:
时间戳可以按下列4种方式之一进行指定:
- 如果
times
参数是一个空指针,则访问时间和修改时间两者都设置为当前时间 - 如果
times
数组任一元素的tv_nsec
字段的值为UTIME_NOW
,相应的时间戳就设置为当前时间,忽略相应的tv_sec
字段 - 如果
times
数组任一元素的tv_nsec
字段的值为UTIME_OMIT
,相应的时间戳就保持不变,忽略相应的tv_sec
字段 - 如果
times
数组任一元素的tv_nsec
字段的值既不是UTIME_NOW
也不是UTIME_OMIT
,相应的时间戳设置为相应的tv_sec
和tv_nsec
字段的值
执行这些函数所要求的权限取决于times
参数的值:
- 如果
times
是一个空指针,或者任一tv_nsec
字段设为UTIME_NOW
,则进程的有效用户ID必须等于该文件的所有者ID。进程对该文件必须具有写权限,或者进程是一个超级用户 - 如果
times
是非空指针- 并且任一
tv_nsec
字段的值既不是UTIME_NOW
也不是UTIME_OMIT
,则进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程。对文件只有写权限是不够的 - 并且2个
tv_nsec
字段的值都为UTIME_OMIT
,就不执行任何的权限检查
- 并且任一
下列函数包含在Single UNIX Specification的XSI扩展选项中,时间戳是用秒和微妙表示的:
13.设备特殊文件
st_dev
和st_rdev
两个字段的有关规则:
- 每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型
dev_t
。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。一个磁盘驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但是次设备号却不同 - 通常使用两个宏:major和minor来访问主、次设备号,大多数实现都定义这两个宏
- 系统中与每个文件名关联的
st_dev
值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的inode - 只有字符特殊文件和块特殊文件才有
st_rdev
值。此值包含实际设备的设备号
可以使用cat /proc/devices
查看系统上的字符设备和块设备号:
chenximing@chenximing-MS-7823:~$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
21 sg
29 fb
89 i2c
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
216 rfcomm
226 drm
247 mei
248 hidraw
249 bsg
250 watchdog
251 rtc
252 dimmctl
253 ndctl
254 tpm
Block devices:
1 ramdisk
259 blkext
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
252 device-mapper
253 virtblk
254 mdp
我的环境中有一块磁盘划分了3个分区:
chenximing@chenximing-MS-7823:~$ df -h | grep 'sda'
/dev/sda2 46G 32G 12G 74% /
/dev/sda1 453M 433M 0 100% /boot
/dev/sda3 275G 30G 232G 12% /home
chenximing@chenximing-MS-7823:~$ ls -l /dev/sda1 /dev/sda2 /dev/sda3
brw-rw---- 1 root disk 8, 1 6月 19 10:11 /dev/sda1
brw-rw---- 1 root disk 8, 2 6月 19 10:11 /dev/sda2
brw-rw---- 1 root disk 8, 3 6月 19 10:11 /dev/sda3
对于下列程序:
#include
#include
#include
int main()
{
struct stat buf1,buf2,buf3;
if(stat("/dev/sda1",&buf1) < 0)
exit(1);
if(stat("/dev/sda2",&buf2) < 0)
exit(1);
if(stat("/dev/sda3",&buf3) < 0)
exit(1);
printf("dev = %d/%d",major(buf1.st_dev),minor(buf1.st_dev));
if(S_ISCHR(buf1.st_mode) || S_ISBLK(buf1.st_mode))
printf(" (%s) rdev = %d/%d",
(S_ISCHR(buf1.st_mode)) ? "character" : "block",
major(buf1.st_rdev),minor(buf1.st_rdev));
printf("\n");
printf("dev = %d/%d",major(buf2.st_dev),minor(buf2.st_dev));
if(S_ISCHR(buf2.st_mode) || S_ISBLK(buf2.st_mode))
printf(" (%s) rdev = %d/%d",
(S_ISCHR(buf2.st_mode)) ? "character" : "block",
major(buf2.st_rdev),minor(buf2.st_rdev));
printf("\n");
printf("dev = %d/%d",major(buf3.st_dev),minor(buf3.st_dev));
if(S_ISCHR(buf3.st_mode) || S_ISBLK(buf3.st_mode))
printf(" (%s) rdev = %d/%d",
(S_ISCHR(buf3.st_mode)) ? "character" : "block",
major(buf3.st_rdev),minor(buf3.st_rdev));
printf("\n");
return 0;
}
输出结果为:
chenximing@chenximing-MS-7823:~$ ./a.out
dev = 0/6 (block) rdev = 8/1
dev = 0/6 (block) rdev = 8/2
dev = 0/6 (block) rdev = 8/3
将"/dev/sda1","/dev/sda2"、"/dev/sda3"分别替换成"/boot"、"/"和"/home",输出结果为:
chenximing@chenximing-MS-7823:~$ ./a.out
dev = 8/1
dev = 8/2
dev = 8/3
三.标准I/O库
标准I/O库处理很多细节,如缓冲区分片、以优化的块长度执行I/O等。这些处理使用户不必担心如何选择使用正确的块长度。这使得它便于用户使用,但是如果不深入了解I/O库函数的操作,也会带来一些问题
1.流
标准I/O库与文件I/O区别:
- 标准I/O库处理很多细节,如缓冲区分片、以优化的块长度执行I/O等
- 文件I/O函数都是围绕文件描述符进行。首先打开一个文件,返回一个文件描述符;后续的文件I/O操作都使用该文件描述符
- 标准I/O库是围绕流进行的
当用标准I/O库打开或者创建一个文件时,就有一个内建的流与之相关联
1.1 流的定向
流的定向决定了所处理的字符是单字节还是多字节的
对于ASCII字符集,一个字符用一个字节表示;对于国际字符集,一个字符可以用多个字节表示。标准I/O文件流可用于单字节或者多字节字符集。流的定向决定了所处理的字符是单字节还是多字节的
- 当一个流最初被创建时,它并没有定向
- 若在未定向的流上使用一个多字节I/O函数,则将该流的定向设置为宽定向的(即处理多字节)
- 若在未定向的流上使用一个单字节I/O函数,则将该流的定向设置为字节定向的(即处理单字节)
- 只有两个函数可以改变流的定向
- freopen:清除一个流的定向
fwide
:设置流的定向(并不改变已定向流的定向)
注意:如果fp
是无效流,由于fwide
从返回值无法得知函数执行成功还是失败。那么我们必须采用这个方法:首先在调用fwide
之前清除errno
。然后在fwide
之后检查errno
的值。通过errno
来检测fwide
执行成功还是失败
1.2 3个标准流
操作系统对每个进程定义了3个流,并且这3个流可以自动地被进程使用,他们都是定义在
中:
- 标准输入:预定义的文件指针(FILE对象)为
stdin
,它内部的文件描述符就是STDIN_FILENO
- 标准输出:预定义的文件指针(FILE对象)为
stdout
,它内部的文件描述符就是STDOUT_FILENO
- 标准错误:预定义的文件指针(FILE对象)为
stderr
,它内部的文件描述符就是STDERR_FILENO
2.FILE对象
当使用fopen
函数打开一个流时,它返回一个指向FILE
对象的指针(书中称作文件指针)。该对象通常是一个结构,包含了标准I/O库为管理该流所需要的所有信息,包括:
- 用于实际I/O的文件描述符
- 指向用于该流缓冲区的指针
- 该流缓冲区的长度
- 当前在缓冲区中的字符数
- 出错标志
应用程序没必要检验FILE
对象,只需要将FILE
指针作为参数传递给每个标准I/O函数
FILE
对象包含有文件描述符,标准I/O函数可以看作是对fd直接操作的系统调用的封装,优点是带有I/O缓冲,从而减少系统调用的次数:
3.缓冲
标准IO库提供缓冲的目的是:尽量减少使用read
和write
调用的次数。标准I/O库对每个I/O流自动地进行缓冲管理,从而避免了程序员需要手动管理这一点带来的麻烦
3.1 3种缓冲类型
标准I/O库提供了三种类型的缓冲:
- 全缓冲:此时在标准I/O缓冲区被填满后,标准I/O库才进行实际的I/O操作
- 行缓冲:此时当输入和输出中遇到换行符时,标准I/O库执行实际的I/O操作。但是注意:
- 只要填满了缓冲区,即使还没有写一个换行符,也立即进行I/O操作
- 任何时候只要通过标准I/O库,从一个不带缓冲的流或者一个行缓冲的流得到输入数据,则会冲洗所有行缓冲输出流。(即要缓冲输入,先冲洗输出缓冲)
- 不带缓冲:标准I/O库不进行缓冲。此时任何I/O都立即执行实际的I/O操作
在一个流上执行第一次I/O操作时,相关标准的I/O函数通常调用 malloc 获取使用的缓冲区
下列函数可以设置流的缓冲类型:
- 如果在一个函数内分配一个自动变量类型的标准I/O缓冲区,为了防止内存泄露,从该函数返回之前,必须关闭流。因为自动变量是栈上分配,函数返回之后自动变量被销毁
- 某些操作系统将缓冲区的一部分存放它自己的管理操作信息,因此可以存放在缓冲区中的实际数据字节数将少于
size
- 通常推荐利用操作系统自动选择缓冲区长度并自动分配缓冲区。在这种情况下若关闭此流,则标准I/O库会自动释放缓冲区
3.2 缓冲区冲洗
- 缓冲区可以由标准的I/O操作自动地冲洗(如,当填满一个缓冲区时)
- 也可以手动调用fflush函数冲洗一个流
冲洗是双向的:1)输出流 ---> 内核 ---> 磁盘或者终端; 2)输入流---> 用户缓冲区
冲洗并不是立即写到磁盘文件中。冲洗只是负责数据传到内核
3.3 标准流与缓冲
- 标准输入与标准输出
- 默认情况下,1)若是指向终端设备的流,则是行缓冲的;2)否则是全缓冲的
- 标准错误
- 默认情况下,是不带缓冲的
- 不会是全缓冲
4.相关调用
4.1 打开流
fopen
常用于打开一个指定的文件,返回一个文件指针freopen
常用于在一个指定的流(标准输入、标准输出或者标准错误)上,打开一个指定的文件fdopen
常用于将文件描述符包装成一个标准I/O流。因为某些特殊类型的文件(如管道、socket
文件)不能用fopen
打开,必须先获取文件描述符,然后对文件描述符调用fdopen
注意:当以读和写类型打开一个文件时(type
中带+
号的类型),有下列限制:
- 如果写操作后面没有
fflush,fseek,fsetpos,rewind
操作之一,则写操作后面不能紧跟读操作 - 如果读操作后面没有
fseek,fsetpos,rewind
操作之一,也没有到达文件末尾,则在读操作之后不能紧跟写操作
默认情况下,流被打开时是全缓冲的。但是如果流引用的是终端设备,则流被打开时是行缓冲的
4.2 关闭流
- 在该文件被关闭之前:
fclose
会自动冲洗缓冲中的输出数据,缓冲区中的任何输入数据被丢弃- 若该缓冲区是标准I/O库自动分配的,则释放此缓冲区
- 当一个进程正常终止时(直接调用
exit
函数,或者从main
函数返回):- 所有带未写缓存数据的标准I/O流都被冲洗
- 所有打开的标准I/O流都被关闭
4.3 读写流
一旦打开了流,可以在3种不同类型的非格式化I/O中选择,对流进行读、写操作:
- 每次一个字符的I/O。一次读、写一个字符。若流是带缓冲的,则标准I/O函数处理所有缓冲
- 1)每次读一个字符:
getc
、fgetc
、getchar
- 2)读取错误判断:
ferror
、feof
、clearerr
- 3)将读取字符压回流中:
ungetc
- 4)每次写一个字符:
putc
、fputc
、putchar
- 1)每次读一个字符:
- 每次一行的I/O。一次读、写一行。每一行都以一个换行符终止
- 5)每次读一行
- 6)每次写一行
- 直接I/O(二进制I/O、一次一个对象I/O)。每次I/O读、写某种数量的对象,每个对象具有指定的长度。常用于从二进制文件中每次读或写一个结构
- 7)二进制I/O
格式化I/O由
printf
和scanf
族函数完成
1)每次读一个字符
注意:
getchar()
等价于getc(stdin)
。它从标准输入中读取一个字符getc
和fgetc
的区别在于:getc
可能通过宏定义来实现,而fgetc
不能实现为宏。因此:getc
的参数不应该是具有副作用的表达式,因为它可能被计算多次fgetc
一定是个函数,因此可以得到其地址,这就允许将fgetc
的地址作为参数传递。而getc
不行- 调用
fgetc
所需的时间可能比调用getc
长,因为函数调用所需时间通常比调用宏长
- 这三个函数在返回下一个字符时,将
unsigned char
类型表示的字符转换成了int
类型。(因为需要通过返回EOF
来标记到达末尾或者出错。而EOF
通常是常量 -1 。所以需要返回int
)
2)读取错误判断
上述3个函数不管是出错还是到达文件尾端,都返回相同值,下面的函数可以进行区分
大多数实现中,为每个流在FILE对象中维护了两个标志:文件出错标志和文件结束标志。clearerr
函数用来清除这两个标志
3)将读取字符压回流中
注意:
- 若根据某个序列向流中压入一串字符,则再从该流中读取的字符序列是逆序的。即最后压入的字符最先读出
- 可以执行任意次数的压入单个字符,但是不支持一次压入多个字符
- 不能压入
EOF
。但是当已经读到文件尾端时,支持压入一个字符。下次读将返回该字符,再读则返回EOF
ungetc
通常用于这样的情形:正在读取一个输入流,然后需要根据某个字符串(标记字符串)来对输入进行切分。那么我们就需要先看一看下一个字符,来决定如何处理当前字符。此时需要方便的将刚查看的字符回送
ungetc
只是将字符压入流缓冲区中,并没有压入底层的磁盘文件或者操作系统内核中
4)每次写一个字符
注意:
putchar(c)
等价于putc(c,stdout)
。它向标准输出中写一个字符putc
和fputc
的区别在于:putc
可能通过宏定义来实现,而fputc
不能实现为宏
5)每次读一行
注意:
- 对于
fgets
函数,必须指定缓冲区的长度n
。该函数一直读到下一个换行符为止,但是不超过n-1
个字符- 无论读到多少个字符,缓冲区一定以
null
字节结尾 - 若某一行包括换行符超过
n-1
个字节,则fgets
只返回一个不完整的行;下次调用fgets
会继续读该行
- 无论读到多少个字符,缓冲区一定以
- 对于
gets
函数,从标准输入总读取字符。由于无法指定缓冲区的长度,因此很可能造成缓冲区溢出漏洞,故该函数不推荐使用。gets与fgets的另一个区别是:gets并不将换行符存入缓冲区中 - 对于发生错误和读到末尾,都是返回
NULL
6)每次写一行
注意:
fputs
和puts
都是将一个以null
字节终止的字符串写到流中,末尾的null
字符不写出!字符串不要求以换行符结尾!puts
将字符串写到标准输出,末尾的null
字符不写出!但是puts
随后又将一个换行符写到标准输出中!而fputs
不会自动添加换行符(虽然puts
不像gets
那样不安全,但是我们也是要避免使用它,以免要记住它在最后是否添加了一个换行符)
7)二进制I/O
使用二进制I/O的基本问题是:它只能用在读取同一个操作系统上已写的数据。如果跨操作系统读写,则很可能工作异常。因为:
- 同一个
struct
,可能在不同操作系统或者不同编译系统中,成员的偏移量不同 - 存储多字节整数和浮点数的二进制格式在不同的操作系统中可能不同
4.4 定位流
有3种方法定位标准I/O流:
ftell
、fseek
:它们都假定文件的位置可以存放在一个长整形中ftello
、fseeko
:使文件偏移量可以不必一定使用长整型,使用off_t数据类型代替了长整型fgetpos
、fsetpos
:由ISO C引入,使用一个抽象数据类型fpos_t记录文件位置。这种数据类型可以根据需要定义一个足够大的数,用以记录文件位置
需要移植到非UNIX系统上运行的应用程序应该使用
fgetpos
和fsetpos
1)ftell与fseek
rewind函数也可以将一个流设置到文件的起始位置
2)ftello与fseeko
3)fgetpos与fsetpos
fgetpos
将文件位置指示器的当前值存入由pos指向的对象中,在以后调用fsetpos
时,可以使用此值将流重新定位至该位置
4.5 格式化I/O
1)格式化输出
格式说明的格式:%[flags][fldwidth][precision][lenmodifier]convtype
- 标志(flags)
除了转换说明和空白字符以外,格式字符串中的其他字符必须与输入匹配。如有一个字符不匹配,则停止后续处理,不再读输入的其余部分
转换说明的格式:%[*][fldwidth][m][lenmodifier]convtype
*
:用于抑制转换。按照转换说明的其余部分对输入进行转换,但是转换结果不存放在参数中fldwidth
:说明最大宽度,即最大字符数lenmodifier
:说明要转换结果赋值的参数大小。见前述说明convtype
:类似于printf族的转换类型字段。但是稍有区别:输入中的带符号的数值可以赋给无符号类型的变量m
:赋值分配符。用于强迫内存分配。当%c,%s
时,如果指定了m
,则会自动分配内存来容纳转换的字符串。同时该内存的地址会赋给指针类型的变量(即要求对应的参数必须是指针的地址)。同时要求程序员负责释放该缓冲区(通过free
函数)
4.6 获取流相应的文件描述符
fileno
不是ISO C标准部分,而是POSIX.1支持的扩展
4.7 创建临时文件
ISO C标准I/O库提供了2个函数以帮助创建临时文件
tmpfile
经常使用的标准UNIX技术是先调用tmpnam
产生一个唯一的路径名,然后,用该路径名创建一个文件,并立即unlink它。对一个文件解除链接并不删除其内容,关闭该文件时才删除其内容。而关闭文件可以是显式的,也可以在程序终止时自动进行
Single UNIX Specification为处理临时文件定义了另外两个函数:mkdtemp
和mkstemp
,它们是XSI的扩展部分:
名字是通过template字符串进行选择的。这个字符串是后6位设置为XXXXXX的路径名。函数将这些占位符替换成不同的字符来构建一个唯一的路径名。如果成功的话,这两个函数将修改template字符串反映临时文件名
5.内存流
内存流:一种标准IO流,虽然它通过 FILE
指针来访问,但是并没有底层的文件 。所有的IO都是通过在缓冲区和主存之间来回传送字节来完成。虽然它看起来像是文件流,但是更适用于字符串操作
- 无论何时以追
a
方式打开内存流时,当前文件位置设为缓冲区中第一个null
字节处。- 若缓冲区中不存在
null
字节,则当前位置设为缓冲结尾的后一个字节
- 若缓冲区中不存在
- 当内存流不是
a
方式打开时,当前位置设置为缓冲区的开始位置 - 如果
buf
是null
,则打开流进行读或者写都没有任何意义。因为此时缓冲区是通过fmemopen
分配的,没办法找到缓冲区的地址。 - 任何时候需要增加流缓冲区中数据流以及调用
fclose、fflush、fseek、fseeko、fsetpos
时都会在当前位置写入一个null
字节
创建内存流的其他两个函数:
open_memstream
创建的流是面向字节的,open_wmemstream
创建的流是面向宽字节的
这两个函数与fmemopen
的不同在于:
- 创建的流只能写打开
- 缓冲区由函数自动创建,不能指定自己的缓冲区
- 关闭流后需要程序员释放缓冲区
- 对流添加字节会增加缓冲区大小
在缓冲区地址和大小使用上要遵守规则:
- 缓冲区地址和长度只有在调用
fclose
或者fflush
后才有效 - 这些值只有在下一次写入或者调用
fclose
之前才有效。因为缓冲区可能增长,也可能需要重新分配
因为避免了缓冲区溢出,内存流非常适用于创建字符串。因为内存流只访问主存,不访问磁盘上的文件,所以对于把标准I/O流作为参数用于临时文件的函数来说,会有很大的性能提升
四.进程环境
1.进程的启动与终止
C程序总是从main
函数开始执行的。main
函数的原型是:
int main(int argc,char* argv[]);
- 参数:
argc
:命令行参数的数目(ISO C和POSIX.1都要求argv[argc]是一个空指针)argv
:由指向各命令行参数的指针所组成的数组。ISOC
和POSIX
都要求argv[argc]
是一个空指针
#include
#include
int main(int argc,char* argv[])
{
printf("%d parameters: \n",argc);
for(int i = 0;i < argc;i++){ //argv[argc]为NULL
printf(" %s\n",argv[i]);
}
return 0;
}
上面的程序可以打印调用程序时的参数:
chenximing@chenximingdeMacBook-Pro ~ ./a.out hello ' ' world
4 parameters:
./a.out
hello
world
当内核通过exec
函数执行 C 程序、在调用main
之前先调用一个特殊的启动例程
- 可执行程序文件将此启动例程指定为程序的起始地址(这是由链接器设置的,而链接器由C编译器调用)
- 启动例程从内核取得命令行参数和环境变量值,为调用
main
函数做好安排
启动例程使得从main返回后立即调用exit函数。如果将启动例程以C代码形式表示(实际上该例程常常以汇编语言编写),则它调用main函数的形式可能是:
exit(main(argc,argv));
1.2 进程终止的方式
有 8 种方式使得进程终止,其中 5 种为正常终止,3 种异常终止:
- 正常终止方式:
- 从
main
函数返回 - 调用
exit
函数 - 调用
_exit
函数或者_Exit
函数 - 多线程的程序中,最后一个线程从其启动例程返回
- 多线程的程序中,从最后一个线程调用
pthread_exit
函数
- 从
- 异常终止方式:
- 调用
abort
函数 - 接收到一个信号
- 多线程的程序中,最后一个线程对取消请求作出响应
- 调用
1.3 终止函数
下列3个函数正常终止一个程序
三个终止函数的区别:
_exit
和_Exit
函数:立即进入内核exit
函数:先执行一些清理处理,然后进入内核
exit
和_Exit
是由 ISO C 说明的,_exit
是由 POSIX 说明的 ,因此头文件不同
1.4 终止状态
exit
、exit
、_Exit
都带有一个整型参数,称为终止状态(或称作退出状态)。大多数 UNIX 系统 shell 都提供检查进程终止状态的方法:
- 下列情况,进程的终止状态是未定义的:
- 若调用
exit
、exit
、_Exit
时不带终止状态 - 若
main
执行了一个无返回值的return
语句 - 若
main
没有声明返回类型为整型
- 若调用
- 下列情况,进程的终止状态是0:
- 若
main
声明返回类型为整型,并且main
执行到最后一条语句时返回(隐式返回)
- 若
main
函数返回一个整型值与用该值调用exit
是等价的:
main 函数中: exit(100);
等价于
return 100;
在LINUX中,退出状态码最高是255,一般自定义的代码值为0~255,如果超出255,则返回取模后的值
1.5 登记终止处理程序
按照ISO C的规定,一个进程可以登记最多32个函数(通常操作系统会提供多于32个的限制。可以用sysconf
函数查询这个限制值),这些函数将由exit
函数自动调用。这些函数称作终止处理程序
exit
调用这些终止处理程序的顺序与它们登记的时候顺序相反- 如果同一个终止处理程序被登记多次,则它也会被调用多次
2.环境表
每个程序都会接收一张环境表
- 与参数表一样,环境表也是一个字符指针数组
- 其中数组中的每个指针指向一个以
null
结束的 C 字符串,这些字符串称之为环境字符串 - 数组的最后一项是
null
- 其中数组中的每个指针指向一个以
- 全局变量
environ
包含了该指针数组的地址:extern char **environ
。我们称environ
为环境指针,它位于头文件unistd.h
中 - 按照惯例,环境字符串由
name=value
这种格式的字符串组成
2.1 获取环境变量
getenv
由ISO C
定义,可以用其获取环境变量值
并不是所有系统都支持修改环境变量:
这些函数内部操作环境表非常复杂,下面是原理:
- 修改:
- 如果新的
value
长度少于或等于现有value
的长度,则只需要将新字符串复制到原字符串所用的空间即可 - 如果新的
value
长度大于现有value
的长度,则必须调用malloc
为新字符串分配空间,然后将新字符串复制到该空间,接着使环境表中对name
的指针指向新分配区并释放旧分配区
- 如果新的
- 增加:
- 如果这是第一次增加一个新的
name
:- 则必须调用
malloc
为新指针表分配空间 - 然后将原来的环境表复制到新分配区
- 并将新的
name=value
字符串的指针存放到该指针表的表尾, - 然后将一个空指针存放在其后
- 然后使
environ
指向新指针表 - 最后释放旧的指针表
- 则必须调用
- 如果这不是第一次增加一个新的
name
,则可知以前已经调用了malloc
:- 则只需要调用
realloc
,以分配比原空间多存放一个指针的空间 - 并将新的
name=value
字符串的指针存放到该指针表的表尾, - 然后将一个空指针存放在其后
- 则只需要调用
- 如果这是第一次增加一个新的
- 删除:
- 则只需要先在环境表中找到该指针,然后将所有的后续指针都向环境表的首部依次顺序移动一个位置即可
修改和增加复杂的原因在于:环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向高地址方向扩展;同时也不能移动在它之下的各栈帧,所以也不能向低地址方向扩展。两者组合使得该空间的长度不能再增加
3.C程序的存储空间布局
注意:
- 栈从高地址向低地址增长。堆顶和栈顶之间未使用的虚拟地址空间很大
- 未初始化数据段的内容并不存放在磁盘程序文件中。需要存放在磁盘文件中的段只有正文段和初始化数据段(
size a.out
令可以查看程序的正文段、数据段 和bss
段长度)
下列程序打印不同段数据的位置:
#include
#include
#include
extern char** environ;
int global1;
int global2 = 1;
int main()
{
//1.环境变量
char **env = environ;
while(*env){
printf("(%p) (%p) %s\n",env,*env,*env);
env++;
}
printf("\n");
//2.全局变量
printf("%p\n",&global1);
printf("%p\n\n",&global2);
//3.栈
int local;
char *localc = (char*)malloc(10);
printf("%p\n",&local);
printf("%p\n\n",&localc);
//4.堆
printf("%p\n\n",localc);
return 0;
}
我的环境下,输出如下:
chenximing@chenximing-MS-7823:~$ ./a.out
(0x7ffe4aa6f4c8) (0x7ffe4aa7078a) XDG_SESSION_ID=1
(0x7ffe4aa6f4d0) (0x7ffe4aa7079b) TERM=xterm-256color
(0x7ffe4aa6f4d8) (0x7ffe4aa707af) SHELL=/bin/bash
(0x7ffe4aa6f4e0) (0x7ffe4aa707bf) XDG_SESSION_COOKIE=526d72c400c09a742df8809a58e50d0e-1529375799.783462-1767108743
(0x7ffe4aa6f4e8) (0x7ffe4aa70810) SSH_CLIENT=192.168.2.1 53068 22
(0x7ffe4aa6f4f0) (0x7ffe4aa70830) SSH_TTY=/dev/pts/25
(0x7ffe4aa6f4f8) (0x7ffe4aa70844) USER=chenximing
...
(0x7ffe4aa6f508) (0x7ffe4aa70d75) MAIL=/var/mail/chenximing
(0x7ffe4aa6f510) (0x7ffe4aa70d8f) PATH=/home/chenximing/bin:/usr/local/go/bin:/usr/lib/jvm/jdk1.8.0_144/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/home/chenximing/bin
(0x7ffe4aa6f518) (0x7ffe4aa70e47) QT_QPA_PLATFORMTHEME=appmenu-qt5
(0x7ffe4aa6f520) (0x7ffe4aa70e68) PWD=/home/chenximing
(0x7ffe4aa6f528) (0x7ffe4aa70e7d) JAVA_HOME=/usr/lib/jvm/jdk1.8.0_144
(0x7ffe4aa6f530) (0x7ffe4aa70ea1) LANG=zh_CN.UTF-8
(0x7ffe4aa6f538) (0x7ffe4aa70eb2) SHLVL=1
(0x7ffe4aa6f540) (0x7ffe4aa70eba) HOME=/home/chenximing
(0x7ffe4aa6f548) (0x7ffe4aa70ed0) GOROOT=/usr/local/go
(0x7ffe4aa6f550) (0x7ffe4aa70ee5) LANGUAGE=zh_CN:zh
(0x7ffe4aa6f558) (0x7ffe4aa70ef7) LOGNAME=chenximing
(0x7ffe4aa6f560) (0x7ffe4aa70f0a) CLASSPATH=.:/usr/lib/jvm/jdk1.8.0_144/lib
(0x7ffe4aa6f568) (0x7ffe4aa70f34) SSH_CONNECTION=192.168.2.1 53068 192.168.2.4 22
(0x7ffe4aa6f570) (0x7ffe4aa70f64) LC_CTYPE=zh_CN.UTF-8
(0x7ffe4aa6f578) (0x7ffe4aa70f79) GOPATH=~/go
(0x7ffe4aa6f580) (0x7ffe4aa70f85) LESSOPEN=| /usr/bin/lesspipe %s
(0x7ffe4aa6f588) (0x7ffe4aa70fa5) XDG_RUNTIME_DIR=/run/user/1000
(0x7ffe4aa6f590) (0x7ffe4aa70fc4) LESSCLOSE=/usr/bin/lesspipe %s %s
(0x7ffe4aa6f598) (0x7ffe4aa70fe6) _=./a.out
0x60106c
0x601050
0x7ffe4aa6f3bc
0x7ffe4aa6f3c0
0x1725010
画图表示:
4.共享库
大多数UNIX系统支持共享库
- 共享库的优点
- 使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接,减少了每个可执行文件的长度
- 可以用库函数的新版本代替老版本而无需对使用该库的程序重新链接编辑(假定参数的数目和类型都没有改变)
- 共享库的缺点
- 动态链接增加了一些运行时开销。这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时
不同系统中,程序可能使用不同的方法说明是否要使用共享库。比较典型的有cc(1)
:
//以下阻止使用共享库
gcc -static hello1.c
//以下,gcc默认使用共享库
gcc hello1.c
可以使用size a.out
观察上面两种情况下可执行文件正文和数据段的长度变化
静态链接库与动态链接库
链接库按是否被编译到程序内部而分成动态与静态链接库
- 静态链接库
- 扩展名为
.a
,通常命名为libxxx.a
- 整合到可执行文件中,因此会导致可执行文件体积增大
- 可独立执行,而不需要再向外部要求读取链接库的内容
- 链接库升级时,需要重新编译生成可执行文件
- 扩展名为
如果在编译时指定了动态链接库及库的路径,但是/etc/ld.so.conf
文件中没有包含该路径,即这个动态链接库没有载入内存,那么运行生成的可执行文件会报错
ldd
命令可以查看某个可执行的二进制文件含有什么动态链接库,对于前面的问题,如果某个动态链接库没有载入内存,即动态链接库的路径没有包含在文件/etc/ld.so.conf
中。使用ldd查看时,这个库会显示“not found”
生成/加载静态链接库与动态链接库
图片来源
5.进程堆空间的管理
堆上空间分配和释放核心都是通过调整program break来实现的,malloc和free维护空闲空间链表,以减少调整program break的系统调用的调用次数
1)调整program break来分配与释放
可以通过brk函数和sbrk函数改变进程的program break位置,从而分配或释放内存。program break与进程的内存布局结构中&end
位置相同
program break的位置抬升后,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配。内核会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页
sbrk(0)将返回program break的当前位置,可以用于跟踪堆的大小,或是监视内存分配函数的行为
2)使用C库函数进行分配释放
malloc相比brk和sbrk,具有以下优点:
- 属于C标准的一部分
- 更易于在多线程程序中使用
- 接口简单,允许分配小块内存
- 允许随意释放内存块,它们被维护于一张空闲内存列表内,在后续分配内存时循环使用
这3个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象
这些分配例程通常用sbrk
系统调用实现
sbrk
可以扩充或缩小进程的存储空间。但是大多数malloc和free的实现都不减小进程的存储空间:释放的空间可供以后再分配,将它们保持在malloc池中而不返回给内核- 大多数实现所分配的存储空间要比所要求的稍大一些,额外的空间用来记录管理信息,比如分配块的长度、指向下一个分配块的指针等等(如果在一个动态分配区的尾部之后或者在起始位置之前进行写操作,会修改管理记录信息。这种类型的错误是灾难性的,但是由于这种错误不会立即暴露出来,因此很难被发现)
malloc
和free
相关的致命性错误- 调用了
malloc
函数但是没有调用free
函数:会发生内存泄漏,该进程占用的存储空间就会连续增加,直到不再有空闲空间。此时过度的换页开销会导致性能下降 - free一个已经释放了的块
- 调用free时所用的指针不是3个alloc函数的返回值
- 调用了
3)malloc和free的实现
malloc的实现
- 首先会扫描(不同实现扫描策略可能不同)之前由free释放的空闲内存块列表,试图找到尺寸大小大于或等于要求的一块空闲内存
- 如果一内存块的尺寸恰好符合要求,那么直接返回给调用者
- 如果内存块较大,那么进行分割,将一块大小相当的内存返回给调用者,剩余内存块保留在空闲列表
- 如果空闲内存列表中根本找不到足够大的空闲内存块(如首次调用malloc时),那么malloc会调用sbrk以分配更多的内存(为了减少对sbrk的调用次数,malloc并不是严格按所需字节数来分配内存,而是以更大幅度来增加program break,并将超出部分置于空闲内存列表)
当malloc分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值。该整数位于内存块的起始处,而实际返回给调用者的内存地址恰好位于 这一长度记录字节之后:
- free会使用内存块本身的空间来存放链表指针,将自身添加到列表中
6.进程资源限制
每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimit函数查询和更改:
- 参数
resource
:相应的资源。可以设置为下列值(不同系统支持不同):RLIMIT_AS
:进程总的可用存储空间的最大长度(字节)(这会影响到sbrk
函数和mmap
函数)RLIMIT_CORE
:core
文件的最大字节数。如果为0,则阻止创建core
文件RLIMIT_CPU
:CPU时间的最大量值(秒),如果超过此软限制时,向该进程发送SIGXCPU
信号RLIMIT_DATA
:数据段的最大字节长度(包括初始化数据、非初始以及堆的总和)RLIMIT_FSIZE
:可以创建的文件的最大字节长度。当超过此软限制时,向该进程发送SIGXFSX
信号RLIMIT_MEMLOCK
:一个进程使用mlock
能够锁定在存储空间中的最大字节长度RLIMIT_MSGQUEUE
:进程为POSIX
消息队列可分配的最大存储字节数RLIMIT_NICE
:为了影响进程的调度优先级,nice
值可设置的最大限制RLIMIT_NOFILE
:每个进程能打开的最多文件数。更改此限制将影响到sysconf
函数在参数_SC_OPEN_MAX
中返回的值RLIMIT_NPROC
:每个实际用户ID
可以拥有的最大子进程数。更改此限制将影响到sysconf
函数在参数_SC_CHILD_MAX
中返回的值RLIMIT_RSS
:最大驻内存集字节长度RLIMIT_SIGPENDING
:一个进程可排队的信号的最大数量。这个限制是sigqueue
函数实施的RLIMIT_STACK
:栈的最大字节长度
struct rlimit{
rlim_t rlim_cur; /* soft limit: current limit */
rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */
};
资源限制影响到调用进程并由其子进程继承
7.Core Dump
内容参考
7.1 Core Dump的概念及用途
Core Dump是一个文件,它记录了程序运行过程中异常终止或崩溃时的内存映像
概念:当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump(中文有的翻译成“核心转储”)。可以认为 core dump 是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息
Dump指的是拷贝一种存储介质中的部分内容到另一个存储介质,或者将内容打印、显示或者其它输出设备。dump 出来的内容是格式化的,可以使用一些工具来解析它
用途:core dump 对于编程人员诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而 core dump 文件可以再现程序出错时的情景
7.2 产生Core Dump
ulimit -c
可以查看shell进程产生的core dump文件大小,默认情况下为0,即不产生core dump文件,若要产生,使用ulimit -c unlimited
开启core dump功能(也可以改成为某一值,注意这种修改方式只会对当前终端环境生效)
常见的几种情况:调用abort()函数、访存错误、非法指令等
以下列出几种会产生Core Dump的信号(不仅限于这几种):
信号 | 动作 | 说明 |
---|---|---|
SIGQUIT | 终止+core | 终端退出符(ctrl+\) |
SIGILL | 终止+core | 非法硬件指令 |
SIGABRT | 终止+core | 异常终止(abort) |
SIGSEGV | 终止+core | 无效内存引用 |
SIGTRAP | 终止+core | 硬件故障 |
- 修改core dump文件名:通过修改
/proc/sys/kernel/core_uses_pid
文件可以让生成 core 文件名自动加上 pid 号。例如echo 1 > /proc/sys/kernel/core_uses_pid
,生成的 core 文件名将会变成 core.pid,其中 pid 表示该进程的 PID - 修改core dump文件的保存位置及文件名格式:例如可以用
echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
设置生成的 core 文件保存在 “/tmp/corefile” 目录下,文件名格式为 “core-命令名-pid-时间戳”
7.3 调试Core Dump
- 首先,使用
gcc
编译源文件,加上-g
以增加调试信息 - 打开 core dump 以使程序异常终止时能生成 core 文件
- 运行程序,当core dump 之后,使用命令
gdb program core
来查看 core 文件(program
为可执行程序名,core
为生成的 core 文件名)
1)无效内存引用
#include
int main()
{
int *p = NULL;
int x = *p;
return 0;
}
生成core dump:
root@068ca8da6d06:~# gcc -g test.c
root@068ca8da6d06:~# ls
a.out test.c
root@068ca8da6d06:~# ./a.out
Segmentation fault (core dumped)
root@068ca8da6d06:~# ls
a.out core test.c
使用gdb调试:
root@068ca8da6d06:~# gdb a.out core
...
...
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000556146c2160a in main () at test.c:6
6 int x = *p;
(gdb) where
#0 0x0000556146c2160a in main () at test.c:6
(gdb) info frame
Stack level 0, frame at 0x7ffe5f70daa0:
rip = 0x556146c2160a in main (test.c:6); saved rip = 0x7f65fcff1b97
source language c.
Arglist at 0x7ffe5f70da90, args:
Locals at 0x7ffe5f70da90, Previous frame's sp is 0x7ffe5f70daa0
Saved registers:
rbp at 0x7ffe5f70da90, rip at 0x7ffe5f70da98
从上面可以看出,我们可以还原程序执行时的场景,并使用 where 可以查看当前程序调用函数栈帧, 还可以使用 gdb 中的命令查看寄存器,变量等信息
五.进程控制
1.进程标识
进程通过进程ID进行标识
进程ID可以复用。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID,从而防止新进程被认为是使用同一ID的某个已终止进程
1.1 2个特殊进程
- 0号进程:调度进程(也被称为系统进程或交换进程)
- 性质:内核的一部分,并不执行任何磁盘上的程序
- 1号进程:init进程
- 性质:普通进程,但以超级用户特权运行。程序文件在早期版本中是
/etc/init
,新版本中是/sbin/init
- 启动时机:在自举(自举是什么鬼?)过程结束时由内核调用
- 职责:
- 读取与系统有关的初始化文件,并将系统引导到一个状态(如多用户)
- 成为所有孤儿进程的父进程
- ...
- 性质:普通进程,但以超级用户特权运行。程序文件在早期版本中是
1.2 进程相关的ID获取函数
2.进程的创建
下列函数可以用来创建进程:
- fork
- vfork(fork 4种变体的一种,另外3种支持的平台不多)
2.1 fork
子进程和父进程继续执行fork调用之后的指令
1)执行顺序
一般来说,fork后父子进程谁先执行是不确定的,取决于内核所用的调度算法
2)数据共享
因为fork之后经常跟随着exec
,所以很多实现中,子进程的数据段、堆和栈指向相应的父进程版本,因此父子进程共享使用,如果子进程有修改,会执行写时复制(COW)(通常将修改所在的一”页“进行复制)。从而不会影响父进程
- 注意父子进程之间I/O缓冲区的复制:
- 文件I/O中write不带缓冲
- 标准I/O库带缓冲。因此要特别注意父子进程间缓冲区的复制
- 注意父子进程的文件描述符:
- 在重定向父进程的标准输出时,子进程的标准输出也会被重定向
fork
的一个特性是父进程的所有打开文件描述符都被”复制“(就好像执行了dup函数)到子进程中,父子进程每个相同的打开描述符共享一个文件表项(故父子进程共享同一个文件偏移量 )
一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父进程和子进程都向标准输出进行写操作。如果父进程的标准输出已重定向,那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量
除了打开的文件描述符之外,子进程还继承了父进程的下列属性:
- 实际用户
ID
- 实际组
ID
- 有效用户
ID
- 有效组
ID
- 附属组
ID
- 进程组
ID
- 会话
ID
- 控制终端
- 设置用户
ID
标志和设置组ID
标志 - 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 信号屏蔽和信号处理
- 对任一打开文件描述符的执行时关闭标志
- 环境
- 连接的共享存储段
- 存储映像
- 资源限制
父子进程的区别为:
fork
返回值不同- 进程
ID
不同 - 进程父进程
ID
不同 - 子进程的
tms_utime,tms_stime,tms_cutime,tms_ustime
的值设置为0 - 子进程不继承父进程设置的文件锁
- 子进程的未处理闹钟被清除
- 子进程的未处理信号集设置为空集
3)fork失败的主要原因
- 系统已经有了太多的进程
- 实际用户的进程总数超过了系统的限制(
CHILD_MAX
规定了每个实际用户在任一时刻拥有的最大进程数)
4)fork的2种用法
- 父进程希望复制自己,使父进程和子进程同时执行不同的代码段(在网络服务中很常见:父进程等待请求,然后调用
fork
并使子进程处理请求) - 父进程要执行一个不同的程序(在shell是很常见。此时子进程从
fork
返回之后立即调用exec
)
2.2 vfork
vfork用于创建一个新进程,该新进程的目的是exec一个新程序,因此子进程并不将父进程的地址空间完全复制到子进程中
在子进程调用exec或exit之前,它在父进程的空间中运行。这种优化在某些UNIX系统的实现中提高了效率,但如果子进程修改数据、进程函数调用、或者没有调用exec或exit就返回都可能会带来未知的结果
vfork
和fork
之间的另一个区别是:vfork
保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)
int globval =6;
int main(int argc,char ** argv) {
int val;
pid_t pid;
val = 88;
printf("before vfork\n");
if ((pid = vfork()) < 0) {
exit(1);
} else if (pid == 0) {
globval++;
val++;
_exit(0); //_exit并不执行标准I/O缓冲区的冲洗操作,如果调用的是exit,输出是不确定的
}
printf("pid=%ld,glob=%d,val=%d\n", (long) getpid(), globval, val);
exit(0);
}
上述程序子进程会修改父进程地址空间中的数据,子进程修改变量后即退出,以下为父进程的输出:
before vfork
pid=30595,glob=7,val=89
3.进程的终止
8种终止方式
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等
3.1 子进程向父进程传递状态
- 子进程正常终止:子进程将其退出状态作为参数传给
exit
、_exit
、_Exit
- 子进程异常终止:内核会产生一个指示子进程异常终止原因的终止状态
不管正常终止或异常终止,内核都向父进程发送SIGCHLD
信号,父进程可以选择忽略该信号(默认动作),或者提供一个信号处理函数
父进程能通过wait
、waitpid
获取子进程的终止状态
父进程调用wait
、waitpid
时可能出现几种情况:
- 如果所有子进程都还在运行,则阻塞
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得子进程的终止状态立即返回
- 如果没有任何子进程,则出错返回
- 对于
waitpid
,如果指定的进程或进程组不存在,或参数pid
指定的进程不是调用进程的子进程,都可能出错
3.2 父子进程以不同顺序终止
- 父进程在子进程之前终止:该进程的所有子进程会变成孤儿进程,这些孤儿进程会被init进程收养,作为新的父进程(过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1)
- 子进程在父进程之前终止:无法确保子进程终止时,父进程已经准备好了检查其终止状态。因此内核为每个终止子进程保存了一定量的信息。当父进程调用
wait
或waitpid
时,可以得到这些信息。这些信息至少包括进程ID、进程的终止状态、进程使用的CPU时间总量。在父进程调用wait
或waitpid
之前,终止子进程被称为僵死进程(init的子进程终止时,init就会调用一个wait
函数取得其终止状态)
4.竞争条件
竞争条件是多个进程企图对共享数据进程某种处理,最终结果取决于进程运行的顺序
如果fork之后的某种逻辑显示或隐式地依赖于在fork之后是父进程先运行还是子进程先运行,那么fork就会是竞争条件活跃的滋生地
防止竞争条件最简单的手段:
- 父进程可以通过
wait
等待子进程先运行完 - 子进程可以通过下列方式等待父进程先运行结束
但是这种轮询方式会大量浪费CPU浪费while(getppid() != 1) sleep(1);
好的办法是使用信号进行通信,或者使用IPC
在父进程和子进程的关系中,常常出现下列情况:在fork之后,父进程和子进程都有一些事情要做。例如,,父进程可能要用子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方,并且在继续运行之前,要等待另一方完成其初始化操作:
TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx */
if ( (pid = fork()) < 0 ){
err_sys("fork error");
} else if(pid == 0){ /* child */
/* child does whatever is necessary ... */
TELL_PARENT(getppid()); /* tell parent we're done */
WAIT_PARENT(); /* and wait for parent */
/* and the child continues on its way ... */
exit(0);
}
/* parent does whatever is necessary ... */
TELL_CHILD(pid); /* tell child we're done */
WAIT_CHILD(); /* and wait for child */
/* and the parent continues on its way ... */
exit(0);
5.exec函数
当进程调用一种exec
函数时,该进程执行的程序完全替换成新程序,而新程序则从main
函数开始执行
- 调用
exec
前后,进程ID
并未改变。因为exec
并不创建新进程 exec
只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆和栈
有7种不同的exec
函数可以供使用,它们被统称称作exec
函数:
- 进程ID和父进程ID
- 实际用户ID和实际组ID
- 附属组ID
- 进程组ID
- 会话ID
- 控制终端
- 闹钟尚余留的时间
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 文件锁
- 进程信号屏蔽
- 未处理信号
- 资源限制
- nice值
- tms_utime、tms_stime、tms_cutime以及tms_cstime值
注意:
- 进程中每个打开的文件描述符都有一个执行时关闭标志。若设置了此标志,则执行
exec
时会关闭该文件描述符;否则该文件描述符仍然保持打开。系统默认行为是不设置执行时关闭标志 - 进程的
实际用户ID
和实际组ID
不变,有效用户ID
和有效组ID
是否改变取决于所执行程序文件的设置用户ID位
和设置组ID位
是否设置- 1)若新程序的
设置用户ID位
已设置,则有效用户ID
变成程序文件所有者的ID
;2)否则有效用户ID
不变 - 1)若新程序的
设置组ID位
已设置,则有效组ID
变成程序文件所有组的ID
;2)否则有效组ID
不变
- 1)若新程序的
6.更改用户ID和更改组ID
用以修改进程的特权及访问控制
UNIX中,特权及访问控制,是基于用户ID和组ID的
- 当程序需要增加特权,或需要访问当前并不允许访问的资源时,需要更换自己的用户ID或组ID,使得新ID具有合适的特权或访问权限
- 当程序需要降低特权,或阻止对某些资源的访问时,也需要更换用户ID或组ID,新ID不具有相应特权或访问这些资源的能力
谁能更改ID需要遵循若干规则,以用户ID
为例(针对setgid
的讨论类似setuid
):
- 如果进程具有超级用户特权,则
setuid
函数将实际用户ID
,有效用户ID
以及保存的设置用户ID
(saved set-user-ID) 全部设置为uid
(此时uid
没有限制) - 如果进程没有超级用户特权,但是
uid
等于实际用户ID
或者保存的设置用户ID
,则setuid
只会将有效用户ID
设置为uid
,不改变实际用户ID
和保存的设置用户ID
- 如果上面两个条件都不满足,则
errno
设置为EPERM
并返回 -1
上述讨论中,假设_POSIX_SAVED_IDS
为真。如果未提供此功能,则对于保存的设置用户ID
部分都无效
操作系统内核为每个进程维护3个用户ID
:实际用户ID
、有效用户ID
、保存的设置用户ID
:
- 只有超级用户进程可以更改
实际用户ID
。通常,实际用户ID
是在用户登录时,由login(1)
程序设置的,而且绝不会改变它。因为login
是一个超级用户进程,当它调用setuid
时,设置所有3个用户ID
- 仅当对程序文件设置了
设置用户ID位
时,exec
函数才设置有效用户ID
。如果设置用户ID位
没有设置,则exec
函数不会改变有效用户ID
,而是维持其现有值- 任何时候都可以调用
setuid
将有效用户ID
设置为实际用户ID
或者保存的设置用户ID
- 调用
setuid
时,有效用户ID
不能随意取值,只能从实际用户ID
或者保存的设置用户ID
中取得
- 任何时候都可以调用
保存的设置用户ID
是由exec
复制有效用户ID
而得到。如果设置了文件的设置用户ID位
,则exec
根据文件的用户ID
设置了进程的有效用户ID
之后,这个副本就保存起来
下图总结了更改这3个用户ID
的不同方法:
6.2 setreuid和setregid
历史上,BSD支持setreuid函数,其功能是交换实际用户ID
和有效用户ID
的值
一个非特权用户总能交换实际用户ID
和有效用户ID
(这就允许一个设置用户ID程序交换用户的普通权限,以后又再次交换回设置用户ID权限。POSIX.1引进了保存的设置用户ID特性后,其规则也相应加强,它允许一个非特权用户将其有效用户ID设置为保存的设置用户ID)
6.3 seteuid和setegid
POSIX.1提供下列2个函数,类似于setuid
和setgid
,但是他们只更改有效ID
,不更改实际ID
:
7.system函数
system
用于将一个字符串作为命令来执行
ISO C定义了system函数,但是其操作对系统的依赖性很强。POSIX.1包括了system接口,它扩展了ISO C定义,描述了system在POSIX.1环境中的运行行为
system
VS fork + exec
:
- 优点:
system
进行了所需的各种出错处理以及各种信号处理 - 缺点:一旦调用
system
的进程具有超级用户权限,则system
执行的命令也具有超级用户权限。因为system
的实现过程中并没有更改有效用户ID
和实际用户ID
的操作- 因此如果一个进程以特殊的权限运行,而它又想生成另一个进程执行另外一个程序,则它应该直接使用
fork + exec
并且在fork
之后,exec
之前改回普通权限 - 设置用户
ID
和设置组ID
程序绝不应该调用system
函数
- 因此如果一个进程以特殊的权限运行,而它又想生成另一个进程执行另外一个程序,则它应该直接使用
8.用户标识
通常失败的原因是:调用此函数的进程没有连接到用户登录时所用的终端。比如守护进程
9.进程调度
进程可以通过调整nice
值选择以更低优先级运行,只有特权进程允许提高调度权限
nice
值的范围在0~(2*NZERO)-1
之间,有些实现支持0~2*NZERO
(NZERO
是系统默认的nice
值)nice
值越低,优先级越高
进程可以通过nice
函数获取或更改它的nice
值,进程只能影响自己的nice
值,不能影响任何其他进程的nice
值:
getpriority
函数可以像nice
函数那样用于获取进程的nice
值,但是它还可以获取一组相关进程的nice
值:
setpriority
函数可用于为进程、进程组和属于特定用户ID的所有进程设置优先级
Single UNIX Specification没有对在fork之后子进程是否继承
nice
值制定规则。而是留给具体实现自行决定。但是遵循XSI的系统要求进程调用exec
后保留nice
值。在FreeBSD 8.0、Linux 3.2.0、MacOS x 10.6.8以及Solaris 10中,子进程从父进程中继承nice
值
10.进程时间
任一进程可调用times
函数获得它自己以及已终止子进程的墙上时钟时间、用户CPU时间、系统CPU时间
struct tms{
clock_t tms_utime; /* 用户CPU时间 */
clock_t tms_stime; /* 系统CPU时间 */
clock_t tms_cutime; /* 已终止子进程的用户CPU时间 */
clock_t tms_cstime; /* 已终止子进程的系统CPU时间 */
};
结构中两个针对子进程的字段包含了此进程用wait
函数族已等待到的各子进程的值
墙上时钟时间作为函数返回值返回。这个值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用相对值。例如,调用times
,保存其返回值。在以后某个时间再次调用times
,从新返回的值中减去以前返回的值,此差值就是墙上时钟时间(一个长期运行的进程可能其墙上时钟时间会溢出,当然这种可能性极小)
所有由此函数返回的clock_t
值都用sysconf(_SC_CLK_TCK)
(每秒时钟滴答数)转换成秒数
六.线程
当一个进程需要另一个实体来完成某事时,Unix上大多数网络服务器通过fork一个子进程来处理。但是fork调用存在一些问题:
- fork是昂贵的。fork要把父进程的内存映像复制到子进程,并在子进程中复制所有描述符。尽管现在使用写时拷贝技术,避免在子进程切实需要自己的副本之前把父进程的数据空间拷贝到子进程。但是fork仍然是昂贵哦的
- fork返回之后父子进程之间信息的传递需要进程间通信(IPC)机制。调用fork之前,父进程向尚未存在的子进程传递信息相当容易,因为子进程将从父进程数据空间及所有描述符的一个副本开始运行。然而从子进程往父进程返回信息却比较费力
线程有助于解决上述问题,它被称为“轻量级进程”,创建可能比进程的创建快10~100倍。但是,伴随这种简易性而来的是同步问题
线程之间的资源共享:
- 同一进程内的线程共享
- 相同的全局内存
- 进程指令
- 大多数数据
- 打开的文件(即描述符)
- 信号处理函数和信号设置
- 当前工作目录
- 用户ID和组ID
- 线程之间不共享
- 线程ID
- 寄存器集合(包括程序计数器和栈指针)
- 栈(用于存放局部变量和返回地址)
- errno
- 信号掩码
- 优先级
这一章介绍的是POSIX线程,也称为Pthread。POSIX线程作为POSIX.1c标准的一部分在1995年得到标准化,大多数UNIX版本将来会支持这类线程。所有Pthread函数都以pthread_打头
1.相关函数
1)pthread_create函数
该函数用于创建一个POSIX线程。当一个程序由exec启动执行时,称为“初始线程”或“主线程”的单个线程就创建了。其余线程则由pthread_create函数创建
如果发生错误,函数返回指示错误的某个正值,不会设置errno变量
创建的线程通过调用指定的函数开始执行,然后显示地(通过调用pthread_exit)或隐式地(通过让该函数返回)终止
线程创建时,并不能保证哪个线程会先运行
2)pthread_join函数
pthread_join类似于进程中的waitpid,用于等待一个给定线程的终止
对于一个非脱离状态的线程,如果没有其它线程调用pthread_join等待线程终止,那么线程终止后的资源无法回收,会造成资源浪费,进而影响同一进程创建的线程数量
#include "apue.h"
#include
void *
thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return((void *)1);
}
void *
thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret);
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
上述程序输出如下:
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2
3)pthread_self函数
线程可以使用pthread_self获取自身的线程ID,类似于进程中的getpid
4)pthread_detach函数
一个线程或者是可汇合的,或者是脱离的:
- 可汇合:一个可汇合线程终止时,它的线程ID和退出状态将保存到另一个线程对它调用pthread_join。如果一个线程需要知道另一个线程什么时候终止,那就最好保持第二个线程的可汇合状态
- 脱离:脱离的线程像守护进程,当它们终止时,所有相关资源都被释放,不能等待它们终止
- 一般在不关心线程返回状态,希望系统在线程终止时自动清理并退出时调用
pthread_detach
- 其它线程调用了
exit
,或是主线程执行return
时,即便是分离的线程也会终止。换而言之,pthread_detach
只是控制线程终止之后所发生的事情,而非何时或如何终止线程
- 一般在不关心线程返回状态,希望系统在线程终止时自动清理并退出时调用
5)pthread_exit函数
线程终止的一个方法:
让一个线程终止的其它方法:
- 线程执行的函数返回,在
pthread_create
参数中,这个函数的返回值是一个void*指针,它指向相应线程的终止状态 - 被同一进程的其它线程调用
pthread_cancel
取消(该函数只是发起一个请求,目标线程可以选择忽略取消或控制如何被取消) - 任何线程调用
return
exit
、_Exit
、_exit
终止时,整个进程就终止了,其中包括它的任何线程
如果主线程调用了
pthread_exit
,而非exit
或return
,那么其它线程将继续运行
下列程序status指向一个栈上的结构,这个栈上的对象被后来的线程覆盖:
#include "apue.h"
#include
struct foo {
int a, b, c, d;
};
void
printfoo(const char *s, const struct foo *fp)
{
printf("%s", s);
printf(" structure at 0x%lx\n", (unsigned long)fp);
printf(" foo.a = %d\n", fp->a);
printf(" foo.b = %d\n", fp->b);
printf(" foo.c = %d\n", fp->c);
printf(" foo.d = %d\n", fp->d);
}
void *
thr_fn1(void *arg)
{
struct foo foo = {1, 2, 3, 4};
printfoo("thread 1:\n", &foo);
pthread_exit((void *)&foo);
}
void *
thr_fn2(void *arg)
{
printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
pthread_exit((void *)0);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
struct foo *fp;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
err_exit(err, "can't join with thread 1");
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}
mac上输出如下:
thread 1:
structure at 0x700000080ed0
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 123145302839296
parent:
structure at 0x700000080ed0
[1] 34604 segmentation fault ./badexit2
6)pthread_equal函数
Linux 3.2.0使用无符号长整型表示
pthread_t
数据类型。Solaris 10将其表示为无符号整形。FreeBSD 8.0和Mac OS X 10.6.8用一个指向pthread
结构的指针来表示pthread_t
数据类型
7)pthread_cancel函数
- 函数只是发起取消请求,目标线程可以忽略取消请求或控制如何被取消(即执行一些清理函数)
线程控制自身对pthread_cancel函数的响应行为:取消选项
8)pthread_cleanup_push和pthread_cleanup_pop函数
以下函数被线程调用时,可以添加或清除清理函数:
下列情况会调用清理函数:
- 线程调用
pthread_exit
时 - 线程响应取消请求时
- 用非零
execute
参数调用pthread_cleanup_pop
时
以下情况不会调用清理函数;
- 线程通过
return
或exit
终止时 execute
参数为0时
不管excute
参数是否为0,pthread_cleanup_pop
函数都会将线程清理函数栈的栈顶函数删除
以下为一个测试程序:
#include
#include
int exceptionFun()
{
pthread_exit((void *)0); //会调用clean
//return 0; //会不会调用clean取决于exceptionFun外的pthread_cleanup_pop的参数
//exit(0); //不会调用clean
//_Exit(0); //不会调用clean
}
void clean(void *arg)
{
printf("clean function\n");
}
void* thr1(void *arg)
{
printf("thread 1 created\n");
pthread_cleanup_push(clean,NULL);
//pthread_exit((void *)0); //会调用clean,第2个线程会启动
//return 0; //不会调用clean,且第2个线程不会启动
//exit(0); //不会调用clean,且第2个线程不会启动
//_Exit(0); //不会调用clean,且第2个线程不会启动
pthread_cleanup_pop(1); //pthread_cleanup_pop(0)时,如果没有在push和pop之间退出,那么不会执行clean
//否则,根据退出时调用的是pthread_exit,return...决定
}
void* thr2(void *arg)
{
printf("thread 2 created\n");
pthread_cleanup_push(clean,NULL);
exceptionFun();
pthread_cleanup_pop(1);
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thr1,NULL);
pthread_join(tid,NULL);
pthread_create(&tid,NULL,thr2,NULL);
pthread_join(tid,NULL);
return 0;
}
2.线程同步
- 1)互斥锁
- 互斥锁的初始化与销毁
- 互斥锁的加锁与解锁
- 互斥锁的定时加锁
- 2)读写锁
- 读写锁的初始化与销毁
- 读写锁的加锁与解锁
- 读写锁的定时加锁
- 3)条件变量
- 条件变量的初始化与销毁
- 等待某个条件变量
- 通知条件已经满足
- 4)自旋锁
- 自旋锁的初始化与销毁
- 自旋锁的加锁与解锁
- 5)屏障
- 屏障的初始化与销毁
- 增加到达屏障点的线程
2.1 互斥锁
也称为互斥量
多线程编程中,多个线程可能修改相同的变量,导致错误发生。互斥锁可以用于保护共享变量:访问共享变量的前提条件是持有该互斥锁,按照Pthread,互斥锁的类型为pthread_mutex_t的变量
1)互斥锁的初始化与销毁
- 如果某个互斥锁变量是静态分配的,必须把它初始化为常值PTHREAD_MUTEX_INITIALIZER
- 如果在共享内存区中分配一个互斥锁,必须通过调用pthread_mutex_init函数在运行时初始化,此时在释放内存前需要调用pthread_mutex_destroy
如果释放互斥锁时,有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥锁加锁,其它线程就会看到互斥锁依然是锁着的,只能回去再次等待它重新变为可用。这种方式下,每次只有一个线程可以向前执行
2)互斥锁的加锁与解锁
使用互斥锁解决修改相同变量的问题(本书作者测试这个程序和前面有问题的版本运行的时间,差别是10%,说明互斥锁并不会带来太大的开销)
使用互斥锁时避免死锁:
- 如果线程试图对一个互斥锁加锁两次,那么它自身就会陷入死锁状态
- 如果使用多个互斥锁,并且每个线程取得其中一个,阻塞于对另一个的请求上,也会死锁
- 1)可以通过控制加锁的顺序来防止
- 2)如果尝试获取另一个锁时失败,那么释放自己占有的锁,过一段时间再试
3)互斥锁的定时加锁
Max OS X 10.6.8还没有支持该函数
2.2 读写锁
也称作共享互斥锁:当读写锁是读模式锁住时,可以说成是共享模式锁住的;当它是写模式锁住时,可以说成是以互斥模式锁住的
读写锁与互斥锁类似,不过读写锁允许更高的并行性:
- 互斥锁要么是锁住状态,要么是不加锁状态,而且一次只有一个线程可以对其加锁
- 读写锁可以有3种状态
- 读模式下加锁状态:多个线程可以用时占有读模式的读写锁,此时任何试图以写模式对该读写锁加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止(通常这种情况下会阻塞随后的读模式锁请求,从而避免读模式长期占用,而等待的写模式锁请求一直得不到满足)
- 写模式下加锁状态:一次只有一个线程可以占有写模式的读写锁
- 不加锁状态
读写锁非常适合于对数据结构读的次数远大于写的情况
1)读写锁的初始化与销毁
读写锁在使用之前必须初始化,释放它们底层的内存之前必须销毁:
Single UNIX Specification在XSI扩张中定义了PTHREAD_RWLOCK_INITIALIZER常量。如果默认属性就足够的话,可以用它对静态分配的读写锁进行初始化
2)读写锁的加锁与解锁
与互斥锁一样,Single UNIX Specification提供了带有超时的读写锁加锁函数:
2.3 条件变量
条件变量可以在某个条件发生之前,将线程投入睡眠
按照Pthread,条件变量是类型为pthread_cond_t的变量
1)条件变量的初始化与销毁
如果条件变量是静态分配的,那么可以使用PTHREAD_COND_INITIALIZER初始化
2)等待某个条件变量
两个函数成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件
为什么每个条件变量都要关联一个互斥锁呢?因为”条件“(这里不是指条件变量)通常是线程之间共享的某个变量的值。允许不同线程设置和测试该变量要求有一个与该变量关联的互斥锁
3)通知条件已经满足
2.4 自旋锁
自旋锁与互斥锁类似,区别是:自旋锁在获取锁之前一直处于忙等(自选)阻塞状态。因为忙等会消耗大量CPU,因此适用于锁持有时间不长(即操作可以较快完成)的场景
事实上,有些互斥锁的实现在试图获取互斥锁的时候会自旋一小段时间,只有在自旋计数到达某一阈值的时候才会休眠
1)自旋锁的初始化与销毁
2)自旋锁的加锁与解锁
在持有自旋锁时,不要调用可能会进入休眠状态的函数。如果调用了这些函数,会浪费CPU资源,因为其他线程需要获取自旋锁需要等待的时间就延长了
2.5 屏障
屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行
pthread_join
就是一种屏障,允许一个线程等待,直到另一个线程退出
1)屏障的初始化与销毁
2)增加到达屏障点的线程
- 如果线程不是(满足屏障要求数目的)最后一个线程:会进入休眠
- 如果线程是(满足屏障要求数目的)最后一个线程:唤醒所有因为屏障进入休眠的线程
一旦到达屏障计数值,而且线程处于非阻塞状态,屏障就可以被重用
只有先调用pthread_barrier_destroy
函数,接着又调用pthread_barrier_init
并传入一个新的计数值,否则屏障计数无法改变
七.线程控制
1.线程限制
Single UNIX Specification定义了与线程操作有关的一些限制,这些限制可以通过sysconf函数进程查询:
我的环境(Max OS X 10.11.6)下:
printf("%ld\n",sysconf(_SC_THREAD_STACK_MIN)); //8192
printf("%ld\n",sysconf(_SC_THREAD_THREADS_MAX)); //-1(正常值,因为没有设置errno)
2.线程属性
2.1 线程属性
线程属性用以初始化线程,定义在结构体pthread_attr_t
中,一般包括:
- 分离状态属性
- 线程栈末尾的警戒缓冲区大小
- 线程栈的最低地址
- 线程栈的最小长度(字节)
下图为POSIX.1定义的线程属性在apue描述的4种操作系统实现中的支持情况:
在MAC OS X 10.11.6下的测试:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
int state;
size_t stack_size;
size_t guardsize;
pthread_attr_getdetachstate(&attr,&state);
pthread_attr_getstacksize(&attr,&stack_size);
pthread_attr_getguardsize(&attr,&guardsize);
printf("state:");
if(state == PTHREAD_CREATE_DETACHED){
printf("PTHREAD_CREATE_DETACHED\n"); //PTHREAD_CREATE_DETACHED
}
else if(state == PTHREAD_CREATE_JOINABLE){
printf("PTHREAD_CREATE_JOINABLE\n");
}
printf("stack_size:%u\n",stack_size); //524288(512*1024)bytes
printf("guardsize:%u\n",guardsize); //4096
pthread_attr_destroy(&attr);
1)线程属性的初始化和销毁
要注意pthread_attr_destroy
的返回值,如果忽略其错误返回可能出现的最坏情况是,如果pthread_attr_init
已经分配了内存空间,就会有少量的内存泄露。另一方面,如果pthread_attr_init
成功地对线程属性进行了初始化,但之后的pthread_attr_destroy
清理工作失败,那么将没有任何补救策略,因为线程属性结构对应用程序来说是不透明的,可以对线程属性结构进行清理的唯一接口是pthread_attr_destroy
,但它失败了
2)分离属性的获取与设置
3)栈属性的获取与设置
对于进程来说,虚地址空间的大小是固定的。因为进程中只有一个栈,所以它的大小通常不是问题。但对于线程来说,同样大小的虚地址空间必须被所有的线程栈共享。如果应用程序使用了许多线程,以致这些线程栈的累计大小超过了可用的虚地址空间,就需要减小默认的线程栈大小。另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧,那么需要的栈大小可能要比默认的大
如果线程栈的虚地址空间都用完了,那么可以使用malloc或mmap来为可替代的栈分配空间,并用下列函数设置或者获取线程栈的最低内存地址:
4)栈大小属性的获取与设置
如果希望改变默认的栈大小,但又不想自己处理线程栈的分配问题,使用pthread_attr_setstacksize
非常有用
5)境界缓存大小的获取与设置
- 如果
guardsize
线程属性被修改了,操作系统可能会把它取为页大小的整数倍 - 如果线程的栈指针溢出到警戒区域,应用程序就可能通过信号接收到出错信息
2.2 取消选项
有两个线程属性没有包含在pthread_attr_t
结构中:
- 可取消状态属性(控制线程可否被取消)
- 可取消类型属性(控制线程何时可取消)
这两个属性影响着线程在响应pthread_cancel
函数调用时所呈现的行为
1)可取消状态属性
新旧状态的设置是一个原子操作
pthread_cancel
调用并不等待线程终止。默认情况下,线程在取消请求发出后还是继续运行,直到线程到达某个取消点。取消点是线程检查它是否被取消的一个位置,POSIX.1保证线程调用下列函数时,取消点都会出现:
PTHREAD_CANCEL_ENABLE
:默认值PTHREAD_CANCEL_DISABLE
:对pthread_cancel
的调用并不会杀死线程。相反,取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCEL_ENABLE
时,线程将在下一个取消点上对所有挂起的取消请求进行处理
如果线程长时间不会调用前面所述函数进入一个取消点,可以通过下列函数添加取消点:
- 存在处于挂起状态的取消请求
- 取消状态为
PTHREAD_CANCEL_ENABLE
否则,该函数没有效果
2)可取消类型属性
3.同步属性
就像线程具有属性一样,线程的同步对象也有属性。在使用某种线程同步机制时,同步属性用以初始化相应的同步机制。比如互斥锁属性用以初始化互斥锁、条件变量属性用以初始化条件变量...
- 互斥锁
- 进程共享属性
- 健壮属性
- 类型属性
- 读写锁
- 进程共享属性
- 条件变量
- 进程共享属性
- 时钟属性
- 屏障
- 进程共享属性
进程共享属性控制着同步对象是否可用于进程与进程之间
3.1 互斥锁属性
互斥锁属性用pthread_mutexattr_t
结构表示
3个值得注意的互斥锁属性:
- 进程共享属性
- 健壮属性
- 类型属性
1)互斥锁属性的初始化和销毁
- 获得默认互斥锁属性
- 调用
pthread_mutex_init
时,传入空指针,即用默认互斥锁属性来初始化互斥锁 - 使用PTHREAD_MUTEX_INITIALIZER常量
- 调用
pthread_mutexattr_init
函数
- 调用
这个属性是可选的。可以通过2种方式检查是否支持该属性:
1)检查系统中是否定义了_POSIX_THREAD_PROCESS_SHARED
符号判断这个平台是否支持进程共享这个属性; 2)可以在运行时把_SC_THREAD_PROCESS_SHARED
参数传给sysconf
函数进行检查)
3)健壮属性的获取和设置
互斥锁的健壮属性与在多个进程间共享的互斥锁有关。意味着,当持有互斥锁的进程终止时,需要解决互斥锁状态恢复的问题。这种情况发生时,互斥锁处于锁定状态,恢复起来很困难。其它阻塞在这个锁的进程将会一直阻塞下去
如果应用状态无法恢复,在线程对互斥锁解锁后,该互斥锁将处于永久不可用状态。为了避免这样的问题,线程可以通过调用pthread_mutex_consistent
函数,指明该互斥锁相关的状态在互斥锁解锁以前是一致的
4)类型属性的获取和设置
类型属性控制着互斥锁的锁定特性。POSIX.1定义了4种类型:
PTHREAD_MUTEX_NORMAL
:标准类型,不做任何特殊的错误检查或死锁检测PTHREAD_MUTEX_ERRORCHECK
:提供错误检查PTHREAD_MUTEX_RECURSIVE
:允许同一线程在互斥锁解锁之前对该互斥锁进程多次加锁。递归互斥锁维护锁的计数,在解锁次数和加锁次数不相同的情况下不会释放锁PTHREAD_MUTEX_DEFAULT
:该类型可以提供默认特性和行为。操作系统在实现的时候可以把这种类型自由地映射到其它互斥锁类型中的一种
下图为不同的类型属性和行为:
读写锁属性用pthread_rwlockattr_t
结构表示
进程共享属性是读写锁的唯一属性
1)读写锁属性的初始化与销毁
进程共享属性是读写锁的唯一属性,可以通过下列函数获取与设置
3.3 条件变量属性
条件变量属性用pthread_condattr_t
结构表示
Single UNIX Specification目前定义了条件变量的2个属性:
- 进程共享属性
- 时钟属性
1)条件变量属性的初始化与销毁
与其他的同步属性一样,条件变量支持进程共享属性。它控制着条件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使用
时钟属性控制计算pthread_cond_timedwait
函数的超时参数tsptr
采用的是哪个时钟
3.4 屏障属性
条件变量属性用pthread_barrierattr_t
结构表示
进程共享属性是屏障的唯一属性
1)屏障属性的初始化与销毁
目前定义的屏障属性只有进程共享属性,它控制着屏障是可以被多进程的线程使用,还是只能被初始化屏障的进程内的多线程使用
4.线程特定数据
线程特定数据也称为线程私有数据,是存储和查询某个线程相关数据的一种机制。它是每个线程私有的数据副本,因此不需要担心与其它线程的同步访问问题
- 无法简单的分配一个每线程数据数组:因为无法通过线程ID去定位数组中的某个具体数据。因为线程ID并不能保证是小而连续的整数。即使是,我们可能还希望有一些额外保护,防止某个线程的数据与其他线程的数据相混淆(比如线程间的数据可能越界写?)
- 线程特定数据提供了让基于进程的接口适应多线程环境的机制。比如errno,线程出现以前,errno定义为进程上下文中全局可访问的整数。为了让线程也能使用那些原本基于进程的系统调用和库例程,errno被重新定义为线程私有数据。这样线程之间不会相互影响
一个线程没有办法阻止另一个线程访问它的数据。线程特定数据也不例外。虽然底层的实现部分并不能阻止这种访问能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其它线程的线程特定数据
每个系统支持有限数量的线程特定数据元素,POSIX要求这个限制不小于128(每个进程),系统为每个进程维护一个称之为Key结构的数组,如下图:
除了进程范围的Key结构数组外,系统还在进程内维护关于每个线程的多条信息,记录在Pthread结构(由系统维护)中:
一般步骤如下:
- 定义了一个全局静态的
pthread_key_t
变量,表示键 - 其中一个线程调用pthread_key_create从进程的
key
数组创建一个未使用的键(为了防止被多次调用,可以使用pthread_once) - 所有线程可以使用这个新键通过pthread_getspecific索引自己的
pkey
数组的相应位置- 如果返回一个空指针,说明相应的线程特定数据元素不存在,可以调用malloc分配,然后调用pthread_setspecific将这个新分配的线程特定数据的指针保存在
pkey
数组中 - 如果返回一个非空指针,那么可以使用这个线程特定数据
- 如果返回一个空指针,说明相应的线程特定数据元素不存在,可以调用malloc分配,然后调用pthread_setspecific将这个新分配的线程特定数据的指针保存在
- 调用pthread_key_create函数时指定的析构函数会释放保存在每个线程
pkey
数组中的线程特定数据
1)pthread_once和pthread_key_create函数
- keyptr:创建一个新的线程特定数据元素时,系统搜索其Key结构数组找到第一个不在使用的元素,元素的索引(0~127)记录在keyptr成员中,作为返回值。随后,线程可以利用记录在keyptr中的索引,在Pthread结构pkey数组的对应索引位置处存储一个指针,这个指针指向malloc动态分配的内存
- destructor:指向析构函数的函数指针,当一个线程终止时,系统扫描该线程的pKey数组,为每个非空的pkey指针调用相应的析构函数,释放其指向的动态内存。如果为NULL,表明没有析构函数与该键关联
下列情况会调用析构函数:
- 当线程调用
pthread_exit
时 - 当线程执行返回,正常退出时
- 线程取消时,只有在最后的清理处理程序返回之后,析构函数才会被调用
下列情况不会调用析构函数:
- 线程调用了
exit
、_exit
、_Exit
或abort
时 - 出现其他非正常的退出时
线程退出时,线程特定数据的析构函数将按照操作系统实现中定义的顺序被调用。当所有的析构函数都调用完成之后,系统会检查是否还有非空的线程特定数据值与键关联,如果有的话,再次调用析构函数。这个过程将会一直重复到线程所有的键都为空,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS
中定义的最大次数的尝试
pthread_once函数:
- onceptr:onceptr参数指向的变量中的值,确保init参数所指的函数在进程范围内只被调用一次
- init:进程范围内,对于一个给定的键,pthread_key_create只能被调用一次。所以init可以指向一个pthread_key_create函数,通过onceptr参数确保只调用一次
2)pthread_getspecific和pthread_setspecific函数
3)pthread_key_delete函数
5.线程和信号
每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着单个线程可以阻止某些信号,但当线程修改了与某个给定信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样,如果一个线程选择忽略某个给定信号,那么另一个线程就可以通过以下两种方式撤销上述信号选择:恢复信号的默认处理行为,或者为信号设置一个新的信号处理程序
进程中的信号是递送到单个线程的:
- 如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去
- 其它的信号则被发送到任意一个线程
5.1 阻止信号发送
sigprocmask
的行为在多线程的进程中没有定义,线程必须使用pthread_sigmask
:
线程可以通过把set
设为NULL,把oset
参数设为sigset_t
结构的地址,来获取当前的信号屏蔽字。这种情况下,how
参数会被忽略
5.2 等待信号
线程调用下列函数等待一个或多个信号出现
注意:
- 在返回之前,
sigwait
将从进程中移除那些处于挂起等待状态的信号。如果具体实现支持排队信号,并且信号的多个实例被挂起,那么sigwait
将会移除该信号的一个实例,其他的实例还要继续排队 - 为了避免错误行为发生,线程在调用
sigwait
前,必须阻塞那些它正在等待的信号。sigwait
会原子地取消信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait
将恢复线程的信号屏蔽字。如果信号在sigwait
被调用的时候没有被阻塞,那么在线程完成对sigwait
的调用之前会出现一个时间窗,在这个时间窗中,信号就可以被发送给线程 - 使用
sigwait
的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中。然后可以安排专用线程处理信号。这些专用线程可以进行函数调用,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程上下文,而非会中断线程正常执行的传统信号处理程序 - 如果多个线程在
sigwait
的调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从sigwait
中返回。如果一个信号被捕获,而且一个线程正在sigwait
调用中等待同一信号,那么这时将由操作系统来决定以何种方式递送信号。操作系统可以让sigwait
返回,也可以激活信号处理程序,但这两种情况不会同时发生
要把信号发送给进程,可以调用kill
。要把信号发送给线程,可以调用pthread_kill
:
闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以,进程中的多个线程不可能互不干扰地使用闹钟定时器
6.线程和fork
当线程调用fork时,就为子进程创建了整个进程地址空间的副本:
- 子进程通过继承整个地址空间的副本,还从父进程那儿继承了每个互斥量、读写锁和条件变量的状态
- 子进程内部只存在一个线程,它由父进程中调用fork的线程的副本构成。如果父进程中的线程占有锁,子进程将同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁、需要释放哪些锁
如果fork后马上调用其中一个exec
函数,就能避免这样的问题。这种情况下,旧的地址空间被丢弃,所以锁的状态无关紧要
在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明,在fork返回和子进程调用其中一个exec
之间,子进程只能调用异步信号安全的函数。这就限制了在调用exec
之前子进程能做什么,但不涉及子进程中锁状态的问题
要清除锁状态,可以调用pthread_atfork
建立fork处理程序:
prepare
:- 由父进程在fork创建子进程前调用
- 任务是获取父进程定义的所有锁
parent
:- fork创建子进程以后,返回之前在父进程上下文中调用
- 任务是对
prepare
fork处理程序获取的所有锁进行解锁
child
:- fork返回之前在子进程上下文中调用
- 与
prepare
fork处理程序一样,也必须释放prepare
fork处理程序获取的所有锁
如果不需要使用其中某个处理程序,可以给特定的处理程序参数传入空指针,它就不会起任何作用了
可以多次调用pthread_atfork
函数从而设置多套fork处理程序。使用多个fork处理程序时,处理程序的调用顺序并不相同
parent
和child
fork处理程序是以它们注册时的顺序进行调用的- 而
prepare
fork处理程序的调用顺序与它们注册时的顺序相反
这样可以允许多个模块注册它们自己的fork处理程序,而且可以保持锁的层次
7.线程和I/O
在多线程进程中,因为线程共享相同的文件描述符,所以应该使用pread
和pwrite
而不是read
和write
,使得偏移量的设定和数据的读取成为一个原子操作
八.高级I/O
下图为UNIX下5种I/O模型的比较
2.记录锁
商用UNIX系统提供了记录锁机制
记录锁的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区
POSIX.1通过fcntl函数提供记录锁功能
F_GETLK
:判断由flockptr所描述的锁是否会被另外一把锁锁排斥(阻塞)。如果存在一把锁,它阻止创建由flockptr所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在这种情况,则除了l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保存不变F_SETLK
:设置由flockptr所描述的锁,设置失败会出错返回。此命令也用来清除由flockptr指定的锁(l_type为F_UNLCK)F_SETLKW
:该命令是F_SETLK的阻塞版本,命令中的W
表示等待。如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁而不能被授予,那么调用进程会被置为休眠。如果所请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒
第三个参数是一个指向flock结构
的指针:
struct flock{
short l_type; /* F_RDLCK,F_WRLCK,or F_UNLCK */
short l_whence; /* SEEK_SET,SEEK_CUR,or SEEK_END */
off_t l_start; /* offset in bytes,relative to l_whence */
off_t l_len; /* length, in bytes; 0 means lock to EOF */
pid_t l_pid; /* returned with F_GETLK */
};
l_type
F_RDLCK
:共享读锁F_WRLCK
:独占性写锁F_UNLCK
:解锁一个区域
lstart
及l_whence
:要加锁或解锁区域的起始字节偏移量l_len
:区域的字节长度l_pid
:由F_GETLK
返回,表示持有锁的进程的 ID
关于加锁或解锁区域的说明还要注意下列几项规则:
- 指定锁的范围起始偏移量的两个元素
l_whence,l_start
与lseek函数中最后两个参数类似 - 锁可以在当前文件尾端处开始,或者越过尾端处开始,但是不能在文件起始位置之前开始
- 如果
l_len
为 0 ,则表示锁的范围可以扩展到最大可能偏移量。这意味着不论向该文件中追加写了多少数据,它们都可以处于锁的范围内 - 为了对整个文件加锁,我们可以设置
l_whence,l_start
指向文件的起始位置,并指定l_len
为 0
1)读写锁的关系
有两种类型的锁:共享性读锁(l_type=F_RDLCK
),独占性写锁(l_type=F_WRLCK
)。互斥规则为:
- 任意多个进程可以在一个给定的字节上有一把共享性读锁。如果在一个给定字节上已经有了一把或者多把读锁,则不能在该字节上添加写锁(当进程A在文件的某个区间设置了一把读锁时,如果进程B试图在同一个区间设置一把写锁,则进程B会被阻塞。现在如果有另外进程C试图在同一个区间设置一把读锁,则进程C能不能顺利加读锁?POSIX.1并没有说明这一情况。如果进程C允许添加读锁,则很可能后续大量的读锁被添加从而导致进程B的写锁得不到设置从而进程B被饿死)
- 在一个给定字节上只能有一个进程有一把独占性写锁。如果在一个给定字节上已经有了一把写锁,则不能在该字节上添加任何读锁或者写锁
2)锁与文件描述符类型的关系
- 加读锁时,该文件描述符必须是读打开的
- 加写锁时,该文件描述符必须是写打开的
3)一般性步骤
通常使用F_GETLK
命令来测试能否建立一把锁,然后用F_SETLK
或者F_SETLKW
试图建立那把锁。但是注意到:这二者不是一个原子操作!因此不能保证在这两次fcntl
调用之间,可能另有一个进程插入并建立一把相同的锁从而导致本进程建立锁的过程失败
4)相邻区域的组合与分裂
第100~199字节是加锁区:
- 记录锁与进程和文件两者相关联
- 当一个进程终止时,它所建立的所有记录锁全部被释放
- 无论一个文件描述符何时关闭,该进程通过这个描述符所引用的文件上的任何一把锁都将被释放(这些锁都是该进程设置的)
fork
产生的子进程并不继承父进程所设置的记录锁。这是因为父进程与子进程是不同的进程,而记录锁是与进程和文件两者相关联(子进程想获得记录锁,可以在继承而来的文件描述符上调用fcntl
函数来设置记录锁)- 在执行
exec
后,新程序可以继承原程序的记录锁。但注意:如果对一个文件描述符设置了执行时关闭标志,则作为exec
的一部分关闭该文件描述符时,将释放相应文件的所有锁
6)文件尾端加锁与解锁
- 当执行
fcntl(F_SETLK,flockptr)
,而flockptr->l_type=F_WRLCK
,flockptr->l_whence=SEEK_END
,flockptr->l_start=0
,flockptr->l_len=0
时,表示从当前文件尾端开始,包括以后所有可能追加写到该文件的任何字节都将加写锁 - 然后执行
write
一个字节时,该字节将被加锁 - 当执行
fcntl(F_SETLK,flockptr)
,而flockptr->l_type=F_UNLCK
,flockptr->l_whence=SEEK_END
,flockptr->l_start=0
,flockptr->l_len=0
时,表示从当前文件尾端开始解锁,但是之前刚写入的一个字节仍然保持在加锁状态!!
如果你想解除的锁包括之前刚写入的一个字节,则应该flockptr->l_len=-1
, 负的长度值表示在指定偏移量之前的字节数
当对文件的一部分加锁时,内核将指定的偏移量变换成绝对文件偏移量。这是因为当前偏移量和文件尾端可能不断变化,但是这种变化不应该影响已经存在的锁的状态,所以内核必须独立于当前文件偏移量和文件尾端而记住锁的位置
7)建议性锁和强制性锁
- 建议性锁:并不能阻止对文件有读写权限的任何其他进程不使用记录锁而访问文件
- 强制性锁:内核会检查每一个
open,read,write
等操作,验证调用进程是否违背了正在访问的文件上的某一把锁
对于一个特定文件,打开其设置组ID
位,同时关闭其组执行位,就开启了对该文件的强制性锁机制。
因为当组执行位关闭时,设置组
ID
位不再有意义(设置组ID
位的目的就是为了那些需要特殊组执行权限)
如果一个进程试图read/write
一个强制性锁起作用的文件,但是将要读/写的部分又由其他进程上了锁,则结果取决于三方面的因素:操作类型read/write
,其他进程在该区域上持有的记录锁的类型(读锁/写锁),read/write
文件描述符是阻塞的还是非阻塞的
- 通常,即使正在打开的文件具有强制性记录锁,
open
也会成功,随后的read/write
遵从上表中的规则 - 但是,如果
open
调用中的标志指定了O_TRUNC
或者O_CREAT
,且正在打开的文件具有强制性记录锁,则不论是否指定O_NONBLOCK
,open
都将出错返回,errno
设置为EAGAIN
一个恶意用户可以使用强制性记录锁,对大家都可读的文件加一把读锁,这样就能阻止任何人写该文件。
强制性锁是可以设法避开的:强制性锁机制对unlink
函数没有影响,因此你可以先将内容写到一个临时文件中,然后删除 原文件,最后将临时文件改名为原文件名
4.异步I/O
这里主要介绍POSIX异步I/O
POSIX异步I/O接口为对不同类型的文件进程异步I/O提供了一套一致的方法。现在所有的平台都要求支持这些接口
4.1 AIO控制块
这些异步I/O接口使用AIO控制块来描述I/O操作,aiocb
结构定义了AIO控制块,该结构至少包括下面这些字段:
struct aiocb{
int aio_fildes; // 文件描述符
off_t aio_offset; // 文件偏移量
volatile void *aio_buf; // IO缓冲区
size_t aio_nbytes; // 传输的字节数
int aio_reqprio; // 优先级
struct sigevent aio_sigevent; // 信号信息
int aio_lio_opcode; // IO操作列表
}
aio_fildes
:表示被打开用来读或者写的文件描述符aio_offset
:读或者写操作从该字段指定的偏移量开始aio_buf
:- 对于读操作,它指定了从文件中读取内容存放的缓冲区
- 对于写操作,它指定了将要向文件写入的内容存放的缓冲区
aio_nbytes
:指定了要读或者写的字节数aio_reqprio
:指定了异步IO
的优先级,操作系统根据该优先级来安排执行顺序。但是操作系统对于该顺序只有 有限的控制能力,因此不一定能够遵循该提示aio_lio_opcode
:参见lio_listio
函数的说明。它指定了该AIO
控制块是用于多操作、写操作、还是空操作aio_sigevent
:指定IO
事件完成后,如何通知应用程序struct sigevent{ int sigev_notify; // 通知类型 int sigev_signo; // 信号编号 union sigval sigev_value; // 通知的参数 void (*sigev_notify_function)(union sigval);// 作为线程执行的函数 pthread_attr_t *sigev_notify_attributes; // 线程属性 };
sigev_notify
:指定了通知的类型,可以为下列三种之一SIGEV_NONE
:异步IO
请求完成后,不通知进程SIGEV_SIGNAL
:异步IO
请求完成后,产生由sigev_signo
字段指定的信号。如果应用程序选择捕捉此信号,且在建立信号处理程序时,指定了SA_SIGINFO
标志,则该信号将被入队列(如果实现支持排队信号)。信号处理程序将被传递一个siginfo
结构,该结构的si_value
字段将被设置成sigev_value
SIGEV_THREAD
:当异步IO
请求完成后,由sigev_notify_function
字段指定的函数被调用成为一个子线程。sigev_value
字段被传入作为其参数。如果sigev_notify_attributes
为NULL
,则该线程为分离状态;如果sigev_notify_attributes
不是NULL
,则该线程的属性由sigev_notify_attributes
指定
4.2 异步读与异步写
在进行异步I/O前需要先初始化AIO控制块。调用aio_read
函数来进行异步读操作,或调用aio_write
函数来进行异步写操作
注意:I/O操作在等待时,必须确保AIO控制块和数据缓冲区保持稳定。它们下面对应的内存必须始终是合法的,除非I/O操作完成,这些内存不应该被复用
如果想强制所有等待中的异步操作不等待而写入持久化的存储(如硬盘)中,可以设立一个AIO控制块并调用aio_fsync
函数:
AIO控制块中的aio_fildes
字段指定了其异步写操作不等待而写入持久化的存储的那个文件
就像aio_read/aio_write
函数一样,在安排了同步时,aio_fsync
操作立即返回。在异步的同步操作(指的是将数据修改同步到硬盘上的这个操作是异步执行的)完成之前,数据不会被持久化。AIO控制块控制我们如何被通知
4.3 获取异步I/O的状态
4.4 获取异步I/O返回值
直到异步操作完成之前,都要小心的不要调用aio_return
函数。操作完成之前,该函数调用的结果是未定义的。
还需要小心地对每个异步操作只调用一次aio_return
函数,一旦调用了该函数,操作系统就可以释放掉包含IO操作返回值的记录
4.5 阻塞进程到异步I/O完成
如果在完成了所有事务时,还有异步操作未完成,可以调用调用该函数来阻塞进程,直到操作完成:
aio_suspend
可能返回三种结果:
- 如果被一个信号中断,则返回-1 ,并将
errno
设置为EINTR
- 如果
list
指定的异步I/O中,没有任何I/O操作完成的情况下,超时时间到达,则返回-1 ,并将errno
设置为EAGAIN
- 如果
list
指定的异步I/O中,有任何I/O操作完成,则返回 0
如果在调用该函数时,所有的异步I/O操作都已完成,那么函数将在不阻塞的情况下直接返回
4.6 取消异步I/O
当我们不想再完成等待中的异步I/O操作时,可以尝试使用aio_cancel
函数来取消它们
如果aiocb
为NULL
,则系统将尝试取消所有在fd
文件上未完成的异步I/O操作。其他情况下尝试取消由AIO控制块描述的单个异步I/O操作。之所以说尝试,是因为无法保证系统能够取消正在进程中的任何异步操作
如果异步I/O操作被成功取消,则对应的AIO控制块调用aio_error
函数将会返回错误ECANCELED
如果异步I/O操作不能被取消,那么相应的AIO控制块不会被修改
4.7 批量提交异步I/O请求
AIO控制块列表描述了一系列I/O请求,可以由该函数提交
> 每个`AIO`控制块本身也可能启动了在各自操作完成时的异步通知。`sigev`指定的异步通知是额外加的,并且只会在所有的`IO`操作完成后发送
* `list`:`AIO`控制块指针的数组,该数组指定了要允许的`IO`操作。如果数组包含空指针,则跳过这些空指针
* `nent`:数组的长度
* `sigev`:指定了当所有`IO`操作完成后,发送的异步通知(如果不想被通知,则将`sigev`设置为`NULL`)。只有当`mode=LIO_NOWAIT`才有意义
4.8 异步I/O的数量限制
异步IO的数量有限制,这些限制都是运行时不变量
- 可以通过
sysconf
函数并把name
设置为_SC_IO_LISTIO_MAX
来设定AIO_LISTIO_MAX
的值 - 可以通过
sysconf
函数并把name
设置为_SC_AIO_MAX
来设定AIO_MAX
的值 - 可以通过
sysconf
函数并把name
设置为_SC_AIO_PRIO_DELTA_MAX
来设定AIO_PRIO_DELTA_MAX
的值
这些常量的意义为:
AIO_LISTIO_MAX
:单个列表IO
调用中的最大IO
操作数量AIO_MAX
:未完成的异步IO
操作的最大数量_SC_AIO_PRIO_DELTA_MAX
:进程可以减少的其异步IO
优先级的最大值
6.存储映射I/O
存储映射I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是:
- 当从缓冲区中取数据时,就相当于读文件中的相应字节
- 将数据存入缓冲区时,相应字节就自动写入文件
因此,就可以在不使用read
和write
的情况下执行I/O
效率:与mmap和memcpy相比,read和write执行了更多的系统调用,并做了更多的复制。read和write将数据从内核缓冲区中复制到应用缓冲区(read),然后再把数据从应用缓冲区复制到内核缓冲区(write)。而mmap和memcpy则直接把数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区。当引用尚不存在的内存页时,这样的复制过程就会作为处理页错误的结果而出现(每次错页读发生一次错误,每次错页写发生一次错误)。如果系统调用和额外的复制操作的开销和页错误的开销不同,那么两者方法中就会有一种比另一种表现更好
mmap的优劣:
- 劣:
- mmap不能用在某些设备(如网络设备或终端设备)之间进行复制
- 在对被复制的文件进行映射后,也要注意该文件的长度是否改变
- 优
- 某些应用程序能得益于存储映射I/O,因为它处理的是存储空间而不是读、写一个文件,所以常常可以简化算法
6.1 mmap建立映射
内核将一个给定的文件映射到一个存储区域中是由mmap
实现的:
注意:
prot
不能超过文件open
模式访问权限。如:如果该文件是只读打开的,则对于映射存储区就不能指定PROT_WRITE
flag
的标志中,MAP_SHARED
和MAP_PRIVATE
必须指定一个,而且二者不能同时指定flag
的标志中,每种实现可能会自定义一些MAP_XXX
标志值,它们是那些实现特有的off
的值和addr
的值(如果指定了MAP_FIXED
)通常被要求是系统虚拟存储页长(虚拟存储页长可以用带参数_SC_PAGESIZE
或者_SC_PAGE_SIZE
的sysconf
函数得到 )的倍数。因为off
和addr
通常指定为 0 ,所以这种要求一般并不重要
下图是存储映射文件的基本情况。其中“起始地址”是mmap
的返回值
与映射区相关的信号有:SIGSEGV
和SIGBUS
-
SIGSEGV
:通常用于指示进程试图访问对它不可用的存储区。如果映射存储区被mmap
指定成了只读的,则进程试图写这个映射存储区时,也产生此信号 -
SIGBUS
:如果映射区的某个部分在访问时不存在,则产生SIGBUS
信号如:用文件长度映射了一个文件,但在引用该映射区之前,另一个进程已将该文件截断,此时如果进程试图访问被截断部分对应的映射区,则接收到
SIGBUS
信号
子进程能够通过fork
继承存储映射区(因为子进程复制了父进程地址空间,而存储映射区是该地址空间的组成部分)。但是一旦子进程调用了exec
,则新程序不再拥有存储映射区了
6.2 mprotect修改映射区权限
6.3 msync冲洗映射区
如果修改的页是通过MAP_SHARED
标志映射到地址空间的,那么对内存映射区的修改并不会立即写回到底层文件中。何时写回脏页(即被修改的页)由内核的守护进程决定,决定的依据有两个:
- 系统负载
- 用来限制在系统失败事件中的数据损失的配置参数
因此,如果只修改了一页中的一个字节,当修改被写回到文件中时,整个页都被写回
如果共享映射中的页已修改,则可以调用msync
函数将该页冲洗到底层文件中。它类似于fsync
,但作用于存储映射区:
如果存储映射是私有的,则不修改底层的文件
6.4 munmap解除映射
当进程终止时,会自动解除存储映射区的映射。也可以直接调用munmap
函数来手动解除映射区
munmap
并不影响被映射的对象。即调用munmap
并不会使映射区的内容写到磁盘文件上
- 对于
MAP_SHARED
存储映射区的磁盘文件的更新,会在我们将数据写到存储映射区后的某个时刻,由内核虚拟存储算法自动运行 - 对于
MAP_PRIVATE
存储映射区:对其做出的修改会被丢弃
注意,对于创建存储映射区时使用的文件描述符,如果关闭该描述符,并不会解除映射区
使用mmap拷贝文件
#include "apue.h"
#include
#include
#define COPYINCR (1024*1024*1024) /* 1 GB */
int
main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
size_t copysz;
struct stat sbuf;
off_t fsz = 0;
if (argc != 3)
err_quit("usage: %s ", argv[0]);
if ((fdin = open(argv[1], O_RDONLY)) < 0)
err_sys("can't open %s for reading", argv[1]);
if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC,
FILE_MODE)) < 0)
err_sys("can't creat %s for writing", argv[2]);
if (fstat(fdin, &sbuf) < 0) /* need size of input file */
err_sys("fstat error");
if (ftruncate(fdout, sbuf.st_size) < 0) /* set output file size */
err_sys("ftruncate error");
while (fsz < sbuf.st_size) {
if ((sbuf.st_size - fsz) > COPYINCR)
copysz = COPYINCR;
else
copysz = sbuf.st_size - fsz;
if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED,
fdin, fsz)) == MAP_FAILED)
err_sys("mmap error for input");
if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,
MAP_SHARED, fdout, fsz)) == MAP_FAILED)
err_sys("mmap error for output");
memcpy(dst, src, copysz); /* does the file copy */
munmap(src, copysz);
munmap(dst, copysz);
fsz += copysz;
}
exit(0);
}
九.进程间通信
1.管道
管道是UNIX系统IPC的最古老形式,所有UNIX系统都提供此种通信机制
管道有2中局限性:
- 历史上,是半双工的。虽然现在某些系统提供全双工管道,但是为了最佳移植性,不应预先假定系统支持全双工管道
- 管道只能在具有公共祖先的两个进程间使用(通常,一个管道由一个进程创建,在进程调用fork之后,这个管道就能在父进程和子进程之间使用了)
尽管有上述局限,半双工管道仍是最常用的IPC形式。每当在管道中键入一个命令序列,让shell执行时,shell都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相连接
1.1 创建管道
POSIX.1允许实现支持全双工管道。对于这些实现,fd[0]和fd[1]以读/写方式打开
- 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。(从技术上讲,如果管道的写端还有进程,就不会产生文件的结束。可以复制一个管道的描述符,使得有多个进程对它具有写打开文件描述符。但是,通常一个管道只有一个读进程和一个写进程)
- 如果写一个读端已被关闭的管道,则产生信号
SIGPIPE
。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno
设置为EPIPE
- 写管道时,常量
PIPE_BUF
规定了内核的管道缓冲区大小。如果对管道调用write。而且要求写的字节数小于等于PIPE_BUG
,则此操作不会与其他进程对同一管道的write操作交叉进行。但是,若有多个进程同时写一个管道,而且我们要求写的字节数超过PIPE_BUF
,那么所写的数据可能会与其他进程所写的数据相互交叉。用pathconf
或fpathconf
函数可以确定PIPE_BUF
1.3 标准I/O库管道函数
pclose
:关闭标准I/O流,等待命令终止,然后返回shell的终止状态
2.协同进程
UNIX系统过滤程序从标准输入读取数据,向标准输出写数据。几个过滤程序通常在shell管道中线性连接。当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程
popen
只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则有连接到另一个进程的两个单向管道:一个连接到其标准输入,另一个则来自其标准输出。我们想将数据写到其标准输入,经其处理后,再从标准输出读取数据
协同进程的标准输入和标准输出连接到管道
3.FIFO
FIFO有时候被称为命名管道,和管道不同的是,它可以在不相关的进程间使用,不必具有继承关系
FIFO
是一种文件类型,通过stat
结构的st_mode
成员的编码可以知道文件是否是FIFO
类型。可以用S_ISFIFO
宏对此进行测试
FIFO有以下2个用途:
- shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件
- 客户进程-服务器进程应用程序中,FIFO用作汇聚点(多个客户进程向服务器同一个众所周知的FIFO写),在客户进程和服务器进程二者之间传递数据
3.1 创建FIFO
创建FIFO类似于创建文件。FIFO的路径名存在于文件系统中
应用程序可以用
mknod
和mknodat
函数创建FIFO。因为POSIX.1原先并没有包括mknod
函数,所以mkfifo
是专门为POSIX.1设计的。mknod
和mknodat
函数现在已包括在POSIX.1的XSI扩展中
3.2 打开FIFO
使用这两个函数创建FIFO时,要用open
来打开它:
- 一般情况下,只读
open
要阻塞到某个其它进程为写而打开这个FIFO为止。类似地,只写open
要阻塞到某个其它进程为读而打开它为止 - 如果指定了
O_NONBLOCK
,则只读open
立即返回。但是,如果没有进程为读而打开一个FIFO,那么只写open
将返回-1,并将errno
设置成ENXIO
3.3 读写FIFO
- 若
write
一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE
- 若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志
一个给定的FIFO有多个写进程是常见的。这意味着,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作。和管道一样,常量PIPE_BUF
说明了可被原子地写到FIFO的最大数据量
4.XSI IPC
有3种称作XSI IPC的IPC:消息队列、信号量、共享存储。它们之间有很多相似之处
4.1 标识符和键
1)标识符
- 每个内核中的IPC结构(消息队列、信号量或共享内存)都用一个非负整数的标识符加以引用
- 与文件描述符不同,IPC标识符不是小的整数。当一个IPC结构被创建,然后又被删除时,与这种结构相关的标识符连续加1,直至到达一个整形数的最大正值,然后又回转到0
2)键
标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个IPC对象都与一个键相关联,将这个键作为该对象的外部名(创建IPC结构时,应指定一个键)。键的类型是基本系统数据类型key_t
,通常在
中被定义为长整形。这个键由内核变换成标识符
4.2 权限结构
每个IPC结构关联了一个ipc_perm
结构(
),规定了权限和所有者,至少包括以下成员:
struct ipc_perm{
uid_t uid; /* 拥有者的有效用户ID */
gid_t gid; /* 拥有者的有效组ID */
uid_t cuid; /* 创建者的有效用户ID */
gid_t cgid; /* 创建者的有效组ID */
mode_t mode; /* 访问模式 */
...
};
- 创建IPC结构时,对所有字段都赋初值
- IPC结构的创建者或超级用户可以调用
msgctl
、semctl
或shmctl
修改uid
、gid
和mode
字段。修改这些字段类似于对文件调用chown和chmod
对于任何IPC结构都不存在执行权限,消息队列和共享内存使用术语”读“和”写“,信号量则用”读“和”更改“。下图显示了每种IPC的6种权限:
所有3种形式的XSI IPC都有内置限制。大多数限制可以通过重新配置内核来改变
4.4 优点和缺点
- 优点
- 可靠
- 流控制
- 面向记录
- 可以用非先进先出的顺序处理
- 缺点
- XSI IPC结构是在系统范围内起作用的,没有引用计数
- 即,不会因为最后一个使用IPC结构的进程终止而删除,它们会一直留在系统中直至一些删除事件发生(作为对比,最后一个引用管道的进程终止时,管道就被完全删除了;对FIFO而言,最后一个引用的进程终止时,虽然FIFO的名字仍保留在系统中,直至被显示删除,但是留在FIFO中的数据已被删除了)
- XSI IPC结构在文件系统中没有名字
- 因此,不能使用一些已有的系统调用访问或修改其属性,为了加以支持,内核中增加了十几个全新的系统调用。不能使用
ls
查看IPC对象,不能用rm
删除 - 因为这些形式的IPC不使用文件描述符,所以不支持I/O复用函数
- 因此,不能使用一些已有的系统调用访问或修改其属性,为了加以支持,内核中增加了十几个全新的系统调用。不能使用
- XSI IPC结构是在系统范围内起作用的,没有引用计数
5.消息队列
后文把消息队列简称为“队列”,把标识符简称为"队列ID"
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识
5.1 与消息队列相关的结构
每个队列都有一个msqid_ds
结构与其关联,这个结构定义了队列的当前状态:
struct msqid_ds{
struct ipc_perm msg_perm;
msgqnum_t msg_qnum; /* 队列中的消息数 */
msglen_t msg_qbytes; /* 队列中消息的字节 */
pid_t msg_lspid; /* 最后调用msgsnd()的进程ID */
pid_t msg_lrpid; /* 最后调用msgrcv()的进程ID */
time_t msg_stime; /* 最后调用msgsnd()的时间 */
time_t msg_rtime; /* 最后调用msgrcv()的时间 */
time_t msg_ctime; /* 最后一次修改队列的时间 */
...
};
下图为消息队列的系统限制:
5.2 创建或打开消息队列
创建队列时,需要初始化msqid_ds
结构的下列成员:
ipc_perm
:按XSI IPC中的描述初始化msg_qnum
、msg_lspid
、msg_lrpid
、msg_stime
和msg_rtime
都设为0msg_ctime
设置为当前时间msg_qbytes
设置为系统限制值
5.3 操作消息队列
上面3条命令也可用于信号量和共享存储
5.4 添加消息
msgsnd
将新消息添加到队列尾端
ptr
:指向一个长整型数,它包含了正的整型消息类型,其后紧接着消息数据(若nbytes
为0则无消息数据)struct mymesg{ long mtype; /* 正的长整型类型字段 */ char mtext[512]; /* */ };
ptr
可以是一个指向mymesg
结构的指针nbytes
:消息数据的长度flag
ICP_NOWAIT
:类似于文件I/O的非阻塞I/O标准- 若消息队列已满(数量或字节总数达到限制)
- 若指定
ICP_NOWAIT
,函数立即出错返回EAGAIN
- 若没指定
ICP_NOWAIT
,进程会阻塞到:1)有空间可用;2)从系统中删除了此队列(会返回EIDRM错误
);3)捕捉到一个信号,并从信号处理程序返回(会返回EINTR
错误)
- 若指定
- 若消息队列已满(数量或字节总数达到限制)
当msgsnd
返回成功时,消息队列相关的msqid_ds
结构会随之更新
5.5 获取消息
msgrcv
成功执行时,内核会更新与该消息队列相关的msgid_ds
结构
6.信号量
信号量是一个计数器,用于为多个进程提供对共享数据对象的访问
为了正确实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的
常用的信号量形式被称为二元信号量,它控制单个资源,其初始值为1。但是一般而言,信号量的初值可以是任意一个正值,该值表明有多少个共享资源单位可供共享应用
下面的特性使得XSI信号量更复杂:
- 信号量并非是单个非负值,而必须定义为含有一个或多个信号量值的集合。当创建信号量时,要指定集合中信号量值的数量
- 信号量的创建是独立于它的初始化的。这是一个致命缺点。因此不能原子地创建一个信号量集合,并且对该集合中的各个信号量赋初值
- 即使没有进程正在使用各种形式的XSI IPC,他们仍然是存在的。有的程序在终止时并没有释放已经分配给它的信号量,我们不得不为这种程序担心
6.1 信号量的相关结构
内核为每个信号量集合维护着一个semid_ds
结构
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems; /* 集合中的信号量数目 */
time_t sem_otime; /* 最后一次调用semop()的时间 */
time_t sem_ctime; /* 最后一次改变的时间 */
...
};
每个信号量由一个无名结构表示,它至少包含下列成员:
struct{
unsigned short semval; /* 信号量的值,总是>=0 */
pid_t sempid; /* 最后一个操作信号量的进程ID */
unsigned short semncnt; /* 等待 semval>curval 的进程数 */
unsigned short semzcnt; /* 等待 semval==0 的进程数 */
...
};
下图是影响信号量集合的系统限制;
创建队列时,需要初始化semid_ds
结构的下列成员:
ipc_perm
结构按XSI IPC中的描述初始化。结构中的mode
成员被设置为flag
中的相应权限位sem_otime
设置为0sem_ctime
设置为当前时间sem_nsems
设置为nsems
6.3 操作信号量
- 返回值:对于除
GETALL
以外的所有GET
命令,函数都返回相应值。对于其他命令,若成功则返回值为0,若出错,则设置errno
并返回-1
函数semop
自动执行信号量集合上的操作数组:
该函数具有原子性,它或者执行了数组中的所有操作,或者一个也不做
exit时的信号量调整
如果在进程终止时,它占用了经由信号量分配的资源,那么就会成为一个问题。无论何时只要为信号量操作指定了SEM_UNDO
标志,然后分配资源(sem_op
值小于0),那么内核就会记住对于该特定信号量,分配给调用进程多少资源(sem_op
的绝对值)。当该进程终止时,无论自愿或不自愿,内核都将检验该进程是否还有尚未处理的信号量调整值。如果有,则按调整值对相应信号量值进行处理
如果用带SETVAL
或SETALL
命令的semctl
设置一个信号量的值,则在所有进程中,该信号量的调整值都将设置为0
7.共享存储
共享存储允许2个或多个进程共享一个给定的存储区
因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC
使用共享存储要注意的是:进程在往共享存储写完成之前,读进程不应该去取数据。通常,信号量用于同步共享存储访问
mmap就是共享存储的一种形式,但是XSI共享存储与其区别在于,XSI共享存储没有相关文件。XSI共享存储段是内存的匿名段
7.1 共享存储的内核结构
内核为每个共享存储段维护着一个结构,至少包含以下成员:
struct shmid_ds{
struct ipc_perm shm_perm;
size_t shm_segsz; /* 共享存储段的字节大小 */
pid_t shm_lpid; /* 最后调用shmop()的进程ID */
pid_t shm_cpid; /* 创建该共享存储段的进程ID */
shmatt_t shm_nattch; /* 当前访问计数 */
time_t shm_atime; /* 最后一次attach的时间 */
time_t shm_dtime; /* 最后一次detach的时间 */
time_t shm_ctime; /* 最后一次change的时间 */
...
};
下图为影响共享存储的系统限制:
创建一个新共享存储段时,初始化shmid_ds
结构的下列成员:
ipc_perm
结构按XSI IPC中的描述初始化。结构中的mode
成员被设置为flag
中的相应权限位shm_lpid
、shm_nattach
、shm_atime
和shm_dtime
都设置为0shm_ctime
设置为当前时间sem_segsz
设置为size
7.3 操作共享存储
- `SHM_LOCK`:在内存中对共享存储段加锁(该命令只能由超级用户执行)
- `SHM_UNLOCK`:解锁共享存储段(该命令只能由超级用户执行)
7.4 与共享存储段连接
可以调用shmat
将共享存储段连接到进程的地址空间中
如果函数成功,内核会将与该共享存储段相关的shmid_ds
结构中的shm_nattch
计数器值加1
7.5 与共享存储段分离
下列函数可以与共享存储段分离。该调用并不从系统中删除其标识符以及其相关的数据结构。该标识符仍然存在,直到某个进程调用shmctl
并使用IPC_RMID
命令特地删除它为止
如果函数成功,共享存储段相关的shmid_ds
结构中的shm_nattch
计数器值减1
进程连接共享存储段的位置
内核将以地址0连接共享存储段放在什么位置上与系统密切相关,下列程序可以进行测试:
#include "apue.h"
#include
#define ARRAY_SIZE 40000
#define MALLOC_SIZE 100000
#define SHM_SIZE 100000
#define SHM_MODE 0600 /* user read/write */
char array[ARRAY_SIZE]; /* uninitialized data = bss */
int
main(void)
{
int shmid;
char *ptr, *shmptr;
printf("array[] from %p to %p\n", (void *)&array[0],
(void *)&array[ARRAY_SIZE]);
printf("stack around %p\n", (void *)&shmid);
if ((ptr = malloc(MALLOC_SIZE)) == NULL)
err_sys("malloc error");
printf("malloced from %p to %p\n", (void *)ptr,
(void *)ptr+MALLOC_SIZE);
if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)
err_sys("shmget error");
if ((shmptr = shmat(shmid, 0, 0)) == (void *)-1)
err_sys("shmat error");
printf("shared memory attached from %p to %p\n", (void *)shmptr,
(void *)shmptr+SHM_SIZE);
if (shmctl(shmid, IPC_RMID, 0) < 0)
err_sys("shmctl error");
exit(0);
}
8.POSIX信号量
POSIX信号量接口意在解决XSI信号量接口的几个缺陷:
- 相比于XSI接口,POSIX信号量接口考虑到了更高性能的实现
- POSIX信号量接口使用更简单:没有信号量集,在熟悉的文件系统操作后一些接口被模式化了
- POSIX信号量在删除时表现更完美
POSIX信号量有2种形式:命名的和未命名的。差异在于创建和销毁的形式上
- 未命名信号量只存在于内存中,并要求能使用信号量的进程必须可以访问内存。意味着它们只能用于同一进程的线程中,或者不同进程中已经映射相同内存内容到他们的地址空间中的线程
- 命名信号量可以通过名字访问,因此可被任何已知它们名字的进程中的线程使用
8.1 创建或获取命名信号量
为了移植性,信号量的命名应该遵循下列规则:
- 名字的第一个字符应该为斜杠(
/
) - 名字不应该包含其他斜杠以此避免实现定义的行为
- 信号量名字的最大长度是实现定义的,不应该鲳鱼
_POSIX_NAME_MAX
个字符长度。因为这是文件系统的实现能允许的最大名字长度的限制
8.2 关闭释放信号量
调用该函数,或者内核自动关闭都不会影响信号量值的状态
8.3 销毁命名信号量
8.4 调节信号量的值
1)减1
不像XSI信号量,我们只能通过一个函数调用来调节POSIX信号量的值
函数sem_timewait
可以选择阻塞一段时间
如果信号量可以立即减1,那么超时值就不重要了,此时即使指定的是过去的某个时间,操作依然会成功
如果超时将返回-1,并且将errno
置为ETIMEDOUT
2)增1
8.6 销毁未命名信号量
8.7 检索未命名信号量的值
注意,我们试图要使用刚读出来的值时,信号量的值可能已经改变。除非使用额外的同步机制来避免这种竞争,否则该函数只能用于测试
Mac OS X 10.6.8不支持该函数