PIPE和FIFO的使用及原理
PIPE和FIFO都是指管道,只是PIPE独指匿名管道,FIFO独指有名管道,我们先看一下管道的数据结构以及他们的使用方式:
1、匿名管道(pipe)
Linux下进程通信 匿名管道pipe2、命名管道(FIFO)
Linux下进程通信 命名管道FIFO
//匿名管道(PIPE)
#include
int pipe (int fd[2]); //创建pipe
ssize_t write(int fd, const void *buf, size_t count); //写数据
ssize_t read(int fd, void *buf, size_t count); //读数据
//有名管道(FIFO)
#include
#include
int mkfifo(const char *path, mode_t mode); //创建fifo文件
int open(const char *pathname, int flags); //打开fifo文件
ssize_t write(int fd, const void *buf, size_t count); //写数据
ssize_t read(int fd, void *buf, size_t count); //读数据
匿名管道
可以看到,匿名管道通过pipe函数创建,pipe函数通过在内核的虚拟文件系统中创建两个pipefs虚拟文件,并返回这两个虚拟文件描述符,有了这两个文件描述,我们就能进行跨进
匿名管道是单向半双工的通信方式,单向即意味着只能一端读另一端写,半双工意味着不能同时读和写,其中文件描述符fd[1]只能用来写,文件描述符f[0]只能用来读,pipe创建好后,我们就可以用Linux标准的文件读取函数read和写入函数write来对pipe进行读写了。
为什么pipe是匿名管道呢?因为pipefs文件是特殊的虚拟文件系统,并不会显示在VFS的目录中,所以用户不可见,既然用户不可见,那么又怎么能进行进程间通信呢?
因为pipe是匿名的,所以它只支持父子和兄弟进程之间的通信。通过fork创建父子进程或者通过clone创建兄弟进程的时候,会共享内存拷贝,
这时,子进程或兄弟进程就能在共享拷贝中拿到pipe的文件描述符进行通信了。我们通过下图看一下通信的流程。
FIFO
FIFO,FIFO是半双工双向通信,所以通过FIFO创建的管道既能读也能写,但是不能同时读和写,FIFO本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,这样的数据结构能保证信息交流的顺序。
FIFO使用也很简单,通过mkfifo函数创建管道,它同样会在内核的虚拟文件系统中创建一个FIFO虚拟文件,FIFO文件在在VFS目录中可见,所以他是有名管道,FIFO创建后,我们需要调用open函数打开这个fifo文件,然后才能通过write和read函数进行读写
pipe和fifo的异同点
相同点
- IPC的本质都是通过在内核创建虚拟文件,并且调用文件读写函数来进行数据通信
- 都只能接收字节流数据
- 都是半双工通信
不同点 - pipe是单向通信,fifo可以双向通信
- pipe只能在父子,兄弟进程间通信,fifo没有这个限制
管道的使用场景
匿名管道只能用在亲属进程之间通信,而且传输的数量小,一般只支持4K,不适合大数据的交换数据和不同进程间的通信,但是使用简单方便,因为是单向通信,所以不存在并发问题。
虽然FIFO能在任意两个进程间进行通信,但是因为FIFO是可以双向通信的,这样也不可避免的带来了并发的问题,我们需要花费比较大的精力用来控制并发问题。
管道在Android系统中的使用场景
在Android 6.0 以下版本中,主线程Looper的唤醒就使用到了管道。
//文件-->/system/core/libutils/Looper.cpp
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
int wakeFds[2];
int result = pipe(wakeFds); //创建pipe
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking. errno=%d",
errno);
result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking. errno=%d",
errno);
mIdling = false;
// Allocate the epoll instance and register the wake pipe.
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance. errno=%d", errno);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeReadPipeFd;
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance. errno=%d",
errno);
}
从第上面代码可以看到,native层的Looper的构造函数就使用了pipe来创建管道,通过mWakeReadPipeFd,mWakeWritePipeFd这两个文件描述符的命名也看出,它是用来做唤醒的,我们就来看一下具体的唤醒的实现吧。
//文件-->/system/core/libutils/Looper.cpp
void Looper::wake() {
ssize_t nWrite;
do {
nWrite = write(mWakeWritePipeFd, "W", 1);
} while (nWrite == -1 && errno == EINTR);
if (nWrite != 1) {
if (errno != EAGAIN) {
ALOGW("Could not write wake signal, errno=%d", errno);
}
}
}
可以看到,唤醒函数其实就是往管道mWakeWritePipeFd里写入一个字母“W”,mWakeReadPipeFd接收到数据后,就会唤醒Looper。