HDU_實驗三(2):實現一個管道通信程序

#實驗描述:

由父進程創建一個管道,然後再創建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 來指明錯誤。

轉載於:
https://lsvm.xyz/2019/04/os-lab-3-2/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章