关于半双工管道pipe
管道的典型用途是为两个不同进程(父进程与子进程)提供进程间的通信手段
当我们需要(假设现在提供的pipe是半双工的)双向数据交流的时候,我们必须创建两个管道:
整个管道只存在一个缓冲区, (在任意一个描述符上)写入管道的任何数据都添加到该缓冲区的末尾,(在任意一个描述符上)从管道读出来的都是取自缓冲区开头的数据
按照这种实现,那么当A端write入数据,接着又调用read时, 可能把自己的数据读回,这显然是不正确的
正确的实现应该如下:
全双工管道由两个半双工管道构成. 写入fd[1]的数据只能从fd[0]读出, 写入fd[0]的数据只能由fd[1]读出
popen和pclose
popen函数创建一个管道并启动另外一个进程, 该进程要么从该管道读出标准输入,要么从该管道写入标准输入
关于参数type:
如果type为'r', 那么调用进程[注意是调用进程,不是被调用进程]读进command的标准输出
如果type为'w', 那么调用进程[注意是调用进程,不是被调用进程]写到command的标准输入
下面是popen的具体实现:
根据实现中的几个判断,可以看出,无论是使用'r'还是'w',都要记得在不同进程将不用的描述符关闭
以下给出一个使用的例子:
关于FIFO
pipe, socketpair是没有名字的管道, 所以不会被用在没有亲缘关系的进程之间
FIFO是指代先进先出的, Unix中的FIFO类似管道, 它是一个 单向数据流(即半双工的!)
每个FIFO有一个路径名与之关联, 从而允许无亲缘关系的进程访问同一个FIFO, FIFO也被称为有名管道
mode参数指定文件的权限
要注意的是, mkfifo已经隐含指定了 O_CREAT | O_EXCL, 所以要么创建新的FIFO, 要么返回EEXIST错误. 那么既然我们可能不需要创建新的FIFO, 就调用open函数打开
无论是要打开一个已经存在的FIFO,还是创建一个新的FIFO,都应该先调用mkfifo, 检查它返回的错误是否为EEXIST,是则再调用open
还要注意, FIFO之前提到是半双工的, 必须被打开写, 或者被打开读, 不能既被写也被读
对管道或FIFO的write总是往末尾添加数据, 对他们的read则总是从开头返回数据, 如果对管道或FIFO调用lseek, 会返回ESPIPE错误
对于这个示例程序, 我们要非常注意的是, 父子进程中打开FIFO的顺序. 如果调换父进程中open的顺序, 那么此程序就不会运作, 而是阻塞在死锁上
接下来就看看产生这种情况的原因:(阻塞与非阻塞, 默认是阻塞的, 如果想设置非阻塞, 错过了open的话可以使用fcntl设置)
阻塞(缺省/默认设置):
只读open
• FIFO已经被只写打开:成功返回
• FIFO没有被只写打开:阻塞到FIFO被打开来写
只写open
• FIFO已经被只读打开:成功返回
• FIFO没有被只读打开:阻塞到FIFO被打开来读
从空PIPE或空FIFO中read
• FIFO或PIPE已经被只写打开:阻塞到PIPE或FIFO中有数据或者不再为写打开着
• FIFO或PIPE没有被只写打开:返回0(文件结束符)
向空PIPE或空FIFO中write
• FIFO或PIPE已经被只读打开:
写入数据量不大于PIPE_BUF(保证原子性,原子性即当有两个进程往同一个管道写入数据时, 数据不会发生混杂的情况):有足够空间存放则一次性全部写入,没有则进入睡眠,直到当缓冲区中有能够容纳要写入的全部字节数时,才开始进行一次性写操作
写入数据量大于PIPE_BUF(不保证原子性):缓冲区一有空闲区域,进程就会试图写入数据,函数在写完全部数据后返回
• FIFO或PIPE没有被只读打开:
给线程产生SIGPIPE(默认终止进程), 返回EPIPE错误
O_NONBLOCK设置:
只读open
• FIFO已经被只写打开:成功返回
• FIFO没有被只写打开:成功返回
只写open
• FIFO已经被只读打开:成功返回
• FIFO没有被只读打开:返回ENXIO错误
从空PIPE或空FIFO中read
• FIFO或PIPE已经被只写打开:返回EAGAIN错误
• FIFO或PIPE没有被只写打开:返回0(文件结束符)
write
• FIFO或PIPE已经被只读打开:
写入数据量不大于PIPE_BUF(保证原子性):有足够空间存放则一次性全部写入,没有则返回EAGAIN错误(不会部分写入)
写入数据量大于PIPE_BUF(不保证原子性):有足够空间存放则全部写入,没有则部分写入,函数立即返回
• FIFO或PIPE没有被只读打开:给线程产生SIGPIPE(默认终止进程)
对于FIFO的一些属性有所了解后, 接下来就尝试编写一个本机使用的服务器进程
此服务器进程打开一个FIFO, 等待客户进程的消息
这里, 还要提一点:
内核为管道和FIFO维护一个 访问计数器, 它的值是访问同一个管道或FIFO的打开着的描述符的个数, 有了这个, 客户,服务器就能成功调用unlink.即使删除了那些正在使用着的也不会受到影响
然而对于其他IPC,(比如System V 消息队列)并没有这样的计数器, 因此当服务器向消息队列写入消息后若删除了该消息队列, 那么客户想要读取时候, 该队列可能已经消失了
关于客户端, 这里就不贴出来了, 逻辑虽然不同,但处理手段类似
让服务器读文件, 也需要注意服务器的是否有权限去读该文件
除了使用客户端与服务器交互, 还能 使用shell与服务器交互
在此例子中, 服务器读出客户请求后, 会阻塞在对客户的FIFO的写open中, 因为客户还没有读打开它的FIFO. 服务器对客户FIFO的open在我们某时调用cat后返回, 因为cat打开客户的FIFO来读
除了以上注意点, 假设同一时间有两个客户同时想给服务器发送消息该如何处理呢?
上面对FIFO的性质提到过, 管道和FIFO的write具有原子性, 因此每个客户的消息都不会发生混杂的情况, 服务器总是能一条消息一条消息的完整读出来处理
当然, 处理这个情况可以依靠管道自身的性质, 但每个客户需要等待的不必要时间就增加了, 服务器只能轮流为每个客户服务. 于是我们想到了使用网络服务器模型, 比如进程池,线程池等等
字节流与消息
至此, 我们介绍的都是管道和FIFO的字节流IO模型, 不存在记录边界, 即读写操作根本不记录数据. 比如读取50个字节, 根本不理会对方是如何写入这50个字节的, 可以50次write, 25次write, 或是某进程写入20字节, 另一个进程写入30个字节, 都是有可能发生的
有时应用可能希望传送的数据是某种数据结构. 那么当数据由长度可变的消息构成, 此时读出者必须知道这些消息的边界以判定何时已读出单个消息.
下面有几种技巧应对这种情况:
这里展示第二种方式:
可以看到, 我们根据数据的长度来接收数据, 虽然有多余的数据, 但却比使用特殊字符来当作结尾符好些, 毕竟不用将数据的特殊字符进行转义了
试想, 如果在不同的机器上传输数据, send和recv的次数肯定是不能一一对应的, 所以这里肯定是忽略了一些因素
int pipe(int pipefd[2]);
函数返回两个文件描述符: pipefd[0]和pipefd[1].
前者打来来读, 后者打开来写
管道的典型用途是为两个不同进程(父进程与子进程)提供进程间的通信手段
当我们需要(假设现在提供的pipe是半双工的)双向数据交流的时候,我们必须创建两个管道:
创建管道1 (fd1[0]和fd1[1])和管道2 (fd2[0]和fd2[1])
fork
父进程关闭fd1的读端,关闭fd2的写端
子进程关闭fd1的写端,关闭fd2的读端
全双工管道socketpair
对于全双工管道,下面是一种不正确的实现:整个管道只存在一个缓冲区, (在任意一个描述符上)写入管道的任何数据都添加到该缓冲区的末尾,(在任意一个描述符上)从管道读出来的都是取自缓冲区开头的数据
按照这种实现,那么当A端write入数据,接着又调用read时, 可能把自己的数据读回,这显然是不正确的
正确的实现应该如下:
全双工管道由两个半双工管道构成. 写入fd[1]的数据只能从fd[0]读出, 写入fd[0]的数据只能由fd[1]读出
popen和pclose
popen函数创建一个管道并启动另外一个进程, 该进程要么从该管道读出标准输入,要么从该管道写入标准输入
FILE *popen(const char *command, const char *type);
参数command是一个shell命令,由sh程序处理, 因为PATH环境变量可用于定位command, 从函数定义来看,返回的是一个FILE指针, 该指针或用于输入或用于输出,取决于type
关于参数type:
如果type为'r', 那么调用进程[注意是调用进程,不是被调用进程]读进command的标准输出
如果type为'w', 那么调用进程[注意是调用进程,不是被调用进程]写到command的标准输入
下面是popen的具体实现:
/*
* popen.c Written by W. Richard Stevens(不过被本博主删了点东西)
*/
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include "ourhdr.h"
#define SHELL "/bin/sh"
FILE *
popen(const char *cmdstring, const char *type)
{
int i, pfd[2];
pid_t pid;
FILE *fp;
/* only allow "r" or "w" */
if ((type[0] != 'r' && type[0] != 'w') || type[1] != 0) {
errno = EINVAL; /* required by POSIX.2 */
return(NULL);
}
if (pipe(pfd) < 0)
return(NULL); /* errno set by pipe() */
if ( (pid = fork()) < 0)
return(NULL); /* errno set by fork() */
else if (pid == 0) { /* child */
if (*type == 'r') {
close(pfd[0]);
if (pfd[1] != STDOUT_FILENO) {
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
}
} else {
close(pfd[1]);
if (pfd[0] != STDIN_FILENO) {
dup2(pfd[0], STDIN_FILENO);
close(pfd[0]);
}
}
execl(SHELL, "sh", "-c", cmdstring, (char *) 0);
_exit(127);
}
/* parent */
if (*type == 'r') {
close(pfd[1]);
if ( (fp = fdopen(pfd[0], type)) == NULL)
return(NULL);
} else {
close(pfd[0]);
if ( (fp = fdopen(pfd[1], type)) == NULL)
return(NULL);
}
return(fp);
}
根据实现中的几个判断,可以看出,无论是使用'r'还是'w',都要记得在不同进程将不用的描述符关闭
以下给出一个使用的例子:
#include <stdio.h>
#include <string.h>
int main(int ac, char *av[])
{
size_t n;
char buf[BUFSIZ], command[BUFSIZ];
FILE *fp;
fgets(buf, BUFSIZ, stdin);
n = strlen(buf);
if(buf[n-1] == '\n')
n--;
snprintf(command, sizeof(command), "cat %s", buf);
fp = popen(command, "r");
while(fgets(buf, BUFSIZ, fp) != NULL)
fputs(buf, stdout);
pclose(fp);
return 0;
}
从popen中执行的进程读取我们需要的数据
关于FIFO
pipe, socketpair是没有名字的管道, 所以不会被用在没有亲缘关系的进程之间
FIFO是指代先进先出的, Unix中的FIFO类似管道, 它是一个 单向数据流(即半双工的!)
每个FIFO有一个路径名与之关联, 从而允许无亲缘关系的进程访问同一个FIFO, FIFO也被称为有名管道
int mkfifo(const char *pathname, mode_t mode);
其中, pathname就是一个普通的Unix路径名, 即为该FIFO的名字
mode参数指定文件的权限
要注意的是, mkfifo已经隐含指定了 O_CREAT | O_EXCL, 所以要么创建新的FIFO, 要么返回EEXIST错误. 那么既然我们可能不需要创建新的FIFO, 就调用open函数打开
无论是要打开一个已经存在的FIFO,还是创建一个新的FIFO,都应该先调用mkfifo, 检查它返回的错误是否为EEXIST,是则再调用open
还要注意, FIFO之前提到是半双工的, 必须被打开写, 或者被打开读, 不能既被写也被读
对管道或FIFO的write总是往末尾添加数据, 对他们的read则总是从开头返回数据, 如果对管道或FIFO调用lseek, 会返回ESPIPE错误
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
//parent process calls
void client(int, int);
//child process calls
void server(int, int);
int main()
{
int readfd, writefd;
pit_t childpid;
if(mkfifo(FIFO1, FILE_MODE)<0 && errno!=EEXIST)
exit(-1);
if(mkfifo(FIFO2, FILE_MODE)<0 && errno!=EEXIST){
unlink(FIFO1); //if this is a failure, we have to delete FIFO1
exit(-1);
}
if((childpid==fork) == 0)
{
readfd = open(FIFO1, O_RDONLY);
writefd = open(FIFO2, O_WRONLY);
server(readfd, writefd);
exit(0);
}
writefd = open(FIFO1, O_WRONLY);
readfd = open(FIFO2, O_RDONLY);
client(readfd, writefd);
waitpid(childpid, NULL, 0);
unlink(FIFO1);
unlink(FIFO2);
return 0;
}
对于这个示例程序, 我们要非常注意的是, 父子进程中打开FIFO的顺序. 如果调换父进程中open的顺序, 那么此程序就不会运作, 而是阻塞在死锁上
接下来就看看产生这种情况的原因:(阻塞与非阻塞, 默认是阻塞的, 如果想设置非阻塞, 错过了open的话可以使用fcntl设置)
阻塞(缺省/默认设置):
只读open
• FIFO已经被只写打开:成功返回
• FIFO没有被只写打开:阻塞到FIFO被打开来写
只写open
• FIFO已经被只读打开:成功返回
• FIFO没有被只读打开:阻塞到FIFO被打开来读
从空PIPE或空FIFO中read
• FIFO或PIPE已经被只写打开:阻塞到PIPE或FIFO中有数据或者不再为写打开着
• FIFO或PIPE没有被只写打开:返回0(文件结束符)
向空PIPE或空FIFO中write
• FIFO或PIPE已经被只读打开:
写入数据量不大于PIPE_BUF(保证原子性,原子性即当有两个进程往同一个管道写入数据时, 数据不会发生混杂的情况):有足够空间存放则一次性全部写入,没有则进入睡眠,直到当缓冲区中有能够容纳要写入的全部字节数时,才开始进行一次性写操作
写入数据量大于PIPE_BUF(不保证原子性):缓冲区一有空闲区域,进程就会试图写入数据,函数在写完全部数据后返回
• FIFO或PIPE没有被只读打开:
给线程产生SIGPIPE(默认终止进程), 返回EPIPE错误
O_NONBLOCK设置:
只读open
• FIFO已经被只写打开:成功返回
• FIFO没有被只写打开:成功返回
只写open
• FIFO已经被只读打开:成功返回
• FIFO没有被只读打开:返回ENXIO错误
从空PIPE或空FIFO中read
• FIFO或PIPE已经被只写打开:返回EAGAIN错误
• FIFO或PIPE没有被只写打开:返回0(文件结束符)
write
• FIFO或PIPE已经被只读打开:
写入数据量不大于PIPE_BUF(保证原子性):有足够空间存放则一次性全部写入,没有则返回EAGAIN错误(不会部分写入)
写入数据量大于PIPE_BUF(不保证原子性):有足够空间存放则全部写入,没有则部分写入,函数立即返回
• FIFO或PIPE没有被只读打开:给线程产生SIGPIPE(默认终止进程)
对于FIFO的一些属性有所了解后, 接下来就尝试编写一个本机使用的服务器进程
此服务器进程打开一个FIFO, 等待客户进程的消息
处理了客户的消息后, 需要返还信息给客户. 每个客户在启动时创建一个自己的FIFO, 用于标记每个客户进程FIFO的不同之处是每个进程拥有的进程ID, 因此客户的FIFO地址包含自己的PID, 客户发送的消息包含客户进程的PID和一个路径名, 路径指向的文件即为客户希望服务器打开后返回的文件内容
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include "unp.h"
#define FILE_MODE 662
#define SERVER_FIFO "/tmp/fifo.serv"
int main(int ac, char *av[])
{
int readfifo, writefifo, useless_fd, fd;
char buf[MAXLEN];
char *ptr = NULL;
char child_fifo[MAXLEN];
int len;
pid_t pid;
//创建服务器自有的FIFO
if(mkfifo(SERVER_FIFO, FILE_MODE)<0 && errno!=EEXIST)
return -1;
//因为客户进程是通过向FIFO发送数据与服务器交流的, 所以服务器会从此FIFO中读取数据, 因为读打开
if((readfifo=open(SERVER_FIFO, O_RDONLY)) < 0)
{
unlink(SERVER_FIFO);
return -1;
}
//这里比较关键的问题就是为什么要写打开呢?
//根据之前我们对FIFO的了解, 当处于默认(阻塞)属性时, 若从空PIPE或空FIFO中read, 那么会返回0.
//如果向服务器发送消息的客户端断开其写端, FIFO变空,那么就导致服务器处于这种case, 从而read返回0. 此时我们不得不关闭服务器的FIFO, 再次open, 阻塞直到下一个客户写打开FIFO, 这样显然很麻烦
//如果我们始终有一个写端打开着呢? 这样一来, 就不用为此烦恼了
//于是, 我们在这里选择打开, 知道服务器结束才将其关闭. 这样,服务器的read就会阻塞等待每条消息
if((useless_fd=open(SERVER_FIFO, O_WRONLY)) < 0)
{
unlink(SERVER_FIFO);
return -1;
}
//获取客户发来的消息
while((len=readline(readfifo, buf, MAXLEN)) > 0)
{
if(buf[len-1] == '\n') //去掉换行符
len--;
buf[len] = '\0';
if(NULL == (ptr=strchr(buf, ' '))) //客户消息是以“ pid /path/to/file” 的形式发来的
{
fprintf(stderr, "receive error\n");
continue;
}
*ptr++ = '\0'; //此时buf代表pid, ptr代表文件路径
pid = atol(buf);
memset(child_fifo, '\0', MAXLEN);
//客户端的FIFO名字为 "/tmp/fifo.xxx" 这里的xxx即为其pid
snprintf(child_fifo, MAXLEN-1, "/tmp/fifo.%ld", pid);
//写打开客户端的FIFO, 这样服务器才能读内容其次向客户写去
if(mkfifo(child_fifo, FILE_MODE)<0 && errno!=EEXIST)
{
fprintf(stderr, "cannot open child fifo\n");
continue;
}
if((writefifo=open(child_fifo, O_WRONLY)) < 0)
{
fprintf(stderr, "cannot open child fifo\n");
continue;
}
fd = open(ptr, O_RDONLY);
//如果打开失败,记得通知
if(fd < 0)
{
len = strlen(buf);
snprintf(buf+n, MAXLEN-n, ": cannot open !\n");
write(writefifo, buf, strlen(buf));
close(writefifo);
}
//打开成功, 那么开始写内容
else
{
memset(buf, '\0', MAXLEN);
while((len=read(fd, buf, MAXLEN)) > 0)
write(writefifo, buf, strlen(buf));
close(fd);
close(writefifo);
}
}
//但是, 实际上我们是没必要手动去关闭文件描述符的
return 0;
}
这里, 还要提一点:
内核为管道和FIFO维护一个 访问计数器, 它的值是访问同一个管道或FIFO的打开着的描述符的个数, 有了这个, 客户,服务器就能成功调用unlink.即使删除了那些正在使用着的也不会受到影响
然而对于其他IPC,(比如System V 消息队列)并没有这样的计数器, 因此当服务器向消息队列写入消息后若删除了该消息队列, 那么客户想要读取时候, 该队列可能已经消失了
关于客户端, 这里就不贴出来了, 逻辑虽然不同,但处理手段类似
让服务器读文件, 也需要注意服务器的是否有权限去读该文件
除了使用客户端与服务器交互, 还能 使用shell与服务器交互
% pid=$$
% mkfifo /tmp/fifo.$pid
% echo "$pid /etc/hehe" > /tmp/fifo.serv
% cat < /tmp/fifo.$pid //读出服务器返回的内容
但是, 使用shell会产生一些假象. 因为我们可以在echo 和 cat之间相隔任意时间, 最后还是能顺利输出, 看上去像是即使没有使用进程, 数据也会以某种方式存留在客户FIFO中. 但事实并不是这样的.
在此例子中, 服务器读出客户请求后, 会阻塞在对客户的FIFO的写open中, 因为客户还没有读打开它的FIFO. 服务器对客户FIFO的open在我们某时调用cat后返回, 因为cat打开客户的FIFO来读
除了以上注意点, 假设同一时间有两个客户同时想给服务器发送消息该如何处理呢?
上面对FIFO的性质提到过, 管道和FIFO的write具有原子性, 因此每个客户的消息都不会发生混杂的情况, 服务器总是能一条消息一条消息的完整读出来处理
当然, 处理这个情况可以依靠管道自身的性质, 但每个客户需要等待的不必要时间就增加了, 服务器只能轮流为每个客户服务. 于是我们想到了使用网络服务器模型, 比如进程池,线程池等等
字节流与消息
至此, 我们介绍的都是管道和FIFO的字节流IO模型, 不存在记录边界, 即读写操作根本不记录数据. 比如读取50个字节, 根本不理会对方是如何写入这50个字节的, 可以50次write, 25次write, 或是某进程写入20字节, 另一个进程写入30个字节, 都是有可能发生的
有时应用可能希望传送的数据是某种数据结构. 那么当数据由长度可变的消息构成, 此时读出者必须知道这些消息的边界以判定何时已读出单个消息.
下面有几种技巧应对这种情况:
1 带内特殊终止序列, 比如上面提到的,进程发送结尾带有换行符的数据,服务器通过换行符来区分不同进程发来的不同消息. 但这种技巧要求如果特殊序列出现在数据中, 必须对其作出转义处理, 防止被认为是消息间的分割符
2 显示长度, 每个记录前都加上这个记录的长度, 网络协议中就常使用这种方法
3 每次只连接一个记录: 应用通过关闭与其对端的连接来指示一个记录的结束. 这就要求为每个记录创建一个连接
这里展示第二种方式:
#include <limits.h>
//in limits.h, PIPE_BUF is 4096
#define MAXMSGDATALEN (PIPE_BUF-2*sizeof(long))
#define STRUCTDATALEN (sizeof(mymesg)-MAXMSGDATALEN)
struct mymesg{
long m_len;
long m_type;
char m_databuf[MAXMSGDATALEN];
};
ssize_t mesg_send(int, struct mymesg *);
ssize_t mesg_recv(int, struct mymesg *);
ssize_t mesg_send(int fd, struct mymesg *mmptr)
{
return (write(fd, mmptr, STRUCTDATALEN + mmptr->m_len));
}
ssize_t mesg_recv(int fd, struct mymesg *mmptr)
{
int n, len;
if((n=read(fd, mmptr, STRUCTDATALEN)) < 0)
return -1;
else if(n == 0) //end of file
return 0;
else if(n != STRUCTDATALEN)
fprintf(stderr, "No expected header\n");
len = mmptr->len;
if(len > 0)
if((n=read(fd, mmptr->m_databuf, len)) != len)
fprintf(stderr,"No appriroate length of data received\n");
return len;
}
可以看到, 我们根据数据的长度来接收数据, 虽然有多余的数据, 但却比使用特殊字符来当作结尾符好些, 毕竟不用将数据的特殊字符进行转义了
试想, 如果在不同的机器上传输数据, send和recv的次数肯定是不能一一对应的, 所以这里肯定是忽略了一些因素