# Readme

> 整理自： 林伟俊 (Wei-Jun Lin)
>
> GitHub：<https://github.com/Weijun-Lin/LinuxProgramming>
>
> Email：   <weijun-lin@foxmail.com>
>
> 上次更新: 2023-10-01

Linux/Unix 入门编程，参考自 《Linux/Unix 系统编程手册》Michael Kerrisk 著。

主要对 Linux/Unix 下文件、信号、进程、线程、IPC 等作简要描述。


# 1. 文件 I/O

Linux/Unix 上的通用 I/O 模型，并主要关注磁盘文件的 I/O 操作

### 概述

所有执行 IO 的系统调用均以文件描述符为目标，包括管道、FIFO、socket、终端设备和普通文件。每个进程中的文件描述符是互不相关的。 各个进程中默认会有三个标准文件描述符：

<table><thead><tr><th>文件描述符</th><th>用途</th><th width="200">POSIX 名称</th><th>stdio 流</th></tr></thead><tbody><tr><td>0</td><td>标准输入</td><td>STDIN_FILENO</td><td>stdin</td></tr><tr><td>1</td><td>标准输出</td><td>STDOUT_FILENO</td><td>stdout</td></tr><tr><td>2</td><td>标准错误</td><td>STDERR_FILENO</td><td>stderr</td></tr></tbody></table>

其中 POSIX 名称定义在头文件 `<unistd.h>` 中。

### 通用 I/O

UNIX I/O 模型的特点为 I/O 的通用性概念。针对系统所有的文件，包括设备、管道、socket 等可用文件描述符表示的资源，都可以通过同一套系统调用执行 I/O 操作。

#### open 打开文件

```c
#include <sys/stat.h>		// 文件属性信息结构体的相关申明
#include <fcntl.h>		// 文件操作的相关声明
/*
@brief	打开一个文件
@param	pathname	要打开文件的路径，如果是符号链接会解引用
@param	flags		位掩码，指定文件访问模式
@param	mode		可忽略，标识文件的所有权限，一般在创建文件时使用
                	- S_IRUSR | S_IWUSR 属主的读写权限
                        - S_IRGRP | S_IWGRP 数组的读写权限
                        - S_IROTH | S_IWOTH 其它的去写权限
@return	成功返回文件描述符，失败返回 -1
*/
int open(const char *pathname, int flags, ... /* mode_t mode*/);
```

flags 可取

```
O_RDONLY	只读
O_WRONLY	只写
O_RDWR		读写
```

以上三个必须选择一个。此外还有一下标志：

```
O_CLOEXEC		设置 close-on-exec 标志，在执行 exec() 后关闭文件描述符
O_CREAT			不存在则创建
O_DIRECT		无缓冲 I/O
O_DIRECTORY	        目标不是目录则失败
O_EXCL			文件存在则返回错误，配合 O_CREAT 使用用于创建文件
O_LARGEFILE	        32 位系统使用，用于打开大文件
O_NOATIME		不修改文件最近访问时间
O_NOFOLLOW	        符号链接不要解引用
O_NOCTTY		目标不能成为控制终端
O_TRUNC			截断文件，使其长度为 0，清空文件
---------- 以下允许通过 fcntl 函数修改 ----------
O_APPEND		尾部追加
O_ASYNC			I/O 可操作时，使用信号通知进程
O_DSYNC			提供同步 I/O 数据的完整性
O_SYNC			文件同步方式写入
O_NONBLOCK	        非阻塞打开文件
```

打开文件失败会返回 -1，并更新错误号 errno，可能的错误号如下所示：

```
EACCES	文件权限原因不能打开文件
EISDIR	文件为目录
EMFILE	打开的文件描述符已经达到进程资源限制的上限
ENFILE	文件打开数量达到系统允许的上限
ENOENT	文件不存在
EROFS	目标文件只读，却以写方式打开
ETXTBSY	目标文件为可执行文件且正在运行，不能修改
```

#### read 读取文件内容

```c
#include <unistd.h>
/*
@brief	从 fd 文件描述符指定的文件读取 count 字节数据到 buffer 中
@return	失败返回 -1，读到文件结束返回 0，否则返回读取的字节数
*/
ssize_t read(int fd, void *buffer, size_t count)
```

#### write 写入文件

```c
#include <unistd.h>
/*
@brief	将 buffer 中 count 字节的数据写入 fd 文件
@return	失败返回 -1，否则返回写入的字节
*/
ssize_t write(int fd, void *buffer, size_t count)
```

#### close 关闭文件

```c
#include <unistd.h>
/*
@brief	关闭指定的文件
@return	失败返回 -1，成功返回 0
*/
int close(int fd)
```

#### lseek 改变文件偏移量

```c
#include <unistd.h>
/*
@brief	改变文件 fd 偏移量
@param  offset 相对于 where 的偏移量
@param	where 可取：
    	- SEEK_SET: 文件开头
        - SEEK_CUR: 当前文件位置
        - SEEK_END: 文件尾部
@return	失败返回 -1，读到文件结束返回 0，否则返回读取的字节数
*/
off_t read(int fd, off_t offset, int where)
```

可以在文件任意位置写入，但当偏移量跨度很大的时候，这部分数据并不会占用磁盘空间，称为文件空洞。

### 深入探究文件 I/O

所有的系统调用均为原子操作，在编写 I/O 代码时要注意进程间可能的竞争关系。

#### 文件控制 fcntl

```c
#include <fcntl.h>
/*
@brief	对打开的文件描述符 fd 执行一系列操作（cmd 表示）
@return	失败返回 -1，成功返回值取决于 cmd
*/
int fcntl(int fd, int cmd, ...);
// 一些常用的 cmd
int old_flags = fcntl(fd, F_GETFL);		// 获取文件状态标志
fcntl(fd, F_SETFL, new_flags);			// 设置文件状态标志
```

#### 文件描述符与文件的关系

对于文件，内核维护以下三个数据结构：

* 进程级的文件描述符表
  * close-on-exec 标志
  * 文件句柄的引用，指向系统的文件表
* 系统级的打开文件表
  * 每一条目是打开文件句柄，指向 i-node 表
  * 文件偏移量
  * 文件状态标志
  * 文件访问模式
  * 与信号驱动 I/O 相关的设置
* 文件系统的 i-node 表
  * 文件类型
  * 指向文件持有锁的指针
  * 文件各种属性

上面的结构揭示了：

* 不同的文件描述符，若指向同一个文件句柄，则共享同一文件偏移量
* 文件标志位的修改也同上
* close-on-exec 为进程私有

#### 复制文件描述符

```c
#include <unistd.h>

// 复制已打开的文件描述符 oldfd，成功则返回新的描述符，指向同一文件句柄，失败则返回 -1
int dup(int oldfd);

// 可指定复制后的描述符为 newfd，若存在则先关闭原来的 newfd
int dup2(int oldfd, int newfd);

// flags 只能为 O_CLOEXEC, Linux 特有函数
#define _GNU_SOURCE
int dup3(int oldfd, int newfd, int flags);
```

#### 特定偏移量处的 I/O

```c
#include <unistd.h>
// 读写特定偏移量的数据，且不改变当前文件偏移量
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

// 相当于将下面的代码封装为系统调用
off_t orig = lseek(fd, 0, SEEK_CUR);
lseek(fd, offset, SEEK_SET);
s = read(fd, buf, count);
lseek(fd, orig, SEEK_CUR);
```

可方便多线程应用，因为系统调用为原子操作。

#### 分散输入和集中输出

```c
#include <sys/uio.h>

struct iovec {
    void *iov_base;
    size_t iov_len;
};
// 读写数据到 iovec 链表声明的所有数据块中
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
```

#### 截断文件

```c
#include <unistd.h>
// 设置文件到指定大小，文件大小大于 length 则删除多余部分，若小于，则添加空字节或文件空洞
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
```

#### 非阻塞 I/O

一般情况下文件 I/O 默认为非阻塞的，除非使用强制文件锁。对于管道、FIFO、套接字、设备要启用非阻塞需要通过 fcntl 函数设置 O\_NONBLOCK 标志。

#### /dev/fd 目录

此目录为虚拟目录，其中 /dev/fd/n 对应进程打开的各个文件描述符。打开/dev/fd 目录中的一个文件等同于复制相应的文件描述符。

#### 创建临时文件

此功能为 GNU C 提供：

```c
#include <stdlib.h>
// template 为临时文件路径，最后 6 个字符必须为 XXXXXX，这几个字符会被替换以保证唯一
// 成功返回文件描述符，否则返回 -1
int mkstemp(char *template);

#include <stdio.h>
// 返回的是 C 文件指针
FILE *tmpfile(void);
```

### 文件 I/O 缓冲

#### 文件 I/O 内核缓冲

read 和 write 系统调用在操作磁盘文件时不会直接发起磁盘访问，而是仅仅在用户空间缓冲区与内核缓冲区高速缓存之间复制数据。可大大减少与磁盘交互的时间。内核缓冲区由内核设置，不能修改。

#### stdio 库的缓冲

```c
#include <stdio.h>

/*
@brief 	修改文件流 stream 的缓冲为 buf 指向的 size 大小区域
@param 	mode 指定缓冲类型
       	- _IONBF: 不进行缓冲
       	- _IOLBF: 行缓冲，标准输入输出一般使用这个
       	- _IOFBF: 全缓冲，磁盘 IO 用的较多
@return 成功返回 0， 失败返回 -1
*/
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
// 相当于 setvbuf(stream, buf, (buf != NULL) ? _IOFBF : _IONBF, BUFSIZ);
void setbuf(FILe *stream, char *buf);

#define _BSD_SOURCE
void setbuffer(FILE *stream, char *buf, size_t size);

// 刷新 stream 的缓冲区到内核缓冲区中，stream 为 NULL，则刷新所有缓冲区
int fflush(FILE *stream);
```

#### 控制文件 I/O 的内核缓冲

有以下两种同步 I/O 类型：

* 同步 I/O 数据完整性：数据已成功全部写到磁盘，或全部从磁盘读入
* 同步 I/O 文件完整性：是数据完整性的超级，还将同步文件元数据

```c
#include <unistd.h>

// 使得文件处于 同步 I/O 文件完整性 状态
int fsync(int fd);

// 使得文件处于 同步 I/O 数据完整性 状态
int fdatasync(int fd);

// 刷新所有内核缓冲区
void sync(void)
```

#### 直接 I/O

通过 O\_DIRECT 绕过内核缓冲区直接与磁盘 I/O 需要注意：

