进程间通信(九)
管道
下面是管道实现文件,pipe_imp.c,其中有客户端与服务器端函数。
试验--管道实现头文件
1 首先是#include:
#include "cd_data.h"
#include "cliserv.h"
2 我们定义一些在此文件的其他函数中所需要的值:
static int server_fd = -1;
static pid_t mypid = 0;
static char client_pipe_name[PATH_MAX + 1] = {‘/0’};
static int client_fd = -1;
static int client_write_fd = -1;
服务器端函数
下面我们需要探讨服务器端函数。下面的试验部分向我们展示了打开与关闭有名管道以及由客户端读取消息的函数。第二个试验部分则显示了打开,发送与关闭客户端管道的代码,客户端管道基于进程ID。
试验--服务器函数
1 server_starting例程创建服务器要由其中读取命令的有名管道。然后打开这个管道用于读取。open操作将会阻塞,直到一个客户端打开这个管道用于写入。我们使用阻塞模式,从而服务器在等待发送给他的命令时可以执行在管道上执行阻塞读取。
int server_starting(void)
{
#if DEBUG_TRACE
printf(“%d :- server_starting()/n”, getpid());
#endif
unlink(SERVER_PIPE);
if (mkfifo(SERVER_PIPE, 0777) == -1) {
fprintf(stderr, “Server startup error, no FIFO created/n”);
return(0);
}
if ((server_fd = open(SERVER_PIPE, O_RDONLY)) == -1) {
if (errno == EINTR) return(0);
fprintf(stderr, “Server startup error, no FIFO opened/n”);
return(0);
}
return(1);
}
2 当服务器结束时,他移除有名管道,从而客户端可以检测到没有服务器在运行。
void server_ending(void)
{
#if DEBUG_TRACE
printf(“%d :- server_ending()/n”, getpid());
#endif
(void)close(server_fd);
(void)unlink(SERVER_PIPE);
}
4 下面代码中所显示的read_request_from_client函数将会阻塞服务器管道中的读取,直到有客户端向其中写入消息:
int read_request_from_client(message_db_t *rec_ptr)
{
int return_code = 0;
int read_bytes;
#if DEBUG_TRACE
printf(“%d :- read_request_from_client()/n”, getpid());
#endif
if (server_fd != -1) {
read_bytes = read(server_fd, rec_ptr, sizeof(*rec_ptr));
...
}
return(return_code);
}
4 在没有客户端打开管道用于写入的情况下,read操作会返回0;也就是说,客户端会检测到EOF。然后服务器会关闭管道并重新打开,从而他会在客户端打开这个管道之前阻塞。这与服务器第一次启动时的情况相类似;我们已经重新初始化了服务器。在前面的函数中插入下面这段代码:
if (read_bytes == 0) {
(void)close(server_fd);
if ((server_fd = open(SERVER_PIPE, O_RDONLY)) == -1) {
if (errno != EINTR) {
fprintf(stderr, “Server error, FIFO open failed/n”);
}
return(0);
}
read_bytes = read(server_fd, rec_ptr, sizeof(*rec_ptr));
}
if (read_bytes == sizeof(*rec_ptr)) return_code = 1;
服务器是单进程的,也许会同时服务多个客户端。因为每一个客户端使用一个不同的管道来接收他的响应,服务器需要写入不同的管道来向不同的客户端发送响应。因为文件描述符是限制资源,服务器只在他在有数据要发送的情况下才会打开客户端管道用于写入。
我们将打开,写入与关闭客户端管道分为三个独立的函数。当我们要向搜索返回多个结果时我们需要这样做,从而我们可以打管道一次,写入多个响应,然后再关闭管道。
试验--搭建管道
1 首先,我们打开客户管道:
int start_resp_to_client(const message_db_t mess_to_send)
{
#if DEBUG_TRACE
printf(“%d :- start_resp_to_client()/n”, getpid());
#endif
(void)sprintf(client_pipe_name, CLIENT_PIPE, mess_to_send.client_pid);
if ((client_fd = open(client_pipe_name, O_WRONLY)) == -1) return(0);
return(1);
}
2 消息都是通过调用这个函数来发送的。我们会在稍后来看一下相对应的客户端函数。
int send_resp_to_client(const message_db_t mess_to_send)
{
int write_bytes;
#if DEBUG_TRACE
printf(“%d :- send_resp_to_client()/n”, getpid());
#endif
if (client_fd == -1) return(0);
write_bytes = write(client_fd, &mess_to_send, sizeof(mess_to_send));
if (write_bytes != sizeof(mess_to_send)) return(0);
return(1);
}
3 最后,我们关闭客户端管道:
void end_resp_to_client(void)
{
#if DEBUG_TRACE
printf(“%d :- end_resp_to_client()/n”, getpid());
#endif
if (client_fd != -1) {
(void)close(client_fd);
client_fd = -1;
}
}
客户端函数
填充服务器的是pipe_imp.c中的客户端函数。他们与服务器端函数十分类似,所不同的就是send_mess_to_server函数。
试验--客户端函数
1 在检测服务器可以访问之后,client_starting函数初始化客户端管道:
int client_starting(void)
{
#if DEBUG_TRACE
printf(“%d :- client_starting/n”, getpid());
#endif
mypid = getpid();
if ((server_fd = open(SERVER_PIPE, O_WRONLY)) == -1) {
fprintf(stderr, “Server not running/n”);
return(0);
}
(void)sprintf(client_pipe_name, CLIENT_PIPE, mypid);
(void)unlink(client_pipe_name);
if (mkfifo(client_pipe_name, 0777) == -1) {
fprintf(stderr, “Unable to create client pipe %s/n”,
client_pipe_name);
return(0);
}
return(1);
}
2 client_ending函数关闭文件描述符并且删除多余的有名管道:
void client_ending(void)
{
#if DEBUG_TRACE
printf(“%d :- client_ending()/n”, getpid());
#endif
if (client_write_fd != -1) (void)close(client_write_fd);
if (client_fd != -1) (void)close(client_fd);
if (server_fd != -1) (void)close(server_fd);
(void)unlink(client_pipe_name);
}
3 send_mess_to_server函数通过服务器管道发送请求:
int send_mess_to_server(message_db_t mess_to_send)
{
int write_bytes;
#if DEBUG_TRACE
printf(“%d :- send_mess_to_server()/n”, getpid());
#endif
if (server_fd == -1) return(0);
mess_to_send.client_pid = mypid;
write_bytes = write(server_fd, &mess_to_send, sizeof(mess_to_send));
if (write_bytes != sizeof(mess_to_send)) return(0);
return(1);
}
与我们在前面所看到的服务器端函数类似,客户端使用三个函数由服务器得到返回的结果。
试验--得到服务器结果
1 这个客户端函数来监听服务器响应。他使用只读取模式打开一个客户端管道,然后作为只写模式响应这个管道文件。
int start_resp_from_server(void)
{
#if DEBUG_TRACE
printf(“%d :- start_resp_from_server()/n”, getpid());
#endif
if (client_pipe_name[0] == ‘/0’) return(0);
if (client_fd != -1) return(1);
client_fd = open(client_pipe_name, O_RDONLY);
if (client_fd != -1) {
client_write_fd = open(client_pipe_name, O_WRONLY);
if (client_write_fd != -1) return(1);
(void)close(client_fd);
client_fd = -1;
}
return(0);
}
2 下面是由服务器得到匹配数据库记录的主要read操作:
int read_resp_from_server(message_db_t *rec_ptr)
{
int read_bytes;
int return_code = 0;
#if DEBUG_TRACE
printf(“%d :- read_resp_from_server()/n”, getpid());
#endif
if (!rec_ptr) return(0);
if (client_fd == -1) return(0);
read_bytes = read(client_fd, rec_ptr, sizeof(*rec_ptr));
if (read_bytes == sizeof(*rec_ptr)) return_code = 1;
return(return_code);
}
3 最后,下面的客户端函数标识服务器响应的结束:
void end_resp_from_server(void)
{
#if DEBUG_TRACE
printf(“%d :- end_resp_from_server()/n”, getpid());
#endif
/* This function is empty in the pipe implementation */
}
工作原理
在start_resp_from_server中用于写入的客户端open操作:
client_write_fd = open(client_pipe_name, O_WRONLY);
用来在阻止当服务器需要快速响应客户端的多个请求所引起的竞争条件。
要详细的解释这一点,考虑下面的事件序列:
1 客户端向服务器发送请求
2 服务器读取请求,打开客户端管道并且发送响应,但是在他关闭客户端之前会被挂起。
3 客户端打开他的管道用于读取,读取第一个响应然后关闭管道。
4 然后客户端发送一个新的命令并且打开客户端管道用于读取。
5 服务器继续运行,关闭其客户端管道。
不幸的时,此时客户端正尝试读取管道,查找其下一个请求的响应,但是read操作会返回0字节,因为并没有进程使得客户端打开用于写入。
通过允许客户打开其管道用于读写,从而移除了重复重新打开管道的需要,我们可以避免这个竞争条件。注意,客户端并没有写入管道,从而并没有垃圾数据的危险。
程序总结
现在我们将我们的CD数据库程序分为客户端与服务器两部分,从而可以使得我们独立开发用户界面与底层数据库技术。我们可以看到一个定义良好的数据库接口可以使得程序的每一个主要元素都充分利用计算机资源。如果我们更为深入一些,我们可以将管道实现改变为网络实现,并且使用一个数据库服务器。我们将会在第15章了解更多的关于网络的内容。
小结
在这一章,我们了解了在进程之间使用管道来传递数据。首先,我们了解了无名管道,使用popen与pipe调用来进行创建,然后讨论了如何使用管道与dup调用,我们可以将数据由一个程序传递到另一个的标准输入。然后我们了解了有名管道,并且了解了如何在无关的程序之间传递数据。最后,我们实现了一个简单的客户端/服务器例子,使用FIFO提供给我们的不仅是处理同步,还有双向的数据流。