實驗八:進程間通訊

  • 2020 年 5 月 26 日
  • 筆記

實驗八:進程間通訊

項目 內容
這個作業屬於哪個課程 班級課程的主頁鏈接
這個作業的要求在哪裡 班級課程的要求鏈接
學號-姓名 17043113—胡斌
作業要求 1.了解進程通訊的常用方式; 2.掌握管道、消息隊列、訊號量、共享記憶體實現進程間通訊的方法

1.舉例說明使用匿名管道進行進程通訊

匿名管道:

當進程使用 pipe 函數,就可以打開位於內核中的這個特殊「文件」。同時 pipe 函數會返回兩個描述 符,一個用於讀,一個用於寫。如果你使用 fstat 函數來測試該描述符,可以發現此文件類型為 FIFO 。而無名管道的無名,指的就是這個虛幻的「文件」,它沒有名字

 

pipe 函數打開的文件描述符是通過參數(數組)傳遞出來的,而返回值表示打開成功(0)或失敗 (-1)。 它的參數是一個大小為 2 的數組。此數組的第 0 個元素用來接收以讀的方式打開的描述符,而第 1 個元 素用來接收以寫的方式打開的描述符。也就是說, pipefd[0] 是用於讀的,而 pipefd[1] 是用於寫 的。 打開了文件描述符後,就可以使用 read(pipefd[0]) 和 write(pipefd[1]) 來讀寫數據了。

 

注意事項 :這兩個分別用於讀寫的描述符必須同時打開才行,否則會出問題。

 

如果關閉讀 ( close(pipefd[0]) ) 端保留寫端,繼續向寫端 ( pipefd[1] ) 端寫數據( write 函數)的進程會收到 SIGPIPE 訊號。 如果關閉寫 ( close(pipefd[1]) ) 端保留讀端,繼續向讀端 ( pipefd[0] ) 端讀數據( read 函數),read 函數會返回 0.

例題:父進程 fork 出一個子進程,通過無名管道向子進程發送字元,子進程收到數據後將字元串中的 小寫字元轉換成大寫並輸出。

 

 

2.舉例說明使用 mkfifo 命令創建命名管道以及簡單演示管道如何工作。

命名管道

1.通過命令 mkfifo 創建管道

man mkfifo

2.通過函數 mkfifo(3) 創建管道

man 3 mkfifo

FIFO文件的特性 a) 查看文件屬性 當使用 mkfifo 創建 hello 文件後,查看文件資訊如下:

 

某些版本的系統在 hello 文件後面還會跟著個 | 符號,像這樣 hello| b) 使用 cat 命令列印 hello 文件內容

 

可以看到cat已經被堵塞了。開啟另一個終端,執行

 

然後你會看到被阻塞的 cat 又繼續執行完畢,在螢幕列印 「hello world」 。 如果你反過來執行上面兩個命令,會發現先執行的那個總是被阻塞。

 

c) fifo 文件特性 根據前面兩個實驗,可以總結: (1)文件屬性前面標註的文件類型是 p ,代表管道 (2)文件大小是 0 (3)fifo 文件需要有讀寫兩端,否則在打開 fifo 文件時會阻塞 如果在 open 的時候,使用了非阻塞方式,肯定是不會阻塞的。 特別地,如果以非阻塞寫的方式 open ,同時沒有進程為該文件以讀的方式打開,會導致 open 返回錯誤(-1),同時 errno 設置成ENXIO.

3.編寫兩個程式使用第2題中創建的管道進行通訊。

例題:編寫兩個程式,分別是發送端 pipe_send 和接收端面 pipe_recv 。程式 pipe_send 從標準 輸入接收字元,並發送到程式 pipe_recv ,同時 pipe_recv 將接收到的字元列印到螢幕。

 

分別開啟兩個終端,分別運行pipe_send和pipe_recv:

 

現在兩個終端都處於阻塞狀態,在運行pipe_send的終端輸入數據,然後就可以在運行的pipe_recv的終端看到相應的輸出:

 

使用組合按鍵結束上述進程。

4.編寫兩個程式分別通過指定的鍵值創建 IPC 內核對象,以及獲取該指定鍵值的 IPC 內核對象。

IPC 內核對象

每個 IPC 內核對象都是位於內核空間中的一個結構體。具體的對於共享記憶體、消息隊列和訊號量,他們在內核空間中都有對應的結構體來描述。 當你使用 get 後綴創建內核對象時,內核中就會為它開闢一塊記憶體保存它。只要你不顯式刪除該內核對象,它就永遠位於內核空間中,除非你關機重啟。

 

進程空間的高 1G 空間( 3GB-4GB )是內核空間,該空間中保存了所有的 IPC 內核對象。 上圖給出不同的 IPC 內核對象在記憶體中的布局(以數組的方式),實際作業系統的實現並不一定是數組,也可能是鏈表或者其它數據結構等等。 每個內核對象都有自己的 id 號(數組的索引)。此 id 號可以被用戶空間使用。所以只要用戶空間知道了內核對象的 id 號,就可以操控內核對象了。 為了能夠得到內核對象的 id 號,用戶程式需要提供鍵值—— key ,它的類型是 key_t ( int 整型)。 系統調用函數( shmget , msgget 和 semget )根據 key ,就可以查找到你需要的內核 id號。 在內核創建完成後,就已經有一個唯一的 key 值和它綁定起來了,也就是說 key 和內核對象是一 一對應的關係。(key = 0為特殊的鍵,它不能用來查找內核對象)

