• 高级I/O之STREAMS


    http://en.wikipedia.org/wiki/STREAMS

    STREAMS(流)是系统V提供的构造内核设备驱动程序和网络协议包的一种通用方法,对STREAMS进行讨论的目的是为了理解系统V的终端接口、I/O多路转接中poll(轮询)函数的使用 以及基于STREAMS的管道和命名管道的实现。

    请注意不要将这里说明的STREAMS(流)与标准I/O库(http://www.cnblogs.com/nufangrensheng/p/3505254.html)中使用的流(stream)相混淆。流机制是由Dennis Ritchie开发的,其目的是用通用、灵活的方法改写传统的字符I/O系统(c-list)并与网络协议相适应,后来稍加增强,名称改用大写字母,成为STREAMS机制,被加入到SVR3。在Linux中,STREAMS子系统是可用的,但是用户必须自行将该子系统安装到系统中,通常它默认为不包括在系统中。

    流在用户进程和设备驱动程序之间提供了一条全双工通路。流无需和实际硬件设备直接会话,流也可以用来构造伪设备驱动程序。图14-5示出了包含一个处理模块的流。各方框之间用两根带箭头的线连接,以突出流的全双工特征,并强调两个方向的处理是相互独立进行的。

    index                                                   images

           图14-4 一个简单的流                                                                   图14-5 具有处理模块的流

    任意数量的处理模块可以压入流。我们使用术语压入,是因为每一新模块总是插入到流首之下,而将以前的模块下压。(这类似于后进先出的栈。)图14-5标出来流的两侧,分别称为顺流(downstream)和逆流(upstream)。写到流首的数据将顺流而下传送,由设备驱动程序读到的数据则逆流而上传送。

    STREAMS模块是作为内核的一部分执行的,这类似于设备驱动程序。当构造内核时,STREAMS模块联编进入内核。如果系统支持动态可装入的内核模块(Linux和Solaris是这样做的),则我们可以试图将没有联编进内核的STREAMS模块压入一个流;但不保证STREAMS模块和驱动程序的任意组合将能正常工作。

    用文件I/O中说明的函数访问流,它们是:open、close、read、write和ioctl。另外,在SVR3内核中增加了3个支持流的新函数(getmsg、putmsg和poll),在SVR4中又加了两个处理流内不同优先级波段消息的函数(getpmsg和putpmsg)。

    打开(open)流时使用的路径名参数通常在/dev目录之下仅仅用ls -l查看设备名,不能判断该设备是不是STREAMS设备。所有STREAMS设备都是字符特殊文件。

    虽然某些有关STREAMS的文献暗示我们可以编写处理模块,并且不加细究地就可将它们压入流中,但是编写这些模块如果编写设备驱动程序一样,需要专门的技术。通常只有特殊的应用程序或函数才压入和弹出STREAMS模块。

    1、STREAMS消息

    STREAMS的所有输入和输出都基于消息。流首和用户进程使用read、write、ioctl、getmsg、getpmsg、putmsg和putpmsg交换消息。在流首、各处理模块和设备驱动程序之间,消息可以顺流而下,也可以逆流而上。

    在用户进程和流首之间,消息由下列几部分组成:消息类型、可选择的控制信息以及可选择的数据。表14-4列出了对应于write、putmsg和putpmsg的不同参数所产生的不同消息类型。控制信息和数据由strbuf结构指定:

    struct strbuf{
        int    maxlen;    /* size of buffer */
        int    len;       /* number of bytes currently in buffer */
        char   *buf;      /* pointer to buffer */
    };

    2697937651789066508

    注:n/aN/A英语“不适用”(Not applicable)等类似单词的缩写,常可在各种表格中看到。N/A比较多用在填写表格的时候,表示“本栏目(对我)不适用”。在没有东西可填写,但空格也不允许此项留白的时候,可以写N/A。在英语国家,也会用n/a或者n.a.来表达,都是同一个意思。

    当用putmsg或putpmsg发送消息时,len指定缓冲区中数据的字节数。当用getmsg或getpmsg接收消息时,maxlen指定缓冲区长度(使内核不会溢出缓冲区),而len则由内核设置为存放在缓冲区中的数据量。消息长度为0是允许的,len为-1说明没有控制信息或数据。

    为什么需要传送控制信息和数据两者呢?提供这两者使我们可以实现用户进程和流之间的服务接口。可能最为人了解的服务接口是系统V的传输层接口(Transport Layer Interface,TLI),它提供了网络系统接口。

    控制信息的另一个例子是发送一个无连接的网络消息(数据报)。为了发送该消息,需要说明消息的内容(数据)和该消息的目的地址(控制消息)。如果不能将数据和控制一起发送,那么就要某种专门设计的方案。例如,可以用ioctl说明地址,然后用write发送数据。另一种技术可能要求地址占用数据的前N个字节,而数据是write写的。将控制信息与数据分开,并且提供处理两者的函数(putmsg和getmsg)是处理这种问题的比较清晰的方法。

    有约25种不同类型的消息,但是只有少数几种用于用户进程和流首之间,其余的只在内核中顺流、逆流传送。(对于编写流处理模块的人员而言,这些消息是非常有用的,但是对编写用户级代码的人员而言,它们可以忽略。)在我们所使用的函数(read、write、getmsg、getpmsg、putmsg和putpmsg)中,只涉及三种消息类型,它们是:

    • M_DATA (I/O的用户数据)。
    • M_PROTO (协议控制信息)。
    • M_PCPROTO (高优先级协议控制信息)。

    流中的消息都有一个排队优先级:

    • 高优先级消息(最高优先级)。
    • 优先级波段消息。
    • 普通消息(最低优先级)。

    普通消息是优先级波段为0的消息。优先级波段消息的波段可在1-255之间,波段愈高,优先级也愈高。高优先级消息的特殊性在于,在任何时候流首只有一个高优先级消息排队。在流首读队列已有一个高优先级消息时,另外的高优先级消息会被丢弃。

    每个STREAMS模块有两个输入队列。一个接收来自它上面模块的消息,这种消息从流首向驱动程序顺序传送。另一个接收来自它下面模块的消息,这种消息从驱动程序向流首逆流传送。在输入队列中的消息按优先级从高到低排列。

    2、putmsg和putpmsg函数

    putmsg和putpmsg函数用于将STREAMS消息(控制信息或数据,或两者)写至流中。这两个函数的区别是后者允许对消息指定一个优先级波段。

    #include <stropts.h>
    int putmsg(int filedes, const struct strbuf *ctlptr,
              const struct strbuf *dataptr, int flag);
    int putpmsg(int filedes, const struct strbuf *ctlptr,
               const struct strbuf *dataptr, int band, int flag);
    两个函数返回值:若成功则返回0,若出错则返回-1

    对流也可以使用write函数,它等效于不带任何控制信息、flag为0的putmsg。

    这两个函数可以产生三种不同优先级的消息:普通、优先级波段和高优先级。表14-4详细列出了这两个函数中几个参数的各种可能组合,以及所产生的不同类型的消息。

    在表14-4中,N/A表示不适用。消息控制列中的“否”对应于空ctlptr参数,或ctlptr->len为-1。该列中的“是”对应于ctlptr非空,以及ctlptr->len大于等于0。这些说明同样适用于消息的数据部分(用dataptr代替ctlptr)。

    3、STREAMS ioctl操作

    http://www.cnblogs.com/nufangrensheng/p/3500358.html中曾提到过ioctl函数,它能做其他I/O函数不处理的事情。STREAMS系统继承了这种传统。

    在Linux和Solaris中,使用ioctl可对流执行将近40种不同的操作。头文件<stropts.h>应包括在使用这些操作的C代码中。ioctl的第二个参数request说明执行哪一个操作。对流执行操作的所有request都以 I_ 开始。第三个参数的作用与request有关,有时它是一个整型值,有时它是一个指向一个整型或一个数据结构的指针。

    实例:isastream函数

    有时需要判断一个描述符是否引用一个流。这与调用isatty函数来判断一个描述符是否引用一个终端设备相类似(见终端I/O之终端标识)。Linux和Solaris为此提供了isastream函数。

    #include <stropts.h>
    int isastream(int filedes);
    返回值:若为STREAMS设备则返回1,否则返回0

    与isatty类似,它通常是用一个只对STREAMS设备才有效的ioctl函数来进行测试的。程序清单14-7是该函数的一种可能的实现。它使用I_CANPUT ioctl来测试由第三个参数说明的优先级波段(本实例中为0)是否可写。如果该ioctl执行成功,则它对所涉及的流并未作任何改变。

    程序清单14-7 检查描述符是否引用STREAMS设备

    #include <stropts.h>
    #include <unistd.h>
    
    int
    isastream(int fd)
    {
        return(ioctl(fd, I_CANPUT, 0) != -1);
    }

    程序清单14-8可用于测试此函数。

    程序清单14-8 测试isastream函数

    #include "apue.h"
    #include <fcntl.h>
    
    int 
    main(int argc, char *argv[])
    {
        int    i, fd;
    
        for(i=1; i<argc; i++)
        {
            if((fd = open(argv[i], O_RDONLY)) < 0)
            {
                err_ret("%s: can't open", argv[i]);
                continue;
            }
            
            if(isastream(fd) == 0)
                err_ret("%s: not a stream", argv[i]);
            else
                err_msg("%s: streams device", argv[i]);
        }
        exit(0);
    }

    实例

    如果ioctl的参数request是I_LIST,则系统返回已压入该流所有模块的名字,包括最顶端的驱动程序(指明最顶端的原因是,在多路转接驱动程序的情况下,有多个驱动程序)。其第三个参数应当是指向str_list结构的指针

    struct str_list{
        int                 sl_nmods;        /* number of entries in array */
        struct str_mlist    *sl_modlist;     /* ptr to first element of array */
    };

    应将sl_modlist设置为指向str_mlist结构数组的第一个元素,将sl_nmods设置为该数组中的项数:

    struct str_mlist{
        char    l_name[FMNAMESZ+1];    /* null-terminated module name */
    };

    常量FMNAMESZ在头文件<sys/conf.h>中定义,其值常常是8。l_name的实际长度是FMNAMESZ+1,增加1个字节是为了存放null终止符。

    如果ioctl的第三个参数是0,则该函数返回值是模块数,而不是模块名。我们将先用这种ioctl调用确定模块数,然后再分配所要求的str_mlist结构数。

    程序清单14-9例示了I_LIST操作。由ioctl返回的名字列表并不对模块和驱动程序进行区分,但是考虑到该列表的最后一项是处于流底的驱动程序,所以在打印时将其表明为驱动程序。

    程序清单14-9 列表流中的模块名

    #include "apue.h"
    #include <fcntl.h>
    #include <stropts.h>
    
    int
    main(int argc, char *argv[])
    {
        int        fd, i, nmods;
        struct str_list list;
        
        if(argc != 2)
            err_quit("usage: %s <pathname>", argv[0]);
    
        if((fd = open(argv[1], O_RDONLY)) < 0)
            err_sys("can't open %s", argv[1]);
        if(isastream(fd) == 0)
            err_quit("%s is not a stream", argv[1]);
    
        /*
        * Fetch number of modules.
        */
        if((nmods = ioctl(fd, I_LIST, (void *) 0)) < 0)
            err_sys("I_LIST error for nmods");
        printf("#modules = %d
    ", nmods);
    
        /*
        * Allocate storage for all the module names.
        */
        list.sl_modlist = calloc(nmods, sizeof(struct str_mlist));
        if(list.sl_modlist == NULL)
            err_sys("calloc error");
        list.sl_nmods = nmods;
    
        /*
        * Fetch the module names.
        */
        if(ioctl(fd, I_LIST, &list) < 0)
            err_sys("I_LIST error for list");
    
        /*
        * Print the names.
        */
        for(i=1; i<=nmods; i++)
            printf(" %s: %s
    ", (i == nmods) ? "driver" : "module", 
                list.sl_modlist++->l_name);
    
        exit(0);
    }

    4、写(write)至STREAMS设备

    在表14-4中可以看到写至STREAMS设备产生一个M_DATA消息。一般情况确实如此,但是也还有一些细节需要考虑。首先,流中最顶部的一个处理模块规定了可顺流传送的最小、最大数据报长度(无法查询该模块中规定的这些值)。如果写的数据长度超过最大值,则流首将这一数据按最大长度分解成若干数据包。最后一个数据包的长度可能不到最大值。

    接着要考虑的是:如果向流写0个字节,又将如何呢?除非流引用管道或FIFO,否则就顺流发送0长度消息。对于管道和FIFO,为与以前版本兼容,系统的默认处理方式是忽略0长度write。可以用ioctl设置管道和FIFO流的写模式,从而更改这种默认处理方式。

    5、写模式

    可以用两个ioctl命令取得和设置一个流的写模式。如果将request设置为I_GWROPT,第三个参数设置为指向一个整型变量的指针,则该流的当前写模式在该整型量中返回。如果将request设置为I_SWROPT,第三个参数是一个整型值,则其值成为该流新的写模式。如同处理文件描述符标志和文件状态标志(见http://www.cnblogs.com/nufangrensheng/p/3500350.html)一样,总是应当先取当前写模式值,然后修改它,而不只是将写模式设置为某个绝对值(很可能会关闭某些原来打开的位)。

    目前,只定义了两个写模式值。

    SNDZERO    对管道和FIFO的0长度write会造成顺流传送一个0长度消息。按系统默认,0长度写不发送消息。

    SNDPIPE     在流上已出错后,若调用write或putmsg,则向调用进程发送SIGPIPE信号。

    流也有读模式,我们先说明getmsg和getpmsg函数,然后再说明读模式。

    6、getmsg和getpmsg函数

    使用read、getmsg或getpmsg函数从流首读STREAMS消息。

    #incldue <stropts.h>
    int getmsg(int filedes, struct strbuf *restrict ctlptr,
               struct strbuf *restrict dataptr, int *restrict flagptr);
    int getpmsg(int filedes, struct strbuf *restrict ctlptr,
               struct strbuf *restrict dataptr, int *restrict bandptr,
               int *restrict flagptr);
    两个函数返回值:若成功则返回非负值,若出错则返回-1

    注意,flagptr和bandptr是指向整型的指针。在调用之前,这两个指针所指向的整型单元中应设置成所希望的消息类型;在返回时,此整型量设置为所读到的消息的类型。

    如果flagptr指向的整型单元的值是0,则getmsg返回流首读队列中的下一个消息。如果下一个消息是高优先级消息,则在返回时,flagptr所指向的整型单元设置为RS_HIPRI。如果希望只接收高优先级消息,则在调用getmsg之前必须将flagptr所指向的整型单元设置为RS_HIPRI。

    getpmsg使用一个不同的常量集。为了只接收高优先级消息,我们可以将flagptr指向的整型单元设置为MSG_HIPRI。为了只接收某个优先级波段或以上波段(包括高优先级消息)的消息,我们可将该整型单元设置为MSG_BAND,然后将bandptr指向的整型单元设置为该波段的非0优先级值。如果只希望接收第1个可用消息,则可将flagptr指向的整型单元设置为MSG_ANY;在返回时,该整型值将改写为MSG_HIPRI或MSG_BAND,这取决于接收到的消息的类型。如果取到的消息并非高优先级消息,那么bandptr指向的整型将包括消息的优先级波段值。

    如果ctlptr是null,或ctlptr->maxlen是-1,那么消息的控制部分仍保留在流首读队列中,我们将不处理它。类似地,如果dataptr是null,或者dataptr->maxlen是-1,那么消息的数据部分仍保留在流首读队列中,我们也不处理它。否则,将按照缓冲区的容量取到消息中尽可能多的控制和数据部分,余下部分仍留在队首,等待下次取用。

    如果getmsg和getpmsg调用取到一消息,那么返回值是0。如果消息控制部分中有一些余留在流首读队列中,那么返回常量MORECTL。类似地,如果消息数据中有一些余留在流首读队列中,那么返回常量MOREDATA。如果控制和数据都有一些余留在流首读队列中,那么返回常量值是(MORECTL|MOREDATA)。

    7、读模式

    如果读(read)STREAMS设备会发生什么呢?有两个潜在的问题:

    (1)如果读到流中消息的记录边界将会怎样?

    (2)如果调用read,而流中下一个消息有控制信息又将如何?

    对第一种情况的默认处理模式称为字节流模式。read从流中取数据直至满足了所要求的字节数,或者已经不再有数据。在这种模式中,忽略流中消息的边界。对第二种情况的默认处理是,如果在队列的前端有控制消息,则read出错返回。可以改变这两种默认处理模式。

    调用ioctl时,若将request设置为I_GRDOPT,第三个参数又是指向一个整型单元的指针,则对该流的当前读模式在该整型单元中返回。如果将request设置为I_SRDOPT,第三个参数是整型值,则将该流的读模式设置为该值。读模式值可由下列三个常量指定:

    RNORM    普通,字节流模式,如上所述这是默认模式。

    RMSGN    消息不丢弃模式。read从流中去数据直至读到所要求的字节数,或达到消息边界。如果某次read只用了消息的一部分,则其余下部分仍留在流中,供下次读。

    RMSGD    消息丢弃模式。这与不丢弃模式的区别是,如果某次读只用了消息的一部分,则余下部分就被丢弃,不再使用。

    在读模式中还可指定另外三个常量,以便设置在读到流中包含协议控制信息的消息时read的处理方法:

    RPROTNORM    协议-普通模式。read出错返回,errno设置为EBADMSG。这是默认模式。

    RPROTDAT       协议-数据模式。read将控制部分作为数据返回给调用者。

    RPROTDIS        协议-丢弃模式。read丢弃消息中的控制信息,但是返回消息中的数据。

    任一时刻,只能设置一种消息读模式以及一种协议读模式。默认读模式是(RNORM|RPROTNORM)。

    实例 

    程序清单14-10是在程序清单3-3(http://www.cnblogs.com/nufangrensheng/p/3498248.html)的基础上改写的,它用getmsg代替了read。

    程序清单14-10 用getmsg将标准输入复制到标准输出

    #include "apue.h"
    #include <stropts.h>
    
    #define BUFFSIZE    4096
    
    int 
    main(void)
    {
        int              n, flag;
        char             ctlbuf[BUFFSIZE], datbuf[BUFFSIZE];
        struct strbuf    ctl, dat;
    
        ctl.buf = ctlbuf;
        ctl.maxlen = BUFFSIZE;
        dat.buf = datbuf;
        dat.maxlen = BUFFSIZE;
        for( ; ; )
        {
            flag = 0;    /* return any message */
            if ((n = getmsg(STDIN_FILENO, &ctl, &dat, &flag)) < 0)
                err_sys("getmsg error");
            fprintf(stderr, "flag = %d, ctl.len = %d, dat.len = %d
    ", 
                flag, ctl.len, dat.len);
            if (dat.len == 0)
                exit(0);
            else if (dat.len > 0)
                if (write(STDOUT_FILENO, dat.buf, dat.len) != dat.len)
                    err_sys("write error");
        }
    }

    本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/

  • 相关阅读:
    spark机器学习从0到1特征抽取–Word2Vec(十四)
    spark机器学习从0到1特征抽取–CountVectorizer(十三)
    spark机器学习从0到1特征提取 TF-IDF(十二)
    spark机器学习从0到1机器学习工作流 (十一)
    vant ui中area组件踩坑记录
    免费CDN:jsDelivr+Github 使用方法
    使用vue-quill-editor实现富文本编辑器
    数组添加元素到特定位置
    scoop——强大的Windows命令行包管理工具
    radio单选框样式
  • 原文地址:https://www.cnblogs.com/nufangrensheng/p/3556555.html
Copyright © 2020-2023  润新知