Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道

背景

上一讲我们介绍了创建子进程的方式。我们都知道,创建子进程是为了与父进程协作(或者是为了执行新的程序,参考 Linux exec族函数解析

我们也知道,进程之间的资源在默认情况下是无法共享的,所以我们需要借助系统提供的 进程间通信(IPC, InterProcess Communication) 有关的接口。

进程间通信

由于进程间的地址空间相对独立。进程与进程间不能像线程间通过全局变量通信,所以进程之间要交换数据必须通过内核。

在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

IPC的方式通常有:

  • Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)
  • System V IPC:消息队列、信号量、共享内存
  • Socket(支持不同主机上的两个进程IPC)

我们在这一讲介绍Unix IPC,包括:管道(pipe)、命名管道(FIFO)与信号(Signal)。

注意:对于管道来说,只有读端存在,写端才有意义;如果读端不在,写端向FIFO或者PIPE写数据,内核将向对应的进程发送SIGPIPE信号(默认终止进程)。

无名管道(pipe)

无名管道,是 UNIX 系统IPC最古老的形式,内部是用环形队列实现的。

在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。

特点:

1)无名,在文件系统中找不到它的存在,只存在于内存中。

2)必须得是亲缘关系的进程(父子,兄弟,祖孙等)

3)操作是不具备原子性(即,不具备完整性)

4)不能用lseek来定位

5)半双工(读写只能一端进行,另一端等待),具有固定的读端和写端。

6)具备阻塞(有读者有写者,当读没有数据或者是写满了的时候会卡在那里等待)

使用下面的函数创建 pipe管道。用于亲缘进程通信,其他操作与普通文件相同。

#include <unistd.h>

int pipe(int pipefd[2]);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h> int pipe2(int pipefd[2], int flags); //
/*
flags 包括O_CLOEXEC、O_DIRECT、O_NONBLOCK。
- CLOEXEC:close-on-exec即当调用exec()函数成功后,文件描述符会自动关闭
- O_DIRECT:任何读写操作都只在用户态地址空间和磁盘之间传送而不经过page cache
- O_NONBLOCK:非阻塞模式,在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。
*/

成功:返回0,同时为 pipefd 赋值。

  • pipefd[0]:读端
  • pipefd[1]:写端

失败:返回-1

读写情况:

  • 有数据时,无论有无写者(持有文件可写权限的描述符的进程,无的意思是指:进程退出,不再持有对应的fd;下同)都能够正常读;
  • 无数据时:如果 有 写者,则阻塞等待;如果 无 写着,则立即返回。

  • 如果 有 读者(读者,持有文件可读权限的描述符的进程):缓冲未满,正常写入;缓冲已满,阻塞等待。
  • 如果 无 读者,无论缓冲区如何,立即收到 SIGPIPE 信号(可以通过先注册的信号捕获 SIGPIPE 进行处理,默认是终止进程)

匿名管道 原理

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。

一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。

当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

匿名管道如何实现亲缘进程间的通信

1)父进程创建管道,得到两个文件描述符指向管道的两端。

2)父进程fork出子进程,子进程也有两个文件描述符指向同一管道。

3)写进程关闭 pipefd[0],读进程关闭 pipefd[1](因为管道只支持单向通信,关闭会比较好)。

数据从写端流入从读端流出,这样就实现了进程间通信。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> int main(int argc, char *argv[])
{
pid_t pid = -1;
int i;
int ret;
int status;
char buffer[20]={'a', 0};
int pipefd[2] = {0}; ret = pipe(pipefd);
if(!ret)
{
printf("Reader fd is : %d\n",pipefd[0]);
printf("Writer fd is : %d\n",pipefd[1]);
} pid = fork();
if(pid == 0)
{
printf("Son as reader\n");
close(pipefd[1]);// 关闭写端
for (i = 0; i < 5; ++i) {
read(pipefd[0], buffer, sizeof(buffer));
printf("%s\n", buffer);
}
exit(0xaa);
}else if(pid > 0)
{
sleep(1);
printf("Father as Writer\n");
close(pipefd[0]);// 关闭读端
for (i = 0; i < 5; ++i) {
write(pipefd[1], buffer, sizeof(buffer));
buffer[i] = 'a' + i;
} ret = wait(&status);
printf("%x\n", WEXITSTATUS(status));
} return 0;
}

命名管道(fifo)

