#實驗描述:
由父進程創建一個管道,然後再創建3個子進程,並由這三個子進程利用管道與父進程之間進行通信:子進程發送信息,父進程等三個子進程全部發完消息後再接收信息。通信的具體內容可根據自己的需要隨意設計,要求能試驗阻塞型讀寫過程中的各種情況,測試管道的默認大小,並且要求利用Posix信號量機制實現進程間對管道的互斥訪問。運行程序,觀察各種情況下,進程實際讀寫的字節數以及進程阻塞喚醒的情況。
實驗目的:通過Linux管道通信機制、消息隊列通信機制的使用,加深對不同類型的進程通信方式的理解。
根據實驗要求可知,這裏直接選用無名管道即可,實驗要求有:
- 試驗阻塞型讀寫過程中的各種情況
- 測試管道的默認大小
- 利用 Posix 信號量機制實現進程間對管道的互斥訪問
#實驗必知
一、什麼是管道
如果你使用過Linux的命令,那麼對於管道這個名詞你一定不會感覺到陌生,因爲我們通常通過符號“|"來使用管道,但是管理的真正定義是什麼呢?管道是一個進程連接數據流到另一個進程的通道,它通常是用作把一個進程的輸出通過管道連接到另一個進程的輸入。
舉個例子,在shell中輸入命令:ls -l | grep string,我們知道ls命令(其實也是一個進程)會把當前目錄中的文件都列出來,但是它不會直接輸出,而是把本來要輸出到屏幕上的數據通過管道輸出到grep這個進程中,作爲grep這個進程的輸入,然後這個進程對輸入的信息進行篩選,把存在string的信息的字符串(以行爲單位)打印在屏幕上。
管道是一個文件,用於連接兩個進程以實現進程通信。管道是半雙工的,即同一時間同一進程只能讀取或者寫入。管道又分爲有名管道和無名管道兩種,無名管道存在於高速緩存 cache 中,用於有親緣關係的父子進程或兄弟進程之間的通信,有名管道存在於磁盤中,是看得見摸得着的真實文件,只要知道路徑名就可以調用,所以它可以用於任意進程之間的通信。
前面提到管道是一個文件,所以不論是有名管道還是無名管道,它們寫入或讀取的方式都是一樣的 —— 使用 write 進行寫入,使用 read 進行讀取。
二、技術信號量,二值信號量
https://www.cnblogs.com/Liu-Jing/p/7435699.html
無名管道創建:
無名管道由pipe()函數創建:
int pipe (int filedis[2]);
返回值:若成功則返回0;否則返回-1;錯誤原因從存於error中
當一個管道建立時,它會創建兩個文件描述符:filedis[0]用於讀管道(管道頭部),filedis[1]用於寫管道(管道尾部)。
#源代碼:
/*
* @file main.c
* @author Arcana
* @date 2018.11.12
* @brief Children process communicate with parent by pipe.
*/
#include "errno.h"
#include "fcntl.h"
#include "semaphore.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "sys/ipc.h"
#include "sys/sem.h"
#include "sys/types.h"
#include "sys/wait.h"
#include "unistd.h"
#define BUF_MAX_SIZE 8192
// 如果x爲假,則報錯,打印出錯代碼所在函數及行數
#define CHECK(x) \
do { \
if (!(x)) { \
fprintf(stderr, "%s:%d: ", __func__, __LINE__); \
perror(#x); \
exit(-1); \
} \
} while (0)
/**
* Create three children processes to test pipe communication.
* @param argc Argument count.
* @param argv Argument vector.
* @return status code.
*/
int main(int argc, char **argv) {
int pipefd[2], pid, i = 0;
int flag = 0;
ssize_t n;
char buf[BUF_MAX_SIZE];
char str[BUF_MAX_SIZE];
// 創建有名信號量,若不存在則創建,若存在則直接打開,默認值爲0
sem_t *write_mutex;
sem_t *read_mutex1;
sem_t *read_mutex2;
write_mutex = sem_open("pipe_test_wm", O_CREAT | O_RDWR, 0666, 0);
read_mutex1 = sem_open("pipe_test_rm_1", O_CREAT | O_RDWR, 0666, 0);
read_mutex2 = sem_open("pipe_test_rm_2", O_CREAT | O_RDWR, 0666, 0);
memset(buf, 0, BUF_MAX_SIZE);
memset(str, 0, BUF_MAX_SIZE);
// 創建管道並檢查操作是否成功
CHECK(pipe(pipefd) >= 0);
// 創建第一個子進程並檢查操作是否成功
CHECK((pid = fork()) >= 0);
// 第一個子進程,利用非阻塞寫測試管道大小
if (pid == 0) {
int count = 0;
close(pipefd[0]);
int flags = fcntl(pipefd[1], F_GETFL);
// 管道默認是阻塞寫,通過`fcntl`設置成非阻塞寫,在管道滿無法繼續寫入時返回-EAGAIN,作爲循環終止條件
fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK);
// 寫入管道
while (!flag) {
n = write(pipefd[1], buf, BUF_MAX_SIZE);
if (n == -1) {
flag = 1;
} else {
count++;
printf("children 1 write %dB\n", n);
}
}
printf("space = %dKB\n", (count * BUF_MAX_SIZE) / 1024);
exit(0);
}
// 創建第二個子進程並檢查操作是否成功
CHECK((pid = fork()) >= 0);
if (pid == 0) {
sem_wait(write_mutex);
close(pipefd[0]);
n = write(pipefd[1], "This is the second children.\n", 29);
printf("children 2 write %dB\n", n);
sem_post(write_mutex);
sem_post(read_mutex1);
exit(0);
}
// 創建第三個子進程並檢查操作是否成功
CHECK((pid = fork()) >= 0);
if (pid == 0) {
sem_wait(write_mutex);
close(pipefd[0]);
n = write(pipefd[1], "This is the third children.\n", 28);
printf("children 3 write %dB\n", n);
sem_post(write_mutex);
sem_post(read_mutex2);
exit(0);
}
// 等待第一個子進程運行完成,父進程繼續運行
wait(0);
close(pipefd[1]);
int flags = fcntl(pipefd[0], F_GETFL);
// 設置非阻塞性讀,作爲循環結束標誌
fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK);
while (!flag) {
n = read(pipefd[0], str, BUF_MAX_SIZE);
if (n == -1) {
flag = 1;
} else {
printf("%dB read\n", n);
}
}
sem_post(write_mutex);
// 等待子進程二、三寫入完畢
sem_wait(read_mutex1);
sem_wait(read_mutex2);
n = read(pipefd[0], str, BUF_MAX_SIZE);
printf("%dB read\n", n);
for (i = 0; i < n; i++) {
printf("%c", str[i]);
}
sem_close(write_mutex);
sem_close(read_mutex1);
sem_close(read_mutex2);
sem_unlink("pipe_test_wm");
sem_unlink("pipe_test_rm_1");
sem_unlink("pipe_test_rm_2");
return 0;
}
#源碼解讀
這裏使用了三個信號量,分別是 write_mutex、read_mutex1 和 read_mutex2,簡單分析一下子進程和父進程之間的關係可以明白:
子進程一先將 64K 的數據寫入管道,父進程才能第一時間將數據全部讀取出來(來自一進程的數據)
父進程將子進程一的數據讀取之後,子進程二、三才能寫入數據
子進程二、三將數據寫入後,父進程隨後才能讀取第二批數據(來自二、三進程的數據)
關係大致如下圖所示:
子進程寫入數據1 和父進程讀取數據1 利用 wait(0) 限制了先後關係,父進程必須接收到子進程結束之後返回的 0,才能繼續運行,否則阻塞。
write_mutex 限制了父進程先讀取數據,然後子進程二、三寫入數據,read_mutex1 和 read_mutex2 分別限制了子進程二、三寫入數據 2,3 和父進程讀取數據 2,3 先後關係,只有子進程二、三均完成後,父進程才允許讀取管道。
子進程一使用了非阻塞性寫,子進程二、三均爲阻塞性寫,父進程爲非阻塞性讀。
非阻塞寫和非阻塞讀的目的在於,阻塞寫時,管道滿了之後進程被阻塞,無法設置終止條件從而結束寫,讀也是一樣,管道空了之後進程被阻塞,無法設置終止條件從而結束讀。
sem_open
https://baike.baidu.com/item/sem_open/8817672?fr=aladdin
創建並初始化有名信號量。sem_open返回指向sem_t信號量的指針,該結構裏記錄着當前共享資源的數目。(二值信號量的初始值通常爲1,計數信號量的初始值則往往大於1。)
fcntl
https://baike.baidu.com/item/fcntl/6860021?fr=aladdin
表頭文件編輯
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
參數介紹編輯
參數fd
參數fd代表欲設置的文件描述符。
參數cmd
參數cmd代表打算操作的指令。
cmd:
F_GETFL 取得文件描述符狀態旗標,此旗標爲open()的參數flags。
F_SETFL 設置文件描述符狀態旗標,參數arg爲新旗標,但只允許O_APPEND、O_NONBLOCK和O_ASYNC位的改變,其他位的改變將不受影響。
int flags = fcntl(pipefd[1], F_GETFL);
// 管道默認是阻塞寫,通過`fcntl`設置成非阻塞寫,在管道滿無法繼續寫入時返回-EAGAIN,作爲循環終止條件
fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK);
sem_post
https://baike.baidu.com/item/sem_post/4281234?fr=aladdin
頭文件:#include <semaphore.h>
說明:
sem_post函數的作用是給信號量的值加上一個“1”,它是一個“原子操作”---即同時對同一個信號量做加“1”操作的兩個線程是不會衝突的;而同 時對同一個文件進行讀、加和寫操作的兩個程序就有可能會引起衝突。信號量的值永遠會正確地加一個“2”--因爲有兩個線程試圖改變它。 當有線程阻塞在這個信號量上時,調用這個函數會使其中一個線程不在阻塞,選擇機制是有線程的調度策略決定的。
返回值:
sem_post() 成功時返回 0;錯誤時,信號量的值沒有更改,-1 被返回,並設置 errno 來指明錯誤。