* 用于传递数据的缓冲区，其内存边界必须对齐为块大小的整数倍
* 数据传输的开始点，亦即文件和设备的偏移量，必须是块大小的整数倍
* 待传递数据的长度必须是块大小的整数倍

#### 混合系统调用与库文件 I/O

```c
#include <stdio.h>
// 获取 stream 的为文件描述符
int fileno(FILE *stream);

// 转换为文件流，mode 同 fopen 函数
FILE *fdopen(int fd, const char* mode);
```


# 2. 信号

Unix/Linux 各种不同信号及其用途

### 基本概念

信号是事件发生时对进程的通知机制，有时也称为软件中断。信号更多用于进程间的一种同步技术。信号定义在头文件 `<signal.h>`中，不同的信号以 `SIGXXXX`的形式命名。 信号一般在进程正在执行，且内核态到用户态的下一次切换时发送，如果该信号由进程自己产生则会马上传递信号。当信号到达后，进程可采取如下措施：

* 忽略信号
* 终止进程
* 产生核心转储文件，该文件可用于后续的调试
* 停止进程
* 恢复运行

#### 信号类型与默认行为

常用信号如下（默认行为，term 信号终止，core 产生核心转储文件并退出，ignore 忽略信号，stop 停止进程，cont 恢复运行）：

| 名称      | 描述           | 默认行为            |
| ------- | ------------ | --------------- |
| SIGABRT | 终止进程         | core            |
| SIGALRM | 实时定时器过期      | term            |
| SIGBUS  | 内存访问错误       | core            |
| SIGCHLD | 子进程终止或停止     | ignore          |
| SIGCONT | 继续运行进程       | cont            |
| SIGEMT  | 硬件错误         | term            |
| SIGHUP  | 挂起           | term            |
| SIGINT  | 终端中断 Ctrl-C  | term            |
| SIGILL  | 非法指令         | core            |
| SIGKILL | 杀死进程（必杀）     | term，无法阻塞、忽略、捕获 |
| SIGPIPE | 管道断开         | term            |
| SIGQUIT | 终端退出 Ctrl-\\ | core            |
| SIGSTOP | 确保停止         | stop，无法阻塞、忽略、捕获 |
| SIGTERM | 进程终止         | term            |

#### 发送信号

**kill**

```c
#include <signal.h>
/*
@brief 向进程 pid 发送 sig 信号
@param pid 标识一个或多个目标进程
       - pid > 0 : 发送给 pid 指定的进程
       - pid = 0 : 发送与当前进程同组的所有进程，包括自己
       - pid < -1: 向组 ID 为该 pid 绝对值的进程组发送信号
       - pid = -1: 发送给此进程有权发送的所有进程
@return	失败返回 -1，成功返回 0
*/
int kill(pid_t pid, int sig);
```

可以使用 `kill(pid, 0)`检查目标进程是否存在，sig 为 0 表示这是一个空信号。

**raise**

```c
#include <signal.h>
// 向进程自身发送信号
// 成功返回 0，负责返回非零值
int raise(int sig);
```

在单线程程序中，相当于调用 `kill(getpid(), sig)`，多线程环境下为 `pthread_kill(pthread_self(), sig)`。调用此函数向进程自身发送信号时，信号立即传递，且在 raise 函数返回之前。此函数唯一可能发生的错误为 EINVAL，即 sig 无效。

**killpg**

```c
#include <signal.h>
// 向进程组 pgrp 发送信号，即 kill(-pgrp, sig)
int killpg(pid_t pgrp, int sig);
```

#### 打印信号

```c
#define _BSD_SOURCE
#include <signal.h>

extern const char *const sys_siglist[];

#define _GNU_SOURCE
#include <string.h>
// 打印 sig 信号的描述，本地字符集相关
char *strsignal(int sig);

#include <signal.h>
// 输出 msg : strsignal(sig)
void psignal(int sig, const char *msg);
```

#### 信号集

许多信号相关的系统调用需要表示一组信号，比如阻塞一组信号，返回一组目前在等待的信号。一组信号可以通过信号集这个数据结构表示，即 `sigset_t`。

```c
#include <signal.h>
// 下面函数用于初始化信号集，分别为空信号集和满信号集
// 为了避免移植性问题，应该用下面函数初始化 sigset_t，不能直接自己初始化
// 因为不同平台 sigset_t 实现可能不一样
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
// 向集合添加或删除信号
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
// 测试信号是否为 set 的成员
int sigismember(const sigset_t *set, int sig);

// GNU C 另外实现了 3 个非标准函数
#define _GNU_SOURCE
#include <signal.h>
// 集合取交，取或，是否为空
int sigandset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigorset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigsiemptyset(const sigset_t *set);
```

#### 信号掩码与信号等待

内核为每个进程维护一个信号掩码，用于阻塞信号对该进程的传递。在掩码内的信号将会延后传递给进程，直到该信号解除阻塞，解除阻塞后会立即传递给进程，如果该信号在阻塞期间内到达多次，在解除阻塞后也只传递一次。

```c
#include <signal.h>
/*
@brief	按 how 指定的方式设置进程信号掩码，并返回现有掩码
@param	how 修改掩码的方式，可取：
    	- SIG_BLOCK: 	将 set 信号加入掩码
        - SIG_UNBLOCK:	将 set 中的信号移除掩码
        - SIG_SETMASK:	将 set 指向的信号赋给信号掩码
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
```

如果进程接受一个被阻塞的信号，它会被加入到等待信号集中：

```c
#include <signal.h>
// 返回等待信号集
int sigpending(sigset_t *set);
```

等待信号集只能说明信号是否发生，不能表面其发生次数，这些论述只针对标准信号，对于实时信号则会进行排队处理。

#### 改变信号处置

**signal**

signal 系统调用是设置信号处置的原始 API，它的行为在不同 Unix 上存在差异，所以一般使用 sigaction 函数建立信号处理器函数。

```c
#include <signal.h>
/*
@brief	设置 sig 信号的处理函数为 handler
@return	成功返回之前的处理函数（函数指针），失败则返回 SIG_ERR
*/
void (*signal(int sig, void (*handler)(int))) (int);
```

它无法在不改变信号处置的同时获取当前的信号处置。handler 可以采用以下定义值：

* SIG\_DFL：将信号处置重置为默认值
* SIG\_IGN：忽略该信号（不处置）

**sigaction**

sigaction 对信号的处置更具灵活性，也可以实现更精准的控制。

```c
#include <signal.h>

struct sigaction {
    void (*sa_handler)(int);	// 信号处理器地址也可取 SIG_DFL,SIG_IGN
                                // 当信号不取 SIG_DFL,SIG_IGN sa_mask sa_flags 才有意义
    
    sigset_t sa_mask;	        // 调用信号处理器时阻塞的信号，避免这些信号中断信号处理器执行
                                // 会自动将引发信号处理器执行的信号添加到掩码中
    				// 意味着不会递归中断调用自己

    int sa_flags;		// 一个位掩码，用于控制信号处理过程中的各种选项，一般取 0
                                // SA_NODEFER 捕获信号时，不将此信号自动添加到掩码中
    				// SA_RESTART 自动重启由信号处理器程序中断的系统调用
                                // SA_SIGINFO 调用信号处理器时携带了额外参数
    void (*sa_restorer)(void);	// 不适用与应用程序
};

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
```

#### 等待信号

```c
#include <unistd.h>
// 暂停信号执行，知道信号处理器函数中断该调用
int pause(void);
```

### 信号处理器函数

信号处理器函数一般设计的越简单越好，以避免引发竞争条件的风险。

#### 可重入函数和异步信号安全函数

如果一个进程的多条线程可以同时安全地调用此函数，则该函数为可重入的。更新全局变量或静态数据结构的函数可能是不可重入的（只用到本地变量的函数肯定是可重入的）。 如果某一函数是可重入的，又或者信号处理器函数无法将其中断时，就称该函数是异步信号安全的。 在编写信号处理器时：

* 确保信号处理器函数代码本身是可重入的，且只调用异步信号安全函数
* 当主程序执行不安全的函数或是去操作信号处理函数也会更新的全局数据结构时，阻塞该信号的传递

信号处理器可能会更新 `errno`，所以需要在函数入口保存一份拷贝，并在函数返回时重新赋值给 `errno`

#### 全局变量与 sig\_atomic\_t 数据类型

可以使用 `sig_atomic_t` 数据类型以保证程序读写操作的原子性，以确保全局变量的共享是安全的。

```c
// volatile 避免编译器将其优化到寄存器，以保证每次都从内存中读取
// SIG_ATOMIC_MIN, SIG_ATOMIC_MAX 规定了该类型的取值范围
volatile sig_atomic_t flag;
```

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

除了直接返回外，还有以下终止方法：

* `_exit()`函数终止进程，不能调用 `exit()`因为其会对缓冲区做额外处理
* `kill`发送信号来杀掉进程
* 从信号处理器函数执行非本地跳转，通过 `sigsetjmp``siglongimp`
* 使用 `abort` 函数终止进程，并产生核心存储

#### SA\_SIGINFO 标志

此标志会给信号处理器函数传递一些附加信息。`sigaction`中信号处理地址的完整申明如下：

```c
struct sigaction {
    union {
        void (*sa_handler)(int);
        // siginfo_t 包含了信号以及发送信号进程的相关信息（暂略），最后一个参数暂时不用
        void (*sa_sigaction)(int, siginfo_t *, void *);
    } __sigaction_handler;
};

#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction

// Example
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGINT, &act, NULL);
```

#### 系统调用的中断和重启

可以利用下面的代码手动重启系统调用（也可以使用 SA\_RESTART 标志，但并不是对所有函数都有效）：

```c
while((cnt = read(fd, buf, BUF_SIZE)) == -1 && errno == EINTR) {
    continue;
if(cnt == -1)
    exit()
```

### 高级特性

#### 核心转储文件

某些信号会创建核心转储文件以方便调试。但有时候并不会产生，原因如下：

* 进程对核心转储文件没有写权限。
* 存在同名、可写的普通文件
* 创建核心转储文件的路径不对
* 文件大小超过了系统对核心转储文件的限制，可通过 ulimit 命令设置
* 进程可创建文件大小的限制
* 文件系统以满，工作目录文件系统仅可读
* Set-user-ID 程序由非其属主（属组）执行时，不创建以避免用户恶意窥探程序数据

#### 一些特殊情况

