进程间通信IPC-管道

进程间通信IPC-管道特点

IPC方式特点
匿名管道1、仅存在于具有亲缘关系的进程(父子进程或者兄弟进程)之间通信
2、半双工通信方式,数据单方向流动,具有固定的读端和写端(若需双向通信需要两个管道)
3、通信数据为无格式流且大小受限
4、匿名管道的生命周期随着进程创建而创建,随着进程终止而消失
5、只存在于内存的特殊文件、不属于文件系统,可以使用普通的read、write等函数,不可以使用lseek等特殊函数
6、4kb的原子性写入
命名管道1、FIFO创建了一个类型为管道的设备文件,它以一种特殊设备文件形式存在于文件系统中,不可以使用lseek等特殊函数
2、FIFO可以在无关的进程之间交换数据,与无名管道不同,进程里只要使用这个设备文件,就可以相互通信
3、通信数据为无格式流且大小受限
4、半双工通信方式,数据单方向流动,具有固定的读端和写端(若需双向通信需要两个管道)
5、4kb的原子性写入

管道缺点:

管道大小仅有64KB(65535B)
所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

管道这种通信方式效率低,不适合进程间频繁地交换数据

匿名管道

常见的使用场景
linux命令 “|”
ps -ax | grep ssh

命令行里的竖线就是一个管道,它的功能是将前一个命令ps -ax的输出,作为后一个命令grep ssh的输入,不难得出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。

函数原型
#include <unistd.h>
int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]端,fd[1]

单进程匿名管道原理图

在这里插入图片描述

父子进程间匿名管道原理图

​ 上述两个管道描述符都是在一个进程里面,并没有起到进程间通信的作用,为此使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个fd[0]fd[1],两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。

​ 匿名管道只存在于内存,不属于文件系统管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。
在这里插入图片描述

管道是半双工,数据单向流动,管道只能一端写入,另一端读出,因此上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。为了避免这种情况,通常的做法是:

父进程关闭读取的 fd[0],只保留写入的 fd[1];
子进程关闭写入的 fd[1],只保留读取的 fd[0];

在这里插入图片描述

兄弟进程间匿名管道原理图

对于命令ps -ax | grep ssh 并不是父子进程之间的管道通信, 执行ps -ax | grep ssh的进程为父进程, 执行ps -ax命令 为子进程A,执行 grep ssh为子进程B, A与B为兄弟进程。

在这里插入图片描述

|匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程,编写 shell 脚本时尽量减少匿名管道的使用,减少创建子进程的系统开销。

父子进程匿名管道通信代码:
pipe_test.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
	int fd[2];       //匿名管道描述符
	pid_t pid;       //进程号
	char buf[128]; 	 //数据缓冲区
	int ret;         //创建管道结果
	ret = pipe(fd);
	if(ret == -1){
		printf("pipe creat  failed\n");
		return -1;
	}

	pid = fork();

	if(pid < 0){
		printf("child thread creat failed\n");
		return -1;
	}
	else if(pid > 0){                  // pid>0当前位于父进程中
		printf("this is father thread!\n");
		close(fd[0]);				   //父进程关闭读端
		write(fd[1],"I am your father!",strlen("I am your father!"));   //父进程像管道写入数据
		wait(NULL);						//等候子进程结束,父进程完成资源回收,阻塞。
	}else{								// pid==0当前位于子进程中
		printf("this is child thread!\n");		
		close(fd[1]);					//子进程关闭写端
		read(fd[0],buf,128);			//子进程读取管道数据,并写入buf中
		printf("father's info:%s\n",buf);
		exit(0);						//子进程结束
	}
	
	return 0;
}
makefie
all:pipe_test   																
pipe_test: pipe_test.o 								
	gcc -o pipe_test pipe_test.o				
clean:										#清理规则,
	rm -rf  pipe_test *.o
运行结果:

在这里插入图片描述

命名管道(FIFO)

​ 命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。

常见的使用场景
linux命令 “mkfifo”
创建命名管道
mkfifo fifo_test

在这里插入图片描述

注意开头第一个字母为p代表管道文件

向管道写数据
echo "hello fifo" > fifo_test

命令执行后该shell被阻塞,因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。

在这里插入图片描述

读取管道数据
cat < fifo_test

在这里插入图片描述

管道里的内容被读取出来了,并打印在了终端上,另外一方面,echo 那个命令也正常退出了。

删除命名管道文件
rm fifo_test
函数原型
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);   // 返回值:成功返回0,出错返回-1
/*
pathname为命名管道文件路径名
其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
*/

当open一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
若没有指定O_NONBLOCK(默认阻塞),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open
要阻塞到某个其他进程为读而打开它。
若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 。

文件权限
文件的权限:r 表示可读取,w 表示可写入,x 表示可执行,分别表示为数字为r=4,w=2,x=1;
权限又按用户的不同分为三类:User、Group、及Other三类用户的权限。
如,对于User用户,若拥有rw权限,则为4+2=6,所以0666中的666代表User、Group、及Other的权限分别是6,6,6,即均为rw权限。
而0666中的0代表不设置特殊的用户id,此处还可设为4,2,1,4代表具有root权限(即suid),2代表sgid,1代表sticky
创建fifo
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
int main()
{
    int ret = mkfifo("./myfifo",0600);  // 创建命名管道文件,用户权限为可读、可写
	if(ret == -1){                      //判断是否创建失败
		printf("mkfifo creat failed");  //打印失败信息
		perror("failed reason");        //打印失败原因
	}
	return 0;
}
open 打开模式

open()的函数原型:

