非阻塞的管道和FIFO
- 管道和FIFO都可以设置非阻塞。它们两者都可以在打开之后通过fcntl函数设置O_NONBLOCK标志来enable。一般而言,我们都是先使用F_GETFL来获取当前文件状态标志,将它与O_NONBLOCK按位或之后,再通过F_SETFL来设置新的文件状态标志。
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) {
ERR_EXIT("F_GETFL ERROR");
}
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0) {
ERR_EXIT("F_SETFL ERROR");
}
- 另外,FIFO还可以通过open函数设置O_NONBLOCK标志,由于管道不能使用open来打开,所以管道不能使用此方法设置非阻塞。
readfd = open(pathname, O_RDONLY|O_NONBLOCK, 0);
阻塞与非阻塞管道和FIFO属性
前面也有讲到,如果进程在打开FIFO读之前没有其他进程已打开该FIFO的写,那么该进程会一直阻塞到有其他进程打开FIFO的写为止。同样,如果进程在打开FIFO**写之前没有其他进程已打开该FIFO的读**,那么该进程会一直阻塞到有其他进程打开FIFO的读为止。
管道和FIFO在创建和打开的语法上有所不同,但一旦打开之后,它们的使用语法基本一致。而对于阻塞的管道和FIFO而言,使用read和write也很容易引起阻塞。
而当管道和FIFO设置成非阻塞之后,它们的读写行为就会有些改变。
非阻塞open FIFO
使用open来打开非阻塞的FIFO,如果该FIFO当前没有打开读,那么会返回ENXIO错误。
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio> //perror
#include <iostream>
#define ERR_EIXT(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
}while(0)
static const char FIFOPATH[] = "/tmp/fifo";
static const mode_t MODE = (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
int main(int argc, char *argv[])
{
if ((mkfifo(FIFOPATH, MODE) < 0) && (errno != EEXIST))
ERR_EIXT("FIFO ERROR");
int writefd = open(FIFOPATH, O_WRONLY | O_NONBLOCK, 0);
if (writefd < 0)
if (errno == ENXIO)
ERR_EIXT("NO READ");
else
ERR_EIXT("OPEN FIFO FOR WRITE ERROR");
std::cout << "OPEN FIFO FOR WRITE SUCCESSFULLY" << std::endl;
close(writefd);
unlink(FIFOPATH);
exit(EXIT_SUCCESS);
}
运行结果:
上面的程序是使用open非阻塞的打开FIFO写,而在这之前没有打开任何该FIFO的读,所以该程序会接收到ENXIO。
如果我们再运行上面的程序之前,先运行打开与FIFOPATH关联的FIFO读(如代码),并且保持打开,那么在运行上面的程序就不会接收到ENXIO,而是成功打开FIFO写。
int main(int argc, char *argv[])
{
if ((mkfifo(FIFOPATH, MODE) < 0) && (errno != EEXIST))
ERR_EIXT("FIFO ERROR");
int readfd = open(FIFOPATH, O_RDONLY | O_NONBLOCK, 0);
if (readfd < 0)
ERR_EIXT("OPEN FIFO FOR READ ERROR");
std::cout << "OPEN FIFO FOR READ SUCCESSFULLY" << std::endl;
sleep(5); //睡眠5秒,以免读在写之前关闭
close(readfd);
unlink(FIFOPATH);
exit(EXIT_SUCCESS);
}
运行上面程序结果:
此时在运行打开FIFO写程序,结果如下:
FIFO写成功打开。
FIFO非阻塞write属性
PIPE_BUF
在讲这个write属性之前,需要先了解一下PIPE_BUF这常数。它表示管道或者FIFO写操作原子性的最大字节数。也就是说,向管道或者FIFO写数据时,只有写的字节数小于等于PIPE_BUF时,这个写操作才能保证原子性(当两个进程想同一个管道或者FIFO写数据时,两个进程所写数据不会交错,必须是先写一个进程数据,写完后再写另外一个进程的数据)。当进程写的字节数大于PIPE_BUF,那么写的数据就不能保证是原子性的,进程间写的数据可能是交错的。linux下默认是4096 bytes通过ulimit -p查看PIPE_BUF的的大小(其单位是512 bytes):
也可以通过以下代码输出:
#inlcude <climits>
std::cout << PIPE_BUF << std::endl;
还有一点需要注意的是,PIPE_BUF的值它不是管道或者FIFO的缓冲区大小,缓冲区的大小一般比PIPE_BUF大的多。
int main(int argc, char *argv[])
{
if ((mkfifo(FIFOPATH, MODE) < 0) && (errno != EEXIST))
ERR_EIXT("FIFO ERROR");
int writefd = open(FIFOPATH, O_WRONLY | O_NONBLOCK, 0);
if (writefd < 0)
if (errno == ENXIO)
ERR_EIXT("NO READ");
else
ERR_EIXT("OPEN FIFO FOR WRITE ERROR");
std::cout << "OPEN FIFO FOR WRITE SUCCESSFULLY" << std::endl;
for (int i = 1; ; ++i) {
if (write(writefd, "1", 1) < 0) {
if (errno == EAGAIN)
ERR_EIXT("FIFO BUFFER IS FULL");
else
ERR_EIXT("FIFO WRITE ERROR");
}
std::cout << i << " bytes writen" << std::endl;
}
close(writefd);
exit(EXIT_SUCCESS);
}
上面的程序主要是用来确定管道或者FIFO的缓冲区大小的。首先打开FIFO的写,然后一次写一个字节,知道满为止。当FIFO设置为nonblock时,如果其缓冲区已满,再向其写数据时会返回一个EAGAIN错误(当FIFO为阻塞情况是,向满的FIFO写数据会阻塞)。其输出结果为:
可以看到当缓冲区已经写入65536 bytes数据时,再写就会产生EAGAIN错误,从而确定缓冲区大小为65536 bytes。
write属性
使用write向管道或者FIFO写数据其结果是成功还是阻塞还是出错需要取决于该管道或者FIFO是否设置了nonblock,还取决于写入数据的字节数(假设是n)是否大于PIPE_BUF,具体讨论如下:
-
阻塞且n<=PIPE_BUF: 这n个字节的数据的写入是原子写入的。如果当前没有足够的空间写入n字节,那么write会阻塞到有足够空间写入位置。
-
非阻塞且n<=PIPE_BUF: 如果缓冲区中还有至少n个字节的空间,那么该n个字节会马上写入到管道或者FIFO中;否则会返回错误EAGAIN。
-
阻塞且n>PIPE_BUF:该n个字节会交错的写入管道或者FIFO中,并且write会一直阻塞到这n个字节全部写入管道或FIFO中。
-
非阻塞且n>PIPE_BUF:如果缓冲区满的话,会write会出错,并且errno设置为EAGAIN。如果缓冲区不满,那么write就会写入部分的数据,所以调用者需要检查write返回的实际写入字节数,并且这些数据可能会与其他进程的数据交错写入。
参考:
《UNP 卷2》
http://man7.org/linux/man-pages/man7/pipe.7.html