* SIGKILL，SIGSTOP：其默认行为无法改变，除非进程处于 TASK\_UNINTERRUPTIBLE 睡眠状态，此状态的进程，系统不会把信号传递给它。相反，TASK\_INTERRUPTIBLE 睡眠状态则会被信号唤醒。
* SIGCONT：若进程在停止状态，此信号总能唤醒进程，即使被阻塞或忽略。因为只能通过此信号恢复程序运行。若重新定义此信号的处理函数，当程序恢复运行后并取消阻塞后才会去调用。SIGSTOP 和 SIGCONT 分别会将其到来前的 SIGCONT 和 SIGSTOP 丢弃。
* 由终端产生的信号若被忽略，则不应该改变其处置。
* 信号一般是异步的，除非此信号为进程自己产生并发送给自己。此时会立即传递信号，除非其被阻塞。

#### 实时信号

实时信号相比标准信号有以下特点：

* 信号范围更大
* 采取队列化管理，实时信号发送多次给进程将会多次传递，标准信号只会传送一次
* 可指定伴随数据
* 传递顺序有保障，会优先传递最小编号的信号

实时信号范围由 `SIGRTMIN`和 `SIGRTMAX`定义。

```c
// 传递给信号的伴随数据
// 一般很少使用 sival_ptr 因为只作用在进程内部
union sigval {
    int sival_int;
    void *sival_ptr;
};

#define _POSIX_C_SOURCE 199309
#include <signal.h>
// 将实时信号 sig 发送给 pid 进程，并附带信息
int sigqueue(pid_t pid, int sig, const union sigval value);
```

在使用 sigaction 处理信号时，siginfo\_t 会设置以下字段：

* si\_signo: 信号编号
* si\_code: 信号来源，实时信号为 SIG\_QUEUE
* si\_value: 即 sigval
* si\_pid，si\_uid: 发送进程的进程 ID 和实际用户 ID

#### 用掩码等待信号

```c
#include <signal.h>

/*
@brief	将当前进程信号掩码替换为 mask，然后挂起进程直到捕获信号
        信号处理函数返回后掩码恢复为之前的值
*/
int sigsuspend(const sigset_t *mask);

// 相当于以不可中断方式调用
sigprocmask(SIG_SETMASK, &mask, &prevMask);
pause();
sigprocmask(SIG_SETMASK, &prevMask, NULL);
```

#### 用同步方式等待信号

```c
#define _POSIX_C_SOURCE 199309
#include <signal.h>
// 挂起进程知道 set 中的信号到达，信息收集到 info 中
// 成功返回收到的信号编号，失败返回 -1
int sigwaitinfo(const sigset_t *set, siginfo_t *info);
// 上面的变体，允许设置等待时限
int sigtimewait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout);
```


# 3. 进程

进程的创建，终止，以及监控

### 进程的创建

#### fork 创建新进程

系统调用 fork 创建一新进程，新进程为调用进程的翻版：

```c
#include <unistd.h>
// 创建新进程，子进程中返回 0，父进程返回子进程的 ID，失败返回 -1
pid_t fork(void);
```

两个进程执行相同的代码段，各自拥有不同的堆栈段，数据段，子进程的这几个段初始为父进程的拷贝。此调用失败一般是因为进程数量达到系统上限。 fork 执行后，谁先执行是不确定的。 执行 fork()时，子进程会获得父进程所有文件描述符的副本，类似与 dup 调用。 直接将父进程的堆栈段复制会造成很大的浪费，现代 Unix 一般采取以下措施避免：

* 内核将代码段标记为只读
* 对于堆栈段，数据段采用写时复制技术

为了规避父子进程执行顺序不确定导致的竞争条件，可以采用同步信号来规避（当然可以使用信号量等 IPC 通信）。例如，利用 `sigsuspend`调用阻塞父进程，直到子进程向其发送信号。

### 进程的终止

#### exit 与 \_exit

进程有两种终止方式，异常终止，与正常终止。异常导致的终止一般由信号引发，可能会产生核心转储文件。\_exit 调用可以正常终止进程，exit 调用也可以，在底层它也通过调用 \_exit 终止进程，不过还会对缓冲区等做额外的操作。

```c
#include <unistd.h>
// status 定义了进程的终止状态
// 父进程可以通过 wait 获取该状态
void _exit(int status);

// 调用退出处理程序 —— atexit() on_exit() 注册的函数
// 刷新 stdio 缓冲区
// 调用 _exit(status)
void exit(int status);
```

程序末尾的 `return n;`等同于执行 `exit(n)`

#### 进程终止的细节

异常终止与正常终止都会发生以下动作：

* 关闭所有打开文件描述符，释放该进程持有的文件锁
* 分离已连接的 System V 共享内存段
* 如果该进程是终端的管理进程，则向前台进程组发送 SIGHUP 信号
* 关闭打开的 POSIX 有名信号量
* 关闭打开的 POSIX 消息队列
* 取消通过 `mmap`创建的内存映射

#### 自定义退出处理函数

可以自定义进程终止时采取的一些操作，即退出处理函数。通过注册退出处理函数可以在进程终止前自动执行一些清理动作。

```c
#include <stdlib.h>
// 成功返回 0，否则返回非零值
// 可以注册多个处理函数，进程退出时函数的执行顺序与注册顺序相反
// fork 创建的子进程会继承父进程的退出处理函数，直到执行 exec()
int atexit(void (*func)(void));

// 非标准
#define _BSD_SOURCE
#include <stdlib.h>
// 会传递两个参数给 func，exit 的 status 参数，以及 arg
int on_exit(void (*func)(int, void*), void *arg);
```

### 监控子进程

监控子进程即父进程需要知道子进程与何时改变了状态。可以通过系统调用 wait 与 SIGCHLD 信号监控子进程。

#### 等待子进程

可以通过 wait 系列函数等待子进程结束，并捕获其状态

```c
#include <sys/wait.h>
// 该系统调用会阻塞当前进程直到有任一子进程终止
// 如果status 非空，子进程如何终止将会通过这个参数返回
// 成功则返回终止的进程 Id 号，出错时返回 -1，可能是因为所有子进程均退出且被 wait 捕获
// 此时会将 errno 置为 ECHILD
pid_t wait(int *status);

/*
@brief 等待特定子进程 pid 结束
@param pid 需要等待的具体子进程
           - pid = 0： 等待与父进程同一个进程组的所有子进程
           - pid > 0： 进程 Id 等于 pid 的进程
           - pid = -1：等待任意子进程
           - pid < -1：等待进程组标识符与其绝对值相等的所有子进程
       status 子进程终止的信息
       option 位掩码，指定一些操作
           - WUNTRACED 	除了返回终止子进程的信息，还返回因信号而停止的子进程信息
           - WCONTINUED	返回因 SIGCONT 信号恢复执行的进程的状态信息
           - WNOHANG	无阻塞返回，没有子进程状态改变则返回 0
@return 返回等待成功的进程 Id，失败返回 -1
*/
pid_t waitpid(pid_t pid, int *status, int option);
```

其中的 status 值可用于区分以下事件：

* 子进程调用 \_exit 终止
* 子进程收到未处理信号终止
* 子进程因为信号而停止
* 指定 WCONTINUED 时，子进程收到 SIGCONT 恢复执行

可以采用以下宏函数判断：

* `WIFEXITED(status)`：若子进程正常结束，返回 true
* `WIFSIGNALED(status)`：若通过信号杀死子进程则返回 true
* `WIFSTOPPED(status)`：若子进程因信号而停止，返回 true，可使用 `WSTOPSIG(status)`返回信号编号
* `WIFCONTINUED(status)`：若子进程收到 SIGCONT 恢复执行，返回 true

```c
#include <sys/wait.h>
/*
@brief 	Linux 2.6.9 之后支持
@param	idtype 指定 id 的类型
    	- P_ALL		忽略 id 等待任何子进程
        - P_PID 	等待进程 ID 为 id 的子进程
        - P_PGID	等待进程组 ID 为 id 的所有子进程 

@param	options 控制等待的子进程事件，下面可以用位运算或，来选择
    	- WEXITED	 等待已终止的子进程
        - WSTOPPED       等待通过信号停止的子进程
        - WCONTINUED     等待由 SIGCONT 恢复的子进程
        - WNOHANG	 无阻塞返回，没有子进程状态改变则返回 0
        - WNOWAIT	 直接返回子进程状态 infop（子进程还处于可 wait 状态）

@param  infop 状态指针，包含以下字段
    	- si_code       取 CLD_EXITED, CLD_KILLED, CLD_STOPPED, CLD_CONTINUED
        - si_pid	状态发生变化的子进程 ID
        - si_signo	总为 SIGCHLD
        - si_status     _exit 调用的 status 值，或者导致子进程状态改变的信号值
        - si_uid	子进程真正用户 ID
*/
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int option);
```

#### 孤儿进程与僵尸进程

因为父子进程的生命周期不一致，所有会产生孤儿进程与僵尸进程。 孤儿进程为父进程已经终止，子进程仍在运行的进程，此时 init 进程将会接管此子进程，成为其新的父进程。所以可以通过 `getppid`调用，查看其父进程 ID 是否为 1 判断其是否为孤儿进程。 僵尸进程指那些在子进程已经退出，但父进程一直未通过 `wait` 调用回收其状态的进程。僵尸进程的大部分资源会被释放，其唯一保留的是在内核中的一条记录，包含了子进程 ID，终止状态等为 `wait`调用使用的信息。父进程直到退出仍未执行 `wait`，则该进程将称为孤儿进程，进一步由 init 进程调用 `wait` 捕获。 僵尸进程不能通过信号杀死（即使是 SIGKILL），从系统中移除僵尸进程的唯一方法就是杀死它的父进程。

#### SIGCHLD 信号

可以通过 SIGCHLD 信号异步捕获子进程终止。SIGCHLD 信号默认处理是将其忽略。标准信号不会进行排队，即使有多个 SIGCHLD 信号，父进程也只能捕获到一个。可在 SIGCHLD 信号处理函数中执行：

```c
while(waitpid(-1, NULL, WNOHANG) > 0) continue;
```

不断轮询查询，直到没有其它终止子进程需要处理。 为了防止在创建信号处理程序时已有子进程退出，所以需要在创建任何子进程之前设置 SIGCHLD 处理程序。

### 程序的执行

#### 执行新程序 execve

通过 execve 族函数可以在进程中执行一个全新的程序。执行此函数可以将新程序加载到某一进程的内存空间，并丢弃旧有程序，包括其堆栈栈、数据等。