創建 IPC 內核對象

man 2 shmget

man 2 msgget

man 2 semget

在創建 IPC 內核對象時,用戶程式一定需要提供 key 值才行。 實際上,創建 IPC 內核對象的函數和獲取內核對象 id 的函數是一樣的,都是使用 get 後綴函數。 比如在鍵值 0x8888 上創建 ipc 內核對象, 並獲取其 id ,應該像下面這樣:

// 在 0x8888 這個鍵上創建內核對象,許可權為 0644,如果已經存在就返回錯誤。 int id = shmget(0x8888, 4096, IPC_CREAT | IPC_EXCL | 0644); int id = msgget(0x8888, IPC_CREAT | IPC_EXCL | 0644); int id = semget(0x8888, 1, IPC_CREAT | IPC_EXCL | 0644); // 第二個參數表示創建幾個訊號量 例題:程式 ipccreate 用於在指定的鍵值上創建 ipc 內核對象。使用格式為 ./ipccreate ,比如 ./ipccreate 0 0x8888 表示在鍵值 0x8888 上創建共享記憶體。

 

 

獲取 ipc 內核對象

程式 ipcget 用於在指定的鍵值上獲取 ipc 內核對象的 id 號。使用格式為 ./ipcget ,比如./ipcget 0 0x8888 表示獲取鍵值 0x8888 上的共享記憶體 id 號。

 

5.編寫一個程式可以用來創建、刪除內核對象,也可以掛接、卸載共享記憶體,還可以列印、設置內核對象資訊。

共享記憶體

前面已經知道如何創建內核對象,接下來分別了解三種內核對象的操作:

man 2 shmop

man 2 shmctl

例題:編寫一個程式 shmctl 可以用來創建、刪除內核對象,也可以掛接、卸載共享記憶體,還可以打 印、設置內核對象資訊。具體使用方法具體見下面的說明:

./shmctl -c : 創建內核對象。 ./shmctl -d : 刪除內核對象。 ./shmctl -v : 顯示內核對象資訊。 ./shmctl -s : 設置內核對象(將許可權設置為 0600 )。 ./shmctl -a : 掛接和卸載共享記憶體(掛接 5 秒後,再執行 shmdt ,然後退出)。

 

 

先在另一個終端執行 ./shmctl -a ,然後在當前終端執行 ./shmctl -v

 

 

先在另一個終端執行 ./shmctl -a ,運行結束後,然後在當前終端執行 ./shmctl -v

 

6.編寫兩程式分別用於向消息隊列發送數據和接收數據。 msg_send 程式定義了一個結構體 Msg ,消息正文部分是結構體 Person 。該程式向消息隊列發送了 10 條消息。

消息隊列

消息隊列本質上是位於內核空間的鏈表,鏈表的每個節點都是一條消息。 每一條消息都有自己的消息類型,消息類型用整數來表示, 而且必須大於 0.每種類型的消息都被對應的鏈表所維護, 下圖 展示了內核空間的一個消息隊列:

 

其中數字 1 表示類型為 1 的消息,數字2、3、4 類似。彩色塊表示消息數據,它們被掛在對應類型的鏈表上。 值得注意的是,剛剛說過沒有消息類型為 0 的消息, 實際上,消息類型為 0 的鏈表記錄了所有消息加入隊列的順序,其中紅色箭頭表示消息加入的順序。

消息隊列相關的函數

man 2 msgop

 

 

消息數據格式

無論你是發送還是接收消息,消息的格式都必須按照規範來。簡單的說,它一般長成下面這個樣子:

struct Msg{ long type; // 消息類型。這個是必須的,而且值必須 > 0,這個值被系統使用 // 消息正文,多少位元組隨你而定 // … } 例題:程式 msg_send 和 msg_recv 分別用於向消息隊列發送數據和接收數據。 msg_send 程式定義了 一個結構體 Msg ,消息正文部分是結構體 Person 。該程式向消息隊列發送了 10 條消息。

程式 msg_send 第一次運行完後,內核中的消息隊列大概像下面這樣:

 

msg_recv 程式接收一個參數,表示接收哪種類型的消息。比如 ./msg_recv 4 表示接收類型為 4 的消息,並列印在螢幕。

 

先運行./msg_send,再運行./msg_recv 接收所有消息:

 

接收類型為 4 的消息,這時要重新運行 ./msg_send :

 

接收類型小於等於 3 的所有消息,這是不用再運行 ./msg_send :

 

還有一個函數來操作消息隊列內核對象的

man 2 msgctl

 

7.編寫程式舉例說明訊號量如何操作。

訊號量

設置和獲取訊號量值的函數 semctl :

man 2 semctl

 

 

請求和釋放訊號量 semop

man 2 semop

 

 

8.編寫程式使用訊號量實現父子進程之間的同步,防止父子進程搶奪 CPU 。

例題:使用訊號量實現父子進程之間的同步,防止父子進程搶奪 CPU