UNIX环境高级编程-第14章- 高级 I/O - 二

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]#

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值