int open(const char *path , int aflag , ... /* mode_t mode* */);
//cosnt char* path是 要开文件的名字(通常是绝对路径)。而aflag参数则有以下常量通过或运算|组成
O_RDONlY 只读打开
O_WRONlY 只写打开
O_RDWR 读写打开
O_EXEC 只执行打开
O_SEARCH 只搜索(对于目录有此选项)

在以上五个常量中必须指定且只能指定一个,而以下常量为可选的。

O_APPEND 每次写入追加到文件末尾。
O_CLOEXEC 把FD_CLOEXEC设定为文件描述符。
O_CREATE 若文件不存在则创建, 需要指定文件权限位 , 即mode_t 参数。
O_DIRECTORY 若path指向的不为目录,则出错。
O_EXCL 若同时指定O_CREATE且文件不存在,则出错。可以将测试文件存在和创建文件封装为原子操作。
O_NOCTTY 若path引用的是终端设备,则不将该设备分配作为该进程的控制终端。
O_NONBLOCK 若path引用的是一个FIFO,一个块特殊文件或者字符特殊文件,则此选项将本次文件的打开操作和后续的IO操作设置为非阻塞模式。
O_SYNC 每次操作需要等待物理IO完成,包括更新文件属性而需要的物理IO。
O_TURNC 若文件存且为只写或读写打开,那么将其长度截断为零。
O_DSYNC 每次写入需要等待物理IO完成,但是如果不影响读取,则不需要更新文件属性。
O_FSYNC 使每一个 以文件描述符为参数的进行的read操作等待,直到所有对文件同一部分的挂起写操作都完成。
读取fifo
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>


int main()
{
	char buf[100] = {0};  //用于保存读取的管道数据

	int fd = open("./myfifo",O_RDONLY);	 //打开管道文件,模式为只读, 默认阻塞
	// int fd = open("./myfifo",O_RDONLY| O_NONBLOCK);	 //打开管道文件,模式为只读, 非阻塞
     if(fd == -1){
        printf("open failed!");
        perror("failed reason: ");
    }
	int n_read = read(fd,buf,100);      //读取管道数据 存入buf
    if(n_read == -1){
        perror("failed reason: ");
    }
	printf("read %d byte from myfifo, info is \":%s\" \n",n_read,buf); //打印读取的管道数据
	close(fd);              //关闭文件描述符

	return 0;
}
写入fifo
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
	char *str = "I am writting data to fifo";   //需要写入管道的数据

	int fd = open("./myfifo",O_WRONLY);         //打开管道文件,模式为只写, 默认阻塞
	// int fd = open("./myfifo",O_WRONLY | O_NONBLOCK);         //打开管道文件,模式为只写, 非阻塞
    if(fd == -1){
        printf("open failed!");
        perror("failed reason: ");
    }
    
	int num = write(fd,str,strlen(str));   //向管道写数据 
    if(num == -1){
        perror("failed reason: ");
    }
	printf("%d",num);
    close(fd);                   //关闭文件描述符
	
	return 0;
}

Makefile

all:creat_fifo read_fifo write_fifo  																
creat_fifo: creat_fifo.o 								
	gcc -o creat_fifo creat_fifo.o
read_fifo: read_fifo.o 								
	gcc -o read_fifo read_fifo.o	
write_fifo: write_fifo.o 								
	gcc -o write_fifo write_fifo.o					
clean:										#清理规则
	rm -rf  creat_fifo read_fifo write_fifo myfifo *.o
运行结果:

首先创建fifo

在这里插入图片描述
在这里插入图片描述

之后运行(代码是默认阻塞模式)write_fifo, write不会阻塞,写入数据成功, read_fifo从管道读取数据解除阻塞状态

在这里插入图片描述
在这里插入图片描述

先运行(代码是默认阻塞模式)write_fifo 无论管道有无数据,都会阻塞,运行(代码是默认阻塞模式)read_fifo可以读取数据

并解除多个write_fifo造成的阻塞

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

若先运行(代码是非阻塞模式)read_fifo 由于管道无数据,直接结束,不会阻塞, 运行之后的代码

在这里插入图片描述

先运行(代码是非阻塞模式)write_fifo ,无论管道有无数据,都会出错,open失败

在这里插入图片描述

先阻塞读,然后非阻塞写,可以成功open并写入数据

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

先阻塞写,然后非阻塞读,错误

在这里插入图片描述
在这里插入图片描述

先非阻塞读,然后阻塞写, 写被阻塞

管道原子性问题:

一个管道的容量是有限的。POSIX规定,少于 PIPE_BUF 的写操作必须原子完成:要写的数据应被连续的写到管道;大于 PIPE_BUF 的写操作可能是非原子的: 内核可能会把此数据与其它进程的对此管道的写操作交替起来。POSIX规定PIPE_BUF至少为512B(linux中为4096B),具体的语义如下: 其中n为要写的字节数
n <= PIPE_BUF, O_NONBLOCK无效:原子的写入n个字节。如果管道当前的剩余空间不足以立即写入n个字节,就阻塞直到有足够的空间。
n <= PIPE_BUF, O_NONBLOCK有效:写入具有原子性,如果有足够的空间写入n个字节,write立即成功返回。否则一个都不写入,返回错误,并设置errno为EAGAIN。
n > PIPE_BUF, O_NONBLOCK无效:非原子写。可能会和其它的写进程交替写。write阻塞直到将n个字节写入管道。
n > PIPE_BUF, O_NONBLOCK有效:如果管道满,则write失败,返回错误,并将errno设置为 EAGIN。如果不满,则返回写入的字节数为1~n,即部分写入,写入时可能有其他进程穿插写入。

结论:
1、当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
2、当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
github传送

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值