FIFO (First in, First out)为一种特殊的文件类型(通过 stat 结构的 st_mode 成员可以知道文件是否是 FIFO 类型,可以用 S_ISFIFO 宏对此进行测试),它在文件系统中有对应的路径。

当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道

FIFO 存在于文件系统中,内容存放在内存中,如果使用 ls -l 观察时,它的文件大小永远是0。

实际上,Linux中的命令的 " | " 就是 fifo。

之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统来为管道命名。

写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。

FIFO的好处在于我们可以通过文件的路径来识别管道,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

创建 FIFO 类似于创建文件,它名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。它。()

FIFO严格遵循先进先出(first in first out), 对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

Linux中通过系统调用mknod()或makefifo()来创建一个命名管道。最简单的方式是通过直接使用shell:mkfifo myfifo(等价于mknod myfifo p)

特点

1)有名

2)任一个进程都可以交互

3)会诞生一个类型p的管道文件

4)操作具备原子性

5)全双工

6)不能lseek来定位

使用下面的函数创建 fifo。读写之前必须先open。

mkfifoat 中的 dirfd 请参考: dirfd参数 有关解析

#include <sys/types.h>
#include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); #include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h> int mkfifoat(int dirfd, const char *pathname, mode_t mode);

参数解析

mode :参数的说明同 open 的 mode 相同。如果open时没有使用O_NONBLOCK参数,不论读端还是写端先打开,先打开者都会阻塞,一直阻塞到另一端打开。

当 open 一个 FIFO,非阻塞标志 O_NONBLOCK 会产生下列影响:

  • 在没有指定该标志的情况下,只读 open 要阻塞到某个其他进程为写而打开这个 FIFO 为止,只写 open 要阻塞到某个其他进程为读而打开它为止。
  • 指定了该标志时,则只读 open 立即返回。但如果没有进程为读而打开一个 FIFO,那么只写 open 将返回 -1,并将 errno 设置成 ENXIO。

    类似于管道,
  • 若 write 一个尚无进程为读而打开的 FIFO,则产生信号 SIGPIPE。
  • 若某个 FIFO 的最后一个写进程关闭了该 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。
  • 一个给定的 FIFO 可能有多个写进程,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作,可被原子地写到 FIFO 的最大数据量也是通过常量 PIPE_BUF 来指定的。

如果mkfifo的路径已经存在时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。

#include <stdio.h>

