I/O复用
概念
内核一旦发现进程指定的一个或者多个I/O条件就绪(也就是说输入已准备好被读取,或者描述符已能承受更多的输出),它就通知进程。这个能力称为I/O复用(I/O multiplexing)。
使用场景
当处理多个多个文件描述符或者监听多个socket时,必须使用I/O复用。
如果一个服务器要同时处理TCP和UDP,一般要使用I/O复用。
I/O模型
阻塞式I/O模型
非阻塞式I/O模型
I/O复用(select和poll)
信号驱动式I/O模型
异步I/O模型
什么叫数据准备好?
数据准备分两个阶段:1、等待网络数据到达,数据被复制到内核缓冲区,即内核空间;2、把内核数据复制到用户进程缓冲区,即用户空间。
阻塞式I/O模型
以recvfrom系统调用为例,用户调用recvfrom,然后切换到内核态,内核等待数据到来,此时有可能阻塞(如果网络数据还没到达的话)。然后网络数据到达后将内核缓冲区数据复制到用户空间,返回成功。这时候从内核态再切换到用户态,处理数据报。
类似这种进程一直等待数据到来,从系统调用开始到它返回,整段时间是被阻塞的。成功返回后开始处理数据报。 (进程等待的时候是被挂起休眠么?还是在干嘛?)
非阻塞式I/O模型
把一个套接字或FD设置为非阻塞,就是通知内核,当数据没准备好时,返回一个错误,而不是一直等待。 如果有数据准备好,则复制到用户空间并返回成功指示。
常见的用法是用一个循环去不断的调用recvfrom,去查看属否有数据准备好,这种方式被称为轮询(polling)。应用进程持续轮询内核,这么做消耗大量CPU资源。所以这种模型很少见。
I/O复用模型
有了I/O复用,如select,poll,我们就可以调用select或者poll,阻塞在这两个系统调用上,而不是阻塞在真正的I/O系统调用上。
我们调用select,等待fd或者socket变为可读。当select返回套接字可读这一条件时,我们再调用recvfrom把所有可读数据从内核复制到用户空间。
信号驱动式I/O模型
用的非常少,项目中目前还没有见过。略。
异步I/O模型
Linux的AIO目前还不成熟,故不做深入。
同步、异步、阻塞、非阻塞
同步和异步关注的是事件就绪时消息通知的方式。由调用者主动询问的方式是同步,由被调用方(往往是内核)主动通知调用方任务完成的方式是异步调用。
阻塞和非阻塞关注的是接口调用后等待数据返回时的状态。被挂起无法执行其他操作的是阻塞,可以理解返回去完成其他认识的是非阻塞模型。
select函数
该函数让进程指示内核等待多个socket,只有在任何一个socket准备好了(可读,可写)或者等待超时后才唤醒该进程。
函数原型如下:
1 #include <sys/select.h> 2 #include <sys/time.h> 3 4 int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就绪描述符的数目,超时返回0,出错返回-1
函数参数介绍如下:
(1)第一个参数maxfdp1指定待测试的描述符的个数,其值为最大待测试的fd+1。(因此把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。因为文件描述符是从0开始的。
(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可理解为一个字符集,可用以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
(3)struct timeval设定等待时间,告知内核等待所指定描述符最多多长时间,秒和微秒组成。
struct timeval { long tv_sec; //seconds long tv_usec; //micro seconds }
timeval参数有三种可能:
第一种是空指针,表示无限等待下去,此时select为阻塞模式;
第二种为一个有效值,表示等待时间。在等待时间内描述符就绪则返回描述符,否则返回0;
第三种是设置为0,根本不等待直接返回,这称为轮询。