十一、Linux进程间通信——管道

十一、Linux进程间通信——管道

目录:

  • 十一、Linux进程间通信——管道
        • 一、进程间通信(InterProcess Communication——IPC)常见方式
        • 二、管道
          • 1.管道的定义
          • 2.管道的原理
          • 3.pipe()函数
    • int pipe(int pipefd[2]);
          • 4.管道的读写行为
    • wc [选项] [文件名]
          • 5.查看缓冲区大小
    • ulimit -a
          • 6.管道的优劣势
        • 三、FIFO有名管道
    • mkfifo [文件名]
          • 1.mkfifo函数
    • int mkfifo(const char *pathname, mode_t mode);
          • 2.FIFO有名管道实现没有血缘关系进程之间通信


在 十、多进程编程 中介绍了,虽然父子进程有独立的数据空间,但它们具有相同的内核空间,因此得以实现进程间通信

一、进程间通信(InterProcess Communication——IPC)常见方式
  • 管道(使用最简单,只能使用在有血缘关系的进程之间)
  • 信号(开销最小)
  • 共享映射区(mmap)(进程间可以无血缘关系)
  • 本地套接字(socket)(最稳定)

二、管道
1.管道的定义

管道是一种基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递;调用 pipe 系统函数即可创建管道,具有以下性质:

  • 其本质是一个伪文件(实际上是内核中的一个缓冲区),因此不占用磁盘空间。Linux有7大文件类型,其中 普通文件、目录和软链接 占用磁盘空间,除了这些之外,管道、套接字、字符设备和块设备 是伪文件,不占用磁盘空间
  • 由两个文件描述符引用,一个表示读端,一个表示写端。
  • 规定数据从管道的写端流入管道,从读端流出
2.管道的原理

原理:管道实际上是内核使用环形队列机制,借助内核缓冲区(4KB)实现
十一、Linux进程间通信——管道_第1张图片
管道具有如下局限性

  • 数据不能由一个进程独立写,独立读,需由两个进程作为读、写端共同完成
  • 管道中数据不可反复读取,一旦读走,管道中的数据将不再存在(环形队列机制)
  • 采用双向半双工通信方式,即数据可以双向流动,但同时只能在单方向上流动
  • 只能在有公共祖先(有血缘关系)的进程间使用管道

常见的通信方式有:单工通信(数据单向流动)、半双工通信(同时单向流动)、全双工通信(可同时双向流动)

3.pipe()函数

包含头文件

#include 

int pipe(int pipefd[2]);

创建并打开一个管道

int pipefd[2] 传出参数,分别代表管道的读写端的文件描述符pipefd[0] 表示读端,pipefd[1] 表示写端
返回值 成功时返回 0,失败返回 -1errno

通过 pipe() 创建管道后,管道的读端和写端均打开,由于父子进程共享文件描述符,因此父子进程均可访问到管道的读写端

✅当 父进程写,子进程读 时:父进程应调用 close(fd[0]); 关闭读端;子进程应调用 close(fd[1]); 关闭写端

✅当 父进程读,子进程写 时:父进程应调用 close(fd[1]); 关闭写端;子进程应调用 close(fd[0]); 关闭读端

然后通过 write()read() 函数进行IPC
十一、Linux进程间通信——管道_第2张图片
例如:通过管道实现子进程写,父进程读

#include 
#include 
#include 
#include 
#include 
#include 

// 出错检查函数
void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    // 管道读写端
    int fd[2];
    // 创建管道
    int ret = pipe(fd);
    sys_err(ret, "pipe error");

	// 创建子进程
    pid_t pid = fork();
    sys_err(pid, "fork error");
    if(pid == 0)
    {
        // 子进程进行写操作
        close(fd[0]); // 关闭读端
        char *str = "Hello Pipe IPC!\n";
        write(fd[1], str, strlen(str));
        close(fd[1]);
    }else
    {
        wait(NULL); // 回收子进程
        // 父进程进行读操作
        int n;
        close(fd[1]); // 关闭写端
        char buf[1024];
        // 如果读到了数据,就进入循环
        while((n = read(fd[0], buf, sizeof(buf))) != 0)
        {
            // 写到屏幕上
            write(STDOUT_FILENO, buf, n);
        } 
        close(fd[0]);
    }

    return 0;
}

在这里插入图片描述

4.管道的读写行为

读取管道行为

  • 若管道中有数据:read 返回实际读到的字节数
  • 若管道中无数据:
    1️⃣且管道写端被全部关闭,read 返回 0(结果和读到文件结尾类似)
    2️⃣且管道写端没有全部关闭,read 阻塞等待(先让出CPU,将来有可能有数据到来)

写入管道行为

  • 若管道读端被全部关闭,进程异常终止(也可使用捕捉 SIGPIPE 信号,使进程不终止)
  • 若管道读端没有全部关闭:
    1️⃣且管道已满,write 阻塞,等待数据被读取再写入 (较为少见,因为新的操作系统会自动扩容)
    2️⃣且管道未满,write 将数据写入,并返回实际写入的字节数