```c
#include <unistd.h>
/*
@brief	执行一个新程序
@param	pathname	新程序的路径名
@param	argv		命令行参数，以 NULL 结束，argv[0] 为命令名
@param	envp		新程序的环境列表即新程序的 environ 数组
@return 成功没有返回，失败返回 -1
         
*/
int execve(const char *pathname, char *const argv[], char *const envp[]);
```

在 execve 之上还有以下 exec 函数：

```c
#include <unistd.h>

// pathname 为完整文件路径名
// filename 为在系统 PATH 中寻找文件，这种调用均以 p 结尾
// 处于安全考虑，当前工作目录一般会排除在当前 PATH 之外
// v 表示 argv 数组
// l 表示将 argv 用数组形式给出

int execle(const char *pathname, const char *arg, ...
            /* , (char*) NULL, char *const envp[] */);

int execlp(const char *filename, const char *arg, ...
           /* , (char*) NULL */);

int execvp(const char *filename, char *const argv[]);
int execv(const char *pathname, char *const argv[]);
int execl(const char *pathname, const char *arg, ...
          /* , (char*) NULL */);

// 从文件描述符执行程序
int fexecve(int fd, char *const argv[], char * const envp[]);
```

默认情况下，exec 会继承在原程序中打开的文件描述符，除非文件设置了 close-on-exec 标志，可利用 fcntl 调用设置。当使用 dup 系列函数创建副本时，总是会清楚副本描述符的 close-on-exec 标志。 exec 执行的新程序后会将原有已设信号处置的信号重置为 SIG\_DFL。但对 SIGCHLD 的处置是不确定的，所以需要手动将其处置置为 SIG\_DFL。在调用 exec 期间，原有进程信号掩码一级挂起信号的设置均得以保存。

#### 执行 shell 命令: system

```c
#include <stdlib.h>
//创建一个子进程运行 shell 并执行 command 
// 无法创建子进程或无法获取其终止状态 返回 -1
// 调用成功则返回子 shell 终止状态（128 + n），n 为信号编号
// 如果不能执行 shell 就相当于子 shell 调用 _exit(127) 一样
int system(const char *command);
```

system 调用会创建两个进程，一个用于 shell 一个用于执行命令。 在 set-user-id 和 set-group-id 程序中应避免使用 system，已避免引起安全隐患。


# 4.1 线程的简单介绍与同步方法

线程的简单介绍与线程同步方法

### 线程介绍

这里的线程主要为 POSIX 线程，即 Pthreads，由 Native POSIX Threads Library（NPTL）实现。 线程是允许应用程序并发执行多个任务的一种机制，一个进程可以包含多个线程，线程可以共享同一份全局内存区域。 使用多进程实现并行有以下不足：

* 进程间的信息难以共享，必须采用进程间通信（IPC）方式
* 调用 fork 创建进程的代价比较高

线程完美的解决了上面两个问题，线程还可共享：

* 进程 ID，父进程 ID
* 进程组 ID 与会话 ID
* 打开的文件描述符
* 信号处置

各个线程的独有属性有：

* 线程 ID
* 信号掩码
* 线程特有数据
* erron 变量，是一个宏
* 栈

后续 Pthreads 系列函数均以 0 表示成功，返回一个正值表示失败，这个正值与传统 Unix 的 errno 的值含义一样。在编译调用了 Pthreads 函数的程序需要添加 `-lpthread`以链接此库。

#### 创建线程

程序刚启动时，只包含一条线程，称为主线程。

```c
#include <pthread.h>
/*
@brief	创建线程
@param	thread 	线程 ID，通过该标识可以引用该进程
@param	attr	指定了新线程的各种属性，NULL 则使用默认属性
@param	start	新线程将执行 start 函数
@param	arg	为传递给 start 的参数
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start)(void *)， void *arg);
```

创建新线程后，系统会优先调度哪一个是不确定的，如果对执行顺序有明确要求可采取一些同步技术（见后续的线程同步小节）。

#### 终止线程

可采用以下方式终止线程：

* 线程 start 函数执行到 return 语句
* 线程调用 pthread\_exit
* 调用 pthread\_cancel 取消线程
* 任意线程调用了 exit，或者主线程（main）执行了 return

```c
#include <pthread.h>
// 终止线程，retval 指定了线程的返回值，其指向的内容不应该分配在线程栈中
// 因为线程终止后，线程栈内的内容不一定有效
void pthread_exit(void *retval);
```

#### 线程 ID

线程 ID 是线程的唯一标识，pthread\_create 可返回线程 ID，线程也可获取自己的线程 ID：

```c
#include <pthread.h>
pthread_t pthread_self(void);
```

不能直接使用 == 运算符判断两个线程 ID 是否相等，因为其底层实现可能是无符号长整型、指针、结构：

```c
#include <pthread.h>
// 判断线程 ID 是否相同
int pthread_equal(pthread_t t1, pthread_t t2);
```

但 NPTL 中，它实际上是一个经强制转化为无符号长整形的指针。 在 Linux 中线程 ID 在所有进程中都是唯一的，不过其它 Unix 实现则不一定。线程 ID 可复用，当某个线程终止后，其 ID 可被后续新线程复用。

#### 连接已终止的线程

类似进程中的 wait 系列函数：

```c
#include <pthread.h>
// 等待 thread 线程终止，并将线程返回值拷贝到 retval 处，若其为非空指针
int pthread_join(pthread_t thread, void **retval);
```

与进程类似，若线程结束后没有连接，则会产生僵尸线程，除非手动将其分离（后续介绍）。如向 pthread\_join 传入一个已经连接过的线程将会导致无法预知的行为，因为线程 ID 可能会被复用。 线程的连接与进程中的 wait 有以下不同：

* 线程之间的关系是对等的，不像进程存在父子关系。进程中的任意线程均可以调用 pthread\_join 与其他线程连接起来。
* 不能以非阻塞方式连接，也不能连接任意线程（如别的进程中的线程）。

#### 线程分离

线程默认清空下是可连接的，但也可指定其为分离状态。分离状态的线程在终止后，系统会自动清理并移除。当不关心线程的返回状态时，这十分有用。

```c
#include <pthread.h>
// 调用 pthread_detach(pthread_self)); 可自行分离
int pthread_detach(pthread_t thread);
```

一旦线程处理分离状态，则不可被连接，也不能重返可连接状态。在其他线程调用了 exit 或者主线程 return 后，分离的线程仍然会被终止。

#### 线程与进程

线程有以下优点：

* 线程间数据共享十分简单
* 创建线程很快

相比与进程有以下缺点：

* 多线程编程时，需要确保调用线程安全函数，或以线程安全的方式调用函数（后续介绍）
* 因为共享共一个全局空间，某个线程的 bug 可能会影响到全局
* 线程共用宿主进程的虚拟地址空间，分配大量线程时，则会出现问题
* 线程中处理信号十分麻烦，在多线程程序中应避免使用信号

### 线程同步

线程同步旨在同步对共享资源的使用，可通过互斥量和条件变量实现。

#### 互斥量

线程可以直接访问全局变量，但必须确保多个线程不会同时修改同一变量。这部分全局变量称之为临界区（共享资源），操作临界区的代码必须是原子的，某线程访问临界区时，不应该被其他线程中断。 互斥量有两种状态：已锁定和未锁定。任何时候只有一个线程可以锁定该互斥量，试图对一个已经上锁的互斥量加锁可能会阻塞进程或者返回失败。 一旦线程锁定某一互斥量，便只有该线程可以解锁。一般情况下，对不同共享资源会使用不同的互斥量，每个线程在访问同一资源时，一般遵循：

1. 针对该共享资源锁定互斥量
2. 访问共享资源
3. 解锁

互斥量类型为 `pthread_mutex_t`，在使用之前必须对其初始化：

```c
#include <pthread.h>
// 静态初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
/*
@brief	动态初始化，attr 为该互斥量的各种属性值，NULL 则使用默认属性
@param	attr 包括了互斥量的类型：
        - PTHREAD_MUTEX_NORMAL		此类型互斥量不具有死锁检测功能
        - PTHREAD_MUTEX_ERRORCHECK	这类互斥量的所有操作均会进行错误检查
        				不过效率会差一些，可用作 Debug
        - PTHREAD_MUTEX_RECURSIVE	递归互斥量维护一个锁计数器，每次锁定都会将计数器值加 1
                                        解锁操作递减计数器，当计数器为 0 时才会释放
        
*/
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 动态初始化的互斥量需要手动销毁
int pthread_mutex_destory(pthread_mutex_t *mutex);

```

加锁和解锁互斥量：

```c
#include <pthread.h>
// 尝试给互斥量加锁，若互斥量已被锁定，则会一直阻塞，直到该互斥量被解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 对为锁定的互斥量解锁，或解锁其他线程锁定互斥量均会返回错误
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 不会阻塞线程，信号量已经被锁定会失败并返回 EBUSY 错误
int pthread_mutex_trylock(pthread_mutex_t *mutex);
```

当线程需要同时访问多个不同资源时，这些资源由互斥量管理，则可能会发生死锁。例如：

* 线程 A 锁定了 mutex1
* 线程 B 锁定了 mutex2
* 线程 A 尝试锁定 mutex2， 线程 B 尝试锁定 mutex1
* 死锁发生

要避免此类死锁问题，最简单的办法就是定义互斥量的层级关系。当多个线程对一组互斥量操作时，总是以相同的顺序对互斥量进行锁定。 或者先使用 pthread\_mutex\_lock 锁定第一个互斥量，后续互斥量使用 pthread\_mutex\_trylock 尝试锁定，如果失败，则解锁之前锁定的互斥量。这种方法效率会低一些，但比较灵活。

#### 条件变量

互斥量防止多个线程同时访问同一共享资源，条件变量允许线程阻塞，直到某个线程就某个共享资源的状态变化通知其他线程。 采用条件变量可以允许一个线程休眠，直至收到另一线程的通知。条件变量一般结合互斥量使用，就互斥量状态改变发出通知。 条件变量也有静态分配和动态分配两种方式：

```c
#include <pthread.h>
// 静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 动态分配，attr 为该条件变量的一些属性，NULL 使用默认属性
int pthred_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 动态分配的条件变量需要手动销毁
int pthread_cond_destory(pthread_cond_t *cond);
```

条件变量的主要操作是发送信号（通知其他线程，此信号并不是系统的那个信号）和等待。等待操作是指收到一个通知前一直处于阻塞状态。

