分类:
2012-07-31 13:22:04
原文地址:第十四章*高级I/O(四)--STREAMS 作者:yourtommy
STREAMS机制由系统V提供,作为进入内核的接口通信驱动的通常方法。我们需要讨论STREAMS来了解系统V的终端接口,I/O复用的poll函数(14.5.2节)的使用,和基于STREAMS的管道和命名管道(17.2和17.2.1节)的实现。
小 心不要和我们在标准I/O库(5.2节)里使用的单词“stream”混淆。流机制由Dennis Ritchie开发作为清理传统字符I/O系统(c-lists)和适应网络协议的方法。流机制稍后被加入到SVR3,在增强了一点和优化名字之后。 STREAMS的完整支持(一个基于STREAMS的终端I/O系统)在SVR4里提供。SVR4实现在[AT&T 1990d]里描述。Rago[1993]同时讨论了用户级STREAMS编程和内核级STREAMS编程。
STREAM是SUS的一个可选特性(包含在XSI STREAM可选组里)。本文讨论的四个平台,只有Solaris提供了STREAMS的本地的支持。STREAMS子系统在Linux上可用,但是你需要自己加上它。它通常不会默认包含。
一个流在一个用户进程和一个设备驱动之间提供了一个全双工(full-duplex)的道路。一个流没有必须和硬件设备交流,一个流也可以和一个伪终端设备驱动一起使用。下面展示被称为一个简单流的基本的图片:
在流头的下面,我们可以把处理模块推到流上面。这通过使用ioctl命令完成。下图展示了有单个处理模块的一个流。我们也展示了这些方块之间的连接,用两个箭头来强调流的全双工的本质,并强调一个方向的处理和另一个方向的处理是分开的。
可以把任意数量的处理模块推到一个流上。我们使用术语“推(push)”,因为每个新的模块都进到流头的下面,把之前任意推过的模块推下去。(这类似于后进 先出的栈。)在上图,我们标出向上的流和向下的流。我们写到一个流头的数据是向下发送的;从设备驱动读的数据是向上发送的。
STREAMS 模块和设备设务驱动相似,因为它们作为内核一部分执行,它们通常在内核编译时被链接到内核里。如果系统支持可动态加载的内核模块(如Linux和 Solaris),那么我们可以使用一个还未链接到内核的STREAMS模块并把它推到一个流上;然而,没有保证说模块和驱动的任意结合可以恰当地在一起 工作。
我们用第三章的函数open、close、read、write和ioctl来访问一个流。此外,SVR3内核加了三个新函数来支持 STREAMS(getmsg、putmsg和poll),SVR4加入了另两个(getpmsg和putpmsg)来处理一个流的不同优先级带宽的消 息。我们在本节稍后描述这五个函数。
我们为流open的pathname通常存在于/dev目录下。简单地用ls -l看下这个设备名,我们不能知道这个设备是否是一个STREAMS设备。所有STREAMS都是字符特殊文件。
尽管一些STREAMS文件暗示我们可以写处理模块并混乱地把它们推到一个流上,但是写这些模块需要和写一个设备驱动相同的技能和小心。通常,只有特定的应用或函数才会推或弹出STREAMS模块。
在 STREAMS之前,终端用已有的c-list机制来处理。向内核加上一个基于字符的设备通常涉及写一个设备驱动器并把所有的东西放在驱动器里。访问新的 设备通常是通过裸的设备,意味着每个用户read或write最终都直接进入设备驱动。STREAMS机制清理了这种交互的方式,允许数据在 STREAMS消息里在流头和驱动之间流动,并允许任意数量的中间处理模块来操作这个数据。
STREAMS消息
所有在STREAMS下的输入和输出都是基于消息的。流头和用户进程用read、write、ioctl、getmsg、getpmsg、putmsg和putpmsg交换数据。消息也在流头、处理模块、和设备驱动之前被传上传下。
在用户进程和流头之间,一个消息由一个消息类型、可先控制信息,和可选数据组成。我们在下表中展示各种消息类型如何被write、putmsg和putpmsg的参数产生。
函数 | 控制? | 数据? | 带宽 | 标志 | 产生的消息类型 |
---|---|---|---|---|---|
write | 无 | 是 | 无 | 无 | M_DATA(普通) |
putmsg | 否 | 否 | 无 | 0 | 没有消息发送,返回0 |
putmsg | 否 | 是 | 无 | 0 | M_DATA(普通) |
putmsg | 是 | 是或否 | 无 | 0 | M_PROTO(普通) |
putmsg | 是 | 是或否 | 无 | RS_HIPRI | M_PCPROTO(高优先级) |
putmsg | 否 | 是或否 | 无 | RS_HIPRI | 错误,EINVAL |
putpmsg | 是或否 | 是或否 | 0-255 | 0 | 错误,EINVAL |
putpmsg | 否 | 否 | 0-255 | MSG_BAND | 没有消息发送,返回0 |
putpmsg | 否 | 是 | 0 | MSG_BAND | M_DATA(普通) |
putpmsg | 否 | 是 | 1-255 | MSG_BAND | M_DATA(优先带宽) |
putpmsg | 是 | 是或否 | 0 | MSG_BAND | M_PROTO(普通) |
putpmsg | 是 | 是或否 | 1-255 | MSG_BAND | M_PROTO(优先带宽) |
putpmsg | 是 | 是或否 | 0 | MSG_HIPRI | M_PCPROTO(高优先级) |
putpmsg | 否 | 是或否 | 0 | MSG_HIPRI | 错误,EINVAL |
putpmsg | 是或否 | 是或否 | 非0 | MSG_HIPRI | 错误,EINVAL |
控制消息和数据由strbuf结构体指定:
struct strbuf {
int maxlen; /* size of buffer */
int len; /* number or bytes currently in buffer */
char *buf; /* pointer to buffer */
};
当我们用putmsg或putpmsg发送一个消息时,len指定缓冲里的数据的字节数。当我们用getmsg或getpmsg接收消息时,maxlen指 定缓冲的大小(所以内核不会溢出这个缓冲),而len由内核设置为存储在缓冲里的数据量。我们将看到一个0字长的消息是可以的,以及-1的len指明没有 控制或数据。
为什么我们同时需要控制信息和数据?提供两者允许我们实现一个用户进程和一个流之间的服务接口。很可能最著名的服务接口,是系统V的传输层接口(Transport Layer Interface,TLI),它提供到网络系统的接口。
另 一个控制信息的例子是发送一个无连接的网络消息(数据报)。为了发送消息,我们需要指定消息(数据)的内容和消息的目标地址(控制信息)。如果我们不能同 时发送控制和数据,那么需要一些特别的策略。例如,我们可以用一个ioctl来指明地址,接着一个数据的write。另一个技术是要求数据占据write 所写的的数据的前N字节。分离控制信息和数据,工提供处理两者的函数(putmsg和getmsg)是处理这个的更简洁的方法。
有大约25 种不同类型的消息,但是只有很少被用在用户进程和流头之间。其它的在内核里的流里传上传下。(这些类型引起写STREAMS处理模块的人的兴趣,但可以被 写用户级代码的人忽略。)我们将只碰到三种消息类型,在使用函数read、write、getmsg、getpmsg、putmsg和putpmsg时。
1、M_DATA(I/O的用户数据);
2、M_PROTO(协议控制信息);
3、M_PCPROTO(高优先级协议控制信息)。
一个流上的每个消息都有一个排除优先级:
1、高优先级消息(最高优先级);
2、优先带宽消息;
3、普通消息(最低优先级)。
普通消息简单地是0带宽的优先带宽消息。优先带宽消息有1-255的带宽,越高的带宽指定越高的优先级。高优先级带宽消息很特殊,因为它一次只有一个被流头排队。多余的高优先级消息被舍弃,当已经有一个在流头的读队列里时。
每 个STREAMS模块有两个输入队列。一个接收上方模块的消息(消息从流头向驱动住下移动),一个接收下方模块的消息(消息从驱动向流头往上移动)。在一 个输入队列上的消息根据优先级排列。我们在前面的表里展示了write、putmsg、和putpmsg的参数如何导致各种优先级消息的产生。
有其它我们没有考虑的消息类型。例如,如果流头从下方收到一个M_SIG消息,那么它产生一个信号。这是终端行处理模块如何向一个控制终端关联的前台进程组发送终端产生的消息。
putmsg和putpmsg函数
一个STREAMS消息(控制信息或数据,或两者)使用putmsg或putpmsg被写到一个流里。两个函数的区别是后者允许我们为消息指定一个优先级带宽。
我们也可以write到一个流,它等价于不带任何控制信息和0的flag的putmsg。
这两个函数可以产生三种不同优先级的消息:普通、优先带宽、和高优先级。前面的表里详细介绍了产生各种消息类型的这两个函数的参数组合。
STREAMS ioctl操作
在3.15节,我们说过ioctl函数处理所有其它I/O函数不能完成的东西。STREAMS系统延续了这个传统。
在
Linux和Solaris之间,有近40个不同的操作可以用ioctl在一个流上执行。这些操作的多数在streamio手册页的有文档。头文
件
例子--isastream函数
我们有时需要确定一个描述符是否指向一个流。这类似于调用isatty函数确定一个描述符是否指向一个终端设备(18.9节)。Linux和Solaris提供了isastream函数。
就 像isatty,这通常是一个简单的函数,仅仅尝试一个只在STREAMS设备上有效的ioctl操作。下面的代码展示这个函数的可能实现。我们使用 I_CANPUT的ioctl命令,它检查第三个参数指定的带宽(这个例子为0)是否可写。如果ioctl成功,那么流不会发生变化。
/etc/motd: not a stream: Inappropriate ioctl for device
(我Linux 3.0上的结果为:
$ sudo ./a.out /dev/tty /dev/fb /dev/null /etc/motd
/dev/tty: not a stream: Success
/dev/fb: can't open: No such file or directory
/dev/null: not a stream: No such file or directory
/etc/motd: not a stream: No such file or directory)
注 意/dev/ty是一个STREAMS设备,如我们在Solaris下所期望的。字符特殊文件/dev/fb不是一个STREAMS设备,但是它支持其它 ioctl请求。这些调用在ioctl请求未知时返回EINVAL。字符特殊文件/dev/null不支持任何ioctl操作,所以错误ENODEV被返 回。最后,/etc/motd是一个普通文件,不是一个字符特殊设备,所以典型的错误ENOTTY被返回。我们没有收到我们可能期望的错 误:ENOSTR(“设备不是一个流”)。
ENOTTY的消息过去是“不是一个磁带打印机”,一个历史的制品,因为UNIX内核返回ENOTTY每当ioctl尝试在一个不指向一个字符特殊设备的描述符上时。这个消息在Solaris上被更新为“设备的不恰当的ioctl”。
如果ioctl请求是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在头文件
如果ioctl的第三个参数是0,模块号的数量计数被返回(作为ioctl的值)而不是模块名。我们将使用这个来确定模块的数量,然后分配所需数量的str_mlist结构体。
下面的代码展示了I_LIST操作。因为返回的名字列表不区分模块和驱动,当我们打印模块名时,所以列表的最后项是流最底部的驱动。
(我Linux3.0上的运行结果是:
$ who
tommy pts/0 2012-03-17 10:50 (:0)
tommy pts/1 2012-03-17 14:28 (:0)
$ ./a.out /dev/pts/0
/dev/pts/0 is not a streamt
$ ./a.out /dev/pts/1
/dev/pts/1 is not a stream)
到STREAMS设备的write
在 前面的表里我们说过一个到STREAMS设备的write产生一个M_DATA消息。尽管这通常是真的,但有些补充的细节要考虑。首先,对一个流,最顶端 的处理模块指定可以向下发送的最小和最大的包尺寸。(我们不能查询模块来得到这些值。)如果write多于最大值,那么流头通常把数据分解成最大值尺寸的 包,最后一个包比最大值小。
下一个要考虑的东西是如果我们write0字节到一个流时会发生什么。除非流指向一个管道或FIFO,否则0长 度的消息被往下发送。对于管道或FIFO,默认是忽略0长度的write,为了和以前版本兼容。我们可以使用ioctl设置流的写模式来改变管道和 FIFO的默认行为。
写模式
两个ioctl命令得到和设置一个流的写模式。设置request为I_GWROPT要求第三个 参数是一个整型指针,流当前的写模式被返回在这个整型里。如果request是I_SWROPT,第三个参数是一个整型,其值变为流的新的写模式。和文件 描述符标志和文件状态标志一样(3.14节),我们应该总是得到当前写模式再修改它,而不是设置模式为一些绝对的值(可能关掉已被启用的一些其它的位)。
当前,只有两个写模式值被定义:
SNDZERO:一个0长度的write到一个管道或FIFO会导致一个0长度的消息往下发送。默认下,这个0长度write不发送消息。
SNDPIPE:导致SIGPIPE被发送给调用write或putmsg的进程,在有流上的错误之后。
一个流也有读模式,在描述getmsg和getpmsg后再讨论。
getmsg和getpmsg函数
STREAMS消息使用read、getmsg或getpmsg从一个流头里读取。
注意flagptr和bandptr是整型指针。这两个指针指向的整数必须在调用之前设置来指定所需消息的类型,这个整型也在返回时被设置为读到消息的类型。
如 果flagptr指向的整型为0,getmsg返回流头的读队列上的下一条消息。如果下一条消息是高优先级消息,那么flagptr指向的整型在返回时被 设为RS_HIPRI。如果我们想只收到高优先级消息,那么我们必须设置flagptr所指的整型为RS_HIPRI,在调用getmsg之前。
getpmsg 使用了不同的常量集。我们可以设置flagptr指向的整型为MSG_HIPRI来只收到高优先级消息。我们可以设置整型为MSG_BAND然后设置 badptr所指的整型为非0优先级值来只接收那个带宽的值或更高的值的消息(包括高优先级消息)。如果我们只想接受第一个可用的消息,我们可以设置 flagptr指向的整型为MSG_ANY;在返回时,这个整型将被覆写为MSG_HIPRI或MSG_BAND,取决于收到消息的类型。如果我们得到的 消息不是一个高优先级消息,那么bandptr指向的整型会包含消息的优先级带宽。
如果ctlptr为空或 ctlptr->maxlen为-1,那么消息的控制部分会留在流头的读队列上,我们将不会处理它。类似的,如果dataptr为空或 dataptr->maxlen为-1,那么消息的数据部分不被处理并停留在流头的读队列上。否则,我们将得到和我们缓冲将容纳的一样多的数据的控 制,任何剩余的会留在队列的头部,以便下次调用。
如果getmsg或getpmsg的调用得到一条消息,那么返回值为0。如果消息的控制部 分有一部分留在流头的读队列上,那么常量MORECTL被返回。相似地,如果消息的数据部分有一部分留在队列上,那么常量MOREDATA被返回。如果控 制和数据都有剩余,那么返回值为(MOREDATA|MOREDATA)。
读模式
我们也需要考虑如果我们从STREAMS设备读时会发生什么。有两个潜在问题。
1、和流上消息关联的记录边界会发生什么?
2、如果我们调用read而下一个流上的消息有控制信息会发生什么?
情 况1的默认处理被称为字节流模式。在这种模式里,一个read从流中拿数据,直到请求的字节数被读取或直到没有更多数据。和STREAMS消息相关的消息 边界在这种模式被忽略。情况2的默认处理导致read返回一个错误,如果在队列开头有一个控制消息。我们可以改变这些默认行为。
使用ioctl,如果我们设置request为I_GRDOPT,那么第三个参数是一个整型指针,流的当前读模式被返回在这个整型里。一个I_SRDOPT的request拿到第三个参数的整型值,并设置读模式为这个值。读模式由以下三个常量的某一个指定:
RNORM:普通的字节流模式(默认),如之前描述的。
RMSGN:不舍弃消息模式,一个read从流里拿数据,直到碰到边界。如果read使用一个部分消息,消息里的剩余数据为后续read留在流上。
RMSGD:消息舍弃模式。和不舍弃模式相似,但如果部分消息被使用,那么剩余的消息被舍弃。
三个补充的常量可以在读模式里指定,来设置read的行为,当它碰到包含在一个流上的协议控制信息时。
RPROTNORM:协议普通模式:read返回一个EBADMSG错误。这是默认值。
RPROTDAT:协议数据模式:read返回控制部分作为数据。
RPROTDIS:协议舍弃模式:read舍弃控制信息,但返回消息里的任何数据。
一次只有一个消息读模式和一个协议读模式可以被设置。默认的读模式为(RNORM|RPROTNORM)。
下面的代码和3.9节的程序相同,但是用getmsg而不是read
程序在用STREAMS同时实现管道和终端的Solaris下运行时,结果为:
$ echo hello, world | ./a.out
flag = 0, ctl.len = -1, dat.len = 13
hello, world
flag = 0, ctl.len = 0, dat.len = 0
$ ./a.out
this is line 1
flag = 0, ctl.len = -1, dat.len = 15
this is line 1
and line 2
flag = 0, ctl.len = -1, dat.len = 11
and line 2
^D
flag = 0, ctl.len = -1, dat.len = 0
$ ./a.out < /etc/motd
getmsg error: Not a stream device
当管道被关闭时(当echo终止时),看起来上面的代码是一个STREAMS挂起,控制长度和数据长度都被设为0.(15.2节讨论管道。)然而对于终端,
输入文件终止符导致只有数据长度被设为0。终端的文件终止符和STREAMS挂起不同。正如意料,当我们重定向标准输入到一个非STREAMS设备
时,getmsg返回一个错误。
(我的Linux3.0所有操作都返回错误。)