> 包括進程通信中信號的概念及信號處理、管道通信編程、內存共享編程、隊列通信編程。
[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接口