```c
#include <pthread.h>

// signal 保证唤醒至少一条遭到阻塞的线程
int pthread_cond_signal(pthread_cond_t *cond);
// broadcast 唤醒所有遭阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 阻塞此线程直到收到条件变量 cond 的通知
int pthread_cond_wait(pthread_cond_t *cond, pthread_t *mutex);
// 使用参数 abstime 设置等待上限，而不会一直阻塞
// 到时且无状态变化通知则返回 ETIMEOUT 错误
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_t *mutex,
                          const struct timespec *abstime);
```

条件变量并不保存状态信息，发送信号通知其他线程时，若无线程在等待该条件变量，这个信号则会被忽略。

#### pthread\_cond\_wait

pthread\_cond\_wait 会阻塞线程直到收到条件变量，其内部会执行如下步骤：

1. 解锁互斥量 mutex，此互斥量与控制目标资源访问的互斥量是同一个
2. 阻塞调用线程，直到另一线程就此条件变量发出信号
3. 重新锁定 mutex

```c
pthread_mutex_lock(&mtx);
while(/* 检查共享资源的状态是否是想要的 */)
    pthread_cond_wait(&cond, &mtx);
pthread_mutex_unlock(&mtx);
```

其总与访问共享资源的同一个互斥量绑定，共享资源的互斥量与条件变量一般有以下形式：

1. 线程锁定互斥量以访问共享资源
2. 检查共享资源状态
3. 如果共享资源并没有处于预期状态，则应该解锁互斥量（以便其他线程可以访问），并阻塞
4. 线程因为条件变量被唤醒后，应立即加锁，因为一般线程会马上访问共享变量

函数 pthread\_cond\_wait 会自动执行 3 4 步的解锁和加锁，且解锁与陷入阻塞为一个原子操作，其他线程不会在此线程陷入阻塞前获取到此互斥量。 上面的代码中并没有用 if 语句判断 pthread\_cond\_wait 控制的条件的状态，而是采用 while 不断检查资源状态，因为从 pthread\_cond\_wait 中返回时不能对判断条件的状态做任何假设，因为可能会存在虚假唤醒的情况。

#### 例子：生产者-消费者

```c
/*
@Brief 生产者消费者问题
*/
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int produced_num = 0;

// 生产者线程
void* producer(void* arg) {
    int idx = (int)arg;
    while(1) {
        pthread_mutex_lock(&mtx);
        ++produced_num;
        printf("thread:%d produced, num: %d\n", idx, produced_num);
        pthread_mutex_unlock(&mtx);
        sleep(1);
        // 发送信号表示已经生产
        pthread_cond_signal(&cond);
    }
    return NULL;
}

int main(int argc, char** argv) {
    int thread_num = 3; // 线程数量
    // 创建线程
    for(int i = 0;i < thread_num;++i) {
        pthread_t t_id;
        int s = pthread_create(&t_id, NULL, producer, (void*)i);
        pthread_detach(t_id);
    }
    // 主线程为消费者
    while(1) {
        pthread_mutex_lock(&mtx);
        // 为 0 则等待生产者生产
        while(produced_num == 0) {
            int s = pthread_cond_wait(&cond, &mtx);
            if(s != 0) {
                exit(EXIT_FAILURE);
            }
        }
        while(produced_num > 0) {
            --produced_num;
            printf("consume, now num:%d\n", produced_num);
        }
        pthread_mutex_unlock(&mtx);
    }
    return 0;
}
```


# 4.2 线程安全、线程取消

线程安全、线程取消以及一些额外细节

### 线程安全

#### 线程安全函数

若函数可同时供多个线程安全调用，则称之为线程安全函数。如以下函数：

```c
static int glob = 0;
static void incr(int loops) {
    int loc, j;
    for(j = 0;j < loops;j++) {
        loc = glob;
        loc++;
        glob = loc;
    }
}
```

一般导致线程不安全的典型原因是：使用了所有线程之间共享的全局或静态变量。可通过互斥量与共享变量关联起来，已保证线程安全。

#### 一次性初始化

一次性初始化是指，无论创建多少线程，初始化动作只发生一次：

```c
#include <pthread.h>

/*
@brief	无论调用过 pthread_once 多少次，都只会调用 init 一次
@param	once_control	一般初始化为：pthread_once_t once_var = PTHREAD_ONCE_INIT;
*/
int pthread_once(pthread_once_t *once_control, void (*init)(void));
```

此函数经常与线程特有数据结合使用。

#### 线程特有数据

线程特有数据为每个线程提供一份独有的数据副本。一般步骤如下：

1. 函数创建一个键（key），用以区分不同函数使用的线程特有函数，可通过 `pthread_key_create` 创建，且只需要创建一次，所以需要配合 `pthread_once` 使用。
2. `pthread_key_create`还允许调用者指定一个自定义解构函数，在线程终止时释放该线程的特有数据块
3. 使用 malloc 创建特有数据
4. 上一步分配的数据地址可通过 `pthread_setspecific`和 `pthread_getspecific`设置和获取。一般先获取数据地址，若为 NULL 说明还没有分配，然后使用 `pthread_setspecific`设置数据地址。

```c
#include <pthread.h>

// 设置 key 以及解构函数
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
// 设置与 key 绑定的特有数据块地址，成功返回 0， 失败返回一个正数
int pthread_setspecific(pthread_key_t key, const void *value);
// 返回与 key 绑定的数据块，没有绑定过返回 NULL
void *pthread_getspecific(pthread_key_t key);
```

一个通用的模板：

```c
#include <pthread.h>

static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t func_key;

static void destructor(void *buf) {
    free(buf);
}

static void createKey(void) {
    int s;
    s = pthread_key_create(&func_key, destructor);
}

void func(void) {
    char *buf;	// 线程特有数据
    pthread_once(&once, createKey);
    buf = pthread_getspecific(func_key);
    if(buf == NULL) {
        buf = malloc(BUF_SIZE);
        pthread_setspecific(func_key, buf);
    }
    // 使用线程特有数据
}
```

需要注意的是，key 的数量是有限制的。

#### 线程局部存储

与线程特有数据类似，线程局部存储提供了持久的没线程存储。但这个是非标准特性，在某些 Unix 发行版上可能不被支持。 只需要通过特殊的声明即可创建线程局部变量：

```c
static __thread type buf[MAX_ERROR_LEN];
```

带有 `__thread`说明符的变量，每个线程都拥有该变量的拷贝，此变量将一直存在，直至线程终止。

### 线程取消

线程取消是指，向线程发送一个请求，要求其立即退出。

```c
#include <pthread.h>
// 向线程发送取消请求，成功返回 0，失败返回一个正值
int pthread_cancel(pthread_t thread);
```

通过以下函数可以帮助线程对取消请求的响应过程加以控制：

```c
#include <pthread.h>

/*
@brief	将调用线程的取消性状态设置为参数 state 给定的值，并将原状态通过 oldstate 返回
    	state 可取
        - PTHREAD_CANCEL_DISABLE	线程不可取消，收到取消请求则挂起此请求，直到启用线程取消请求
        - PTHREAD_CANCEL_ENABLE		线程可以取消，默认值
*/
int pthread_setcancelstate(int state, int *oldstate);
/*
@brief	如果线程可以取消，以下函数规定了线程取消的方式
    	type 可取
        - PTHREAD_CANCEL_ASYNCHRONOUS	异步取消
        			        可能会在任何时间（也许是立即取消）取消线程
                                        此时清理函数仍然会执行，但函数当前状态是不确定的
                                        一般可异步取消的线程不能分配任何资源和互斥量或锁
        - PTHREAD_CANCEL_DEFERED        取消请求保持挂起，直到取消点，为默认类型
*/
int pthread_setcanceltype(int type, int *oldtype);
```

