进程间通信(5) - 命名管道(FIFO)
1. 前言
本篇文章的所有例子,基于RHEL6.5平台。前一篇文章介绍了匿名管道。点此链接。2.介绍
管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。POSIX标准中的FIFO又名有名管道或命名管道。我们知道前面讲述的POSIX标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。FIFO最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。
所以,FIFO的两个特性:
**和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流。
**和管道不同的是,FIFO可以支持任意两个进程间的通信。
3.mknod函数
<span style="font-family:Arial;font-size:12px;">#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int mknod(const char *pathname, mode_t mode, dev_t dev);</span>使用方法: mknod 管道名称 p
4.mkfifo函数
<span style="font-family:Arial;font-size:12px;">#include<sys/types.h> #include<sys/stat.h> //成功则返回0,失败返回-1 int mkfifo(const char * pathname,mode_t mode);</span>使用方法:mkfifo -m 权限 管道名称
参数说明:
pathname:一个Linux路径名,它是FIFO的名字。即每个FIFO与一个路径名相对应。
第二个参数与打开普通文件的open()函数中的mode 参数相同,指定的文件权限位。即创建该FIFO时,指定用户的访问权限,有以下值:S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。
如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。
mkfifo函数默认指定O_CREAT | O_EXECL方式创建FIFO,如果创建成功,直接返回0。如果FIFO已经存在,则创建失败,会返回-1并且errno置为EEXIST。对于其他错误,则置响应的errno值;
当创建一个FIFO后,它必须以只读方式打开或者只写方式打开,所以可以用open函数,当然也可以使用标准的文件I/O打开函数,例如fopen来打开。由于FIFO是半双工的,所以不能够同时打开来读和写。
其实一般的文件I/O函数,如read,write,close,unlink都可用于FIFO。对于管道和FIFO的write操作总是会向末尾添加数据,而对他们的read则总是会从开头数据,所以不能对管道和FIFO中间的数据进行操作,因此对管道和FIFO使用lseek函数,是错误的,会返回ESPIPE错误。
mkfifo的一般使用方式是:通过mkfifo创建FIFO,然后调用open,以读或者写的方式之一打开FIFO,然后进行数据通信。
下面是FIFO的一个简单的测试代码:
<span style="font-family:Arial;font-size:12px;">#include <iostream> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> using namespace std; #define FIFO_PATH "/root/fifo" int main() { if (mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST) { cout<<"create fifo failed."<<endl; return -1; } if (fork() == 0) { int readfd = open(FIFO_PATH, O_RDONLY); cout<<"child open fifo success."<<endl; char buf[256]; read(readfd, buf, sizeof(buf)); cout<<"receive message from pipe: "<<buf<<endl; close(readfd); exit(0); } sleep(3); int writefd = open(FIFO_PATH, O_WRONLY); cout<<"parent open fifo success."<<endl; char *temp = "hello world"; write(writefd, temp, strlen(temp) + 1); close(writefd); }</span>输出:
[root@MiWiFi-R1CM csdnblog]# ./a.out
parent open fifo success.
child open fifo success.
receive message from pipe: hello world
由上面的运行结果可以看到,子进程以读方式open的操作会阻塞到父进程以写方式open;关于这一点以及read和write的操作会在后面管道和FIFO的属性部分进行介绍;
POSIX标准不仅规定了对mkfifo IPC的支持,还包括了对mkfifo shell命令的支持,所以符合POSIX标准的UNIX中都含有mkfifo命令来创建有名管道。
[root@MiWiFi-R1CM csdnblog]# mkfifo fifotest[root@MiWiFi-R1CM csdnblog]# echo "hello world" > fifotest &
[1] 2726
[root@MiWiFi-R1CM csdnblog]# cat < fifotest
hello world
[1]+ Done echo "hello world" > fifotest
这里在第二行最后加上‘&’使进程转到后台运行,是因为FIFO以只写方式打开需要阻塞到FIFO以只读方式打开为止,所以必须要作为后台程序运行,否则进程会阻塞在前端,无法再进行相关输入。
5.mknod与mkfifo区别
mknod系统调用会产生由参数path所指定的文件,生成文件类型和访问权限由参数mode决定。
在很多unix的版本中有一个C库函数mkfifo,与mknod不同的是多数情况下mkfifo不要求用户有超级用户的权限。
利用命令创建命名管道p1.
#mkfifo -m 0644 p1
#mknod p2 p
#ll
<span style="font-family:Arial;font-size:12px;">#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> main() { if(mkfifo("p1",0644) < 0) { perror("mkfifo"); exit(-1); } return; }</span>
6.管道打开规则
有名管道比管道多了一个打开操作:open。
由于在POSIX标准中,管道和FIFO都是通过文件描述符来进行操作的,默认的情况下,对他们的操作都是阻塞的,当然也可以通过设置来使对他们的操作变成非阻塞的。我们都知道可以有两种方式来设置一个文件描述符为O_NONBLOCK非阻塞的:
--调用open时,指定O_NONBLOCK标志。例如:
int fd = open(FILE_NAME, O_RDONLY | O_NONBLOCK);
--通过fcntl文件描述符控制操作函数,对一个已经打开的描述符启用O_NONBLOCK标志。其中对于管道必须使用这种方式。示例如下:
int flag;
flag = fcntl(fd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
下图主要说明了对管道和FIFO的各种操作在阻塞和非阻塞状态下的不同,这张图对对于理解和使用管道和FIFO是非常重要的。
从上图我们看到关于管道和FIFO的读出和写入的若干规则,主要需要注意的有以下几点:
· 以只读方式open FIFO时,如果FIFO还没有以只写方式open,那么在阻塞模式下,该操作会阻塞到FIFO以只写方式open为止。
· 以只写方式open FIFO时,如果FIFO还没有以只读方式open,那么在阻塞模式下,该操作会阻塞到FIFO以只读方式open为止。
· 从空管道或空FIFO中read,如果管道和FIFO已打开来写,在阻塞模式下,那么该操作会阻塞到管道或FIFO有数据为止,或管道或FIFO不再以写方式打开。如果管道和FIFO没有打开来写,那么该操作会返回0;
· 向管道或FIFO中write,如果管道或FIFO没有打开来读,那么内核会产生SIGPIPE信号,默认情况下,该信号会终止该进程。
另外对于管道和FIFO还需要说明的若干规则如下:
· 如果请求write的数据的字节数小于等于PIPE_BUF(POSIX关于管道和FIFO大小的限制值),那么write操作可以保证是原子的,如果大于PIPE_BUF,那么就不能保证了。
那么由此可知write的原子性是由写入数据的字节数是否小于等于PIPE_BUF决定的,和是不是O_NONBLOCK没有关系。下面是在阻塞和非阻塞情况下,write不同大小的数据的操作结果:
在阻塞的情况下:
· 如果write的字节数小于等于PIPE_BUF,那么write会阻塞到写入所有数据,并且 写入操作是原子的。
· 如果write的字节数大于PIPE_BUF,那么write会阻塞到写入所有数据,但写入操作不是原子的,即write会根据当前缓冲区剩余的大小,写入相应的字节数,然后等待下一次有空余的缓冲区,这中间可能会有其他进程进行write操作。
在非阻塞的情况下:
· 如果write的字节数小于等于PIPE_BUF,且管道或FIFO有足以存放要写入数据大小的空间,那么就写入所有数据;
· 如果write的字节数小于等于PIPE_BUF,且管道或FIFO没有足够存放要写入数据大小的空间,那么就会立即返回EAGAIN错误。
· 如果write的字节数大于PIPE_BUF,且管道或FIFO有至少1B的空间,那么就内核就会写入相应的字节数,然后返回已写入的字节数;
· 如果write的字节数大于PIPE_BUF,且管道或FIFO无任何的空间,那么就会立即返回EAGAIN错误。
对FIFO打开规则的验证(主要验证写打开对读打开的依赖性)
<span style="font-family:Arial;font-size:12px;">#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define FIFO_SERVER "/tmp/fifoserver" int handle_client(char*); int main(int argc, char** argv) { int r_rd; int w_fd; pid_t pid; if ((mkfifo(FIFO_SERVER, O_CREAT | O_EXCL) < 0) && (errno != EEXIST)) printf("cannot create fifoserver\n"); handle_client(FIFO_SERVER); return 0; } int handle_client(char* arg) { int ret; ret = w_open(arg); switch (ret) { case 0: { printf("open %s error\n", arg); printf("no process has the fifo open for reading\n"); return -1; } case -1: { printf("something wrong with open the fifo except for ENXIO"); return -1; } case 1: { printf("open server ok\n"); return 1; } default: { printf("w_no_r return ----\n"); return 0; } } unlink(FIFO_SERVER); } int w_open(char*arg) //0 open error for no reading //-1 open error for other reasons //1 open ok { if (open(arg, O_WRONLY | O_NONBLOCK, 0) == -1) { if (errno == ENXIO) { return 0; } else return -1; } return 1; }</span>输出:
[root@MiWiFi-R1CM csdnblog]# ./a.out
open /tmp/fifoserver error
no process has the fifo open for reading
7.管道读写规则
通过open打开,默认是阻塞方式打开,如果open指定O_NONBLOCK则以非阻塞打开。
O_NONBLOCK和O_NDELAY所产生的结果都是使I/O变成非搁置模式(non-blocking),在读取不到数据或是写入缓冲区已满会马上return,而不会搁置程序动作,直到有数据或写入完成。
它们的差别在于设立O_NDELAY会使I/O函式马上回传0,但是又衍生出一个问题,因为读取到档案结尾时所回传的也是0,这样无法得知是哪种情况;因此,O_NONBLOCK就产生出来,它在读取不到数据时会回传-1,并且设置errno为EAGAIN。
不过需要注意的是,在GNU C中O_NDELAY只是为了与BSD的程序兼容,实际上是使用O_NONBLOCK作为宏定义,而且O_NONBLOCK除了在ioctl中使用,还可以在open时设定。
<span style="font-family:Arial;font-size:12px;">#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> main() { int fd; if((fd = open("p1",O_RRONLY,0)) < 0)//只读打开管道 // if((fd = open("p1",O_WRONLY,0)) < 0)//只写打开管道 { perror("open"); exit(-1); } printf("open fifo p1 for write success!\n"); close(fd); }</span>
从FIFO中读取数据:
约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。
·如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
·对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
·读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
·如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
向FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
对于设置了阻塞标志的写操作:
·当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
·当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
对于没有设置阻塞标志的写操作:
·当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
·当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;
对FIFO读写规则的验证:
下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。
#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define FIFO_SERVER "/tmp/fifoserver" int main(int argc, char** argv) //参数为即将写入的字节数 { int fd; char w_buf[4096 * 2]; int real_wnum; memset(w_buf, 0, 4096 * 2); if ((mkfifo(FIFO_SERVER, O_CREAT | O_EXCL)<0) && (errno != EEXIST)) printf("cannot create fifoserver\n"); if (fd == -1) { if (errno == ENXIO) printf("open error; no reading process\n"); } fd = open(FIFO_SERVER, O_WRONLY | O_NONBLOCK, 0); //设置非阻塞标志 //fd=open(FIFO_SERVER,O_WRONLY,0); //设置阻塞标志 real_wnum = write(fd, w_buf, 2048); if (real_wnum == -1) { if (errno == EAGAIN) printf("write to fifo error; try later\n"); } else { printf("real write num is %d\n", real_wnum); } real_wnum = write(fd, w_buf, 5000); //5000用于测试写入字节大于4096时的非原子性 //real_wnum=write(fd,w_buf,4096); //4096用于测试写入字节不大于4096时的原子性 if (real_wnum == -1) { if (errno == EAGAIN) printf("try later\n"); } return 0; }没有任何东西输出:
[root@MiWiFi-R1CM csdnblog]# ./a.out
// 下面的程序是与上面的程序一起测试写FIFO的规则,第一个命令行参数是请求从FIFO读出的字节数
<span style="font-family:Arial;font-size:12px;">#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define FIFO_SERVER "/tmp/fifoserver" main(int argc,char** argv) { char r_buf[4096*2]; int fd; int r_size; int ret_size; r_size=atoi(argv[1]); printf("requred real read bytes %d\n",r_size); memset(r_buf,0,sizeof(r_buf)); fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0); //fd=open(FIFO_SERVER,O_RDONLY,0); //在此处可以把读程序编译成两个不同版本:阻塞版本及非阻塞版本 if(fd==-1) { printf("open %s for read error\n"); exit(); } while(1) { memset(r_buf,0,sizeof(r_buf)); ret_size=read(fd,r_buf,r_size); if(ret_size==-1) if(errno==EAGAIN) printf("no data avlaible\n"); printf("real read bytes %d\n",ret_size); sleep(1); } pause(); unlink(FIFO_SERVER); }</span>
程序应用说明:
把读程序编译成两个不同版本:
·阻塞读版本:br
·以及非阻塞读版本nbr
把写程序编译成两个四个版本:
· 非阻塞且请求写的字节数大于PIPE_BUF版本:nbwg
· 非阻塞且请求写的字节数不大于PIPE_BUF版本:版本nbw
· 阻塞且请求写的字节数大于PIPE_BUF版本:bwg
· 阻塞且请求写的字节数不大于PIPE_BUF版本:版本bw
下面将使用br、nbr、w代替相应程序中的阻塞读、非阻塞读
验证阻塞写操作:
1 当请求写入的数据量大于PIPE_BUF时的非原子性:
nbr 1000
bwg
2 当请求写入的数据量不大于PIPE_BUF时的原子性:
nbr 1000
bw
验证非阻塞写操作:
3 当请求写入的数据量大于PIPE_BUF时的非原子性:
nbr 1000
nbwg
4 请求写入的数据量不大于PIPE_BUF时的原子性:
nbr 1000
nbw
不管写打开的阻塞标志是否设置,在请求写入的字节数大于4096时,都不保证写入的原子性。但二者有本质区别:
对于阻塞写来说,写操作在写满FIFO的空闲区域后,会一直等待,直到写完所有数据为止,请求写入的数据最终都会写入FIFO;
而非阻塞写则在写满FIFO的空闲区域后,就返回(实际写入的字节数),所以有些数据最终不能够写入。
对于读操作的验证则比较简单,不再讨论。
8.通信模式
命名管道提供了两种基本通信模式:字节模式和消息模式。在字节模式中,消息以一个连续的字节流的形式,在客户机与服务器之间流动。这意味着,对客户机应用和服务器应用来说,在任何一个特定的时间段内,它们不能准确知道有多少字节从管道中读入或者写入管道。因此,在一方写入某个数量的字节,并不表示在另一方会读出等量的字节。这样一来,客户机和服务器在传输数据的时候,便不必关心数据的内容。而在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发。每次在管道上发出了一条消息后,它必须作为一条完整的消息读入。
命名管道最大的特点便是建立一个简单的客户机/服务器程序设计体系。在这个体系结构中,在客户机与服务器之间,数据既可单向传递,亦可双向流动。这一点相当重要,因为我们可以自由地收发数据,无论应用程序是一个客户机还是一个服务器。对命名管道服务器和客户机来说,两者最大的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接受管道客户机的连接请求。对一个客户机应用来说,它只能同一个现成的命名管道服务器建立连接。在客户机应用和服务器应用之间,一旦建好连接,两个进程都能对标准的Wi n 3 2函数,在管道上进行数据读取与写入。这些包括R e a d F i l e和Wr i t e F i l e等。
要想实现一个命名管道服务器,要求必须开发一个应用程序,通过它创建命名管道的一个或多个“实例”,再由客户机进行访问。对服务器来说,管道实例实际就是一个句柄,用于从本地或远程客户机应用接受一个连接请求。按下述步骤行事,便可写出一个最基本的服务器应用:
1) 使用API函数CreatNamedPipe,创建一个命名管道实例句柄。
2) 使用API函数ConnectNamedPipe,在命名管道实例上监听客户机连接请求。
3) 分别使用ReadFile和WriteFile这两个A P I函数,从客户机接收数据,或将数据发给客户机。
4) 使用API函数DisconnectNamedPipe,关闭命名管道连接。
5) 使用API函数CloseHandle,关闭命名管道实例句柄。
实现一个命名管道客户机时,要求开发一个应用程序,令其建立与某个命名管道服务器的连接。注意客户机不可创建命名管道实例。然而,客户机可打开来自服务器的、现成的实例。下述步骤讲解了如何编写一个基本的客户机应用:
1) 用API函数WaitNamedPipe,等候一个命名管道实例可供自己使用。
2) 用API函数CreatFile,建立与命名管道的连接。
3) 用API函数WriteFile和ReadFile,分别向服务器发送数据,或从中接收数据。
4) 用API函数CloseHandle,关闭打开的命名管道会话
9.管道和FIFO的限制
系统内核对于管道和FIFO的唯一限制为:OPEN_MAX和PIPE_BUF;
OPEN_MAX:一个进程在任意时刻可以打开的最大描述符数。
PIPE_BUF标识一个管道可以原子写入管道和FIFO的最大字节数,并不是管道或FIFO的容量。
关于这两个系统限制,POSIX标准中都有定义的不变最小值:POSIX_OPEN_MAX和_POSIX_PIPE_BUF,这两个宏是POSXI标准定义的编译时确定的值,他们是标准定义的且不会改变的,POSIX标准关于这两个值的限制为:
cout<<_POSIX_OPEN_MAX<<endl;
cout<<_POSIX_PIPE_BUF<<endl;
//运行结果为:
20
512
我们都知道,关于POSIX的每个不变最小值都有一个具体的系统的实现值,这些是实现值由具体的系统决定,通过调用以下函数在运行时确定这个实现值:
#include <unistd.h>
//成功返回具体的值,失败返回-1
long sysconf(int name);
long fpathconf(int filedes, int name);
long pathconf(char *path, int name);
其中sysconf是用于返回系统限制值,这些值是以_SC_开头的常量,pathconf和fpathconf是用于返回与文件和目录相关的运行时的限制值,这些值都是以_PC_开头的常量;
下面是在Linux 2.6.18下的测试代码:
cout<<sysconf(_SC_OPEN_MAX)<<endl;
cout<<pathconf(FIFO_PATH, _PC_PATH_MAX)<<endl;
//运行结果为:
1024
4096
当然上面两个系统限制值的具体实现值也可以通过ulimit命令来查看
[root@idcserver program]# ulimit -a
open files (-n) 1024
pipe size (512 bytes, -p) 8
这两个值在Linux 2.6.18下都是不允许修改的,也是没有必要修改的。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。