14.4 STREAMS
STREAMS(流)是系统 V 提供的构造内核设备驱动程序和网络协议包的一种通用方法。流在用户进程和设备驱动程序之间提供了一条全双工通路,下面是流在用户进程和设备驱动程序之间的流图:写到流首的数据将顺流而下传送,由设备驱动程序读到的数据则逆流向上传送;
STREAMS 消息
STREAMS 的所有输入和输出都是基于消息,流首和用户进程使用 read、write、ioctl、getmsg、getpmsg、putmsg 和 putpmsg 交换信息。在流首、各处理模块和设备驱动程序之间,消息可顺流而下,也可逆流而上。
在用户进程和流首之间,消息由以下几部分组成:消息类型、控制信息和数据;其中控制信息和数据由以下结构指定:
struct strbuf
{
int maxlen; /* size of buffer */
int len; /* number of bytes currently in buffer */
char *buf; /* pointer to buffer */
};
当使用putmsg 或 putpmsg 发送消息时,len 指定缓冲区中数据的字节数;当使用 getmsg 或 getpmsg 接收消息时,maxlen 指定缓冲区长度,而 len 则由内核设置为存放缓冲区的数据量;消息长度允许为0,len 为 -1 时说明没有控制信息和数据。
在我们所使用的函数(read,write,getmsg,getpmsg,putmsg 和 putpmsg)中,只涉及三种消息类型,他们是:
M_DATA(I/O 的用户数据);
M_PROTO(协议控制信息);
M_PCPROTO(高优先级协议控制信息);
流中的消息都有一个排队优先级:
高优先级消息(最高优先级);
优先级波段消息;
普通消息(最低优先级);
普通信消息是优先级波段为0 的消息,优先级波段可在 1~255 之间,波段愈高,优先级也愈高,在任何时刻流首只有一个高优先级消息排队,若在流首读队列已有一个高优先级消息,则另外的高优先级消息会被丢弃。
putmsg 和 putpmsg 函数
putmsg 和 putpmsg 函数用于将 STREAMS消息写至流中,这两个函数的区别是后者允许对消息指定一个优先级波段。
/* 流 */
/*
* 函数功能:将STREAMS消息写至流;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <stropts.h>
int putmsg(int filedes, const struct strbuf *ctlptr, const struct strbuf *datptr, int flags);
int putpmsg(int filedes, const struct strbuf *ctlptr, const struct strbuf *datptr, int band, int flags);
/*
* 说明:
* 对流使用write函数等价于不带任何控制信息、flags为0的putmsg函数;
* 这两函数可以产生三种不同优先级的消息:普通、优先级波段和高优先级;
*/
STREAMS 的 ioctl 操作
ioctl 函数如下:
/* Perform the I/O control operation specified by REQUEST on FD.
One argument may follow; its presence and type depend on REQUEST.
Return value depends on REQUEST. Usually -1 indicates error. */
int ioctl (int __fd, unsigned long int __request, ...) ;
ioctl 的第二个参数request说明执行哪一个操作。所有request都以I_开始。第三个参数的作用与request有关,有时它是一个整型值,有时它是指向一个整型或一个数据结构的指针。
/*
* 函数功能:判断描述符是否引入一个流;
* 返回值:若为STREAMS设备则返回1,否则返回0;
* 函数原型:
*/
#include <stropts.h>
int isastream(int filedes);
/*
* 说明:
* 该函数是通过ioctl函数来进行的,可有如下实现:
*/
#include <stropts.h>
#include <unistd.h>
int isastream(int fd)
{
return(ioctl(fd, I_CANPUT, 0) != -1);
}
写模式
可以使用两个 ioctl 命令取得和设置一个流的写模式,如果将 request 设置为 I_GWPORT,第三个参数设置为指向一个整型变量的指针,则该流的当前写模式在该整型变量中返回。如果将 request 设置为 I_SWPORT,第三个参数是一个整型值,则其值成为该流新的写模式,我们可以先获取当前写模式值,然后修改它,则进行设置。目前只定义了两个写模式值。
SNDZERO:对管道和 FIFO 的0长度 write 会造成顺流传送一个0长度消息。按系统默认,0长度写不发送消息。
SNDPIPE:在流上已出错后,若调用 write 和 putmsg,则向调用进程发送SIGPIPE 信息。
读模式
读STREAMS设备有两个潜在的问题:
(1)如果读到流中消息的记录边界将会怎样?
(2)如果调用 read,而流中下一个消息由控制信息又将如何?
对第一种情况的默认处理模式称为字节流模式。read 从流中取数据直至满足了所要求的字节数,或者已经不再有数据。在这种模式中,忽略流中消息的边界。第二种情况的默认处理是,read 出错返回。可以改变这两种默认处理模式。
调用 ioctl 时,若将 request 设置成 I_GRDOPT,第三个参数又是指向一个整型单元的指针,则对该流的当前读模式在该整型单元中返回。如果将 request 设置为 I_SRDOPT,第三个参数是整型值,则将该流的读模式设置为该值。读模式可由下列三个常量指定:
RNORM:普通,字节流模式,如上述这是默认模式。
RMSGN:消息不丢弃模式,read从流中取数据直到读到所要求的字节数,或者到达消息边界。如果某次read只用了消息的一部分,则其余部分仍留在流中,以供下一次读。
RMSGD:消息丢弃模式,这与不丢弃模式的区别是,如果某次只用了消息的一部分,则余下部分就被丢弃,不再使用。
在读模式中还可指定另外三个变量,以便设置在读到流中包含协议控制信息的消息时read的处理方法:任一时刻,智能设置一种消息读模式和一种协议读模式,默认读模式是:(RNORM | RPROTNORM)。
RPROTNORM:协议-普通模式。read 出错返回,errno 设置为 EBADMSG。这是默认模式。
RPROTDAT:协议-数据模式。read 将控制部分作为数据返回给调用者。
RPROTDIS:协议-丢弃模式。read 丢弃消息中的控制信息。但是返回消息中的数据。
getmsg 和 getpmsg 函数
/*
* 函数功能:将从流读STREAMS消息;
* 返回值:若成功则返回非负值,若出错则返回-1;
* 函数原型:
*/
#include <stropts.h>
int getmsg(int filedes, const struct strbuf *ctlptr, const struct strbuf *datptr, int *flagptr);
int getpmsg(int filedes, const struct strbuf *ctlptr, const struct strbuf *datptr, int *bandptr, int *flagptr);
/*
* 说明:
* 如果flagptr指向的整型单元的值是0,则getmsg返回流首读队列中的下一个消息;
* 如果下一个消息是最高优先级消息,则在返回时,flagptr所指向的整型单元设置为RS_HIPRI;
* 如果只希望接收高优先级消息,则在调用getmsg之前必须将flagptr所指向的整型单元设置为RS_HIPRI;
* getmsg可以设置待接收消息的优先级波段;
*/
14.5 I/O多路转换
当我们想要多次对描述符进行 read 时,多路转接技术能够满足该要求。I/O 多路转接技术首先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行 I/O 时,该函数才返回,返回时,告诉进程哪些描述符已经准备好可以进行 I/O 操作。
14.5.1 select 和 pselect 函数
/* IO多路转接*/
/*
* 函数功能:
* 返回值:准备就绪的描述符数,若超时则返回0,出错则返回-1;
* 函数原型:
*/
#include <sys/select.h>
int select(int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *tvptr);
/*
* 说明:
* 参数maxfdpl是“最大描述符加1”;
* 参数readfds、writefds、exceptfds是指向描述符集的指针,即描述符可读、可写或处于异常条件的;
* 时间参数有三种取值:
* tvptr == NULL;
* 永远等待;若捕获到信号则中断此无限期等待;当所指定的描述符中的一个已准备好或捕获到信号则返回;
* 若捕获到信号,则select返回-1,errno设置为EINTR;
*
* tvptr->tv_sec == 0 && tvptr->tv_usec == 0;
* 完全不等待;测试所有描述符并立即返回,这是得到多个描述符的状态而不阻塞select函数的轮回方法;
*
* tvptr->sec != 0 || tvptr->usec != 0;
* 等待指定的秒数和微妙数;当指定的描述符已准备好,或超过指定的时间立即返回;
* 若超过指定的时间还没有描述符准备好,则返回0;
*
* tvptr的结构如下:
*/
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
在上面的函数当中,存在着fd_set 的数据结构,我们可以通过以下函数对该数据结构进行处理:
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset); //测试描述符fd是否在描述符集中设置;若fd在描述符集中则返回非0值,否则返回0
void FD_CLR(int fd, fd_set *fdset); //清除在fdset中指定的位fd;
void FD_SET(int fd, fd_set *fdset); //设置fd在fdset中指定的位;
void FD_ZERO(fd_set *fdset); //清除整个fdset;即所有描述符位都为0;
声明了一个描述符集后,必须使用 FD_ZERO 清空其所有位达到初始化,然后才可以设置各个位;从 select 返回时,使用 FD_ISSET 测试该集中的一个给定位是否仍旧设置;
select 函数有三个可能的返回值:
(1)返回值-1表示出错。这种情况下,将不修改其中任何描述符集。
(2)返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经超过,则发生这种情况。此时描述符集都被清0.
(3)正返回值表示已经准备好的描述符数,该值是三个描述符集中已准备好的描述符之和。三个描述符集中仍旧打开的位对应于已“准备好”的描述符。
对于“准备好”的意思要做一些更具体的说明:
若对读集 readfds 中的一个描述符的 read 操作将不会阻塞,则此描述符是准备好的。
若对写集 writefds 中的一个描述符的 write 操作将不会阻塞,则此描述符是准备好的。
若异常状态集 exceptfds 中的一个描述符有一个未决异常状态,则此描述符时准备好的。
对于读、写和异常状态,普通文件描述符总是返回准备好的。
/*
* 函数功能:获取准备好的描述符数;
* 返回值:准备就绪的描述符数,若超时则返回0,出错则返回-1;
* 函数原型:
*/
#include <sys/select.h>
int pselect(int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *tsptr, const sigset_t *sigmask);
pselect与select有以下几个不同:
(1)select 的超时用 timeval 结构指定,pselect 使用 timespec 结构。
(2)pselect 的超时值被申明为 const,这保证了调用 pselect 不会改变此值。
(3)对于 pselect 可使用一个可选择的信号屏蔽字。若 sigmask 为空,那么在于信号有关的方面,pselect 的运行状况和 select 相同。否则,sigmask指向一个信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字,在返回时恢复以前的信号屏蔽字。
14.5.2 poll 函数
该函数与 select 函数类似,只是程序员接口不同。该函数不是为每个状态构造描述符集,而是构造一个 pollfd 结构数组,每个数组元素指定一个描述符编号以及对其所关心的状态。
/*
* 函数功能:和select函数类似;
* 函数原型:
*/
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
/*
* 说明:
* timeout == -1; 永远等待。
* timeout == 0; 不等待,测试所有的描述符并立即返回。
* timeout > 0; 等待timeout毫秒,当指定的描述符之一已经准备好,或指定的时间值已经超过时立即返回。
*/
pollfd 结构数组如下:
struct pollfd{
int fd; /* file descriptor to check,or <0 to ignore */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};
测试程序:
#include "apue.h"
#include <sys/select.h>
#include <sys/types.h>
int main(void)
{
char rbuf[1024];
fd_set rd_fds;
int ret,len;
struct timeval tv;
for(; ;)
{
FD_ZERO(&rd_fds);
FD_SET(STDIN_FILENO,&rd_fds);
tv.tv_sec = 5;
tv.tv_usec = 0;
ret = select(1,&rd_fds,NULL,NULL,&tv);
if(ret < 0)
{
err_sys("select error");
break;
}
else if(ret == 0)
printf("timeout,waiting next loop\n"); //返回0表示超时
else
{
printf("ret = %d\n",ret); //返回正值表示已经准备好的描述符数
if(FD_ISSET(STDIN_FILENO,&rd_fds))
{
len =read(STDIN_FILENO,rbuf,1023);
rbuf[len] = '\0';
printf("Read buf are: %s\n",rbuf);
}
}
}
exit(0);
}
该函数是实现每隔5秒钟,从标准输入读取数据;若超过5秒才写数据,则会返回0;输出结果:
[root@localhost 14]# ./a.out
hello world
ret = 1
Read buf are: hello world
timeout,waiting next loop
timeout,waiting next loop
[root@localhost 14]#
14.7 readv和writev函数
当我们想要一次性对一个文件进行读、写多个非连续的缓冲区时,readv 和 writev 函数能够实现该功能。这两函数也称为散布读和聚集写。其定义如下:
/* 读、写多个非连续的缓冲区 */
/*
* 函数功能:读取数据到多个非连续的缓冲区,或从多个非连续缓冲区写数据到文件;
* 返回值:若成功则返回已读、写的字节数,若出错则返回-1;
* 函数原型:
*/
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
/*
* 说明:
* iovec的指针结构如下:
*/
struct iovec
{
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
下图说明了 readv 和 writev 的参数和 iovec 结构:
writev 以顺序 iov[0],iov[1] 至iov[iovcnt-1] 从缓冲区中聚集输出数据。writev 返回输出的字节总数。readv 则将读入的数据按照上述同样顺序散布到缓冲区中,readv 总是先填满一个缓冲区,然后再填写下一个。readv 返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。
测试程序:
#include "apue.h"
#include <sys/uio.h>
#include <stdlib.h>
int main(void)
{
struct iovec iov[2];
char *buf1 = (char *)malloc(5);
char *buf2 = (char *)malloc(1024);
memset(buf1, 0, 5);
memset(buf2, 0, 1024);
iov[0].iov_base = buf1;
iov[1].iov_base = buf2;
iov[0].iov_len = 5;
iov[1].iov_len = 1024;
ssize_t nread, nwrite;
nread = readv(STDIN_FILENO, iov, 2);
if(nread == -1)
err_sys("readv error");
else
{
printf("readv:\n");
printf("buf1 is: %s\t length is: %d\n",buf1, strlen(buf1));
printf("buf2 is: %s\t length is: %d\n",buf2, strlen(buf2));
}
printf("writev:\n");
nwrite = writev(STDOUT_FILENO, iov, 2);
if(nwrite == -1)
err_sys("writev error");
free(buf1);
free(buf2);
exit(0);
}
输出结果:
[root@localhost 14]# ./a.out
helloworld,this is SEU
readv:
buf1 is: hello length is: 5
buf2 is: world,this is SEU
length is: 18
writev:
helloworld,this is SEU
[root@localhost 14]#
14.9 存储映射I/O
存储映射 I/O 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是从缓冲区中取数据,就相当于读文件的相应字节,同理,将数据写入缓冲区,则相应字节就会自动写入文件。这样可以不使用 read 和 write 函数的情况下执行 I/O。
将一个给定的文件映射到缓冲区可以使用 mmap 函数;
/* 存储映射IO */
/*
* 函数功能:将一个给定文件映射到存储区域中;
* 返回值:若成功则返回缓冲区的起始地址,若出错则返回MAP_FAILED;
* 函数原型:
*/
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
/*
* 说明:
* addr参数用于指定映射存储区的起始地址,通常将其设置为0,这样由系统自动分配起始地址;
* filedes指定要被映射文件的描述符,在映射之前,先要打开该文件;
* len是映射的字节数;
* off是要映射字节在文件中的起始偏移量;
* prot是对映射存储区的保护要求,具体参数是以下的按位"或"组合:
* PROT_READ 映射区可读
* PROT_WRITE 映射区可写
* PROT_EXEC 映射区可执行
* PROT_NONE 映射区不可访问
*
* flag参数影响映射存储区的多重属性:
* MAP_FIXED 返回值必须等于addr;
* MAP_SHARED 说明本进程对映射区所进行的存储操作的配置;指定存储操作修改映射文件;
* MAP_PRIVATE 对映射区的存储操作导致创建该映射文件的一个私有副本,所有后来对映射区的引用都是该副本;
*/
存储映射文件的存储空间如下图所示:
调用 mprotect 可以更改一个现有映射存储区的权限:
/*
* 函数功能:更改一个现有映射存储区的权限;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/mman.h>
int mprotect(void *addr, size_t len, intprot);
/*
* 说明:
* 参数prot和mmap函数的参数一样;
* 起始地址addr必须是系统页长的整数倍;
*/
如果在共享存储映射区中的页已被修改,那么我们可以调用 msync 将该页冲洗到被映射的文件中:
/*
* 函数功能:将页冲洗到被映射的文件中;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/mman.h>
int msync(void *addr, size_t len, intflags);
/*
* 说明:
* 若映射是私有的,则不修改被映射的文件,地址必须与页边界对齐;
* 参数flags取值如下:
* MS_ASYNC 执行异步写
*MS_SYNC 执行同步写
*MS_INVALIDATE 使高速缓存的数据失效
*/
当我们使用 mmap 函数成功映射存储区之后,我们可以关闭文件描述符filedes,此操作并不会解除映射区,想要解除映射区必须调用munmap 函数:
/*
* 函数功能:解除映射存储区;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/mman.h>
int munmap(void *addr, size_t len);
munmap 不会影响被映射的对象,也就是说,调用 munmap 不会使映射区的内容写到磁盘文件上。
对于 MAP_SHARED 区磁盘文件的更新,在写到存储映射区时按内核虚拟内存算法自动进行,在解除了映射后,对于 MAP_PRIVATE 存储区的修改被丢弃。
测试程序:
#include "apue.h"
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd;
void *dst;
struct stat f_stat;
char buf[MAXLINE];
memset(buf, 0, MAXLINE);
char *src = "mmap munmap msync 12";
if(argc != 2)
err_quit("usage: a.out <pathname>");
if((fd = open(argv[1], O_RDWR)) < 0)
err_sys("open argv[1] file error");
if(fstat(fd, &f_stat) == -1 )
err_sys("fstat error");
if((dst = mmap(NULL, f_stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
err_sys("mmap error");
close(fd);
memcpy(dst, src, 20);
if((msync(dst, f_stat.st_size, MS_SYNC)) == -1) //MAP_SHARED指定存储操作修改映射文件; 当上面为MAP_PRIVATE时,即若映射是私有的,则不修改被映射的文件
err_sys("msync error");
if((munmap(dst, f_stat.st_size)) == -1)
err_sys("munmap error");
exit(0);
}
原始文件mmap.txt 里面的内容为:
[root@localhost 14]# cat test.txt
This is a test...
abcdefg
hijklmn
opqrstu
vwxyz.
映射存储之后的内容为:
[root@localhost 14]# ./a.out test.txt
[root@localhost 14]# cat test.txt
mmap munmap msync 12cdefg
hijklmn
opqrstu
vwxyz.
[root@localhost 14]#
注意:以上是mmap以共享模式映射,即MAP_SHARED。若是以私有模式MAP_PRIVATE映射,并不会修改源文件。
[root@localhost 14]# cat test.txt
This is a test...
abcdefg
hijklmn
opqrstu
vwxyz.
[root@localhost 14]# ./a.out test.txt
[root@localhost 14]# cat test.txt
This is a test...
abcdefg
hijklmn
opqrstu
vwxyz.
[root@localhost 14]#