管道与FIFO属性了解

关于半双工管道pipe
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的次数肯定是不能一一对应的, 所以这里肯定是忽略了一些因素
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值