管道

管道时一种最基本的IPC机制,作用于有血缘关系的进程使用。
它由pipe函数创建,提供一个单向数据流

pipe函数原型:

#include<unitsd>
int pipe(int fd[2]);		//成功返回0,失败返回-1

函数会返回两个文件描述符:fd[0]和fd[1]

fd[0]:标识着管道的读端
fd[1]:标识着管道的写端

管道的创建

单个进程内的管道:

虽然管道是单个进程创建的,但它的用途主要是为下图父子进程提供进程间通信手段。首先,由一进城创建一个管道后调用fork派生一个自身的副本。


然后,父进程关闭这个管道的读出端,子进程关闭同意管道的写入端。这就在父子进程间提供了一个单向数据流

管道创建实例

在Linux shell中输入以下命令:

who | sort | lp

shell会执行上述步骤创建三个进程和其间的两个管道。它还把每个管道的读出端复制到相应进程的标准输入,把每个管道的写入端复制到相应进程的标准输出

创建双向管道数据流

我们知道,管道是半双工的,只能提供一个方向的数据流。当需要双向数据流的时候,我们必须创建两个管道,每个方向一个

步骤如下:

  1. 创建管道1,管道2
  2. fork
  3. 父进程关闭管道1读出端
  4. 父进程关闭管道2写入端
  5. 子进程关闭管道1写入端
  6. 子进程关闭管道2读出端

创建双向管道实例

在这里我们写一个客户——服务器例子。main函数创建链各个管道并用fork生成一个子进程。客户作为父进程运行,服务器作为子进程运行。


#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h> 

#include<string>
#include<iostream>
using namespace std;

using pipe_ = int;

void client(pipe_, pipe_), server(pipe_, pipe_);

int main()
{
   
	int pipe1[2], pipe2[2];
	pid_t pid;

	pipe(pipe1);	//create two pipes 
	pipe(pipe2);	

	if ((pid = fork()) == 0)	//chile process for server
	{
   
		close(pipe1[1]);		//close write
		close(pipe2[0]);		//close read

		server(pipe1[0], pipe2[1]);
		exit(0);
	}
	else		//parents for client
	{
   
		close(pipe1[0]);
		close(pipe2[1]);

		client(pipe2[0], pipe1[1]);

		waitpid(pid, NULL, 0);
		exit(0);
	}
}

void client(pipe_ readfd, pipe_ writefd)
{
   
	string buff;
	cin >> buff;
	char infor[10];

	write(writefd,buff.c_str(), buff.size());
	cout << "send to server:" << buff << endl;

	ssize_t n;
	while ((n = read(readfd, infor, 10)) > 0)
	{
   
		cout << "from server:" << infor << endl;
		//write(STDOUT_FILENO, buff.c_str(), n);
	}
}

void server(pipe_ readfd, pipe_ writefd)
{
   
	string buff;
	ssize_t n;

	if ((n = read(readfd, (void *)buff.c_str(), 128)) == 0)
	{
   
		cout << "server read error!" << endl;
	}

	write(writefd, buff.c_str(), n);
}

result:
$ ./main
hello
send to server:hello
from server:hello

全双工管道

我们说,管道时单工通信。那么全双工管道是什么呢?


全双工管道其实就可以看成一个存储池(缓冲区),有两个管子连在其上。
写入管道的任何数据都添加到该缓冲区的末尾,从任一管道中读出的都是缓冲区开头的数据

其实全双工管道是由两个半双工管道构成的。写入fd[1]的数据只能从fd[0]读出,写入放到fd[0]的数据只能从fd[1]读出。

#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>

#include<string>
#include<iostream>
using namespace std;

using pipe_ = int;
int main()
{
   
	pipe_ pipe1[2];
	pid_t pid;
	int n;
	//string infor;
	char infor[10];

	socketpair(AF_UNIX, SOCK_STREAM, 0, pipe1);	//create pipe
	if ((pid = fork()) == 0)			//child process
	{
   
		sleep(3);
		if ((n = read(pipe1[0],infor, 10)) == -1)
		{
   
			cout<<"child: read error & return:"<< n <<endl;
		}
		cout << "child read:" << infor << endl;
		write(pipe1[0], "world", 6);
		exit(0);
	}
	else								//parents process
	{
   
		write(pipe1[1], "hello", 6);
		if ((n = read(pipe1[1], infor, 10)) == -1)
		{
   
			cout << "parents: read error & return:" << n << endl;
		}
		cout << "parents read:" << infor << endl;
		exit(0);
	}
}

result:
$ ./main
child read:hello
parents read:world

这里可以看到:
笔者在这里是想用c++中的string类的,但是后来放弃?至于原因,可以查看下面的链接:

FIFO

管道没有名字,这是它最大的劣势!
我们无法在无亲缘关系的两个进程间创建一个管道并将它用作IPC通道。

