一.進程通信的概念
爲什麼要進程通信?
進程通信:顧名思義,應該是兩個進程間進行通信。
進程之間具有獨立性,每個進程都有自己的虛擬地址空間,進程A不知道進程B的虛擬地址空間的數據內容(類似於一個人不知道另一個人腦子裏在想啥)
二.進程間通信方式的分類
進程間通信方式的共同點:進程間需要“介質”—兩個進程都能訪問到的公共資源。
常見的通信方式:
-
a.文件(最簡單的方法)
假如用vim打開一個test.c文件,這時候會自動產生一個以文件名結尾的.swap文件,用於保存數據。
當正常關閉時,此文件會被刪除。當文件非正常關閉時(比如編輯代碼時突然斷網),如果此時再次通過vim打開該文件,就會提示存在.swap文件,此時你可以通過它來恢復文件:vim -r filename.c 恢復以後把.swap文件刪掉,就不會再出現一堆提示了。所以該文件存在就是爲了進行進程中的通信。 -
b.管道
1.管道定義:一個進程連接到另一個進程的數據流。
ps aux | grep test
,將前一個進程(ps)的輸出作爲後一個進程(grep)的輸入兩進程間通過管道進行通信。
ps aux | -l
:wc指word count,-l指行數,將ps aux進程的標準輸出作爲wc -l的標準輸入。
2.管道分類
匿名管道和命名管道。
匿名管道
管道是在內核中的一塊內存(構成了一個隊列),使用一對文件描述符來操作內核中的內存。當前的文件描述符就是內存的句柄,此時讀文件描述符—從隊列中取數據,寫文件描述符—往隊列中插數據。Linux中,一切皆文件,所以可以藉助管理文件的思想來管理內存。
1.匿名管道特點
- 使用完需要及時關閉文件描述符close();
- 匿名管道必須用於具有親緣關係之間的進程(父子進程,爺孫進程,兄弟進程),兩個操作不同管道的進程之間無法行通信。
- 管道提供流式服務。
- 管道的生命週期隨進程,所有引用管道的進程退出,管道就釋放。
- 內核會對管道進行同步和互斥。
- 管道是半雙工的,數據只能向一個方向流動。需要兩個進程之間雙向通信時,就需要兩個管道。
2.操作管道的函數
#include <unistd.h>
//函數原型
int pipe(int fd[2]);
//輸出型參數 fd:文件描述符數組,其中fd[0]表示讀端(讀數據), fd[1]表示寫端(寫數據)
//返回值:返回值<0時,pipe失敗,否則成功
下面我就來使用pipe函數創建一對文件描述符,通過這一對問價描述符來操作內核中的管道。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//pipe
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe");
return 1;
}
//write words to pipeline
char buf_write[1024]="hello pipe!";
write(fd[1],buf_write,strlen(buf_write));
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);//a place for '\0'
buf_read[n]='\0';
printf("%s\n",buf_read);
//close fd
close(fd[0]);
close(fd[1]);
return 0;
}
運行後看結果:輸出結果是從fd[0]中讀取出來的,它對應的就是通過write寫進去的管道中的數據。
熟悉了一下pipe函數的用法後,下面需要進行兩個進程間的通信。
原理:當fork出來一個子進程時,會複製父進程的PCB,由於PCB中包括文件描述符表,所以文件描述符表也會被複制一份,子進程也能訪問到相同的管道這個時候就可以實現一個進程往管道中寫數據,一個進程從管道中讀數據。(父子進程讀寫都沒什麼區別)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//pipe
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe");
return 1;
}
pid_t ret1=fork();
if(ret1>0)
{
//father 讀數據
char buf_write[1024]="hell0,father!\n";
write(fd[1],buf_write,strlen(buf_write));
}
else if (ret==0)
{
//child 寫數據
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);
buf_read[n]='\0';
printf("child read:%s\n",buf_read);
}
else{
perror("fork");
}
//close fd
close(fd[0]);
close(fd[1]);
return 0;
}
運行結果:子進程確實打印了父進程寫入管道的數據。父進程會向管道中寫入數據,子進程就會從管道中讀數據
如果嘗試父進程寫,父子進程同時讀?
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//pipe
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe");
return 1;
}
pid_t ret1=fork();
if(ret1>0)
{
//father 讀數據
char buf_write[1024]="hell0,father!\n";
write(fd[1],buf_write,strlen(buf_write));
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);
buf_read[n]='\0';
printf("father read:%s\n",buf_read);
}
else if (ret==0)
{
//child 寫數據
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);
buf_read[n]='\0';
printf("child read:%s\n",buf_read);
}
else{
perror("fork");
}
//close fd
close(fd[0]);
close(fd[1]);
return 0;
}
結果是父進程先讀到數據。
假如我在寫數據的下面加上一個sleep(1),即延遲父進程讀數據過程。再次編譯執行後,結果就是子進程先讀到數據,並且好像阻塞住了。
那麼關於父子進程誰先讀到數據,取決於誰的read函數先執行。
管道內置的“同步互斥機制”限制了:
- 不會出現兩個管道一個讀一半數據的錯亂情況。
- 如果管道中的數據一旦被讀,就相當於出隊列,這個時候管道就爲空,如果管道爲空,如果有多個進程嘗試來讀數據,都會讀不到,就會在read函數處阻塞。
- 如果管道滿了,就會在write函數處阻塞。
這就可以解釋上面運行結果的阻塞了,先打開一個新的會話窗口,用ps aux 來查看一下當前的進程。再通過gdb attach+進程號來調試正在運行的進程,看父進程是否在read函數處阻塞。
進入調試後敲bt,查看調用棧,可以看到有兩行,第一行的這個函數就是read函數,證明當前父進程就是阻塞在read函數處。
接下來再來看看在什麼情況下管道會滿,我現在嘗試一直網管道寫數據,只寫不讀。
int count=0;
while(1)
{
write(fd[1],"a",1);
printf("count:%d\n",count);
count++;
}
可以看到,管道最大容量是65535。此時如果像上面方法一樣再用gdb attach調試當前進程,就可以證明,管道寫滿後,就會在write函數處阻塞。
爲什麼pipe要放在fork的上面?
命名管道
命令行創建語句:mkfifo filename
下面就來嘗試一下使用該語句創建一個命名管道,可以看到一種新的文件類型p類型。
下面就來嘗試使用該命名管道進行通信。將讀數據和寫數據放在兩個可執行程序中,對應兩個進程,一個嘗試讀取,另一個嘗試寫入。
//read
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//操作命名管道,與文件一樣
//1.先打開命名管道,只讀
int fd =open("./myfifo",O_RDONLY);
if(fd<0)
{
perror("read open");
return 1;
}
//2.讀數據
while(1)
{
char buf[1024]={0};
ssize_t n=read(fd,buf,sizeof(buf)-1);
if(n<0)
{
perror("read");
return 1;
}
if(n==0)//所有寫端關閉,讀段已經結束
{
printf("read over\n");
return 0;
}
buf[n]='\0';
printf("readbuf:%s\n",buf);
}
close(fd);
return 0;
}
//write
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//先打開管道(文件)
int fd=open("./myfifo",O_WRONLY);
if(fd<0)
{
perror("write open");
return 1;
}
//寫數據
while(1)
{
//提示用戶輸入一個數據
printf("enter>:");
fflush(stdout);
char buf[1024]={0};
ssize_t n=read(0,buf,sizeof(buf)-1);//0--是stdin的文件描述符
if(n<0)
{
perror("write");
return 1;
}
buf[n]='\0';
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
兩個代碼編譯後,如果先執行./fiforeadtest,則會阻塞在open處,因爲此時該管道只有一個人按read方式打開,沒有人按write方式打開,那麼read就不能打開這個文件。
執行./fifowritetest後(但不輸入寫入內容時),會阻塞在read函數處,因爲管道中爲空。
只有在執行./fifowritetest且輸入寫入內容後,read函數才能讀出內容。
那麼這個時候,每寫入一個字符串,就會顯示該字符串到顯示屏,讓我想起了聊天小窗口。
匿名管道和命名管道的區別
- 匿名管道只限於具有親緣關係之間的進程(父子進程,爺孫進程,兄弟進程),兩個操作不同管道的進程之間無法行通信。而對於命名管道,任何的多個進程之間都能通信。
其餘的部分兩種管道都相同,要注意用mkfifo創建出來的 myfifo這個文件僅僅是管道的一個入口,管道的本體依然是內核中的一個內存。所談到的生命週期是圍繞着內核中的內存來討論的。
- c.System V 進程間通信
System V 共享內存
相比於管道來說,共享內存要更加高效,直接訪問內存即可完成通信。而管道涉及到用戶態和內核態之間的數據相互拷貝,效率比較低。共享 內存的生命週期隨內核,共享內存會一直存在到手動釋放或者系統重啓。
共享內存的使用方式
1.在內核中創建出共享內存的對象,並打開(這裏創建和打卡的都是物理內存)
2.多個進程附加到這個共享內存對象上(shmat 使得共享內存和進程之間建立聯繫–虛擬地址空間與物理內存之間取得映射)
3.直接讀寫使用這個共享內存
1.在內核中創建出共享內存的對象
//創建對象
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
//key--身份標識(鍵值對) 可找到一個具體的共享內存對象
//shmflg--選項 對應兩個宏:IPC_CREAT(已經存在直接打開,不存在就創建)|IPC_EXCL(已經存在,就會返失敗)位圖方式表示。
//獲取key
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//pathname--路徑(必須是存在的路徑)
//proj_id--一個隨機的數字
//只要這兩個參數相同,得到的key就會永遠相同
//舉例
key_t key=ftok(".",0x1);
if(key==-1)
{
perror("ftok");
return 1;
}
printf("%d\n",key);
下面我們就來創建對象
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
int main()
{
//獲取key
key_t key=ftok(".",0x3);
if(key==-1)
{
perror("ftok");
return 1;
}
printf("key:%d\n",key);
//創建對象
int ret=shmget(key,1024,IPC_CREAT | IPC_EXCL| 0666);//0666--權限
if(ret<0)
{
perror("shmget");
return 1;
}
printf("ret:%d\n",ret);
return 0;
}
創建結果:
驗證:用ipcs -m 查看當前系統上的共享內存,可以看見,ret爲622597的共享內存已經創建。(一旦電腦關機,共享內存就會結束)
2.多個進程附加到這個共享內存對象上
因爲考慮到代碼重複,我將之前創建對象的代碼封裝了一下,寫成一個方法放在一個myshm.h的文件中,調用這個方法時,引入這個頭文件即可。
//myshm.h
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
int CreateShm()
{
key_t key=ftok(".",0x3);
if(key==-1)
{
perror("ftok");
return 1;
}
printf("key:%d\n",key);
int ret=shmget(key,1024,IPC_CREAT | IPC_EXCL| 0666);//0666--權限
if(ret<0)
{
perror("shmget");
return 1;
}
printf("ret:%d\n",ret);
return ret;
}
修改後的createmem.c:
分別創建一個reader.c和一個writer.c對應一會通過共享內存進行通信的兩個進程。
將進程附加到內存上時使用的函數(類似於malloc的使用):
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
//shmid--共享內存的句柄 shmget的返回值,
//shmaddr--指定與物理內存映射的虛擬地址空間,一般傳空指針,系統說了算
//shmflg--flag一般默認爲0
//返回值:void*
int shmdt(const void *shmaddr);//解出物理內存與虛擬地址空間的映射關係
//writer
#include "myshm.h"
#include <string.h>
int main()
{
//往共享內存中寫數據
//1.創建共享內存
int shmid=CreateShm();
//2.附加進程到共享內存上
char*p=(char*)shmat(shmid,NULL,0);
//3.使用該空間
strcpy(p,"hello sharespace!");
return 0;
}
//reader
#include "myshm.h"
int main()
{
//從共享內存中讀數據
//1.創建/打開共享內存
int shmid=CreateShm();
//2.附加到共享內存上
char* p=(char*)shmat(shmid,NULL,0);
//3.直接使用
printf("reader:%s\n",p);
return 0;
}
最後編譯reader.c和write.c,得到結果。
System V 消息隊列
廣義消息隊列:
消息隊列也是一個隊列,每個元素都帶有一個類型。每次按照指定類型(業務場景下的類型)先進先出,找對應指定類型的第一個。
System V 消息隊列僅限於進程間通信。很多客戶端服務器會共用消息隊列服務器集羣(中間件)。
System V 信號量
是一個計數器(描述可用資源的個數),主要用於進行進程間的同步和互斥。每次有進程想申請一個可用資源時,計數器-1(P操作),有進程想釋放一個可用資源時,計數器+1(V操作).
如果計數器爲0,還有進程想申請資源,則會有兩種可能:1.進程掛起等待。2.進程放棄申請資源
- d.POSIX 進程間通信
- e.網絡(最重要最主要的進程間通信方式)