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/

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