FIFO在计算机术语中指的是先进先出,UNIX中的FIFO类似于管道。是一个半双工的数据流!不同于管道的是,每个FIFO有一个路径名与之关联,从而允许无亲缘关系的进程访问同一个FIFO。FIFO也就被称为有名管道

FIFO由mkfifo函数创建:

#include<sys/types.h>
#include<sys/stat.h>

int mkfifo(const char* pathname, mode_t mode);	//成功返回0,出错返回-1

mkfifo函数已经隐含指定O_CREAT | O_EXCL。也就是说,要么创建一个新的FIFO,要么就会返回一个EEXIST错误(因为管道文件已存在)。
如果希望打开一个已存在的FIFO文件,应该用open而不是mkfifo

mkfifo命令也可以创建FIFO,可以从shell脚本或命令行中使用它

FIFO实例

有亲缘关系的FIFO实例

#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>

#include<string>
#include<iostream>
using namespace std;

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FIFO1 "./fifo.1"
#define FIFO2 "./fifo.2"

using pipe_ = int;
void client(pipe_, pipe_), server(pipe_, pipe_);

int main()
{
   
	pipe_ readfd, writefd;
	pid_t pid;

	//create fifo1 and fifo2
	if ((mkfifo(FIFO1, FILE_MODE) == -1) && (errno != EEXIST))
	{
   
		cout << "make fifo1 error" << endl;
	}
	if ((mkfifo(FIFO2, FILE_MODE) == -1) && (errno != EEXIST))
	{
   
		unlink(FIFO1);		//delete fifo1
		cout << "make fifo2 error" << endl;
	}

	if ((pid = fork()) == 0)	//child process
	{
   
		readfd = open(FIFO1, O_RDONLY, 0);
		writefd = open(FIFO2, O_WRONLY, 0);

		server(readfd, writefd);
		exit(0);
	}
	else
	{
   
		writefd = open(FIFO1, O_WRONLY, 0);
		readfd = open(FIFO2, O_RDONLY, 0);

		client(readfd, writefd);

		waitpid(pid, NULL, 0);

		//deal data
		close(readfd);
		close(writefd);
		unlink(FIFO1);
		unlink(FIFO2);
		exit(0);
	}
}

void client(pipe_ readfd, pipe_ writefd)
{
   
	string buff;
	cin >> buff;
	char infor[10];

	write(writefd, buff.c_str(), buff.size());
	cout << "send to server:" << buff << endl;

	ssize_t n;
	while ((n = read(readfd, infor, 10)) > 0)
	{
   
		cout << "from server:" << infor << endl;
		//write(STDOUT_FILENO, buff.c_str(), n);
	}
}

void server(pipe_ readfd, pipe_ writefd)
{
   
	char buff[128] = {
   0};
	ssize_t n;
	if ((n = read(readfd, buff, 128)) == 0)
	{
   
		cout << "server read error!" << endl;
	}

	write(writefd, buff, n);
}

result:
$ ./main
hello
send to server:hello
from server:hello

无亲缘关系的FIFO

这个只需要把有亲缘关系的FIFO中父子进程替换成两个不相关的进程理解就行。

pipe.h

#pragma once
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

#include<iostream>

using namespace std;
using pipe_ = int;

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FIFO1 "./fifo.1"
#define FIFO2 "./fifo.2"

server.cpp

#include"pipe.h"

void server(pipe_, pipe_);

int main()
{
   
	pipe_ readfd, writefd;

	//create fifo1 and fifo2
	if ((mkfifo(FIFO1, FILE_MODE) == -1) && (errno != EEXIST))
	{
   
		cout << "make fifo1 error" << endl;
	}
	if ((mkfifo(FIFO2, FILE_MODE) == -1) && (errno != EEXIST))
	{
   
		unlink(FIFO1);		//delete fifo1
		cout << "make fifo2 error" << endl;
	}

	readfd = open(FIFO1, O_RDONLY, 0);
	writefd = open(FIFO2, O_WRONLY, 0);

	server(readfd, writefd);
	exit(0);
}

void server(pipe_ readfd, pipe_ writefd)
{
   
	char buff[128] = {
   0};
	ssize_t n;
	if ((n = read(readfd, buff, 128)) == 0)
	{
   
		cout << "server read error!" << endl;
	}

	write(writefd, buff, n);
}

client.h

#include "pipe.h"

void client(pipe_, pipe_);

