進程間通信
一般簡稱爲IPC----InterProcess Communication。是指在不同進程之間傳播或交換信息。
進程間通信有這麼幾個方式:管道(命名管道/匿名管道),共享隊列,消息隊列,信號量
進程間爲什麼要通信呢?
因爲進程的獨立性,因此想要通信必須能夠共同訪問一個相同的媒介。
進程間通信的目的:數據傳輸,數據共享,進程間的訪問控制。
也正因爲通信的目的不同,使用場景不同,因此操作系統提供了多種進程間通信方式:
管道----傳輸數據
共享內存----共享數據
消息隊列----傳輸數據
信號量----進程間的訪問控制
管道
管道是半雙工通信,雙向選擇的單向通信(即數據只能在一個方向上流動),具有固定的讀端和寫端
它是進程間的數據資料傳輸。在通信過程中,先將數據放到buf中,在將數據拷貝到自己的buf中在進行操作
管道生命週期隨進程,如果進程消亡了,那麼通信也就結束了
匿名管道
int pipe(int pipefd[2]);
//pipefd輸出型參數
//數組pipefd用於返回引用結尾的兩個文件描述符。
pipefd[0] 從管道讀數據
pipefd[1] 從管道寫數據
如果成功,則返回 0;不成功,則返回-1
只能用於具有親緣關係的進程間通信
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
int main(){
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0){
perror("pipe error");
return -1;
}
int pid = fork();
if(pid < 0){
perror("fork error");
return -1;
}else if(pid == 0){
close(pipefd[1]);
}else{
sleep(1);
close(pipefd[1]);
char buf[1024] = {0};
int ret = read(pipefd[0], buf, 1023);
printf("read buf:[%d - %s]\n",ret, buf);
}
return 0;
}
先建立管道,之後再創建子進程。這個時候就要考慮到管道的讀寫特性了
若管道中沒有數據,則read會阻塞,直到讀到數據返回
若管道中數據寫滿了,則write會阻塞,直到數據被讀取,管道中有空閒位置,寫入數據後返回
若管道中所有的讀端都被關閉,則write會觸發異常----SIGPIPE(信號標誌)----導致進程退出
若管道中的所有寫有寫端都被關閉,則read返回0----通知用戶沒人寫了
父子進程兩端都要進行關閉
所以代碼中將管道中寫端關閉了,所以讀端返回的是0.
雖然管道提供了雙向選擇,但是如果我們沒有用到某一端,就把這一端關閉掉
管道同步與互斥特性
當讀寫數據的大小<管道pipe_buf ,是保證操作原子性-----這時操作不可被打斷
互斥:保證對一個臨界資源(公共資源,比如全局變量)同一時間的唯一訪問性(我操作的時候你不能操作)
同步:保證對一個臨界資源訪問的時序可控性(我操作完了你才能操作)
|
管道符就是匿名訪問
int main(){
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0){
perror("pipe error");
return -1;
}
int pid1 = fork();
if(pid1 == 0){
close(pipefd[0]);//關閉從管道讀數據
dup2(pipefd[1],1);//將文件描述符表中 向管道寫數據 替換 標準輸出
execlp("ps","ps","-ef",NULL);//程序替換
}
int pid2 = fork();
if(pid2 == 0){
close(pipefd[1]);
dup2(pipefd[0],0);
execlp("grep","grep","ssh",NULL);
}
close(pipefd[0]);//不用的時候將讀端和寫端都關閉
close(pipefd[1]);
waitpid(pid1,NULL,0);
waitpid(pid2,NULL,0);
return 0;
}
grep讀數據時不知道自己需要多少數據,過濾之後再次讀取。
代碼用圖示來解答一下
命名管道(FIFO)
FIFO是一種文件類型,可以用於任意進程間通信。
可見於文件系統,因爲創建命名管道會隨之在文件系統中創建一個命名管道文件
類似於在進程中使用文件來傳輸數據,只不過FIFO類型文件同時具有管道的特性
因爲所有的進程都能夠通過打開管道文件,進而獲取管道的操作句柄,因此命名管道可以用於同一主機上任意進程間通信
int mkfifo(const char *pathname, mode_t mode);
pathname:管道文件名
mode:創建權限 0664
fifo_read.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main(){
char *file = "./test.fifo";
umask(0);
int ret = mkfifo(file, 0664);
if(ret < 0){
if(errno != EEXIST){
perror("mkfifo errno");
return -1;
}
}
printf("open file\n");
int fd = open(file, O_RDWR);
if(fd < 0){
perror("open error");
return -1;
}
printf("open success!!\n");
while(1){
char buf[1024] = {0};
int ret = read(fd, buf, 1023);
if(ret > 0){
printf("read buf:[%s]\n", buf);
}else if(ret == 0){
printf("write closed~~~\n");
}else{
perror("read error");
}
printf("---------\n");
}
close(fd);
return 0;
}
fifo_write.c
int main(){
char *file = "./test.fifo";
umask(0);
int ret = mkfifo(file,0664);
if(ret < 0){
if(ret != EEXIST){
perror("mkfifo error");
return -1;
}
}
printf("open file\n");
int fd = open(file, O_WRONLY);
if(fd < 0){
perror("open error");
return -1;
}
printf("open success!!\n");
while(1){
char buf[1024] = {0};
scanf("%s", buf);
write(fd, buf, strlen(buf));
}
return 0;
}
這有點類似於服務器–客戶端之間建立連接
fifo_write類似於客服端發送一個請求 fifo_read類似於服務器返回一個請求
命名管道利用了文件系統創建文件進行通信。當write寫入了數據,將數據從自己的buf寫到test.fifo文件中,之後read在將數據從test.fifo中讀取到自己的buf中,最後輸出。
命名管道的讀寫特性
若管道沒有被以寫的方式打開,這時如果只讀打開則會阻塞,直到文件被以寫的方式打開
若管道沒有被以讀的方式打開,這時如果只寫打開則會阻塞,直到文件被以讀的方式打開
若管道以讀寫的方式打開,則不會阻塞
匿名管道和命名管道的區別
匿名管道:速度慢,容量有限,只有父進程進程能通訊
命名管道:任何進程間都能通訊,但速度慢
共享內存
共享內存的定義:在物理上開闢一塊空間,內存直接映射到虛擬內存中,如果一塊內存被多個進程映射,那麼多個進程訪問同一塊內存,則可以實現通信。 是最快的進程間通信。因爲相較於其他進程間通信方式(將數據從用戶態拷貝到內核態,用的時候,從內核態拷貝到用戶態),共享內存直接將一塊內存映射到用戶空間,用戶可以直接通過地址對內存進行操作,並反饋到其他進程,少了兩步數據拷貝的過程。
共享內存使用流程
1、創建/打開共享內存
int shmget(key_t key, size_t size, int shmflg);
key: 共享內存標識符
size: 共享內存大小
shmflg:打開方式/創建權限
IPC_CREAT 共享內存不存在則創建
IPC_EXCL 與IPC_CREAT同用,若存在則報錯,不存在則創建
返回值:操作句柄shmid 失敗:-1
key_t ftok(const char *pathname, int proj_id);
pathname: 文件名
proj_id: 數字
通過文件的 inode節點號 和 proj_id 共同得出一個key值
2、將共享內存映射到虛擬地址空間(建立映射關係)
void shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 創建共享內存返回的操作句柄
shmaddr:用於指定映射在虛擬空間的首地址 通常置NULL
shmflg:0----可讀可寫
返回值:映射首地址(通過這個地址對共享內存進行操作) 失敗:(void*)-1
3、對共享內存進行基本的內存操作,memcpy
4、解除映射關係 shmdt
int shmdt(const void *shmaddr)
shamddr: 映射返回的首地址
5、刪除共享內存 shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid: 操作句柄
cmd: IPC_RMID 刪除共享內存
buf: 設置或者獲取共享內存信息,用不着置NULL
共享內存並不是立即刪除的,只是拒絕後續映射連接,當共享內存
映射連接數爲0時,則刪除共享內存
shm_read.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE 4096
int main(){
int shmid;
//1、創建共享內存
shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);
if(shmid < 0){
perror("shmget error");
return -1;
}
//2、將共享內存映射到虛擬地址空間
char *shm_start = (char*)shmat(shmid, NULL, 0);
if(shm_start == (void*)-1){
perror("shmat error");
return -1;
}
while(1){
printf("%s\n", shm_start);
sleep(1);
}
//4、解除映射
shmdt(shm_start);
//5、刪除共享內存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
shm_write.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE 4096
int main(){
int shmid;
//1、創建共享內存
shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);
if(shmid < 0){
perror("shmget error");
return -1;
}
//2、將共享內存映射到虛擬地址空間
char *shm_start = (char*)shmat(shmid, NULL, 0);
if(shm_start == (void*)-1){
perror("shmat error");
return -1;
}
int i = 0;
while(1){
sprintf(shm_start, "明天又是可以學習的一天!!!+%d\n",i++);
sleep(1);
}
//4、解除映射
shmdt(shm_start);
//5、刪除共享內存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
共享內存雙方都可以修改
共享內存沒有同步與互斥
刪除一塊共享內存,並不會立即刪除,而是判斷映射連接數,若爲0則刪除,不爲0則拒絕後續連接,直到爲0刪除
消息隊列
消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
消息隊列的建立過程爲
創建消息隊列---->添加數據節點---->獲取數據節點---->刪除
msgget---->msgsnd---->msgrcv(接收數據)---->msgctl
消息隊列傳輸的是有類型的數據塊,用戶可以根據自己的需要選擇性的獲取某些數據類型
信號量
內核中的一個計數器----具有等待隊列(PCB等待隊列),具有等待和喚醒功能
用於資源計數,若計數小於等於0,表示沒有資源,則需要等待
若計數大於0,表示有資源,則可以獲取資源,然後計數-1
如果放置了資源,則計數+1,並且喚醒等待的進程
實現進程間的同步和互斥(資源計數爲0或1的時候才具有互斥)
小結
消息隊列和信號量現在的使用不是特別多,瞭解一下就可以。重點還是共享內存和管道的學習。
在代碼過程中,我們應該可以感覺到,這種通信方式有點類似於服務器和客戶端之間的處理過程。但是具體的實現是不同的。
匿名管道是通過對讀端和寫端的關閉和開啓,在buf緩存區對數據進行拷貝和使用。
而命名管道是通過文件系統的打開和關閉,將數據進行讀寫。讀寫必須同時打開,否則另一端會被阻塞。
而共享內存是在同一個地址映射的一塊虛擬地址被多個進程訪問,這時也就是多個進程同時訪問同一個內存。此時共享內存直接映射一塊內存到用戶空間,用戶直接通過地址對內存進行操作,並反饋到其他進程。