线程调用 fork 时，子进程会继承调用线程的取消性类型及状态。 SUSv3 规定必须是取消点的函数为： ![image.png](https://cdn.nlark.com/yuque/0/2023/png/29221640/1688384785232-d373c808-c768-4443-ba44-844e6d854ef9.png#averageHue=%23f3f3f3\&clientId=u6d672197-5e2a-4\&from=paste\&height=529\&id=u8887060b\&originHeight=925\&originWidth=1444\&originalType=binary\&ratio=1.75\&rotation=0\&showTitle=false\&size=291984\&status=done\&style=none\&taskId=ufe9194a4-6784-4056-91ba-4fc84760980\&title=\&width=825.1428571428571) 也可以通过以下函数自己设置取消点：

```c
#include <pthread.h>
// 在此处创建一个取消点
void pthread_testcancel();
```

每个线程都有一个清理函数栈，在收到取消请求时，线程会自动调用函数栈内的函数。

```c
#include <pthread.h>
// 向函数栈内添加函数
void pthread_cleanup_push(void (*routine)(void*), void *arg);
// 将函数栈顶部的函数弹出，execute 非 0 时还会执行这个弹出的函数
void pthread_cleanup_pop(int execute);
```

这两个函数在某些系统上可能用宏实现，所以这两个配对的操作需要在同一个语法块。例如，以下代码就不正确：

```c
pthread_cleanup_push(func, arg);
...
if(cond) {
    pthread_cleanup_pop(0);
}
```

### 一些细节

#### 信号、进程、线程

Unix 信号远早于 Pthreads，所以信号与线程模型之间存在一些冲突。在实际开发中，多线程模型应尽量避免使用信号。 信号与线程有以下注意点：

* 信号动作属于进程层面。进程的任一线程收到动作为 stop 或 terminate 的信号，则会终止该进程所有线程。
* 对信号的处置属于进程层面，进程中的所有线程共享对每个信号的处置设置。如果使用 `sigaction`为某信号设置了处理函数，当收到信号时，任何线程都会去调用该处理函数。
* 信号的发送也可以针对某个线程，下面几种信号是面向线程的：
  * 信号的产生源于线程上下文对特定硬件指令的执行（SIGBUS，SIGFPE，SIGKILL，SIGSEGV）
  * 线程对已断开的管道进行写操作时产生的 SIGPIPE
  * 由 `pthread_kill` 或 `pthread_sigqueue` 发出的信号
  * 其他机制产生的信号全是面向进程的。
* 多线程程序收到一个信号，只会选择一条线程来接收此信号。
* 信号掩码是针对线程的。
* 进程挂起信号，以及每条线程挂起的信号，内核分别维护记录。

#### 操作线程掩码

刚创建的新线程会从其创建者处继承信号掩码的一份拷贝。

```c
#include <signal.h>
// 与 sigprocmask 用法完全一致
// 成功返回 0，失败返回一个正数
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
```

#### 向线程发送信号

```c
#include <signal.h>
// 向线程发送 thread 发送 sig 信号
// 成功返回 0，失败返回一个正数
int pthread_kill(pthread_t thread, int sig);

#define _GNU_SOURCE
#include <signal.h>
// 将 pthread_kill 与 sigqueue 功能合并
// 成功返回 0，失败返回一个正数
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
```

#### 多线程中处理异步信号

没有任何 Pthreads API 为异步信号安全函数，所以不能在信号处理函数中加以调用。所以多线程程序中处理异步信号，通常不应该将信号处理函数作为接收信号到达的通知机制，一般推荐的方法如下：

* 所有线程都阻塞可能接受的所有异步信号，一般在创建线程之前由主线程阻塞，新线程会拥有一份拷贝。
* 再创建一个专用线程，调用 `sigwaitXXX`函数接收信号。

当收到信号时，专有线程可以安全地修改共享变量（结合互斥量），并可以调用并非异步信号安全的函数，也可以就条件变量发出信号。 如有多个线程在等待同一信号，那么信号到达时只有一个线程会实际接收到，并且无法确定收到信号的会是哪一条线程。

#### 线程与进程控制

这里主要描述线程执行 `exec`，`fork`，`exit`后的一些注意点：

* 线程与 `exec`：任一线程调用 `exec`系列函数后，调用程序将被完全替换。除了调用 `exec`的线程外，其他线程立即消失，且不会为线程特有函数执行解构，与清理函数。进程所有的互斥量和条件变量均会消失。调用 `exec`后，调用线程的线程 ID 是不确定的。
* 线程与 `fork`：多线程程序调用 `fork` 时仅会将发起调用的线程复制到子进程中，其他线程均在子进程中消失，也不会执行相关的解构函数，会导致一些问题：进程的互斥量等也会复制到子进程中，会导致十分棘手的问题产生。函数 `pthread_atfork`可以注册函数，在调用 `fork` 的之前执行注册函数。
* 线程与 `exit`：任何线程调用了 `exit`，所有线程均消失，不会执行特有数据的解构。

#### 线程实现模型

* 多对一（M：1）实现（用户级线程）：线程创建、调度、同步等均在用户空间的线程库处理，内核并不知道多线程的存在。优势在于速度十分快，因为不需要切换到内核状态，不需要内核支持，移植比较容易。也存在一些缺陷：一个线程阻塞，则所有线程都被阻塞。
* 一对一（1：1）实现（内核级线程）：此实现中，每一个线程映射一个单独的 KSE，内核对每个线程做调度处理。1：1 实现消除了 M：1 实现的各种弊端，尽管速度没有用户级线程快，但综合上还是优于用户及线程。NPTL 采用的为 1：1 模型。
* 多对多（M：N）实现（两级模型）：旨在结合前两个模型的优点，但其设计过于复杂。

对于 Pthreads API，Linux 有过两种实现：

* LinuxThreads：最初的实现，已经过时
* NPTL(Native POSIX Threads Library)：始于 Linux 2.6 版本，现在所有线程库开发均基于此，之前介绍的也为 NPTL 多线程实现。


# 5. 管道与FIFO

管道与 FIFO 是 UNIX 系统上最古老的 IPC 方法。FIFO 是管道的一个变体，可用于任意进程间的通信。

### 管道

#### 概述

管道可看作一个单向字节流，管道两端分别为一个读进程和一个写进程。管道有以下特点：

* 管道是单向的字节流，意味着管道不存在消息边界的概念，不能随机访问数据，只能每次顺序读取任意大小的数据块，读取后管道内便不存在此数据。
* 管道可以确保写入不超过 PIPE\_BUF 字节的操作是原子的。这可以保证多个进程写入时，只要数据大小不超过 PIPE\_BUF 则数据不会混合。Linux 上，PIPE\_BUF 的值为 4096。
* 管道的容量是有限的，读数据的进程应该被设计成可以尽可能快地读取数据。

#### 创建和使用管道

```c
#include <unistd.h>
// 创建管道，返回两个打开的文件描述符 filedes[0] filedes[1]，分别为读取端和写入端
// 成功返回 0，失败返回 -1
// 因为是文件描述符，可以使用 read write 调用在管道上执行 IO
int pipe(int filedes[2]);
```

管道一般用于父子进程通信，子进程会继承父进程的文件描述符。因为其是单向的，所以父子进程需要明确读写的角色，并把自己不需要的文件描述符关闭。 关闭未使用的管道描述符可以防止耗尽文件描述符的限制。 从管道中读取数据的进程会关闭其持有的管道的写入描述符，这样当其他进程完成输出并关闭其写入描述符之后，读者就能够看到文件结束（在读完管道中的数据之后），否则 read 会一直阻塞进程。 当一个进程试图向一个管道中写入数据但没有任何进程拥有该管道的打开着的读取描述符时，内核会向写入进程发送 一个 SIGPIPE 信号，默认情况下该信号会杀死进程。所以在实际中一般会忽略 SIGPIPE 信号，而通过 write 调用返回的 EPIPE 错误来获取管道状态。

### FIFO

FIFO 与管道是类似的，FIFO 在文件系统中拥有一个名称，所以也称为命名管道，可用于非相关进程之间的通信。上面管道介绍的一些特性同样适用与 FIFO。 与管道一样，当所有引用 FIFO 的描述符都被关闭之后，所有未被读取的数据会被丢弃。

```c
#include <sys/stat.h>
// 创建一个命名管道，此命令还是一个 Linux 命令：
// mkfifo [ -m mode ] pathname
// 在不使用此 FIFO 后调用 unlink 删除此文件
// 成功返回 0，失败返回
int mkfifo(const char *pathname, mode_t mode);
```

其中的 mode 与文件权限一致，可取：

```
S_IRUSR：用户读权限
S_IWUSR：用户写权限
S_IXUSR：用户执行权限
S_IRGRP：组读权限
S_IWGRP：组写权限
S_IXGRP：组执行权限
S_IROTH：其他用户读权限
S_IWOTH：其他用户写权限
S_IXOTH：其他用户执行权限
```

上面的值可以取 OR 运算指定，这些权限会按进程的 umask 值来取掩码。具有合适权限的任何进程都可以打开 FIFO。 一旦创建好 FIFO 后，就可以像一个普通文件一样打开它，然后与管道一样操作它。打开一个 FIFO 时需要为其设置一个读取进程与写入进程。在默认情况下，打开一个 FIFO 读取数据（open 函数 O\_RDONLY 标记）将会阻塞直到另一个进程打开 FIFO 以写入数据（open 函数 O\_WRONLY 标记）。如果想取消此阻塞，则在 open 调用中添加 O\_NOBLOCK 标记。

<figure><img src="https://2210531937-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBlhv2ECKp6X7qkBSugdB%2Fuploads%2FXNW8Zx9pKYV2O9080db8%2Fimage.png?alt=media&amp;token=97d152c6-bc0c-4509-bdb8-c862819b897d" alt=""><figcaption></figcaption></figure>

&#x20;从一个包含 p 字节的管道或 FIFO 中读取 n 字节的语义如下：

<figure><img src="https://2210531937-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBlhv2ECKp6X7qkBSugdB%2Fuploads%2FueHyYGXIW78afQCUlLMp%2Fimage.png?alt=media&amp;token=4550e3f3-8660-4a4f-8342-dd0a2db94553" alt=""><figcaption></figcaption></figure>

&#x20;向一个管道或 FIFO 写入 n 字节的语义如下：

&#x20;

<figure><img src="https://2210531937-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBlhv2ECKp6X7qkBSugdB%2Fuploads%2FC96dw4ZAwhQ2q6yOrQzT%2Fimage.png?alt=media&amp;token=433df2b8-7778-4f31-bdb7-273396b06c11" alt=""><figcaption></figcaption></figure>

下面是一个利用 FIFO 通信的例子，客户端将命令行的数据发送给服务端，服务端将发送来的字符串转为大写后发送回去：&#x20;

辅助头文件：

```c
// fifo_info.h  包装了读写函数和消息结构
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>
#include <stdio.h>

// 服务端 接收客户端信息
#define FIFO_SERVER "./fifo_server.fifo"
// 客户端 给服务端发送信息，这里只有一个客户端所以写死了，多个客户端可以设计一种命名约定（用 PID 标识）
#define FIFO_CLIENT "./fifo_client.fifo"

#define BUF_SIZE (PIPE_BUF - sizeof(int))


typedef struct {
    int length;
    char buf[BUF_SIZE];
} Message;

// 从 fd 中读取数据存入 msg
int read_fifo(int fd, Message *msg) {
    if(read(fd, &(msg->length), sizeof(msg->length)) == -1) {
        perror("Read Message Length Error!");
        return -1;
    }
    if(read(fd, msg->buf, msg->length) == -1) {
        perror("Read Message Buf Error!");
        return -1;
    }
    return 0;
}

// 向 fd 写入消息
int write_fifo(int fd, const Message *msg) {
    if(write(fd, &msg->length, sizeof(msg->length)) == -1) {
        perror("Write Message Length Error!");
        return -1;
    }
    if(write(fd, msg->buf, msg->length) == -1) {
        perror("Write Message Buf Error!");
        return -1;
    }
    return 0;
}
```

服务端：

```c
// fifo_server.c FIFO 服务器，将客户端的字符串转为大写
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>

#include "fifo_info.h"

int main() {
    if(mkfifo(FIFO_SERVER, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) != 0) {
        perror("Make FIFO_SERVER Error.");
        return -1;
    }
    
    int fd = open(FIFO_SERVER, O_RDONLY);
    // 使其阻塞，否则后面的 read 会返回 EOF
    int fd2 = open(FIFO_SERVER, O_WRONLY);
    Message msg;
    setbuf(stdout, NULL);

    while(1) {
        // 读取数据
        if(read_fifo(fd, &msg) != 0) {
            return -1;
        }
        // 改为大写
        for(int i = 0;i < msg.length;++i) {
            msg.buf[i] = toupper(msg.buf[i]);
        }
        // 打开客户端 FIFO 发送数据
        int fd_c = open(FIFO_CLIENT, O_WRONLY);
        if(fd_c < 0) {
            perror("Open Client FIFO Error.");
            return -1;
        }
        if(write_fifo(fd_c, &msg) != 0) {
            return -1;
        }
        close(fd_c);
    }

    return 0;
}
```

客户端：

```c
// fifo_client 客户端 将命令行数据发送给服务器
#include <signal.h>
#include <fcntl.h>
#include <string.h>

#include "fifo_info.h"

int main(int argc, char *argv[]) {
    if(mkfifo(FIFO_CLIENT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) != 0) {
        perror("Make FIFO_CLIENT Error.");
        return -1;
    }
    setbuf(stdout, NULL);
    int fd_server = open(FIFO_SERVER, O_WRONLY);
    Message msg;

    for(int i = 1;i < argc;++i) {
        msg.length = strlen(argv[i]);
        memcpy(msg.buf, argv[i], msg.length);

        // 发送数据
        if(write_fifo(fd_server, &msg) != 0) {
            return -1;
        }

        int fd_client = open(FIFO_CLIENT, O_RDONLY);
        if(read_fifo(fd_client, &msg) != 0) {
            return -1;
        }
        close(fd_client);

        write(STDOUT_FILENO, msg.buf, msg.length);
        printf("\n");
    }

    close(fd_server);

    return 0;
}
```

运行结果：

```
❯ ./fifo_server&
[1] 5555
❯ ./fifo_client hello world linux
HELLO
WORLD
linux
```


# 6. 内存映射

### 概述

内存映射可用于 IPC 以及其他很多方面。主要通过 mmap 系统调用创建。 内存映射分为两种：

* 文件映射：将一个文件的一部分直接映射到虚拟内存中。
* 匿名映射：没有对应的映射文件，这种映射的分页会被初始化为 0

内存映射可被不同进程共享，一般有以下两种情况：

* 两个进程映射了同一个文件的同一个区域，此时它们会共享物理内存的相同分页
* 通过 fork 创建的子进程会继承父进程的映射副本

内存映射可以是私有或者共享的：

* 私有映射（MAP\_PRIVATE）：在映射上的操作其他进程不可见。内核会采取写时复制技术完成这个任务。
* 共享映射（MAP\_SHARED）：映射内容的变更对每一个进程可见，对于文件映射变更会发生在底层文件上，但不一定是立即发生。

各种映射的用途一般为： ![image.png](https://cdn.nlark.com/yuque/0/2023/png/29221640/1689154297014-3e72bd7b-346e-43ad-ace5-2c405839cf3b.png#averageHue=%23f1f0f0\&clientId=ua2453854-3fb3-4\&from=paste\&height=138\&id=u447543f0\&originHeight=241\&originWidth=1200\&originalType=binary\&ratio=1.75\&rotation=0\&showTitle=false\&size=59817\&status=done\&style=none\&taskId=u38a67b4a-9bdf-4ed8-9833-63c3268ae10\&title=\&width=685.7142857142857) 子进程在执行 exec 后，原有映射会丢失。

### 创建一个映射

mmap 系统调用可以创建一个映射：

```c
#include <sys/mman.h>

/*
@brief 创建一个内存映射
@param addr	映射被放置的虚拟地址，一般取 NULL，让内核选择一个合适的地址
        	非 NULL 时，需要内核会为其舍入到最近的一个分页边界处，并且不能与现有映射冲突
@param length	指定了映射的字节数，会被提升为分页大小的倍数
@param prot	位掩码，映射的保护信息
            	- PROT_NONE	区域无法访问
                - PROT_READ	区域内容可读取
                - PROT_WRITE	区域内容可修改
                - PROT_EXEC     区域内容可执行
@param flags	控制映射操作的选项位掩码
            	- MAP_PRIVATE	创建私有映射
                - MAP_SHARED	创建共享映射
                - MAP_ANONYMOUS 创建匿名映射
@param fd	文件描述符，与 offset 一起用于文件映射，确定映射的开始位置
@param offset	文件偏移量，匿名映射将忽略这两个参数，也需要是分页大小的倍数
@return 成功返回映射的地址，否则返回 MAP_FAILED

*/
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
```

### 解除映射区域

通过 munmap 调用解除映射区域：

```c
#include <sys/mmap>
// 解除 addr 处长度为 length 字节的映射，addr 是分页对齐的
// 一般 addr 是 mmap 返回的地址，length 是之前创建映射的大小
// 自己设置的 addr 会被舍入到分页大小的整数倍
int munmap(void *addr, size_t length);
```

### 同步映射区域

内核会自动将发生在 MAP\_SHARED 映射内容上的变更写入到底层文件中，但在默认情况下，内核不保证这种同步操作会在何时发生，可以通过 msync 调用显示控制。

```c
#include <sys/mman.h>

/*
@brief 将 addr 处 length 大小字节同步到底层文件
@param flags 可取：
             - MS_SYNC		同步写入，阻塞直到所有被修改过的分页写入文件
             - MS_ASYNC 	异步的文件写入
             - MS_INVALIDATE 	上面的标志可以结合这个标识，使映射数据的缓存副本失效。
*/
int msync(void *addr, size_t length, int flags);
```


# 7.1 POSIX IPC 介绍

POSIX IPC 与 System V IPC 的比较以及简单介绍

Linux/Unix 上的 IPC（进程间通信）方法一般分为 System V IPC 与 POSIX IPC 两种。POSIX IPC 是对 System V IPC 的改进，但 System V IPC 移植性较好，因为比较久远。 System V IPC 工具具有以下缺点：

* System V IPC 是无连接的，意味着内核无法维护当前使用该对象的引用计数，所以应用程序需要使用额外的代码来知道合适可以删除一个对象。
* System V IPC 编程接口与传统 UNIX I/O 模型不一致，使用了整数键值和 IPC 标识符来引用一个 IPC 对象，而不是路径名和文件描述符。使得编程接口十分复杂。

POSIX IPC 工具则消除了上面两个缺点，提供更简单的接口也更加符合传统的 UNIX 模型。所以这里主要对 POSIX IPC 作介绍。&#x20;

在 Linux 上使用 POSIX IPC 机制需要与实时库 librt 链接起来，即指定 -lrt 选项。 POSIX IPC 对象编程接口总结：&#x20;

<figure><img src="https://2210531937-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBlhv2ECKp6X7qkBSugdB%2Fuploads%2F2RdpOLnv777JvfALTalK%2Fimage.png?alt=media&amp;token=37376f86-a663-41b7-b984-ceec74061632" alt=""><figcaption></figcaption></figure>

### IPC 对象名字

要访问一个 POSIX IPC 对象就必须要通过某种方式来识别出它。在 SUSv3 中规定的唯一一种用来标识 POSIX IPC 对象的可移植的方式是使用以斜线打头后面跟着一个或多个非斜线字符的名字，如/myobject。但也没有禁止形式与此不同的名字。

### 创建或打开对象

每种 IPC 机制都有一个关联的 open 调用（mq\_open()、sem\_open()以及 shm\_open()），它与用于打开文件的传统的 UNIX open()系统调用类似。给定一个 IPC 对象名，IPC open 调用会完成下列两个任务中的一个：

* 使用给定名字创建一个对象，并打开此对象，返回其句柄
* 打开一个已有对象并返回其句柄

IPC open 调用返回句柄与传统 open 调用返回文件描述符类似。所有的 IPC open 调用至少需要三个参数：

* name：标识待创建或待打开的对象
* oflag：位掩码，一般包含
  * O\_CREAT：对象不存在则创建，如果没有指定这个标记，且对象不存在则返回 ENOENT 错误
  * O\_EXCL：如果同时也指定了 O\_CREAT 并且对象已经存在，那么就返回一个错误（EEXIST）。这两步是原子操作（5.1 节）。这个标记在不指定 O\_CREAT 时是不起作用的
  * O\_RDONLY、O\_WRONLY、O\_RDWR 与 open 中的含义一致
* mode：权限掩码，与文件上的 open 一致

### 关闭 IPC 对象

消息队列和信号量调用对应的 close 函数即可关闭，POSIX 共享内存对象则通过 munmap 解除映射。

&#x20;IPC 对象在进程终止或执行 exec 时会自动被关闭。

### IPC 对象删除和对象持久性

与打开文件一样，POSIX IPC 对象也有引用计数，内核会维护对象上的打开引用计数。与 System V IPC 对象相比，这种方式使得应用程序能够更加容易地确定何时可以安全地删除一个对象。

每个 IPC 对象都有一个对应的 unlink 调用，其操作类似于应用于文件的传统的 unlink 系统调用。unlink 调用会立即删除对象的名字，然后在所有进程使用完对象（即当引用计数等于 0 时）之后销毁该对象。 当一个对象被断开链接之后，指定同一个对象名的 IPC open 调用将会引用一个新对象（在不指定 O\_CREAT 时会失败）。&#x20;

与 System V IPC 一样，POSIX IPC 对象也拥有内核持久性。对象一旦被创建，就会一直存在直到被断开链接或系统被关闭。这样一个进程就能够创建一个对象、修改其状态，然后退出并将对象留给在后面某个时刻启动的一些进程访问。


# 7.2 POSIX 消息队列

POSIX 消息队列允许进程之间以消息的形式交换数据。

### 打开、关闭和断开消息队列

创建或打开一个消息队列：

```c
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>

/*
@param name 	消息队列标识，一般为以 / 开头的字符串
@param oflag	可取：
            	- O_CREAT 	 不存在时创建
                - O_EXCL	 与 O_CREAT 搭配使用，排它地创建队列
                - O_RDONLY	 只读打开
                - O_WRONLY	 只写打开
                - O_RDWR	 读写打开
                - O_NONCLOCK 非阻塞模式打开
@param mode	与 attr 在指定 O_CREAT 创建消息队列时使用
            	mode 为位掩码指定了新消息队列上的权限，与文件权限一致
@param attr	指定消息队列的特性，一般指定为 NULL 采用默认特性

@return	返回消息队列描述符，失败返回 -1

*/
mqd_t mq_open(const char *name, int oflag, ...
              /* mode_t mode, struct mq_attr *attr */);
```

关闭一个消息队列：

```c
#include <mqueue.h>
// 成功返回 0，失败返回 -1
int mq_close(mqd_t mqdes);
```

此调用将关闭消息队列描述符，进程在不使用消息队列时应该关闭，放置出现进程耗尽消息队列描述符的情况。mq\_close 并不会删除该队列。 删除一个消息队列：

```c
#include <mqueue.h>
int mq_unlink(const char *name);
```

mq\_unlink 函数删除通过 name 标识的消息队列，并将队列标记为在所有进程使用完该队列之后销毁该队列（这可能意味着会立即删除，前提是所有打开该队列的进程已经关闭了该队列）。

### 消息队列特性

mq\_open 中的 attr 可以指定新创建的消息队列的特性，它是一个 mq\_attr 结构变量，定义如下：

```c
#inclued <mqueue.h>

struct mq_attr {
    long mq_flags;	// 0 或者 O_NONBLOCK
    long mq_maxmsg;	// 消息队列的最大消息数量
    long mq_msgsize;	// 消息队列每条消息的最大字节数量
    long mq_curmsgs; 	// 消息队列当前的消息数量
};
```

其中 mq\_maxmsg 和 mq\_msgsize 在消息队列创建时就确定下来，之后便无法改变。系统也会对这两个特性有一个限制。 获取消息队列特性：

```c
#include <mqueue.h>
// 返回消息队列特性，可以通过此调用获取当前消息队列的消息数量
int mq_getattr(mqd_t mqdes, struct mq_attr *attr);
```

修改消息队列特性：

```c
#include <mqueue.h>
// 修改消息队列特性，若 oldattr 不为 NULL，则返回原来的消息特性
int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr);
```

SUSv3 规定使用 mq\_setattr 唯一可以修改的特性就是 O\_NONBLOCK 的状态。

### 交换消息

发送一个消息：

```c
#include <mqueue.h>

/*
@brief 向指定的消息队列发送消息
@param mqdes 	消息队列描述符
@param msg_ptr 	需要发送的消息指针
@param msg_len 	消息长度，必须小于队列的 mq_msgsize
@param msg_prio 消息优先级，0 为最低，消息队列中的消息按优先级排序
                优先级有一个系统上限，可通过 sysconf(_SC_MQ_PRIO_MAX) 获取
@return 成功返回 0 失败返回 -1
*/
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
```

如果消息队列已经满了，那么后续的 mq\_send 调用会阻塞直到队列存在可用空间。或者开启了 O\_NONBLOCK 标记，会立即返失败并返回 EAGIN 错误。 接收一个消息：

```c
#include <mqueue.h>

/*
@brief 从 mqdes 引用的消息队列中删除一条优先级最高、存在时间最长的消息，
       并将此消息放置在 msg_ptr 指定的缓冲区中
@param msg_len 	缓冲区大小，必须大于等于队列的 mq_msgsize 特性
               	否则调用失败并返回 EMSGSIZE 错误
@param msg_prio 如果不为 NULL，则接收优先级为 msg_prio 指定的消息
*/
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
```

如果消息队列当前为空，那么 mq\_receive 会阻塞直到存在可用的消息或在 O\_NONBLOCK 标记起作用时会立即失败并返回 EAGAIN 错误。

### 与 System V 消息队列比较

POSIX 消息队列具备的优势：

* 具有消息通知特性，允许消息进入一个空队列时通过信号或者线程函数异步通知（消息通知这里没有做介绍）。
* Linux 上可以使用 poll、select、epoll 来监控 POSIX 消息队列，System V 则没有。

有以下劣势：

* 可移植性较差，Linux 内核 2.6.6 之后提供了对消息队列的支持。
* System V 消息队列可以根据类型来选择消息，更加灵活。


# 7.3 POSIX 信号量

POSIX 信号量用来同步进程和线程对共享资源的访问。

### 概述

SUSv3 规定了两种类型的 POSIX 信号量：

* 命名信号量：不相关的进程通过这个名字能够访问同一个信号量。
* 未命名信号量：没有名字，位于内存中一个约定的位置，可以在进程之间或一组线程之间共享。进程间共享中，需要位于一个共享内存区域中。

POSIX 信号量是一个整数，其值不能小于 0。如果一个进程试图将一个信号量的值减小到小于 0，那么取决于所使用的函数，调用会阻塞或返回一个表明当前无法执行相应操作的错误。Linux 2.6 并带有 NPTL 线程库的版本上，完整的 POSIX 信号量实现才可用。

### 命名信号量

#### 打开一个命名信号量

```c
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

/*
@brief 打开或创建一个命名信号量
@param name	标识了信号量，名字格式斜杠开头，如 /myobject
@param oflag	位掩码，标识创建还是打开一个信号量，可取
            	- O_CREAT   创建一个信号量
                - O_EXCL    配合 O_CREAT 使用，如果已存在则创建失败
                - 0	    打开一个信号量
@param mode	oflag 为 O_CREAT 使用，指定了权限可取
            	- O_RDONLY	仅读
                - O_WRONLY	仅写
                - O_RDWR	读写，默认
@param value	无符号整数。指定了新信号量的初值
@retrun	成功返回指向信号量的指针，否则返回 SEM_FAILED
*/
sem_t *sem_open(const char *name, int oflag, ...
                /* mode_t mode, unsigned int value */);
```

SUSv3 规定只能在原始返回的 sem\_t 指针上操作，其副本操作是未定义的，即不能：

```c
sem_t *sp, sem2;
sp = sem_open(...);
sem2 = *sp;
sem_wait(&sem2); // WRONG, Not Defined
```

#### 关闭一个信号量

```c
#include <semaphore.h>
int sem_close(sem_t *sem);
```

当一个进程打开一个命名信号量时，系统会记录进程与信号量之间的关联关系。sem\_close 会终止这种关联关系（即关闭信号量），释放系统为该进程关联到该信号量之上的所有资源，并递减引用该信号量的进程数。 在进程终止或者调用了 exec 时，会被自动关闭，关闭信号量并不代表删除信号量。

#### 删除一个信号量

```c
#include <semaphore.h>
// 删除 name 标识的信号量，当所有进程均使用完（close）这个信号量后销毁
int sem_unlink(const char *name);
```

### 信号量操作

#### 等待一个信号量

```c
#include <semaphore.h>
// 如果当前信号量大于 0，将信号量 -1，并立即返回；信号量为 0 则阻塞直到其大于 0
int sem_wait(sem_t *sem);
```

如果一个阻塞的 sem\_wait 调用被一个信号处理器中断了，那么它就会失败并返回 EINTR 错误，不管在使用 sigaction 建立这个信号处理器时是否采用了 SA\_RESTART 标记。 非阻塞版本：

```c
#include <semaphore.h>
// 如果递减操作不能被立即执行，失败并返回 EAGAIN 错误
int sem_trywait(sem_t *sem);
```

#### 发布一个信号量

```c
#include <semaphore.h>
// 递增一个信号量
int sem_post(sem_t *sem);
```

此操作会唤醒一个因此信号量而被阻塞的进程（或线程）。哪一个进程被唤醒则是不确定的。

#### 获取信号量当前值

```c
#include <semaphore.h>
// 获取 sem 指向的信号量的当前值.写入 sval 指向的内存，成功返回 0，失败返回 -1
int sem_getvalue(sem_t *sem, int *sval);
```

注意在 sem\_getvalue 返回时，sval 中的返回值可能已经过时了。

### 未命名信号量

未命名信号量的操作与信号量是一样的，它的创建与删除会用到额外的两个函数，这两个函数不应该用在命名信号量上。 在线程间共享的信号量不需要名字。在相关进程间共享的信号量不需要名字。如果一个父进程在一块共享内存区域中（如一个共享匿名映射）分配了一个未命名信号量，那么作为 fork 操作的一部分，子进程会自动继承这个映射，从而继承这个信号量。

#### 初始化一个未命名信号量

```c
#include <semaphore.h>

/*
@brief 初始化一个未命名信号量
@param sem 	信号量指针
@param pshared	表明信号量是在线程间共享还是在进程间共享
            	- 0:   信号量将在线程间共享
                - else:信号量在进程间共享
@param value	信号量初始值
@return	成功返回 0，失败返回 -1
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
```

对一个已经初始化的信号量再次初始化将导致未定义的行为。并且与命名信号量一样，必须通过 sem 指定的 sem\_t 变量上操作，在其副本上的操作是未定义的。

#### 销毁一个未命名信号量

```c
#include <semaphore.h>

// 销毁一个信号量，只有在不存在进程或线程等待一个信号量时才可以安全销毁这个信号量
int sem_destroy(sem_t *sem);
```

### 信号量限制

下面限制定义在 limits.h 中：

* SEM\_NSEMS\_MAX：一个进程能够拥有的 POSIX 信号量最大数目
* SEM\_VALUE\_MAX ：可取的信号值最大值


# 7.4 POSIX 共享内存

前面提到的内存映射允许无关进程共享内存区域以执行相应的 IPC，但具有一些缺点，比如它需要在硬盘上创建相应的文件，存在额外的 IO 开销。为了克服这些缺点，提出了 共享内存 API。 使用 POSXI 共享内存一般需要以下步骤：

1. 使用 `shm_open`函数打开一个共享内存对象，此函数返回一个引用该对象的文件描述符。
2. 将上一步获得的文件描述符传入 `mmap`调用并指定 flags 参数中指定 MAP\_SHARED，即可将共享内存对象映射到虚拟地址空间。此文件描述符上可以调用传统 Unix 文件描述符系统调用。

### 创建共享内存对象

`shm_open` 函数创建和打开一个新的共享内存对象或打开一个既有对象，它的参数与 open 函数类似：

```c
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

/*
@brief	打开一个文件
@param	name		标识出了待创建或待打开的共享内存对象
@param	flags		掩码表，可取：
                	- O_CREAT  对象不存在时创建，若同时指定 O_EXCL，则对象存在时返回错误
                        - O_RDONLY 只读访问
                        - O_RDWR   读写访问
                        - O_TRUNC  将对象截断为 0
@param	mode		标识文件的所有权限，在创建共享内存对象时使用
			仅仅打开一个已有对象需要将其设置为 0
                	- S_IRUSR | S_IWUSR 属主的读写权限
                        - S_IRGRP | S_IWGRP	数组的读写权限
                        - S_IROTH | S_IWOTH	其它的去写权限
@return	成功返回文件描述符，失败返回 -1
*/

int shm_open(const char *name, int oflag, mode_t mode);
```

`shm_open`返回的文件描述符会设置 close-on-exec 标记，因此当程序执行了一个 exec 时文件描述符会被自动关闭。 共享内存对象在创建后其初始长度会被设置为 0，所以在调用 `mmap` 之前调用 `ftruncate`来设置对象的大小。并且在内存映射后继续调用 `ftruncate` 来操作映射内存大小。

### 删除共享内存对象

SUSv3 要求 POSIX 共享内存对象至少具备内核持久性，即它们会持续存在直到被显式删除或系统重启。当不再需要一个共享内存对象时就应该使用`shm_unlink`删除它：

```c
#include <sys/mman.h>

int shm_unlink(const char *name);
```

`shm_unlink`函数会删除通过 name 指定的共享内存对象。删除一个共享内存对象不会影响对象的既有映射（它会保持有效直到相应的进程调用 `munmap` 或终止），但会阻止后续的 `shm_open` 调用打开这个对象。一旦所有进程都解除映射这个对象，对象就会被删除，其中的内容会丢失。