int main()
{
   
	pipe_ readfd, writefd;

	//create fifo1 and fifo2
	if ((mkfifo(FIFO1, FILE_MODE) == -1) && (errno != EEXIST))
	{
   
		cout << "make fifo1 error" << endl;
	}
	if ((mkfifo(FIFO2, FILE_MODE) == -1) && (errno != EEXIST))
	{
   
		unlink(FIFO1);		//delete fifo1
		cout << "make fifo2 error" << endl;
	}

	writefd = open(FIFO1, O_WRONLY, 0);
	readfd = open(FIFO2, O_RDONLY, 0);

	client(readfd, writefd);

	//deal data
	close(readfd);
	close(writefd);
	unlink(FIFO1);
	unlink(FIFO2);
	exit(0);
}

void client(pipe_ readfd, pipe_ writefd)
{
   
	string buff;
	cin >> buff;
	char infor[10];

	write(writefd, buff.c_str(), buff.size());
	cout << "send to server:" << buff << endl;

	ssize_t n;
	while ((n = read(readfd, infor, 10)) > 0)
	{
   
		cout << "from server:" << infor << endl;
		//write(STDOUT_FILENO, buff.c_str(), n);
	}
}
result:
server:
	./server
client:
	./client
	hello 
	send to server:hello
	from server:hello

FIFO 单服务器,多用户

FIFO的真正又是在于服务器可以长期运行。作为服务器的进程创建一个FIFO,并且打开FIFO来读。之后某个时刻启动的客户打开该FIFO来写。这样很容易实现单向通信
但是如果需要服务器向客户端写一些东西,那就比较麻烦了。

所以这里我只实现简单的单向通信。

pipe.h

#pragma once
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

#include<memory.h>
#include<iostream>

using namespace std;
using pipe_ = int;

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SERV_FIFO "./fifo.serv"
#define MAX 128

server.cpp

#include"pipe.h"

int main()
{
   
	pipe_ readfifo;
	//pipe_ dummyfd, fd;
	char  buff[MAX + 1];

	ssize_t n;

	//create serv_fifo
	if ((mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST))
	{
   
		cout << "serv_fifo create error!" << endl;
	}

	readfifo = open(SERV_FIFO, O_RDONLY, 0);
	//dummyfd = open(SERV_FIFO, O_WRONLY, 0);

	while (1)
	{
   
		if ((n = read(readfifo, buff, MAX)) > 0)
		{
   
			if (strcmp(buff, "end") == 0) break;
			cout << "reaceive from client:" << buff << endl;
			memset(buff, 0, MAX);
		}
	}
	unlink(SERV_FIFO);
}

client.cpp

#include "pipe.h"

int main()
{
   
	pipe_  writefifo;
	writefifo = open(SERV_FIFO, O_WRONLY, 0);
	
	string buff;
	cin >> buff;
	write(writefifo, buff.c_str(), buff.size());
}
result:
server:
	$ ./server
	reaceive from client:hello
	reaceive from client:world
client:
	$ ./client 
	hello
	$ ./client
	world
	$ ./client  
	end
  • 迭代服务器与并发服务器区别(未完成)

管道和FIFO的额外属性

我们就管道和FIFO的打开、读出和写入记录一下它们的某些属性。

管道和FIFO的打开

一个描述符能够以两种方式设置成非阻塞

  1. 调用open时可以指定O_NONBLOCK标志
	writefd = open(FIFO1, O_WRONLY | O_NONBLOCK, 0);
  1. 如果一个描述符已经打开,那么可以调用fcntl启用O_NONBLOCK标志。使用fcntl时,我们需要先使用F_GETFL命令取得当前文件状态标志,将它与O_NONBLOCK按位或(位添加),再使用F_SETFL命令存储这些文件状态标志。
	int flags;
	if(flags = fcntl(fd, F_EGTFL, 0) < 0)
	{
   
		cout<< " fcntl error"<< endl;
	}
	flags |= O_NONBLOCK;
	if(flags = fcntl(fd, F_EGTFL, 0) < 0)
	{
   
		cout<< " fcntl error"<< endl;
	}

管道和FIFO的读写

  • 如果请求读出的数据量多余管道或FIFO中当前可用数据量,那么只返回这些可用的数据。
  • 如果请求写入的数据的字节数小于或等于PIPE_BUF,那么write操作就可以保证是原子的。反之,则不是原子的。
  • O_NONBLOCK标志对于write操作的原子性没有影响。
  • 如果向一个没有为读打开着的管道或FIFO写入,那么内核将产生一个SIGPIPE信号。

管道和FIFO限制

系统对于管道和FIFO的唯一限制为:

  • OPEN_MAX 一个进程在任意时刻打开的最大描述符数
  • PIPE_BUF 可原子地往一个管道或FIFO的最大数据量

我们可以通过stsconf函数查询OPEN_MAX,然后通过ulimit或者limit命令从shell中修改它。

PIPE_BUF的值通常定义在<limits.h>头文件,但是Posix认为它是一个路径名变量。这意味着它的值可以随所指定的路径名而变化。

参考文献

[1] W.Richard. UNXI网络编程——卷2. 人民邮电出版社. 2010.7