例如:实现终端命令 ls | wc -l

wc [选项] [文件名]

根据选项对文件执行指令;
如果不指定文件,可以使用管道 | ,则以管道内容为目标;
若没有指定文件名,也没有使用管道,则以键盘输入stdin中的内容为目标


[选项]
-c 显示一个文件的字节数
-m 显示一个文件的字符数
-l 显示一个文件的行数
-L 显示一个文件中的最长行的长度
-w 显示一个文件的字数

因此 ls | wc -l 就是通过管道,统计 ls 打印的内容的文件行数(Terminal以空格为一行)

代码

#include 
#include 
#include 
#include 
#include 
#include 

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    int fd[2];
    int ret = pipe(fd);
    sys_err(ret, "pipe error");

    pid_t pid = fork();
    sys_err(pid, "fork error");
    if(pid == 0)
    {
        // 关闭读端
        close(fd[0]);
        // 将输出屏幕的内容重定向到管道写端
        ret = dup2(fd[1], STDOUT_FILENO);
        sys_err(ret, "dup2 error");

        execlp("ls", "ls", NULL); // 执行 ls
        sys_err(-1, "execlp ls error");
    }else
    {
        // 阻塞等待子进程
        wait(NULL);
        // 关闭写端
        close(fd[1]);
        // 由从键盘获取重定向到从管道读端读取
        ret = dup2(fd[0], STDIN_FILENO);
        sys_err(ret, "dup2 error");

        execlp("wc", "wc", "-l", NULL); // 执行 wc -l
        sys_err(-1, "execlp wc error");
    }

    return 0;
}

在这里插入图片描述
注意管道允许有一个写端,多个读端,即一个进程写,多个进程读,但数据一旦被一个进程读走了就不会再被读到了

注意管道允许有一个读端,多个写端,即多个进程写,一个进程读,但写入的顺序不能保证,需要通过一些机制来进行调整,如 sleep()

5.查看缓冲区大小

ulimit -a

查看缓冲区大小
十一、Linux进程间通信——管道_第3张图片
管道的大小,默认为4096(512 × 8)

6.管道的优劣势

优势

  • 简单,相比信号、套接字实现进程间通信,简单得多

劣势

  • 只能单向通信,双向通信需要建立两个管道
  • 只能用于父子、兄弟进程(有共同祖先)间通信;这个问题可以通过 FIFO 有名管道解决

三、FIFO有名管道

创建有名管道的方法有两个:1.终端命令,2.库函数 mkfifo(第 3 卷)

mkfifo [文件名]

创建有名管道

1.mkfifo函数

包含头文件

#include 
#include 

#include 已包含 #include

int mkfifo(const char *pathname, mode_t mode);

创建有名管道

const char *pathname 文件路径
mode_t mode 创建有名管道时指定的权限,受 umask 约束,即 权限 = mode & ~(umask)
返回值 成功时返回 0,失败返回 -1errno

例如:创建有名管道

#include 
#include 
#include 
#include 

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    int ret = mkfifo("myfifo", 0664);
    sys_err(ret, "mkfifo error");

    return 0;
}
2.FIFO有名管道实现没有血缘关系进程之间通信

例如:通过FIFO有名管道实现两个文件间的通信,即两个没有血缘关系的进程之间的通信(假设已创建有名管道,且文件名为 myfifo

fifo_write.c 文件代码

#include 
#include 
#include 
#include 
#include 
#include 

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    // 判断用户是否有输入管道文件
    if(argc < 2)
    {
        printf("Please enter like this: ./a.out fifoname");
    }
    int fd = open(argv[1], O_WRONLY);
    sys_err(fd, "open error");

    int i = 0;
    char buf[4096];
    // 每隔1秒向管道中写入内容
    while(1)
    {
        sprintf(buf, "It's No.%d\n", i++);
        write(fd, buf, strlen(buf));
        sleep(1);
    }
    close(fd);

    return 0;
}

fifo_read.c 文件代码

#include 
#include 
#include 
#include 
#include 
#include 

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    // 判断用户是否有输入管道文件
    if(argc < 2)
    {
        printf("Please enter like this: ./a.out fifoname");
    }
    int fd = open(argv[1], O_RDONLY); // 打开管道
    sys_err(fd, "open error");

    int n;
    char buf[4096];
    // 向管道中读取内容
    while((n = read(fd, buf, sizeof(buf))) != 0)
    {
        sys_err(n, "read error");
        write(STDOUT_FILENO, buf, strlen(buf)); // 把读到的内容打印到屏幕上
    }
    close(fd);

    return 0;
}

十一、Linux进程间通信——管道_第4张图片
注意

  • FIFO有名管道文件也具有阻塞特性
  • 和普通管道一样,管道中数据不可反复读取,一旦读走,管道中的数据将不再存在(环形队列机制)

你可能感兴趣的:(Linux系统编程,linux,嵌入式)