Linux进程间通信——管道 & FIFO
目录
管道
管道时一种最基本的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,管道2
- fork
- 父进程关闭管道1读出端
- 父进程关闭管道2写入端
- 子进程关闭管道1写入端
- 子进程关闭管道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的打开
一个描述符能够以两种方式设置成非阻塞。
- 调用open时可以指定
O_NONBLOCK
标志
writefd = open(FIFO1, O_WRONLY | O_NONBLOCK, 0);
- 如果一个描述符已经打开,那么可以调用
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