读取与写入FIFO
使用O_NONBLOCK模式会影响作用在FIFO上的read与write调用的行为。
在一个空的阻塞FIFO(例如,没有使用O_NONBLOCK打开的)上的read调用将会等待直到有数据可以读取。相反,在非阻塞且没有数据的FIFO上进行read调用将会返回0字节。
在一个完全阻塞的FIFO上的write调用将会等待直到数据可以写入。在一个不能全部接受所有将要写入数据的FIFO上的write调用将会: 如果请求PIPE_BUF字节或是小于且数据不能写入时将会失败。 如果请求大于PIPE_BUF字节将会写入部分数据,返回实际写入的字节数据,其值为0。
FIFO的大小是一个重要的考虑因素。在每次可以有多少数据在FIFO中存在一个系统相关的限制。这就是#define PIPE_BUF值,通常、定义在limits.h中。在Linux与许多其他的类Unix系统,这个值通常为4096字节,但是在某些系统上,这个值可以小至512字节。系统默认在一个使用O_WRONLY模式打开的FIFO上执行PIPE_BUF或是更少字节的写入操作,其结果则是全部写入或是没有任何写入。
尽管这个限制在单一FIFO写入端与单一FIFO读取端的情况下并不是十分重要,但是在使用一个FIFO允许多个程序向一个FIFO读取端发送请求的情况下则是十分常见的。如果多个不同的程序同时尝试向FIFO写入数据,由不同程序来的数据块交错在一起的情况是很严重的,每一个write操作都应是原子的。你认为呢?
然而,如是我们保证我们所有的write请求都发送到阻塞FIFO,而且在大小上小于PIPE_BUF字节,系统将会保证数据不会交错在一起。通常,严格限制通过FIFO发送PIPE_BUF字节大小的数据块是一个好主意,除非我们只使用一个读取端与一个写入端。
试验--使用FIFO的进程交互
要演示不相关的进程如何使用有名管道进行交互,我们需要两个单独的程序,fifo3.c与fifo4.c。
1 第一个程序是我们的生产者程序。如果需要他会创建管道,然后尽快向其中写入数据。
注意,为了演示的目的,我们并不介意数据是什么是,所以我们并不初始化一个缓冲区。
#include #include #include #include #include #include #include #include
#define FIFO_NAME "/tmp/my_fifo" #define BUFFER_SIZE PIPE_BUF #define TEN_MEG (1024 * 1024 * 10)
int main() { int pipe_fd; int res; int open_mode = O_WRONLY; int bytes_sent = 0; char buffer[BUFFER_SIZE + 1];
if (access(FIFO_NAME, F_OK) == -1) { res = mkfifo(FIFO_NAME, 0777); if (res != 0) { fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME); exit(EXIT_FAILURE); } }
printf("Process %d opening FIFO O_WRONLY\n", getpid()); pipe_fd = open(FIFO_NAME, open_mode); printf("Process %d result %d\n", getpid(), pipe_fd);
if (pipe_fd != -1) { while(bytes_sent < TEN_MEG) { res = write(pipe_fd, buffer, BUFFER_SIZE); if (res == -1) { fprintf(stderr, "Write error on pipe\n"); exit(EXIT_FAILURE); } bytes_sent += res; } (void)close(pipe_fd); } else { exit(EXIT_FAILURE); }
printf("Process %d finished\n", getpid()); exit(EXIT_SUCCESS); }
2 我们的第二个程序,消费者,比较简单。他由FIFO中读取并丢弃数据。
#include #include #include #include #include #include #include #include
#define FIFO_NAME "/tmp/my_fifo" #define BUFFER_SIZE PIPE_BUF
int main() { int pipe_fd; int res;
int open_mode = O_RDONLY; char buffer[BUFFER_SIZE + 1]; int bytes_read = 0;
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n",getpid()); pipe_fd = open(FIFO_NAME, open_mode); printf("Process %d result %d\n",getpid(),pipe_fd);
if(pipe_fd != -1) { do { res = read(pipe_fd, buffer, BUFFER_SIZE); bytes_read += res; }while(res > 0); (void)close(pipe_fd); } else { exit(EXIT_FAILURE); } printf("Process %d finished, %d bytes read\n", getpid(), bytes_read); exit(EXIT_SUCCESS); }
当我们同时运行这些程序,使用time命令来统计读取的时间,我们会得到下面的输出:
$ ./fifo3 & [1] 375 Process 375 opening FIFO O_WRONLY $ time ./fifo4 Process 377 opening FIFO O_RDONLY Process 375 result 3 Process 377 result 3 Process 375 finished Process 377 finished, 10485760 bytes read real 0m0.053s user 0m0.020s sys 0m0.040s [1]+ Done fifo3
工作原理
两个程序都以阻塞模式使用FIFO。我们首先启动fifo3,他会阻塞,等一个读取端打开FIFO。当fifo4被启动时,写入端就会停止阻塞并且开始向管道写入数据。同时,读取端开始由管道读取数据。
time命令的输出向我们展出读取端只运行十分之一秒,读取进程中的10兆字节。这向我们展示出,管道是程序之间交换数据的一个有效方式。
高级主题:使用FIFO的客户端/服务器
作为我们最后关于FIFO的讨论,我们将会探讨如何使用有名管道来构建一个非常简单的客户端/服务器程序。我们希望有一个服务器进程接受请求,处理请求,并将结果数据返回给请求方:客户端。
我们将会允许多个客户端进程向服务器发送数据。为了简单的目的,我们认为将要被处理的数据可以被分为多个数据块,而每一个数据块的尺寸小于PIPE_BUF。当然,我们可以以多种方式来实现这个系统,但是为了演示如何使用有名管道,我们只有考虑管道一种方法。
因为服务器每次只处理一个信息块,具有一个被服务器读取而可以被每一个客户端写入的FIFO是很合理的。通过以阻塞模式打开FIFO,服务器与客户端可以在需要的时候自动阻塞。
将处理的数据返回给客户端是非常困难的。我们将会考虑第二个管道,为了返回的数据,每一个客户端有一个管道。通过将客户端的进程标识符(PID)以原始数据的方式发送给服务器,双方可以使用这个标识符为返回的管道生成一个唯一的名字。
试验--客户端/服务器程序的例子。
1 首先,我们需要一个头文件,client.h,这个文件定义了客户端与服务器所需要的通用数据定义,同时也包含所需要的系统头文件。
#include #include #include #include #include #include #include #include
#define SERVER_FIFO_NAME "/tmp/serv_fifo" #define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"
#define BUFFER_SIZE 20
struct data_to_pass_st { pid_t client_pid; char some_data[BUFFER_SIZE-1]; };
2 现在我们来看一下服务器程序,server.c。在这一部分,我们创建然后打开服务器管道,将其设置为只读,阻塞模式。在休眠之后(为了演示的目的),服务器读取客户端所发送的数据,这些数据具有data_to_pass_st结构。
#include "client.h" #include
int main() { int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data; int read_res; char client_fifo[256]; char *tmp_char_ptr;
mkfifo(SERVER_FIFO_NAME, 0777); server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY); if(server_fifo_fd == -1) { fprintf(stderr, "Server fifo failure\n"); exit(EXIT_FAILURE); }
sleep(0);
do { read_res = read(server_fifo_fd, &my_data, sizeof(my_data)); if(read_res > 0) {
3 在下一步,我们在刚由客户端所读取的数据上进行一些处理:我们将some_data中的所有字符转换为大写并且组合CLIENT_FIFO_NAME与所接收到的client_pid。
tmp_char_ptr = my_data.some_data; while(*tmp_char_ptr) { *tmp_char_ptr = toupper(*tmp_char_ptr); tmp_char_ptr++; } sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);
4 然后我们将处理后的数据发送回去,以只读阻塞模式打开客户端管道。最后,我们通过关闭文件然后删除FIFO的方式关闭服务器FIFO。
client_fifo_fd = open(client_fifo, O_WRONLY); if(client_fifo_fd != -1) { write(client_fifo_fd, &my_data, sizeof(my_data)); close(client_fifo_fd); } } }while(read_res > 0); close(server_fifo_fd); unlink(SERVER_FIFO_NAME); exit(EXIT_SUCCESS); }
5 下面是客户端,client.c。如果服务器FIFO已经作为一个文件存在,程序的第一个部分则打开这个服务器FIFO。然后获得其本身的进程ID,进程ID会构成发往服务器的数据。客户端FIFO被创建,并为下一部分作好准备。
#include #include
int main() { int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data; int times_to_send; char client_fifo[256];
server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY); if(server_fifo_fd == -1) { fprintf(stderr, "Sorry, no server\n"); exit(EXIT_FAILURE); }
my_data.client_pid = getpid(); sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid); if(mkfifo(client_fifo, 0777) == -1) { fprintf(stderr, "Sorry, can not make %s\n", client_fifo); exit(EXIT_FAILURE); } 6 对于每一个这样的五次循环,客户端数据发往服务器。然后客户端FIFO被打开,并且读取返回的数据。最后,服务器FIFO被关闭而且客户端FIFO被由内存中移除。
for(times_to_send = 0;times_to_send < 5;times_to_send++) { sprintf(my_data.some_data, "Hello from %d", my_data.client_pid); printf("%d send %s, ", my_data.client_pid, my_data.some_data); write(server_fifo_fd, &my_data, sizeof(my_data)); client_fifo_fd = open(client_fifo, O_RDONLY); if(client_fifo_fd != -1) { if(read(client_fifo_fd, &my_data, sizeof(my_data)) > 0) { printf("received: %s\n", my_data.some_data); } close(client_fifo_fd); } } close(server_fifo_fd); unlink(client_fifo); exit(EXIT_SUCCESS); }
要测试这个程序,我们需要运行一个服务器拷贝与多个客户端。为了使得他们近似同时启动,我们使用下面的shell命令:
$ server & $ for i in 1 2 3 4 5 do client & done $
这会启动一个服务器进程与五个客户端进程。由客户端所得到的输出如下面所示:
531 sent Hello from 531, received: HELLO FROM 531 532 sent Hello from 532, received: HELLO FROM 532 529 sent Hello from 529, received: HELLO FROM 529 530 sent Hello from 530, received: HELLO FROM 530 531 sent Hello from 531, received: HELLO FROM 531 532 sent Hello from 532, received: HELLO FROM 532
正如我们所看到的,不同的客户端请交错在一起,而每一个客户端都会得到返回给他的正确的处理数据。注意,这些请求的交错在随机的,而所收到的请求的顺序在不同的机器之间以及在同一个机器的不同运行之间都会不同。
工作原理
现在我们将会解释客户端队列与服务器操作的交互,有些内容我们到目前为止还没有涉及。
服务器以只读与阻塞模式创建FIFO。他完成这个操作并且等待第一个客户端打开同样的FIFO用于写入。同时,服务器进程会解除阻塞并且执行sleep调用,所以客户端的写入操作会进行排除操作。(在真实的程序中,sleep调用应被移除;在这里我们只是用他来演示具有多个并发客户端的程序可以正确操作)
同时,在客户端打开FIFO之后,他创建自己唯一的有名FIFO用于读取由服务器返回的数据。然后客户端将数据发送到服务器(如果管道已满或是服务器仍在休眠则阻塞自己),然后阻塞在其自己的FIFO上的read操作,等待回复。
一旦收到客户端所发送的数据,服务器会处理这些数据,打开客户端管道用于写入,并且将数据发送回客户端,这会使得客户端解除阻塞。当客户端解除阻塞以后,他可以由他的管道读取由服务器所发送的数据。
整个过程不断重复,直到最后一个客户端关闭服务器管道,使得服务器的read调用失败,因为并没有进程使得服务器管道打开用于写入。如果这是一个需要等待更多客户端的真实服务器进程,我们需要用下面的任一种方法进行相应的修改:
打开一个到其自身服务器管道的文件描述符,从而read操作总是阻塞而不是返回0。 当read返回0字节时关闭并重新打开服务器管道,从而服务器进程会在open调用后阻塞等待客户端,就如同他第一次启动时一样。
这些技术都在使用有名管道重写的CD数据库程序中进行了演示 |