> 包括进程通信中信号的概念及信号处理、管道通信编程、内存共享编程、队列通信编程。
[TOC]
## 1 进程间通信
能够实现进程间通信的方法有:
- 信号(signal):进程之间相互通信或操作的一种机制
- 管道(pipe):同一台机器的两个进程间双向通信
- 套接字(socket):允许在不同机器上的两个进程间进行通信
- System V IPC机制:
- 消息队列(message queue):适用于信息传递频繁而内容较少
- 信号量(semaphore):用于实现进程之间通信的同步问题
- 共享内存(shared memory):信息内容较多
## 2 信号
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,可以在用户空间和内核之间直接交互。信号事件的发生有两个来源包括硬件来源(例如按下`Ctrl+C`)和软件来源(例如`kill` `raise` `alarm` `setitimer` `sigation` `sigqueue`,以及一些非法运算)
通过 `kill -l` 列出系统支持的信号,如下所示:
```
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
```
以上信号可分为三类:
- [1,31] 都以"SIG"开头,属于非实时信号
- [34, 49] 都以"SIGREMIN" 开头,从Unix系统中继承下来的信号,称为不可靠信号(非实时信号)
- [50, 64]的都以"SIGRTMAX" 开头,为了解决不可靠信号问题而进行更改和扩充的信号,称为可靠信号(实时信号)
一个完整的信号声明周期为:
1. 在内核进程产生信号
2. 在用户进程进行信号注册和信号注销
3. 信号处理
一旦有信号产生,用户进程对信号的响应有三种方式:
- 执行默认操作(Linux对每种信号都规定了默认操作)
- 捕捉信号(定义信号处理函数,当信号发生时,执行相应的处理函数)
- 忽略信号(不作处理,但是SIGKILL和SEGSTOP无法忽略)
> Linux 常见信号的含义及其默认操作
信号名 | 含义 | 默认操作
---- | ---- | ----
SIGHUP | 在用户终端连接结束时发出 | 终止
SIGINT | 中断键 `Ctrl + C` | 终止
SIGQUIT | 退出键 `Ctrl + \` | 终止
SIGILL | 另一个进程企图执行一条非法指令时发出 | 终止
SIGFPE | 发生致命的算术运算错误时发出 | 终止
SIGKILL | 立即结束程序的运行,不能被阻塞、处理或忽略 | 终止
SIGALRM | 定时器完成时发出,可用alarm函数来设置 | 终止
SIGSTOP | 挂起键 `Ctrl + Z`,用于暂停一个进程,不能被阻塞、处理或忽略 | 暂停进程
SIGCHLD | 子进程结束时向父进程发出 | 忽略
> 以下程序演示了父进程向子进程发送信号,结束子进程
```c
int main() {
pid_t pid_res;
int kill_res;
pid_res = fork();
if (pid_res < 0) { // 创建子进程失败
perror("创建子进程失败");
exit(1);
} else if (pid_res == 0) { // 子进程
raise(SIGSTOP); // 调用rasise 函数,发送SIGSTOP使子进程暂停
exit(0);
} else {
printf("子进程号为:%d\n", pid_res);
if ((waitpid(pid_res, NULL, WNOHANG)) == 0) {
if ((kill_res = kill(pid_res, SIGKILL)) == 0) {
printf("kill %d返回:%d\n", pid_res, kill_res);
} else {
perror("kill 结束子进程失败")
}
}
}
}
/*
int kill(pid_t, pid, int sig);
- pid > 0 表示将信号传给识别码为 pid 的进程
- pid = 0 表示将信号传给和当前进程在相同进程组的所有进程
- pid = -1 表示将信号广播传送给系统内所有的进程
- pid < 0 表示将信号传给进程组识别码为 pid 绝对值的所有进程
*/
```
以下是signal函数的示例,展示了按下两次`Ctrl + C` 后,终止进程的过程
```c
// Ctrl + C
void fun_ctrl_c();
int main() {
signal(SIGINT, fun_ctrl_c); // 令 SIGINT 信号调用函数 fun_ctrl_c
while(1) {
printf("无限循环,按<Ctrl + C>\n")
sleep(3);
}
exit(0);
}
void fun_ctrl_c() {
printf("按下Ctrl + C");
(void) signal(SIGINT, SIG_DFL); // 恢复 SIGINT 信号的默认操作
}
/*
#include <signal.h>
void (*signal(int signum, void(* handler)(int))) (int);
- SIG_IGN:忽略参数 signum 指定的信号
- SIG_DFL:重设参数 signum 指定的信号的默认操作
返回先前的处理函数指针,如有错误返回SIG_ERR,即-1
*/
```
## 3 管道
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区中存取数据。
- pipe 为无名管道,用于相关进程之间的通信,如父进程和子进程,通过 `pipe()` 系统调用来创建,当最后一个使用它的进程关闭对它的引用时,pipe将自动撤销
- FIFO 为命名管道,在磁盘上有对应的节点,但没有数据块(即只拥有名字和相应的访问权限),通过mknod()系统调用和mkfifo()函数来建立,当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。
> 以下例子展示了无名管道(pipe)的创建和读写
```c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
// 管道的创建和读写
int main() {
pid_t pid_res;
int r_num;
int pipe_fd[2]; // pipe_fd[0]为管道读取端,pipe_fd[1]为管道写入端
char buf_r[100], buf_w[100];
memset(buf_r, 0, sizeof(buf_r)); // 将buf_r的前sizeof(buf_r)元素用0填充
if(pipe(pipe_fd) < 0) {
printf("创建管道失败");
return -1;
}
pid_res = fork();
if(pid_res < 0) {
perror("创建子进程失败");
exit(0);
} else if(pid_res == 0) { // 子进程代码块
close(pipe_fd[1]);
if((r_num = read(pipe_fd[0], buf_r, 100)) > 0) {
printf("读取到:%s\n", buf_r);
close(pipe_fd[0]);
exit(0);
}
} else { // 父进程代码块
close(pipe_fd[0]);
printf("请从键盘输入写入管道的字符串:");
scanf("%s", buf_w);
if((write(pipe_fd[1], buf_w, strlen(buf_w)))!= -1) {
close(pipe_fd[1]);
waitpid(pid_res, NULL, 0); // 阻塞父进程,等待子进程退出
exit(0);
}
}
}
```
> 以下例子通过命名管道模拟了一个聊天功能,包括 `a.c` 与 b.c` 两个文件,其中 `b.c` 与 `a.c`的区别是交换了 FILO_NAME_1 和 FILO_NAME_2
```c
// a.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#define FILO_NAME_1 ".__fifo__1"
#define FILO_NAME_2 ".__fifo__2"
int main() {
mkfifo(FILO_NAME_1, S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
mkfifo(FILO_NAME_2, S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
printf("创建管道成功\n");
int wfd = open(FILO_NAME_1, O_RDWR);
int rfd = open(FILO_NAME_2, O_RDWR);
if (wfd <=0 || rfd <= 0) {
return 0;
}
printf("a的终端\n");
fd_set read_fd;
struct timeval net_timer;
char str[32];
int i;
int len;
while (1) {
FD_ZERO(&read_fd);
FD_SET(rfd, &read_fd);
FD_SET(fileno(stdin), &read_fd);
net_timer.tv_sec = 5;
net_timer.tv_usec = 0;
memset(str, 0, sizeof(str));
if (i = select(rfd+1, &read_fd, NULL, NULL, &net_timer) <= 0) {
continue;
}
if (FD_ISSET(rfd, &read_fd)) {
read(rfd, str, sizeof(str)); // 读取管道,将管道内容存入str
printf("a读取到:%s\n", str);
}
fgets(str, sizeof(str), stdin);
len = write(wfd, str, strlen(str)); // 写入管道
}
close(rfd);
close(wfd);
}
```
> 高级管道可以参见 `popen` 函数
## 4 消息队列
消息队列是一系列保存在内核中的消息的列表,用户进程可以向消息队列存取消息。
消息队列产生后,除非明确的删除,产生的队列会一致保留在系统中,队列的个数是有限的(注意不要泄露),使用已达到上限,msgget调动会失败,提示"no space left on device"
其常用函数为:
函数名称 | 函数功能 | 函数原型 | 函数返回值
---- | ---- | ---- | ----
fotk | 由文件路径和工程ID生成标准key(通过 ftok 建立一个 用于 IPC 通信的 ID 值) | `key_t ftok(char *pathName, char projectId);` | 成功返回 key_t 的值,失败返回 -1,失败原因存于 errno 中
msgget | 创建或打开消息队列 | `int msgget(key_t key, int msgFlag` | 执行成功返回消息队列识别号,失败返回-1,失败原因存于 errno 中
msgsnd | 将消息送入消息队列 | `int msgsnd(int msgId, struct msgbuf *msgp, int msgSize, int msgFlag)` | 执行成功返回0,失败返回-1,失败原因存于 errno 中
msgrcv | 从消息队列读取消息 | `int msgrcv(int msgId, struct msgbuf *msgp, int msgSize, long msgType, int msgFlag)` | 执行成功返回实际读取的消息数据长度,否则返回-1,错误原因存于 errno 中
msgctl | 控制消息队列 | `msgctl (int msqId, int cmd, struct msqid_ds *buf)` |
> 利用消息队列进行通信的示例
```c
/* 利用消息队列进行通信 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <unistd.h>
struct msgbuf {
long msg_type;
char msg_text[512];
};
int main() {
int qid;
key_t key;
if (key = ftok(".", 'a') == -1) { // 产生标准的 key
perror("产生标准的key出错");
exit(1);
}
if ((qid=msgget(key, IPC_CREAT|0666))==-1) {
perror("创建消息队列出错");
exit(1);
}
printf("打开消息队列:%d\n", qid);
struct msgbuf msg;
puts("输入要加入到消息队列的消息:");
if (fgets((&msg)->msg_text, 512, stdin) == NULL) {
puts("没有消息");
exit(1);
}
msg.msg_type = getpid();
int len = strlen(msg.msg_text);
if (msgsnd(qid, &msg, len, 0) < 0) {
perror("添加消息出错");
exit(1);
}
if (msgrcv(qid, &msg, 512, 0, 0) < 0) {
perror("读取消息出错");
exit(1);
}
printf("读取的消息是:%s\n", (&msg)->msg_text);
if (msgctl(qid, IPC_RMID, NULL) < 0) {
perror("删除消息队列出错");
exit(1);
}
exit(0);
}
```
## 5 共享内存
共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中。
- 共享内存的好处是通信效率非常高。
- 可以通过内存映射机制实现,也可以通过Unix System V共享内存机制实现。
- 常用函数如下:
- mmap:建立共享内存映射
- munmap:接触共享内存映射
- shmget:获取共享内存区域的ID
- shmat:建立映射共享内存
- shmdt:接触共享内存映射
------
内存映射(memory map)机制使进程之间通过映射同一个普通文件实现共享内存,通过 mmap() 系统调用实现,普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用 read 和 write 等文件操作函数
> 以下示例中,先调用 mmap 映射内存,然后调用 fork 函数创建进程;在调用 fork 函数之后,子进程继承父进程匿名映射的地址空间,同样也继承 mmap 函数的返回地址,
```c
// 匿名内存映射
#include <sys/types.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char name[4];
int age;
} people;
int main(int argc, char** argv) {
int i;
people *p_map;
char tmp;
p_map = (people *) mmap(NULL, sizeof(people) * 10, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
pid_t pid_res = fork();
if (pid_res < 0) {
perror("创建子进程失败");
exit(0);
} else if (pid_res == 0) { // 子进程代码块
sleep(2);
for (i = 0; i < 5; i++) {
printf("子进程读取 - 第 %d 个人的年龄是:%d\n", i+i, (*(p_map+i)).age);
}
(*p_map).age = 110;
munmap(p_map, sizeof(people) * 10); // 解除内存映射关系
exit(0);
} else {
tmp = 'a';
for (i = 0; i < 5; i++) {
tmp += 1;
memcpy((*(p_map + i)).name, &tmp, 2);
(*(p_map+i)).age = 20 + i;
}
sleep(5);
printf("父进程读取 - 五个人的年龄和是:%d\n", (*p_map).age);
munmap(p_map, sizeof(people) * 10);
}
}
/*
> gcc test.c -o test
> /data/Code/C/$ ./test
子进程读取 - 第 0 个人的年龄是:20
子进程读取 - 第 2 个人的年龄是:21
子进程读取 - 第 4 个人的年龄是:22
子进程读取 - 第 6 个人的年龄是:23
子进程读取 - 第 8 个人的年龄是:24
父进程读取 - 五个人的年龄和是:110
*/
```
> 与 mmap 系统调用通过映射一个普通文件实现共享内存不同,UNIX System V 共享内存是通过映射特殊文件系统 shm 中的文件实现进程间的共享内存通信
- 空白目录
- 精简版Spring的实现
- 0 前言
- 1 注册和获取bean
- 2 抽象工厂实例化bean
- 3 注入bean属性
- 4 通过XML配置beanFactory
- 5 将bean注入到bean
- 6 加入应用程序上下文
- 7 JDK动态代理实现的方法拦截器
- 8 加入切入点和aspectj
- 9 自动创建AOP代理
- Redis原理
- 1 Redis简介与构建
- 1.1 什么是Redis
- 1.2 构建Redis
- 1.3 源码结构
- 2 Redis数据结构与对象
- 2.1 简单动态字符串
- 2.1.1 sds的结构
- 2.1.2 sds与C字符串的区别
- 2.1.3 sds主要操作的API
- 2.2 双向链表
- 2.2.1 adlist的结构
- 2.2.2 adlist和listNode的API
- 2.3 字典
- 2.3.1 字典的结构
- 2.3.2 哈希算法
- 2.3.3 解决键冲突
- 2.3.4 rehash
- 2.3.5 字典的API
- 2.4 跳跃表
- 2.4.1 跳跃表的结构
- 2.4.2 跳跃表的API
- 2.5 整数集合
- 2.5.1 整数集合的结构
- 2.5.2 整数集合的API
- 2.6 压缩列表
- 2.6.1 压缩列表的结构
- 2.6.2 压缩列表结点的结构
- 2.6.3 连锁更新
- 2.6.4 压缩列表API
- 2.7 对象
- 2.7.1 类型
- 2.7.2 编码和底层实现
- 2.7.3 字符串对象
- 2.7.4 列表对象
- 2.7.5 哈希对象
- 2.7.6 集合对象
- 2.7.7 有序集合对象
- 2.7.8 类型检查与命令多态
- 2.7.9 内存回收
- 2.7.10 对象共享
- 2.7.11 对象空转时长
- 3 单机数据库的实现
- 3.1 数据库
- 3.1.1 服务端中的数据库
- 3.1.2 切换数据库
- 3.1.3 数据库键空间
- 3.1.4 过期键的处理
- 3.1.5 数据库通知
- 3.2 RDB持久化
- 操作系统
- 2021-01-08 Linux I/O 操作
- 2021-03-01 Linux 进程控制
- 2021-03-01 Linux 进程通信
- 2021-06-11 Linux 性能优化
- 2021-06-18 性能指标
- 2022-05-05 Android 系统源码阅读笔记
- Java基础
- 2020-07-18 Java 前端编译与优化
- 2020-07-28 Java 虚拟机类加载机制
- 2020-09-11 Java 语法规则
- 2020-09-28 Java 虚拟机字节码执行引擎
- 2020-11-09 class 文件结构
- 2020-12-08 Java 内存模型
- 2021-09-06 Java 并发包
- 代码性能
- 2020-12-03 Java 字符串代码性能
- 2021-01-02 ASM 运行时增强技术
- 理解Unsafe
- Java 8
- 1 行为参数化
- 1.1 行为参数化的实现原理
- 1.2 Java 8中的行为参数化
- 1.3 行为参数化 - 排序
- 1.4 行为参数化 - 线程
- 1.5 泛型实现的行为参数化
- 1.6 小结
- 2 Lambda表达式
- 2.1 Lambda表达式的组成
- 2.2 函数式接口
- 2.2.1 Predicate
- 2.2.2 Consumer
- 2.2.3 Function
- 2.2.4 函数式接口列表
- 2.3 方法引用
- 2.3.1 方法引用的类别
- 2.3.2 构造函数引用
- 2.4 复合方法
- 2.4.1 Comparator复合
- 2.4.2 Predicate复合
- 2.4.3 Function复合
- 3 流处理
- 3.1 流简介
- 3.1.1 流的定义
- 3.1.2 流的特点
- 3.2 流操作
- 3.2.1 中间操作
- 3.2.2 终端操作
- 3.3.3 构建流
- 3.3 流API
- 3.3.1 flatMap的用法
- 3.3.2 reduce的用法
- 3.4 collect操作
- 3.4.1 collect示例
- 3.4.2 Collector接口