linux下的几种IO模型
概述
在linux系统中经常要用到一些IO操作(如:read()
、write()
、accept()
等)的系统调用。这些IO操作可以设置为阻塞(~O_NONBLOCK)和非阻塞(O_NONBLOCK)两种IO模型,对于一些简单的应用,运用这两种IO能够胜任,但是当需要操作的文件IO变多时,利用这两种IO模型再搭配上多线程,能够实现对多个IO口的监视和操作,但是,这样会使工作变得复杂而且程序编写不当还引应发出各种各样的问题,所以,linux系统提供了一些能同时对大量IO进行监视的API,这些API(IO模型)能更好的提高对大量文件IO操作效率。
下文中各IO模型的通知模式分为水平触发通知和边沿触发通知,两者的触发判断条件都是IO处于就绪状态(也就是数据都准备好了)为触发。
-
水平触发:当有IO触发了系统调用后,我们可以在任意时刻去重复检查IO都能查出触发结果。
-
边沿触发:只能再促发了系统调用时获得一个信号,获得信号后要尽可能把IO的内容处理完,因为一旦查看后,就无法重复查看IO的状态,只有当下一个IO再次触发后才能查看到IO的状态,并处理。
其中:selec、poll只能水平触发,而信号驱动IO只能边沿触发,epoll两种触发都可以。
注意:下文的所有IO模型通常都搭配非阻塞IO来使用。
多路IO复用
文件描述符集合
当有多个文件描述符需要监视时,Linux为我们提供一个文件描述符集合的数据类型fd_set
,该数据类型以位掩码的形式实现,对该数据类型的操作从过宏来完成:
void FD_ZERO(fd_set *fdset)
:将指定集合初始化为空。
void FD_SET(int fd, fd_set *fdset)
:把指定fd添加到集合。
void FD_clr(int fd, fd_set *fdset)
:把指定fd从集合中移除
int FD_ISSET(int fd, fd_set *fdset)
:查看指定fd是否在集合中,是则返回true。
select()
select主要以三个事件(读、写、异常)为单位对文件描述符集合(fd_set
)进行监视。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
:三个要监视的集合中最大文件描述符fd再加一(fdmax+1)。
readfds
:用来设定检测输入就绪的文件描述符,得到结果后,结果会覆盖掉原来设置时设定的文件描述符。
writefds
:用来设定检测输出就绪的文件描述符,得到结果后,结果会覆盖掉原来设置时设定的文件描述符。
exceptfds
:用来设定检测异常就绪的文件描述符,得到结果后,结果会覆盖掉原来设置时设定的文件描述符。
timeval
:主要用于设置阻塞时间,NULL为阻塞
返回值:>0,3个集合中标记为就绪fd的总数。0-超时,集合清空。-1-出错。
从参数nfds可以看出,select在内核中检查文件描述符的方式应该是从0开始一直检查到nfds的位置,所以检查的文件描述符是否密集及最大文件描述符的大小都影响着该函数的性能。
poll()
poll()
文件描述符为单位对struct pollfd fds[]
进行监视。
int poll(struct follfd fds[], nfds_t nfds, int timeout);
监视对象是一个结构体数组,该结构体数组定义为:
struct pollfd{
int fd; //文件描述符
short events; //设定的监视事件
short revent; //触发监视事件后的返回值
}
事件都是以掩码的形式进行设定,系统已通过宏封装好,常用的事件有:
掩码 | 描述 |
---|---|
POLLIN | 可读取非高优先级的数据 |
POLLPRI | 可读取高优先级数据 |
POLLRDHUP | 对端套接字关闭 |
POLLOUT | 普通数据可写 |
POLLWRNORM | 同上 |
POLLWRBAND | 优先级数据可写 |
POLLERR | 有错误发生(仅revent可用) |
如果暂时对指定文件描述符的动作不感兴趣,可将events设为0,另外,给指定fd取负值也可使得events被忽略,且revents总返回0。
timeout
:-1为一直阻塞;0-不阻塞,只执行一次;>0,延时timeout
毫秒。
由于poll()的设定事件和返回会事件存放在两个不同的变量中,所以,不用像select()那样每次读取完结果都要重新设定监视事件。
而且poll所监视的文件描述符数量不受限制,而select只能监视FD_SETSIZE
个文件描述符。
epoll()
epoll()
是linux中特有的一个更高性能(相较于select和poll)的接口,它既能水平触发,又能边沿触发。
epoll的创建
int epoll_create(int size);
size
指定要检查的文件描述符的个数,(该参数并无实际作用)。
返回值:epfd,epoll实例的文件描述符,与前面的select,poll不同,epoll实例是通过一个文件描述符来控制的;当epoll实例不再使用时,应用close(int fd)
将其关闭。
设置epoll的相关信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
epfd
:epoll实例的文件描述符
op
:对epfd要执行的操作。如:
参数 | 作用 |
---|---|
EPOLL_CTL_ADD | 添加fd到ev所指向的兴趣列表中 |
EPOLL_CTL_MOD | 修改描述符fd上设定的事件,需要用到ev指向的结构体信息 |
EPOLL_CTL_DEL | 将fd从兴趣列表中移除 |
ev
:该参数的结构体定义如下:
struct epoll_event{
uint32_t events
epoll_data_t data;
}
其中,events是描述事件用的一个掩码,而data是一个联合体,可用于设定fd成为就绪态后的返回值。
epoll_data_t定义如下:
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}
初始化例子:
struct epoll_event ev
int fd
ev.data.fd = fd;
ev.events = EPOLLIN;
epoll可监视的文件描述符的总数在/proc/sys/fd/epoll目录下的文件中的max_user_watches来查看和修改。
ev.events
字段上的掩码的值;
位掩码 | 描述 |
---|---|
EPOLLIN | 非高优先级数据可读 |
EPOLLOUT | 普通数据可写 |
EPOLLET | 采用边沿触发事件通知 |
EPOLLERR | 有错误发生 |
EPOLLHUP | 出现挂断 |
epoll的事件等待
int epoll_wait(int epfd,struct epoll_event *evlist, int maxevents, int timeout);
evlist
所指向的结构体数组中存储着有关就绪态态文件描述符的信息(这些信息的类型通过ctl函数中的ev参数设定)。
返回值:为evlist数组中的元素个数,如果超过事件没有就绪态出现,则返回0;出错返回-1。
信号驱动IO
信号驱动IO的做法是:当文件描述符上有可执行IO操作时,进程会让内核为自己发送一个信号(signal),收到信号后执行对应操作。
使用信号驱动IO的步骤如下:
-
为信号安装一个信号处理函数,默认情况下该信号为SIGIO.
-
设置文件描述的属主,也就是设置接收信号的进程(正数)或进程组(负的gid),设置方法为:
fcntl(fd, F_SETOWN, pid);
-
设定fd为
O_NONBLOCK
非阻塞IO。然后,使能信号驱动IO标志位O_ASYNC
。flags = fcntl(fd, F_GETFL);//获取当前IO标记位 fcntl(fd, F_SETFL, flags|O_NONBLOCK|O_ASYNC)
-
设置成功后,执行其它任务,当IO操作准备就绪后,线程fd就会收到由内核发出的信号,收到信号后便会执行信号处理函数。
-
用于信号驱动IO是边沿触发,所以一旦触发,应尽可能的把IO的数据处理完,也就是直到操作IO的系统调用失败为止,此时的错误码为
EAGIN
或EWOULDBLOCK
另外,若想改变发送的信号,通过fctnl
中的F_SETSIG
来设定,通过F_GETSIG
,来查看触发信号。
信号安装函数signaction()
在设定信号时,要把sa_flags
设为SA_RESTART
,还可以在安装信号时把sa_flags
的SA_SIGINFO
或上去后,便能把一个跟信号参数相关的结构体siginfo_t
作为参数传入到信号处理函数。