(王道408考研操作系统)第二章进程管理-第一节4:进程通信(配合Linux)
文章目錄
- 一:什么是進程通信
- 二:如何實現進程間通信及其分類
- 三:通信方式1-共享存儲(共享內存)
- (1)課本基礎內容
- (2)補充-Linux中的進程通信
- 四:通信方式2-管道
- (1)管道是什么
- (2)匿名管道
- A:讀端和寫端
- B:建立匿名管道的函數
- C:最簡單的進程間通信-演示
- D:管道四大特性
- E:管道的特點
- F:從內核角度理解管道
- G:管道總結
- (3)命名管道
- A:命名管道和匿名管道的區別
- B:如何創建命名管道
- C:演示-管道實現服務端和客戶端的通信
- 五:通信方式3-消息隊列
一:什么是進程通信
“微信想要獲取手機存儲權限”這就是一個典型的進程通信的例子,進程是操作系統分配資源的最基本單位,每個進程擁有各自獨立的地址空間,在正常情況下,它們是互不干擾的,體現了進程的獨立性
那么獨立性體現的是數據的互不干擾,而通信體現的卻是數據的交互,這兩點看起來豈不是“矛盾”的嗎?但是實則不然,我們所說的獨立并不是完全獨立,進程與進程之間也會產生協作關系,這種協作是需要通過通信來完成的
進程間實現通信的目的無外乎以下幾種
- 數據傳輸:一個進程需要將它的數據發送給另外一個進程
- 資源共享:多個進程之間共享資源
- 通知事件:一個進程需要向另一個或另一組進程發送消息,通知發生了什么事件
- 進程控制:有些進程希望控制另一些進程,比如說調試功能
二:如何實現進程間通信及其分類
先不要管這樣那樣的分類方式,想要實現進程間通信,其核心思想就是:用盡一切辦法讓他們看見同一份內存空間
至于這個內存空間是由誰提供的,或者是以什么方式提供的就決定了進程間通信的方式。如果操作系統以文件的方式提供的,就叫做管道;如果操作系統避開文件層,直接從內核構建通信,就是System V IPC
1:管道
- 匿名管道pipe
- 命名管道
2:System V IPC
- System V消息隊列
- System V共享內存
- System V信號量
3:POSIX IPC
- 消息隊列
- 共享內存
- 信號量
- 互斥量
- 條件變量
三:通信方式1-共享存儲(共享內存)
(1)課本基礎內容
前面在進程地址空間的時候,我們知道了每個進程看到的都是虛擬內存,頁表則負責將虛擬內存映射到真實的物理內存處
既然頁表是負責映射的,那么是否可以在物理內存上開辟一片空間,然后通過頁表讓他們都映射到一片內存空間,這樣就符合“看到同一片內存空間”的規則呢?
答案是可以的。這樣的話,兩個進程在進行讀寫時實際操縱的是同一片內存,進程1的讀寫操作就可以讓進程2看到了
課本中所說的基于存儲區的共享,其中區域就在堆區和棧區的中間,這里可以處理通信,也可以存放我們常常聽到過的動態庫等內容
(2)補充-Linux中的進程通信
不管是管道,還是共享內存,進程間通信的本質就是讓他們看見相同的內存資源。大家需要明白的一點是,進程間通信不是嘴上說說那么簡單,想要實現兩個進程看見同一份內存資源,以及看見資源后如何寫入,讀取,同時對于這份內存如何把不同進程關聯上去,如何保證關聯的穩定等等 ,這都是需要去管理的,況且操作系統會存在大量的進程通信,所以對于操作系統,想要管理好進程間通信,一定是先組織,再描述,也就是底層會存在大量與此相關的數據結構
正如描述進程時的task_strct,描述文件時的file struct,描述進程間通信的結構體則是shmid_ds
剩余內容篇幅較長,如有興趣可以移步Linux系統編程28:進程間通信之共享內存和相關通信接口(ftok,shmget,shmctl,shmat,shmdt)
四:通信方式2-管道
(1)管道是什么
管道是UNIX中一種古老的通信方式,管道本質其實是一個文件
如上,命令行who的標準輸出原本是屏幕,但是卻輸出到了管道文件中,發生了重定向,然后wc命令再從以管道文件作為標準輸入,然后輸出到屏幕中
其中who | wc -l這種屬于匿名管道
(2)匿名管道
A:讀端和寫端
從who | wc -l可以看出,who作為一個進程是把內容寫入管道文件,使用的是管道的寫端,wc從管道中讀入數據,使用的是管道的讀端。
所以兩個進程利用管道通信時,一個進程要使用管道的寫端寫入數據,另一個進程則要使用管道的讀端讀入數據,所以管道文件就要用兩個文件描述符進行控制,一個控制讀端,一個控制寫端
所以下面是父進程創建了管道
接著父進程創建子進程
可以發現此時父子進程可以同時對管道進行寫入的和讀取,但是管道只能一端寫入一端讀入,所以要進行調整
B:建立匿名管道的函數
pipe函數用于建立匿名管道,其頭文件是unistd.h
其函數原型為int pipe(int fd[2]),其中fd是一個有兩個元素fd[0],fd[1]的數組,傳入函數pipe后在其內部分別以讀寫的方式打開管道文件,默認情況下,fd[0]和fd[1]會分別獲得文件描述符,其中fd[0]表示讀端,fd[1]表示寫端
模擬實現一下pipe函數可能就是下面這樣的
有很多同學在這里會感到疑惑,因為用于進程間通信的管道文件就只有一個,為什么會有兩個文件描述符呢?(默認是3和4)
其實這一點在之前的基礎IO中我沒有表示特別清楚,以讀方式的打開一個文件,會分配一個描述符(假設是3),然后再以寫方式打開剛才的你文件也會分配一個描述符(假設是4),這里的3和4操作的是一個文件,只不過一個負責讀,一個負責寫
比如下面這個例子就可以說明這個情況
C:最簡單的進程間通信-演示
這里我們可以根據上面讀端和寫端的那個流程,首先調用pipe函數,接著創建子進程
現在的場景就是這樣
接著,我們讓子進程寫入數據到管道,父進程從管道讀取數據。于是關閉子進程的讀端也即是fd[0],關閉父進程的寫端也就是fd[1]
還有一點十分重要:文件描述符數組(pipefd)是創建子進程之前就有的,而且調用pipe函數之后,數組內容就沒有改變過,所以不發生寫時拷貝,所以數組是父子進程共有的。但是files struct是父子進程各自擁有的,對子進程來說close(fd[0]),就相當于抹殺了子進程對該文件的讀權限
為了觀察方便,對父子進程都是用死循環。子進程每隔一秒讀入一段信息this is the data that the child process wrote用來證明子進程寫入了數據;對于父進程則取讀取數據,一旦讀完數據,就輸出the father process got the information,用來證明父進程讀取到了數據
效果如下,這樣就完成了一個最簡單的進程間通信
D:管道四大特性
特性一:如果寫端(這里是子進程)不關閉文件描述符,且不寫入(簡稱為讀端條件不就緒),那么讀端可能會長時間阻塞(當管道有歷史數據時會先讀完,管道為空,且寫端不寫入會長時間堵塞),也就是讀端快,寫端慢
比如,將上面例子中,子進程的睡眠由1秒提升至5秒,就會發現雖然父進程在死循環且沒有睡眠的情況下,也會和子進程同步
特性二:當寫端在寫入時,寫端條件不就緒(比如管道已經滿了),寫端就要被阻塞,也就是寫端快,讀端慢
比如,修改上面的例子如下,在子進程中使用cout查看子進程寫入管道的次數,然后父進程每隔1s讀取一次
#include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h>int main() {int pipefd[2]={0};pipe(pipefd);pid_t id=fork();if(id==0)//child{close(pipefd[0]);const char* msg="This is the data that the child process wrote";int cout=0;//統計次數while(1){write(pipefd[1],msg,strlen(msg));printf("The number of times the child process writes:%d\n",cout++);}}else//father{close(pipefd[1]);char buffer[64];while(1){ssize_t ret=read(pipefd[0],buffer,sizeof(buffer)-1);if(ret>0){buffer[ret]='\0';printf("The father process got the information:%s\n",buffer);sleep(1);}}}return 0; }效果如下,寫端瞬間將管道充滿,然后讀端慢慢的從管道中讀數據
特性三:如果寫端關閉文件描述符,那么讀端當讀完管道內容后,或讀到文件結尾(此時read的返回值是0)
如下,當子進程讀入上次后,關閉子進程的寫端,跳出循環退出子進程,父進程仍舊每秒從管道中讀取數據一次,并且輸出read接口的返回值
#include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h>int main() {int pipefd[2]={0};pipe(pipefd);pid_t id=fork();if(id==0)//child{close(pipefd[0]);const char* msg="This is the data that the child process wrote";int cout=0;while(1){write(pipefd[1],msg,strlen(msg));printf("The number of times the child process writes:%d\n",cout++);if(cout==10){close(pipefd[1]);//讀10次后關閉寫端break;}}exit(2);}else//father{close(pipefd[1]);char buffer[64];while(1){ssize_t ret=read(pipefd[0],buffer,sizeof(buffer)-1);if(ret>0){buffer[ret]='\0';printf("The father process got the information:%s\n",buffer);sleep(1);}printf("the father process read the end of file which the return value of 'read' is %ld\n",ret);//read接口的返回值}}return 0; }
刷屏太快,將其重定向到文件中。
對比特性一,特性一中是寫端不關閉文件描述符還寫的特別慢,因此讀端也被牽制住,造成讀端堵塞。而當寫端文件描述符關閉之后,這個管道文件唯一的輸入來源就切斷了,因此如果不給其結束標記,那么就會造成讀端永久阻塞
特性四:如果讀端關閉文件描述符,那么寫端有可能被操作系統結束掉
如下,讓子進程不斷寫入數據,讓父進程讀取5次數據后,就關閉讀端,使用如下腳本觀察進程
while :; do ps axj | grep test.exe | grep -v grep; echo "#######################";sleep 1;done #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h>int main() {int pipefd[2]={0};pipe(pipefd);pid_t id=fork();if(id==0)//child{close(pipefd[0]);const char* msg="This is the data that the child process wrote";int cout=0;while(1){write(pipefd[1],msg,strlen(msg));printf("The number of times the child process writes:%d\n",cout++);} }else//father{close(pipefd[1]);char buffer[64];int cout=0;while(1){ssize_t ret=read(pipefd[0],buffer,sizeof(buffer)-1);if(ret>0){buffer[ret]='\0';printf("The father process got the information:%s\n",buffer);sleep(1);cout++;}if(cout==5){close(pipefd[0]);//讀5次后就關閉讀端}}}return 0; }可以很明顯當父進程讀5次后,子進程退出,變為了僵尸狀態
當讀端關閉之后,就沒有進程讀取數據了,那么寫入的操作就變成了一種無用操作,所以操作系統發現了這種浪費資源的行為后,就發送了13號信號,結束了子進程
根據前面的進程等待的知識,我們也可以獲取退出信號,正是SIGPIPE
E:管道的特點
F:從內核角度理解管道
Linux下一切皆文件
如下便是進程打開的文件的file結構體,其中有一個結構體path
跳轉過去,其中的dentry表示該file所在的目錄結構體
跳轉過去,當找到其所在的目錄后,其結構體內就存儲了目錄的inode
根據目錄的inode可以找到目錄的數據塊,而之前說過目錄中存儲的就是文件名和inodei·映射關系,于是就可找到該文件file的inode,如下
而inode中有一個union,它是迎來標識文件類型的,可以發現第一個便是管道文件
G:管道總結
至此我們便可以從更深的層次中理解管道的本質。sleep 1000 | sleep 2000,分別是兩個進程,它們的父進程均是bash,所以bash創建了管道,然后關閉了它對管道的通信,這兩個sleep命令則利用管道進行通信
who | wc -l,bash創建了管道,who和wc利用管道通信,who發生輸出了重定向,將輸出重定向的管道文件中,wc發生了輸入重定向,將輸入來源從鍵盤更改為管道文件,Linux一切皆文件,這就管道的本質
(3)命名管道
A:命名管道和匿名管道的區別
前面說過,匿名管道的限制就是只能在具有共同祖先(具有親緣關系)的進程間通信,而不適合與毫無相干的兩個進程
如果我們想在兩個不相干的進程之間進行通信,可以使用FIFO文件完成,也被稱為命名管道,命名管道實際是一種類型為“p”的文件
B:如何創建命名管道
匿名管道由pipe函數創建并打開,命名管道則有mkfifo函數創建
命名管道可以從命令行上創建
也可以從程序中創建,其函數原型為
int mkfifo(const char* filename,modet_t mode); //filename是創建管道文件的路徑+文件名 //mode是權限值C:演示-管道實現服務端和客戶端的通信
如下我將虛擬機中的centos系統作為服務端,在其上創建一個文件叫做server.c,服務端用來讀取數據。
在Windows中,以Windows作為客戶端,客戶端用來寫入數據,利用xshell遠程登錄主機,然后創建一個文件client.c。這就像xshell是QQ窗口,我像Linux主機,也就是騰訊服務器發送消息,然后服務端回傳回來。雖然不是特別準確,但是足以說明命名管道在·的作用
Makefile如下
.PHONY:all all:client.exe server.exe client.exe:client.cgcc -o $@ $^ server.exe:server.cgcc -o $@ $^.PHONY:clean clean:rm client.exe server.exe fifo
首先編寫服務端代碼,服務端中利用mkfifo創建管道文件,然后打開這個管道文件,不斷讀取數據
在客戶端則直接打開管道文件,然后寫入數據
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>int main() {int fd=open("fifo",O_WRONLY);//直接打開管道文件if(fd>=0){ char buffer[64];//從鍵盤讀入數據到這個緩沖區while(1){ printf("客戶端-請輸入消息:");ssize_t ret=read(0,buffer,sizeof(buffer)-1);//從鍵盤讀入數據if(ret>0){ buffer[ret]='\0';write(fd,buffer,ret);//讀入ret個數據就向管道中寫入ret個數據}}} }現在,在虛擬機中運行服務端,然后在xshell中運行客戶端,然后客戶端輸入數據,服務端就會接受到,客戶端下線,服務端也會下線
這里還有一個非常有趣的點:那個fifo文件是0個字節,自始至終它都是一個字節
這表明它們之間通信時,并沒有直接在這個文件上進行IO操作,因為如果進行IO操作其實代價就太大了。這里的fifo其實僅僅起到了一種標志的作用,它的底層其實和匿名管道是差不多的
上面演示的是調用系統調用接口mkfifo進行操作,而mkfifo其實也是一個命令,也就是直接可以創建管道完成通信
然后在客戶端輸入一段腳本,將一段文字不斷輸入到管道中,接著在服務端不斷讀取
五:通信方式3-消息隊列
在消息傳遞系統中,進程的數據交換是以格式化的**信息(Message)**為單位的。最典型的例子就是計網中的報文,如TCP/IP報文
進程通過系統提供的發送消息和接受消息兩個原語進行數據交換
總結
以上是生活随笔為你收集整理的(王道408考研操作系统)第二章进程管理-第一节4:进程通信(配合Linux)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Navicat(连接) -1之SSL 设
- 下一篇: linux 多线程安全定时器