#include <stdlib.h>
#include <unistd.h> #include <sys/stat.h>
#include <fcntl.h> #include <sys/types.h>
#include <sys/wait.h> #include <errno.h> #define FIFO_FILE "fifo_test" int main(int argc, char *argv[])
{
pid_t pid = -1;
int i;
int ret;
int status;
char buffer[20]={'a', 0};
int fd = 0; ret = mkfifo(FIFO_FILE, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
if(ret == -1)
{
if(errno == EEXIST)
{
printf("FIFO FILE existed.\n");
}else
{
perror("fifo");
return -1;
}
} pid = fork();
if(pid == 0)
{
printf("Son as reader\n");
printf("[Son] Waiting Writer\n");
fd = open(FIFO_FILE, O_RDONLY);
printf("Reader Pass\n"); for (i = 0; i < 5; ++i) {
read(fd, buffer, sizeof(buffer));
printf("%s\n", buffer);
}
exit(0xaa);
}else if(pid > 0)
{
sleep(1);
printf("Father as Writer\n");
printf("[Dad] Waiting Reader\n");
fd = open(FIFO_FILE, O_WRONLY);
printf("Writer pass\n");
for (i = 0; i < 5; ++i) {
write(fd, buffer, sizeof(buffer));
buffer[i] = 'a' + i;
} ret = wait(&status);
printf("%x\n", WEXITSTATUS(status));
} return 0;
}

Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道的更多相关文章

  1. Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号

    Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号 背景 上一讲我们介绍了Unix IPC中的2种管道. 回顾一下上一讲的介绍,IPC的方式通常有: Unix IPC包括:管道 ...

  2. Linux 系统编程 学习 总结

    背景 整理了Liunx 关于 进程间通信的 很常见的知识. 目录 与 说明 Linux 系统编程 学习:000-有关概念 介绍了有关的基础概念,为以后的学习打下基础. Linux 系统编程 学习:00 ...

  3. Linux 系统编程 学习:00-有关概念

    Linux 系统编程 学习:00-有关概念 背景 系统编程其实就是利用系统中被支持的调度API进行开发的一个过程. 从这一讲开始,我们来介绍有关Linux 系统编程的学习. 知识 在进行Linux系统 ...

  4. Linux 系统编程 学习:04-进程间通信2:System V IPC(1)

    Linux 系统编程 学习:04-进程间通信2:System V IPC(1) 背景 上一讲 进程间通信:Unix IPC-信号中,我们介绍了Unix IPC中有关信号的概念,以及如何使用. IPC的 ...

  5. Linux 系统编程 学习:05-进程间通信2:System V IPC(2)

    Linux 系统编程 学习:05-进程间通信2:System V IPC(2) 背景 上一讲 进程间通信:System V IPC(1)中,我们介绍了System IPC中有关消息队列.共享内存的概念 ...

  6. Linux 系统编程 学习:06-基于socket的网络编程1:有关概念

    Linux 系统编程 学习:006-基于socket的网络编程1:有关概念 背景 上一讲 进程间通信:System V IPC(2)中,我们介绍了System IPC中关于信号量的概念,以及如何使用. ...

  7. Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信

    Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信 背景 上一讲我们介绍了网络编程的一些概念.socket的网络编程的有关概念 这一讲我们来看UDP 通信. 知识 U ...

  8. Linux 系统编程 学习:01-进程的有关概念 与 创建、回收

    Linux 系统编程 学习:01-进程的有关概念 与 创建.回收 背景 上一讲介绍了有关系统编程的概念.这一讲,我们针对 进程 开展学习. 概念 进程的身份证(PID) 每一个进程都有一个唯一的身份证 ...

  9. Linux 系统编程 学习:09-线程:线程的创建、回收与取消

    Linux 系统编程 学习:09-线程:线程的创建.回收与取消 背景 我们在此之前完成了 有关进程的学习.从这一讲开始我们学习线程. 完全的开发可以参考:<多线程编程指南> 在Linux ...

随机推荐

  1. Android App 侧边栏菜单的简单实现

    效果图 Layout 注意事项 想要实现侧边栏,需要配合使用DrawerLayout.因为会用到嵌套布局,所以根布局不能是 ConstraintLayout,最好使用 LinearLayout 布局. ...

  2. Centos-当前登录用户信息- w who

    w who 显示当前登录系统的用户,但w显示的更为详细 who 相关参数 # 默认输出 用户名.登录终端.登录时间 -a 列出所有信息 -b    系统最近启动日期 -m   当前终端信息,相当于 w ...

  3. Django-发送注册、忘记密码邮件验证-send_mail

    用户邮箱注册.发送验证码流程图 那,如何解决? 1.  setting配置邮箱参数 # 邮箱设置,需要在邮箱中开启smtp服务 # 提供服务的主机域名 EMAIL_HOST = 'smtp.163.c ...

  4. LeetCode刷题的一点个人建议和心得

    目录 1.    为什么我们要刷LeetCode? 2.    LeetCode的现状和问题 3.    本文的初衷 4.    LeetCode刷题建议 4.1入门数据结构,打基础阶段 4.2 建立 ...

  5. 解决Dubbo无法发布被事务代理的Service问题

    在HelloServiceImpl类上加入@Transactional注解后,虽然工程可以正常跑起来,但是通过dubbo管理控制台可以看到里面并没有服务发布上来. 此时启动服务提供者和服务消费者,并访 ...

  6. JDK 中的栈竟然是这样实现的?

    前面的文章<动图演示:手撸堆栈的两种实现方法!>我们用数组和链表来实现了自定义的栈结构,那在 JDK 中官方是如何实现栈的呢?接下来我们一起来看. 这正式开始之前,先给大家再解释一下「堆栈 ...

  7. Winsock 编程详解

    转载请注明出处!本文地址:https://www.cnblogs.com/teternity/p/WinSock.html Winsock 编程 目录 通用函数讲解 WSAStartup WSACle ...

  8. 动态枢轴网格使用MVC, AngularJS和WEB API 2

    下载shanuAngularMVCPivotGridS.zip - 2.7 MB 介绍 在本文中,我们将详细介绍如何使用AngularJS创建一个简单的MVC Pivot HTML网格.在我之前的文章 ...

  9. helm包管理工具

    K8S正常部署应用是如下方式 kubectl create deployment web --image=nginx --dru-run=client -o yaml > web.yaml ku ...

  10. java 的 callback

    Java 本身没有回调这一说,但是面向对象可以模拟出来. 1. 回调接口对象 ICommand package com.git.Cmder; public interface ICommand { v ...