建站提交历史文章,原文写作时间 2023 年 2 月前后。

进程通信

进程通信概述

  • 进程是一个独立的资源分配单元,不同进程间的资源是分别独立的。每个进程拥有独立的内存映射表,一个进程无法直接访问另一个进程的数据。

  • 进程通信IPCInter Process Communication)用于完成进程间信息交互和状态传递,从而实现数据传输、资源共享、信号传递、调试控制等功能。

  • 进程通信主要包括本机进程通信与远程进程通信,本章节将介绍本机进程通信,包括:匿名管道有名管道信号消息队列内存映射共享内存信号量

管道(匿名管道)

管道概述与原理

管道基本结构
  • 管道文件位于内核区,是Linux进程间通信最古老的方式之一,用于实现拥有 公共祖先 的进程间通信
  • 管道Pipe匿名管道)是一个拥有一个读端与一个写端队列数据结构(其底层实际为循环队列),具有先入先出的特点,管道内的数据以流的形式存在,称为管道流管道具有内存限制,可以认为是管道通信模式的中转站与缓冲区。
  • 管道是一个半双工的工作模式,但通常以单工模式通信。
    • 单工:仅可用于单向数据传输。
    • 双工:允许双向数据传输。
    • 半双工:允许双向数据传输,但不可以同时进行。
  • 虽然管道只有一个读端与一个写端,但两端可以链接数量不受限制的进程。多个进程可以同时操作读端写端,但过程是不可逆的:多个写端进程写入的数据按时间顺序混杂在管道,某一管道流只能被一个读端进程获取。
  • 管道读写是不可逆的,不可以使用lseek等重定位文件指针。
进程操作管道
  • 管道文件父进程创建,以文件描述符的形式存在于PCB中,父进程同时持有读端写端
  • 文件描述符父进程 fork子进程,所有分支进程持有完全相同的读端写端 文件描述符,并同时具有操作同一管道文件的权限。
  • 注:在实际开发中,如果子进程(或父进程)无需读端写端权限,应在进程初期立即关闭相应文件描述符,从而使得:
    • 避免在长期开发中误用不符合需求的权限操作带来的调试负担。
    • 减少非必要文件描述符占用有限的文件描述符表
    • 如果管道是阻塞的,充分利用管道阻塞特征。

管道阻塞特征

  • 管道具有阻塞(默认,常用)与非阻塞属性。阻塞操作可以保障进程同步通信,但某些场景将制约程序效率。
  • 管道内容不足时,可能读取不足数量的管道流管道内容接近满时,可能写入不足数量的管道流。这些属于正常读写范畴,是多进程编程需要注意的问题。

