Linux管道FIFO
管道FIFO
2.1 管道基本概念
管道是针对于本地计算机的两个进程之间的通信而设计的通信方法,管道建立后,实际上是获得两个文件描述符:一个用与读取而另一个用于写入。任何从管道写入端写入的数据,可以从管道读取端读出。
管道通信具有以下特点:
-
管道是半双工的,数据只能向一个方向流动,需要双方通信时,要建立起两个管道。
管道存放在内存中,是一种独立的文件系统。
2.2 无名管道的创建与读写
系统调用pipe()用于创建一个管道,其函数原型如下:
int pipe(int pipefd[2]);
pipe()将建立一对文件描述符,放到参数pipefd
中。pipefd[0]
文件描述符用来从管道中读取数据,pipefd[1]
用于写入数据到管道。
单个进程中的管道几乎没有任何意义,通常,进程会先调用pipe,接着调用fork,从而创建从父进程到子进程的通道。
fork出子进程后,父进程的文件描述表也复制到子进程,于是如下图:
关闭掉父进程的fd[0], 以及子进程的fd[1],则父进程可以发送消息给子进程,反之亦然。
源代码:pipe_test.c
void child_process(int pipefd[]) { int i = 0; char writebuf[128] = {0}; /*子进程关闭读文件描述符*/ close(pipefd[0]); while(1) { sprintf(writebuf, "write pipe : %d", i); /*子进程往管道写入数据*/ write(pipefd[1], writebuf, strlen(writebuf)); printf("write pipe: %s\n", writebuf); i = (i + 1) % 10; sleep(1); } } void father_process(int pipefd[]) { char readbuf[128] = {0}; /*父进程关闭写文件描述符*/ close(pipefd[1]); while(1) { /*父进程从管道中读取数据*/ read(pipefd[0], readbuf, sizeof(readbuf)); printf("read pipe: %s\n", readbuf); } } int main(int argc, char** argv) { int pipefd[2]; pid_t pid; if( pipe(pipefd) == -1) { perror("pipe:"); return -1; } pid = fork(); if(pid == 0) { child_process(pipefd); } else if(pid != -1) { father_process(pipefd); } else { perror("fork:"); } return 0; }
运行结果:子进程写数据,父进程读出数据并且打印。
[email protected]:~$ ./pipe_test write pipe: write pipe : 0 read pipe: write pipe : 0 write pipe: write pipe : 1 read pipe: write pipe : 1 write pipe: write pipe : 2 read pipe: write pipe : 2 write pipe: write pipe : 3
注意:匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
如果管道读端被关闭,那么这个时候如果继续write()
管道,会发出信号SIGPIPE
。如果管道写端被关闭,
那么这个时候如果继续read()
管道直接返回0。
2.3 命名管道FIFO
FIFO是first in first out(先进先出)的缩写,FIFO也称为“命名管道”。FIFO是一种特殊类型的管道,它在文件系统中有一个相应的文件,称为管道文件。
FIFO文件可以通过mkfifo()
函数创建。在FIFO文件创建之后,任何一个具有适当权限的进程都可以打开FIFO文件。
mkfifo()函数原型为:
int mkfifo(const char* pathname, mode_t mode);
参数:
pathname
一个FIFO文件的路径名。
mode
与普通文件creat()
函数中的mode参数相同。
返回值:
如果要创建的文件已经存在,返回-1, errno
为EEXIST
错误, 成功返回0。
一般文件的I/O函数都可以用于FIFO,如open()、read()、write()、close()等等。
源代码fifo_write_test.c
void handle_sig(int sig) { printf("signal pipe\n"); exit(-1); } int main(int argc, char** argv) { int fd; int ret; char buf[128]; int i = 0; /*处理管道信号*/ signal(SIGPIPE, handle_sig); if(mkfifo("./fifo", 0640) == -1) { if(errno != EEXIST) { perror("mkfifo"); return -1; } } /*只写方式打开管道*/ fd = open("./fifo", O_WRONLY); if(fd == -1) { perror("open"); return -1; } while(1) { sprintf(buf, "data %d", i++); /*往管道写数据*/ ret = write(fd, buf, strlen(buf)); printf("write fifo [%d] %s\n", ret, buf); sleep(1); } return 0; }
源码中,通过mkfifo来创建FIFO文件,并且以只写 的方式打开,只有当两边的管道都打开的时候才能写进去,否则阻塞在write()
函数上,如果管道另一端打开后被关闭,那么这个时候如果继续write()
FIFO管道,会发出信号SIGPIPE
.
源代码fifo_read_test.c
int main(int argc, char** argv) { int fd; int ret; char buf[128]; if(mkfifo("./fifo", 0640) == -1) { if(errno != EEXIST) /*如果错误类型是fifo文件已经存在,则继续执行*/ { perror("mkfifo"); return -1; } } /*以只读方式打开管道*/ fd = open("./fifo", O_RDONLY); if(fd == -1) { perror("open"); return -1; } while(1) { memset(buf, 0, sizeof(buf)); /*读管道*/ ret = read(fd, buf, sizeof(buf) - 1); printf("read fifo [%d] : %s\n", ret, buf); sleep(1); } return 0; }
源代码中,通过mkfifo()
函数创建FIFO管道,如果已经存在那么就直接只读方式打开,如果另一端没有被打开,则阻塞在read()
函数上,如果另一端打开后关闭,则read()
一直读到EOF也就是0个字节。
2.4 管道容量
管道有它的容量大小,通过man 7 pipe来查看。默认情况下为64k字节,也可以通过fcntl函数来查看:
//在任何头文件包含之前添加这个宏定义 int fcntl(int fd, int cmd, ... /* arg */ ); /* cmd:F_GETPIPE_SZ 获取管道容量大小,设置管道容量大小F_SETPIPE_SZ */
例如:
int main() { if(mkfifo("./fifo", 0640) == -1) { if(errno != EEXIST) { perror("mkfifo"); return -1; } } /*只写方式打开管道*/ int fd = open("./fifo", O_WRONLY | O_NONBLOCK); if(fd == -1) { perror("open"); return -1; } printf("pipe size %d\n", fcntl(fd, F_GETPIPE_SZ)); fcntl(fd, F_SETPIPE_SZ, 4096 * 3); /*必须填写4096的整数倍*/ printf("pipe size %d\n", fcntl(fd, F_GETPIPE_SZ)); return 0; }
2.5 注意事项
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1.如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。讲信号时会讲到怎样使SIGPIPE信号不终止进程。
4.如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。