1.非阻塞I/O
非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open,read和write。如果这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。对于一个给定的描述符有两种方法对其指定非阻塞I / O:
(1) 如果是调用open以获得该描述符,则可指定O_NONBLOCK标志
(2) 对于已经打开的一个描述符,则可调用fcntl打开O)NONBLOCK文件状态标志
2.I/O多路转接
select
select函数使我们可以执行I/O多路转接。传向select的参数告诉内核:我们所关注的描述符;对于每个描述符我们所关心的状态,以及我们愿意等待的时间。从select返回时,内核告诉我们:以准备好的描述符的数量,对于读、写或异常这三个状态中的每一个,那些描述符已经准备好。
#include <sys/select.h> int select (int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr); //返回准备好的描述符的计数,超时返回0,错误返回-1。
第一个参数maxfdp1的意思是“最大描述符加1”。也可将第一个参数设置为FD_SETSIZE,这是<sys/select.h>中的一个常数,它说明了最大的描述符数(经常是1024)。如果将第三个参数设置为我们所关注的最大描述符编号值加一,内核就只需在此范围内寻找打开的位,而不必在三个描述符集中的数百位内搜索。中间的三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读(readfds)、可写(writefd)或处于异常条件(wxcepfds)的各个描述符。每个描述符集存放在一个fd_set数据类型中。这种结构相当于一个描述符的数组,它为每个可能的描述符设置1位。
select的中间三个参数中的任意一个或全部都可以是空指针,这表示对相应状态不关系。如果所有三个指针都是空指针,则select提供了较sleep更精确的计时器。其等待时间可以小于1秒。
tvptr指定最后等待的时间,它的结构是:
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* and microseconds */ };
a. tvptr==NULL:永远等待。如果捕捉到一个信号则中断此无限等待。当所指定的描述符中的一个已经准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR.
b. tvptr->tv_sec==0&&tvptr_usec==0 完全不等待。测试所有的描述符并立即返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。
c. tvptr->tv_sec!=0||tvptr_usec!=0 等待指定的秒数或微秒数。当指定的描述符之一已准备好,或当指定的时间值已超过时立即返回。如果在超时还没有一个描述符准备好,则返回值是0
select有三个可能的返回值。
a. 返回值-1表示出错。这是可能发生的,例如在所指定的描述符都没有准备好时捕捉到一个信号。
b. 返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经超过,则发生这种情况。
c. 返回一个正值说明了已经准备好的描述符数,在这种情况下,三个描述符集中仍旧打开的位是对应于已准备好的描述符位。
对fgset数据类型可以进行的处理是: (a)分配一个这种类型的变量, (b)将这种类型的一个变量赋与同类型的另一个变量,或(c)对于这种类型的变量使用下列四个宏:
#include <sys/select.h> int FD_ISSET(int fd, fd_set *fdset); //如果fd被设置返回非0,否则返回0。 void FD_CLR(int fd, fd_set *fdset); void FD_SET(int fd, fd_set *fdset); void FD_ZERO(fd_set *fdset);
调用FD_ZERO将一个fd_set变量的所有位置为0位。调用FD_SET设置一个fd_set变量的指定位,调用FD_CLR则将一指定位清除。最后,调用FD_ISSET测试一指定位是否设置
poll函数
poll函数类似于select,但是其调用形式则有所不同
include <poll.h> int poll(struct poolfd fdarray[], nfds_t nfds, int timeout); //返回准备好的描述符的计数,到时返回0,错误返回-1。
与select不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。
struct pollfd { int fd; /* file descriptor to check, or <0 to ignore */ short events; /* event of interest on fd */ short revents; /* event that occurred on fd */ };
在fdarray数组里的元素数由nfds指定。
poll的最后参数指明我们想要等待多久。和select一样,有三种情况
a.timeout == -1:永远等待 b.timeout == 0:不等待 c.timeout > 0
常量 说明
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
epoll
1. int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是
告诉内核需要监听什么事,struct epoll_event结构如下
//保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; //感兴趣的事件和被触发的事件 struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
效率:select <poll< epoll
主要却别在于内核实现上,select采用数组形式进行用户空间与内核空间的数组交互;poll在内核使用链表队列形式对监听的事件进行处理;epoll则是在内核实现了一个微型的文件系统,负责事件的管理。有兴趣的可以研究下,主要集中在select.c这个文件中。
3.存储映射I/O
存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的
#include <sys/mman.h> void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off); //成功返回被映射区域的开始地址,错误返回MAP_FAILED。
addr参数用于指定映射存储区的起始地址,len是映射的字节数,filedes指定要被映射文件的描述符,off是要映射字节在文件中的起始位移量,prot参数指定映射区域的保护,此函数的返回地址是:该映射区的起始地址。
prot参数取值:
调用mprotect来改变一个已有映射上的权限。 #include <sys/mman.h> int mprotect(void *addr, size_t len, int prot); //成功返回0,错误返回-1。 调用msync来冲洗对被映射文件的改变 #include <sys/mman.h> int msync(void *addr, size_t len, int flags); //成功返回0,错误返回-1。 调用了munmap之后,存储映射区就被自动去除。 #include <sys/mman.h> int munmap(caddr_t addr, size_t len); //成功返回0,错误返回-1