管道阻塞特征:{操作读端:{    sizerefnum有进程引用写端无进程引用写端管道非空正常读取,返回实际读取量正常读取,返回实际读取量管道为空阻塞,等待管道写入EOF,返回0操作写端:{    sizerefnum有进程引用读端无进程引用读端管道未满正常写入,返回实际写入量触发SIGPIPE信号,异常退出管道已满阻塞,等待管道读出触发SIGPIPE信号,异常退出管道非阻塞特征:{操作读端:{    sizerefnum有进程引用写端无进程引用写端管道非空正常读取,返回实际读取量正常读取,返回实际读取量管道为空返回-1,资源暂不可用EOF,返回0操作写端:{    sizerefnum有进程引用读端无进程引用读端管道未满正常写入,返回实际写入量触发SIGPIPE信号,异常退出管道已满返回-1,管道已满触发SIGPIPE信号,异常退出\begin{array}{rl} 管道阻塞特征: & \left\{ \begin{array}{l} 操作读端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用写端 & 无进程引用写端 \\ \hline 管道非空 & 正常读取,返回实际读取量 & 正常读取,返回实际读取量 \\ 管道为空 & 阻塞,等待管道写入 & EOF,返回0\\ \end{array} \right. \\ \\ 操作写端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用读端 & 无进程引用读端 \\ \hline 管道未满 & 正常写入,返回实际写入量 & 触发SIGPIPE信号,异常退出 \\ 管道已满 & 阻塞,等待管道读出 & 触发SIGPIPE信号,异常退出 \\ \end{array} \right. \end{array} \right. \\ \\ 管道非阻塞特征: & \left\{ \begin{array}{l} 操作读端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用写端 & 无进程引用写端 \\ \hline 管道非空 & 正常读取,返回实际读取量 & 正常读取,返回实际读取量 \\ 管道为空 & 返回\text{-1},资源暂不可用 & EOF,返回0\\ \end{array} \right. \\ \\ 操作写端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用读端 & 无进程引用读端 \\ \hline 管道未满 & 正常写入,返回实际写入量 & 触发SIGPIPE信号,异常退出 \\ 管道已满 & 返回\text{-1},管道已满 & 触发SIGPIPE信号,异常退出 \\ \end{array} \right. \end{array} \right. \\ \\ \end{array}

管道函数

  • 创建与读写管道
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
// create a pipe
// pipefd:
// {file descriptor to read into pipe, file descriptor to write into pipe}
// return value:
// return 0 for success, or -1 for error
int pipe(int pipefd[2]);

// read
ssize_t read(int fd /* pipefd[0] */, void *buf, size_t count);

// writ
ssize_t write(int fd /* pipefd[1] */, const void *buf, size_t count);
  • 设置管道非阻塞
1
2
3
4
// set to non-block mode (default mode is blocked)
#include <fcntl.h>
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);

实际案例:模拟实现ps aux | grep root

  • ps aux:获取全部进程详情。
  • grep root:从管道中读取并筛选后输出。
  • |:管道符。可以将符号前的标准输出与符号后的标准输入分别重定向到管道两端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// 匿名管道: 模拟实现 ps aux | grep root
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define stdin_ 0
#define stdout_ 1
#define stderr_ 2
#define piperead pipefd[0]
#define pipewrite pipefd[1]
#define min(x, y) ((x) < (y) ? (x) : (y))

int pipefd[2];
int ret;

// 模拟实现 ps aux, 此处使用 exec 函数簇调用
void ps() {
ret = close(piperead); // 关闭管道读, 避免后续误操作
ret = dup2(pipewrite, stdout_); // 输出重定向到管道写
int pid = fork();
if (pid) {
// 主进程
ret = wait(NULL); // 同步控制
char buf[] = "Exit!\n"; // 发射退出信号
write(pipewrite, buf, sizeof(buf));
} else {
// 子进程 (根进程的二级子进程)
ret = execlp("ps", "ps", "aux", NULL);
}
}

// 检查筛选条件: 以 jamhus_tao 开始的记录
// 将只打印归属 jamhus_tao 用户的进程详情
int check_user(char *buf) {
const char temp[] = "jamhus_tao";
const int len = strlen(temp);
for (int i = 0; i < len; i++) {
if (buf[i] == '\n') {
return 0;
} else if (buf[i] != temp[i]) {
return 0;
}
}
return 1;
}

// 检查退出信号: Exit!
int check_exit(char *buf) {
const char temp[] = "Exit!";
const int len = strlen(temp);
for (int i = 0; i < len; i++) {
if (buf[i] == '\n') {
return 0;
} else if (buf[i] != temp[i]) {
return 0;
}
}
return 1;
}

// 模拟实现 grep jamhus_tao, 实际效果有所差异
void grep() {
ret = close(pipewrite); // 关闭管道写, 避免后续误操作
ret = dup2(piperead, stdin_); // 输出重定向到管道读
char s[1024];
int len = 0;
char c;
while (read(stdin_, &c, 1)) { // 同步控制, 管道 read 默认阻塞
// 以下代码 (包括两个函数) 用于解析管道流, 非本章讨论核心
s[len++] = c;
if (c == '\n') { // 检查换行: 递交记录操作
if (check_exit(s)) {
exit(0); // 退出信号
} else if (check_user(s)) {
s[min(len-1, 150)] = '\0'; // 考虑终端宽度有限, 为使输出整齐, 截去150长度后输出
puts(s);
}
len = 0;
}
}
}

int main() {
ret = pipe(pipefd);
int pid = getpid();
// 多进程框架: 根进程只用于管理子进程, 不直接完成任何工作
for (int i = 0; i < 2; i++) {
pid = fork();
if (pid == 0) {
switch (i) {
case 0:
// ps 子进程函数, 用于完成 ps aux
ps();
break;
case 1:
// grep 子进程函数, 用于完成 grep jamhus_tao
grep();
break;
default:
break;
}
exit(0); // 子进程结束
}
}
// 父进程
ret = close(piperead);
ret = close(pipewrite);
int ret = 0;
while (ret >= 0) {
ret = wait(NULL); // 等待子进程
}
return 0;
}

有名管道

有名管道概述与原理

  • 有名管道FIFO命名管道)以一个文件实体的形式存在于磁盘中,但有名管道的大小始终为0 BytesFIFO文件被打开,将在内存中生成管道缓冲区,所有管道流经过该缓冲区,但始终不经过磁盘。磁盘中的FIFO文件仅用于定位有名管道,不存储任何数据。
  • 管道不同的,有名管道可用于实现无 公共祖先 的进程间通信
  • 管道具有类似的结构和原理。

有名管道阻塞特征

  • 有名管道阻塞特征基本与管道阻塞特征相同,需要注意的是:读端必须先于写端打开,详见下表。

管道阻塞特征:{操作读端:{    sizerefnum打开读端有进程引用写端无进程引用写端管道非空阻塞,等待写端准备连接正常读取,返回实际读取量正常读取,返回实际读取量管道为空阻塞,等待写端准备连接阻塞,等待管道写入EOF,返回0操作写端:{    sizerefnum打开写端有进程引用读端引用读端文件描述符全部关闭管道未满阻塞,等待读端连接正常写入,返回实际写入量触发SIGPIPE信号,异常退出管道已满阻塞,等待读端连接阻塞,等待管道读出触发SIGPIPE信号,异常退出管道非阻塞特征:{操作读端:{    sizerefnum打开读端有进程引用写端无进程引用写端管道非空直接打开正常读取,返回实际读取量正常读取,返回实际读取量管道为空直接打开返回0,不更改接收变量EOF,返回0操作写端:{    sizerefnum打开写端有进程引用读端引用读端文件描述符全部关闭管道未满返回-1,不存在设备正常写入,返回实际写入量触发SIGPIPE信号,异常退出管道已满返回-1,不存在设备返回-1,管道已满触发SIGPIPE信号,异常退出\begin{array}{rl} 管道阻塞特征: & \left\{ \begin{array}{l} 操作读端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 打开读端 & 有进程引用写端 & 无进程引用写端 \\ \hline 管道非空 & 阻塞,等待写端准备连接 & 正常读取,返回实际读取量 & 正常读取,返回实际读取量 \\ 管道为空 & 阻塞,等待写端准备连接 & 阻塞,等待管道写入 & EOF,返回0\\ \end{array} \right. \\ \\ 操作写端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 打开写端 & 有进程引用读端 & 引用读端文件描述符全部关闭 \\ \hline 管道未满 & 阻塞,等待读端连接 & 正常写入,返回实际写入量 & 触发SIGPIPE信号,异常退出 \\ 管道已满 & 阻塞,等待读端连接 & 阻塞,等待管道读出 & 触发SIGPIPE信号,异常退出 \\ \end{array} \right. \end{array} \right. \\ \\ 管道非阻塞特征: & \left\{ \begin{array}{l} 操作读端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 打开读端 & 有进程引用写端 & 无进程引用写端 \\ \hline 管道非空 & 直接打开 & 正常读取,返回实际读取量 & 正常读取,返回实际读取量 \\ 管道为空 & 直接打开 & 返回0,不更改接收变量 & EOF,返回0\\ \end{array} \right. \\ \\ 操作写端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 打开写端 & 有进程引用读端 & 引用读端文件描述符全部关闭 \\ \hline 管道未满 & 返回\text{-1},不存在设备 & 正常写入,返回实际写入量 & 触发SIGPIPE信号,异常退出 \\ 管道已满 & 返回\text{-1},不存在设备 & 返回\text{-1},管道已满 & 触发SIGPIPE信号,异常退出 \\ \end{array} \right. \end{array} \right. \\ \\ \end{array}

有名管道操作

  • 命令行生成管道文件
1
$ mkfifo <pathname>  // 生成管道文件
  • 创建、打开与读写管道文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <sys/types.h>
#include <sys/stat.h>
// create a FIFO special file
// pathname:
// FIFO file path to create
// mode:
// create mode, same to mode in "open"
// return value:
// return 0 for success, or -1 for errors including file exist and etc.
int mkfifo(const char *pathname, mode_t mode);
// $ man 3 mkfifo

#include <fcntl.h>
// open a FIFO file in read or write mode
// pathname:
// FIFO file to read or write
// flags:
// O_RDONLY for FIFO read mode,
// O_WRONLY for FIFO write mode,
// O_NONBLOCK (optional) for non-block IO
// return value:
// return file descriptor of FIFO, or -1 for error
int open(const char *pathname, int flags);

// read
ssize_t read(int fd, void *buf, size_t count);

// write
ssize_t write(int fd, const void *buf, size_t count);

实际案例:本机聊天功能

  • 为简化案例,本聊天功能仅实现 一对一 一来一回式 交互。

  • 本聊天功能使用一体式程序,使用两方分离式程序更方便些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 错误处理函数: 打印错误信息并退出
void err(int ret, const char cmd[]) {
if (ret == -1) {
perror(cmd);
exit(0);
}
}

// 父进程函数: 处理子进程正常退出与异常退出问题
void parent() {
int wstatus;
int ret = wait(&wstatus);
if (access("chatr.fifo", F_OK) == 0) {
// 删除临时管道文件
unlink("chatr.fifo");
}
if (access("chatw.fifo", F_OK) == 0) {
// 删除临时管道文件
unlink("chatw.fifo");
}
if (WIFSIGNALED(wstatus)) {
printf("对方异常退出。\n");
}
}

// 子进程函数 - 主函数
void child() {
int flag = 0; // 对话发起方标记
if (access("chatr.fifo", F_OK) == -1) {
mkfifo("chatr.fifo", 0664);
flag = 1; // 首先创建管道文件的为对话发起方
}
if (access("chatw.fifo", F_OK) == -1) {
mkfifo("chatw.fifo", 0664);
flag = 1; // 首先创建管道文件的为对话发起方
}
int fdr, fdw;
if (flag) {
// 如果是对话发起方
printf("正在等待加入聊天...\n");
/* 注意:
chatr.fifo 与 chatw.fifo 文件在主客进程中打开次序必须保持一致,
否则由于阻塞机制存在, 进程将一直阻塞.
*/
fdr = open("chatr.fifo", O_RDONLY);
err(fdr, "open");
fdw = open("chatw.fifo", O_WRONLY);
err(fdw, "open");
printf("你是聊天的发起人。\n");
} else {
// 如果是对话接受方
printf("正在加入聊天...\n");
fdw = open("chatr.fifo", O_WRONLY);
err(fdw, "open");
fdr = open("chatw.fifo", O_RDONLY);
err(fdr, "open");
printf("你是聊天的接受人。\n");
}

char buf[1024];
int turn = flag;
while (1) {
if (turn) {
printf("到你发言了!\n> ");
gets(buf);
if (strcmp(buf, "/exit") == 0) {
int ret = write(fdw, buf, sizeof(buf));
err(ret, "write");
printf("你终止了对话。\n");
exit(0);
} else {
int ret = write(fdw, buf, sizeof(buf));
err(ret, "write");
printf("你: %s\n", buf);
}
} else {
printf("等待对方回应...\n");
int ret = read(fdr, buf, sizeof(buf));
err(ret, "read");
buf[ret] = '\0';
if (strcmp(buf, "/exit") == 0) {
printf("对方终止对话。\n");
exit(0);
} else {
printf("他: %s\n", buf);
}
}
turn = !turn;
}
}

int main() {
int pid = fork();
err(pid, "fork");
if (pid) {
parent();
} else {
child();
}
return 0;
}

效果呈现:

信号

信号概述

  • 信号Linux进程间通信最古老的方式之一,是事件发生时的通知机制,有时也称为软件中断,一种异步通信模式。相比于管道的主动获取,信号由接收进程被动获取,可以中断进程用于完成一个突发事件。信号具有简单高优先级无法携带大量信息的特点。
  • 信号具有三种状态:产生未决递达。信号未决时被阻塞在内核区,等待信号阻塞解除;信号递达,被进程处理或忽略。
  • 信号分为主要两种:实时信号系统信号实时信号不具有特定意义,可以由用户指定;系统信号通常具有特定含义。
  • 信号递达时,进程会中断当前的程序,转而处理与信号相关联的程序,当信号处理完后继续执行当前进程。
  • 信号阻塞后释放,多个信号将同时从未决状态进入递达状态。该过程进程处理不同信号拥有不同优先级。默认动作高于自定义动作是首要优先级;在实时信号中,高值信号优先级高于低值信号;在系统信号中,以及实时信号系统信号间,则具有复杂的优先级关系。
  • 多个相同信号同时递达实时信号会被多次处理,系统信号只处理一次。

(注:接下来在常用信号中将介绍实时信号系统信号以及默认动作。在信号集章节中,将涉及信号阻塞;在捕捉信号章节中,将涉及自定义动作。)

常用信号

  • 查看信号列表$ kill -l1~31为常规信号,其余为实时信号。
  • 查看信号说明$ man 7 signal
信号宏 信号值 事件说明 默认动作
SIGINTSIGINT 22 <CtrlCtrl+CC> 终端向运行中子进程发出此信号 TermTerm
SIGQUITSIGQUIT 33 <CtrlCtrl+\\backslash> 终端向运行中子进程发出此信号 TermTerm
SIGKILLSIGKILL 99 强制终止进程 TermTerm
SIGSEGVSIGSEGV 1111 访问无效内存(Segment Fault段错误 CoreCore
SIGPIPESIGPIPE 1313 向无读端管道写数据(Broken Pipe管道破裂 TermTerm
SIGALRMSIGALRM 1414 计时器结束向自己发出此信号 TermTerm
SIGCHLDSIGCHLD 1717 子进程结束,父进程收到此信号 IgnIgn
SIGCONTSIGCONT 1818 如果进程暂停,继续进程 ContCont
SIGSTOPSIGSTOP 1919 暂停进程 StopStop
SIGMAX    SIGMIN^{~~~~SIGMIN}_{\to SIGMAX} 346434 \to 64 Linux实时信号,用户自定义意义 TermTerm
  • 默认动作:

    • Term:终止进程。
    • Ign:忽略。
    • Core:终止进程并记录。
    • Stop:中断进程。
    • Cont:继续进程。
  • SIGKILLSIGSTOP信号不能被产生、阻塞与忽略。

信号集

信号集概述
  • 信号具有三种状态产生未决递达。信号是否未决并阻塞在进程内核区是由阻塞信号集信号掩码)决定的。
  • 信号集是一个64bit整数,标志1~64信号。信号集分为未决信号集阻塞信号集自定义信号集
    • 阻塞信号集:位于内核区,其中为1的标志位表示阻塞该信号。用户可以通过Linux函数操作阻塞信号集
    • 未决信号集:位于内核区,其中为1的标志位表示该信号未决。在阻塞后试图递达的信号将未决,并在未决信号集标记;在阻塞解除后该标记位立即置0,信号递达
    • 自定义信号集:位于用户区。事实上任何64bit整数都可以是自定义信号集自定义信号集仅是拥有信号集意义的整数,在操作阻塞信号集时作为参数传递。
信号集编码
  • 信号集操作通过位运算进行,对于x号信号,其对应的标志位为x-1。例如:一个包含279信号的信号集的值为0b 0001 0100 0010
  • 标准C库提供了一系列信号集运算函数。但事实上我们不必记忆这些函数,理解原理后使用位运算即可解决。
1
2
3
4
5
6
7
8
9
10
11
#include <signal.h>
// remove all the signums from the set
int sigemptyset(sigset_t *set); // set = 0; return 0;
// add all the signums into the set
int sigfillset(sigset_t *set); // set = -1ull; return 0;
// add the signum into set
int sigaddset(sigset_t *set, int signum); // set = set | 1 << signum - 1; return 0;
// remove the signum from the set
int sigdelset(sigset_t *set, int signum); // set = set & ~(1 << signum - 1); return 0;
// examine if the signum is in the set
int sigismember(const sigset_t *set, int signum); // return set >> signum - 1 & 1;
操作阻塞信号集
  • sigprocmask,用户无法直接操作阻塞信号集,必须使用内核提供的函数完成操作。
  • 注意:SIGKILLSIGSTOP信号在操作中会被过滤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <signal.h>
// examine and change the block signal set
// how:
// SIG_BLOCK:
// add the signals with 1 flag in the given set into the block signal set
// SIG_UNBLOCK:
// remove the signals with 1 flag in the given set from the block signal set
// SIG_SETMASK:
// set the block sognal set to the given set
// set:
// Operating signal set whose actual meaning is depending on "how".
// oldset:
// to receive the former block signal set, and can be NULL
// return value:
// return 0 for success, -1 for error
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

// About more:
// $ man 2 sigprocmask
  • 只获取oldset如下:
1
2
3
const unsigned long long zero = 0;
unsigned long long oldset;
sigprocmask(SIG_BLOCK, &zero, &oldset);

发送信号

  • kill用于向任意进程或任意进程组发射信号。

  • pid:分为下述情况:

    • < -1:将信号发送到所有组ID-pidpid的绝对值)的进程。
    • -1:将信号发送到所有允许发送到的进程。
    • 0:将信号发送到所有组ID与调用进程组ID相同的进程。
    • > 0:将信号发送到PIDpid的进程。
  • 返回值:成功返回0,失败返回-1

1
2
3
4
5
#include <sys/types.h>
#include <signal.h>
// send a signal to a process
int kill(pid_t pid, int sig);
// $ man 2 kill
  • raise向当前进程发送信号,以下是等价的。(但是kill的返回值是0/1raise的返回值是0/nonzero
1
2
3
4
#include <signal.h>
// send a signal to the caller
int raise(int sig);
// $ man 3 raise
1
kill(getpid(), sig);
  • abort向当前进程发送SIGABRT信号,以下是等价的。(但是kill的返回值是0/1abort无返回值)
1
2
3
4
#include <stdlib.h>
// send SIGABRT to the caller
void abort(void);
// $ man 3 abort
1
kill(getpid(), SIGABRT);

计时器信号

alarm 计时器
  • alarm计时器,执行一次,非阻塞,执行结束触发SIGABRT信号。每执行alarm函数将刷新计时器,计时器时间设置为seconds
  • seconds:计时器时间。需要注意的是,此操作会直接重置计时器,而之前的未触发的信号也不会触发;如果设置为0,则计时器置为不可用,不会触发信号。
  • 返回值:返回当前计时器时间(seconds设置前),初始为0
1
2
3
4
#include <unistd.h>
// set an alarm clock for delivery of a signal
unsigned int alarm(unsigned int seconds);
// $ man 2 alarm
itimer 计时器
  • itimer计时器,循环执行,非阻塞,每轮结束触发响应信号(由which指定)。每执行setitimer函数将刷新计时器,计时器实时参数重置为new_value,并获取当前计时器实时参数(new_value设置前),初始为{{0, 0}, {0, 0}}。执行getitimer函数可获取当前计时器实时参数到curr_value而不重置计时器。
  • which:(下列选择一个)
    • ITIMER_REAL:真实时间,触发SIGALRM信号。
    • ITIMER_VIRTUAL:用户时间,触发SIGVTALRM信号。
    • ITIMER_PROF:用户与内核时间,触发SIGPROF信号。
    • 真实事件=用户时间+切换时间+内核时间真实事件 = 用户时间 + 切换时间 + 内核时间
    • 无特殊需求通常选用ITIMER_REAL
    • getitimer中需与setitimer使用相同的which参数。
  • new_valueold_valuecurr_value原型如下:
1
2
3
4
5
6
7
8
9
// struct itimerval, in sys/time.h
struct itimerval {
struct timeval it_interval; // 时间间隔, 如为 {0, 0} 计时器不循环
struct timeval it_value; // 距离下次触发时间, 初始状态可以大于 it_interval; 如设置为 {0, 0} 计时器不可用
};
struct timeval {
time_t tv_sec; // 毫秒
suseconds_t tv_usec; // 微秒
};
  • new_value:设置新参数。
  • old_value:接收旧参数,可为NULL
  • curr_value:接收当前参数。
  • 返回值:成功返回0,失败返回-1
1
2
3
4
5
6
7
8
#include <sys/time.h>
// get value of an interval timer
int getitimer(int which, struct itimerval *curr_value);
// $ man 2 getitimer

// set value of an interval timer
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
// $ man 2 setitimer
  • 以下两者是等价的:
1
setitimer(ITIMER_REAL, new_value, old_value);
1
2
getitimer(ITIMER_REAL, old_value);
setitimer(ITIMER_REAL, NULL);

捕捉信号

signal 捕捉信号
  • signal函数系统兼容性较差,不推荐使用。但相较于sigaction函数,signal函数更为简单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <signal.h>
typedef void (*sighandler_t)(int);
// ANSI C signal handling
// signum:
// signal number
// handler:
// one of the following:
// SIG_DFL for using the default action
// SIG_IGN for ignore the signal
// callback function pointer
// return value:
// Return the last handler, or SIG_ERR for error. The inital handler is SIG_DFL
sighandler_t signal(int signum, sighandler_t handler);

// About more
// $ man 2 signal

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <signal.h>
#include <stdio.h>

void func(int sig) {
printf("Get a signal %d\n", sig);
}

int main() {
signal(SIGINT, func);
raise(SIGINT); // 向自己发射 SIGINT 信号
while (1); // 发现尝试使用 Ctrl+C 退出进程将失败 (用 Ctrl+\ 或 kill)
return 0;
}
sigaction 捕捉信号
  • sigaction函数具有更高的系统兼容性,并且支持更丰富的功能,尽管它们很少被用到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <signal.h>
struct sigaction {
void (*sa_handler)(int); // handler 1, same to "signal"
void (*sa_sigaction)(int, siginfo_t *, void *); // handler 2, more options, see "About more"
sigset_t sa_mask; // temporary block signal mask which will rollback after calling
int sa_flags;
// usual 0 (handler 1), SA_SIGINFO for choosing handler 2, more flags see "About more"
void (*sa_restorer)(void); // deprecated
};
// change signal action
// signum:
// signal number
// act:
// the structure describe callback function, see "struct sigaction"
// oldact:
// to receive the last action
// return value:
// return 0 for success, -1 for error
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

// About more
// $ man 2 sigaction

示例:(与上个示例等价)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <signal.h>
#include <stdio.h>

void func(int sig) {
printf("Get a signal %d\n", sig);
}

int main() {
struct sigaction action = {func};
sigaction(SIGINT, &action, NULL);
raise(SIGINT); // 向自己发射 SIGINT 信号
while (1); // 发现尝试使用 Ctrl+C 退出进程将失败 (用 Ctrl+\ 或 kill)
return 0;
}

信号量

内存映射

内存映射原理

  • 内存映射Memory Mapping)技术是开辟一块与映射磁盘空间等大的内存空间,操作映射内存即可自动同步映射磁盘空间。值得注意的是,在Linux内存映射实现中,映射内存必须是页(即4096Bytes)的倍数,不足4096Bytes的倍数向上取整倍。
  • Linux中,映射内存位于虚拟内存空间共享区。同时,在内存映射时,无需对内核介入与数据缓存而直接写入。因此,内存映射不仅操作更为方便,在大数据规模时效率上也有一定优势。
  • 内存映射同样支持匿名模式,这将不需要指定磁盘文件。

内存映射实现

  • mmapmunmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <sys/mman.h>
// map a file or device into memory
// addr:
// Address base of the memory, it's a hint of address to kernel but not exactly the address.
// Usually NULL is given.
// length:
// address bound of the mapping memory in bytes
// usually lseek(fd, 0, SEEK_END) is given
// prot:
// optional:
// PROT_READ:
// read permisson to the mapping memory
// PROT_WRITE:
// write permisson to the mapping memory, PROT_WRITE should given with PROT_READ
// PROT_EXEC:
// execute permisson to the mapping memory
// flags:
// exactly one bellow:
// MAP_SHARED:
// mapping memory can shared with other process
// usually MAP_SHARED is choosing
// MAP_PRIVATE:
// mapping in copy-on-write mode, and can not shared with other process
// optional:
// MAP_ANON (MAP_ANONYMOUS):
// anonymous mapping, not backed by any file
// "fd" should be -1 and "offset" should be 0 if it's chosen.
// fd:
// file descriptor
// offset:
// offset in the file
// ususally 0 is given
// return value:
// return exact address, or -1 for error
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

// unmap a file or device
// addr:
// address base of mapping memory
// length:
// address bound of mapping memory
int munmap(void *addr, size_t length);

// About more
// $ man 2 mmap

共享内存

共享内存原理

  • 共享内存Shared Memory)与内存映射技术相仿。但不同的是,内存映射建立了与磁盘空间的同步关系(不考虑匿名内存映射),这意味着内存映射同样需要操作磁盘;而共享内存物理内存中开辟空间,为不同进程建立相同的虚拟内存映射,无需操作磁盘,效率更高。
  • 共享内存必须是页(即4096Bytes)的倍数,不足4096Bytes的倍数向上取整倍。
  • 共享内存位于虚拟内存空间共享区

共享内存操作

  • shmget
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/ipc.h>
#include <sys/shm.h>
// get or create a shared memory
// key:
// the shared memory key to get or create (depending on "shmflg")
// size:
// the size of shared memory to create
// if getting a shared memory, size can be any but no more than exactly size of shared memory
// shmflg:
// (default):
// get a shared memory, otherwise raise an error
// IPC_CREAT:
// if the shared memory id not found, a new memory will be create
// IPC_EXCL:
// if the shared memory id already exists, an error will be raise
// it should be used with IPC_CREAT
// * others:
// if IPC_CREAT is chosen, you should OR-ed with the permission mode on "shmflg"
// return value:
// the shmid (different to key), or -1 for error
int shmget(key_t key, size_t size, int shmflg);

// About more
// $ man 2 shmget
  • shmatshmdt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//     attach the shared memory to the process
// shmid:
// shmid getting from "shmget"
// shmaddr:
// Address base of the memory, it's a hint of address to kernel but not exactly the address.
// Usually NULL is given.
// shmflg:
// (default):
// read and write permisson
// SHM_EXEC:
// execute permission
// SHM_RDONLY:
// readonly (remove the write permisson)
// return value:
// exact memory address, or -1 for error
void *shmat(int shmid, const void *shmaddr, int shmflg);

// detach the shared memory from the process
// shmaddr:
// address of shared memory getting from "shmat"
// return value:
// return 0 for success, or -1 for error
int shmdt(const void *shmaddr);

// About more
// $ man 2 shmat
  • shmctl
1
2
3
4
5
6
7
8
9
10
11
12
//     free the shared memory (and other functioms, see in more)
// shmid:
// shmid getting from "shmget"
// cmd:
// IPC_RMID:
// remove the shared memory
// buf:
// NULL
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

// About more
// $ man 2 shmctl

消息队列

查看 IPC 设备

  • 此处IPC设备特指System V的进程间通信设备,包括:消息队列Message Queue)、共享内存Shared Memory)、信号量Semaphore Array)。

  • ipcs命令查看IPC设备

1
2
3
4
$ ipcs -q  // 查看消息队列设备
$ ipcs -m // 查看共享内存设备
$ ipcs -s // 查看信号数组设备
// $ ipcs --help
  • ipcrm命令移除IPC设备
1
2
3
4
5
6
7
8
9
10
11
$ ipcrm -a            // 移除所有IPC设备
$ ipcrm --all=msg // 移除所有消息队列设备
$ ipcrm --all=shm // 移除所有共享内存设备
$ ipcrm --all=sem // 移除所有信号数组设备
$ ipcrm -q <id> // 按 id 移除消息队列设备
$ ipcrm -Q <key> // 按 key 移除消息队列设备
$ ipcrm -m <id> // 按 id 移除共享内存设备
$ ipcrm -M <key> // 按 key 移除共享内存设备
$ ipcrm -s <id> // 按 id 移除信号数组设备
$ ipcrm -S <key> // 按 key 移除信号数组设备
// $ ipcrm --help