网络IO模型
- 同步I/O是必须等待IO操作完成后,控制权才返回用户进程
- 异步I/O是无需等待IO操作完成就将控制权返回给用户进程
- 一个网络IO涉及两个系统对象,当一个read操作发生时,会经历两个阶段:等待数据准备;将数据从内核拷贝到进程中
- 常见的五种I/O模型如下所示
阻塞IO模型
- Linux中默认情况下,所有的socket都是默认阻塞的,大部分socket接口都是阻塞的
- 在服务器端使用多线程或是多进程,使单个连接的阻塞不会影响其他连接
- 多进程的开销较大,为教唆客户提供服务时,通常使用多线程
- 使用pthread_creat()创建新线程,使用fork()创建新进程
- 服务器实现一对多连接,主进程持续等待客户端的连接请求,如果有连接创建新线程
- 使用线程池降低创建和销毁线程的频率,维持一定数量的线程
- 如UDP,UDP数据准备好读取的概念比较简单,数据报已经收到或者没有
非阻塞IO模型
- 使用非阻塞模型来提高使用效率,应用进程对非阻塞描述符循环调用recv函数称为轮询
fcntl(fd, F_SETFL, O_NONBLOCK);
- recv()函数在被调用后立即返回
- 返回值大于0,接收数据完毕,返回值即是接收到的字节数
- 返回0,表示连接已断开
- 返回-1,且errno等于EAGAIN,表示recv操作还没执行完成
- 返回-1,且errno不等于EAGAIN,表示recv操作遇到系统错误errno
多路IO复用模型
- 基本原理是有个函数(select())会不断轮询所负责的所有socket,当用户进程调用了select整个进程被阻塞,当任何一个socket中的数据准备好了,select就会返回,用户进程调用read,将数据从内核拷贝到用户进程
- 多路IO复用模型中,对于每一个socket一般都设置为非阻塞的,但是进程被select函数阻塞
- select()能够检测来自客户端的connect()
- select的参数readfds按照bit为标记描述字
- writefds和exceptfds应该标记所有需要检测的可写数据和错误时间的的标记,使用FD_SET()
- 用FD_ISSET()检查所有的标记位
- select不适合用来实现事件驱动
- select可以等待多个描述符就绪
信号驱动式I/O模型
- 内核在描述符就绪时发送SIGIO信号通知我们,然后可以启动一个I/O操作
- 等待数据报到达期间,进程不被阻塞
异步IO模型
- 用户发起read操作之后,开始做其他事,从内核的角度当他收到一个异步的read操作之后,立即返回,不会对用户进程产生任何阻塞,内核等待数据准备完成后,然后将数据拷贝到用户内存中,内核给用户进程发送一个信号,返回read操作已完成的信息
- 异步I/O通知我们I/O操作已经完成了
- 调用aio_read函数向内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告诉内核当整个操作完成时怎么通知我们
select
1 | #include <sys/select.h> |
- 套接字准备好读:
- 套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小,使用SO_RCVLOWAT套接字选项设置下限
- 连接读的半部分关闭
- 监听套接字的已完成连接数不为0
- 套接字错误待处理
- 套接字准备好写:
- 套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小并且该套接字已连接或不需要连接, 则将这个套接字设置为非阻塞
- 连接的写操作的半个部分关闭
- 使用非阻塞的connect的套接字已建立连接,或者connect以失败告终
- 有套接字错误待处理
- 综合1、2,当套接字上发生错误时,select标记即可读又可写
select的最大描述符数
1
2
3
4
5#include <sys/seclect.h>
#ifndefine FD_SETSIZE
#define FD_SETSIZE 256
#endif现在的Unix系统可以包含无线数目的描述符,而如果要是seclect适用,则需要修改FD_SIZE的值,需要重新编译
poll
poll也能用来执行多路复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include <poll.h>
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
//timeout指定等待的毫秒数,无论IO是否准备好,poll都会返回
//timeout指定为负数值时,表示无限超时,poll一直挂起知道某件事发生
//timeout为0表示poll调用立即返回,并列出准备好IO的文件描述符
/*调用成功,函数返回revents域中国不为0的文件描述符的个数,在timeout等待时间结束,没有套接准备好返回0,调用失败返回-1*/
//如果在超时前没有任何事件发生,返回0,失败返回-1,并设置errno
//第一个参数是指向结构数组第一个元素的指针
//结构数组中的元素个数是由nfds参数指定的
//timeout指定poll函数返回前等待的时间,INFTIM表示永远等待(定义在<pool.h>中)
struct pollfd{
int fd;
short events; //监视文件描述符的时间掩码,由用户来设置这个域的属性
short revents; //文件描述符的操作结果事件掩码,内核在调用返回时设置这个域
//要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态
};每个pollfd结构体指定一个被监视的文件描述符,可以传递多个结构体只是poll()监视多个文件描述符
- 在events域中请求的事件都有可能在revents中返回
- poll不需要显式的请求异常情况报告
- select所用的描述符集合是fd_set的结构体中,poll的描述符存放在pollfd数组中
- 分配一个pollfd结构数组的并把数组中元素的数目通知内核是调用者的责任,内核不在需要一个fd_set固定值
epoll
- epoll使用户一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中
- 在用户空间和内核空间之间的数据只需要拷贝一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <sys/epoll.h>
//包含三个接口
int epoll_creat(int size);
/*告诉epoll内核要监听的数目,是最大监听fd+1*/
int epoll_stl(int epfd, int op, int fd, struct epoll_event *event);
/*事件注册函数,epfd是epoll_creat的返回值,op表示动作,fd表示要监听的文件描述符,*event告诉内核监听什么事*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*等待时间产生,events用来表示从内核得到事件的集合,maxevents告诉内核这个events有多大,且不能大于size,timeout是超时时间*/
struct epoll_event{
_unit32_t events;
epoll_data_t data;
};
select、poll和epoll的区别和各自的优缺点
- select、poll和epoll都是多路复用,本质上都是同步IO
- poll不要求在计算开发者最大文件描述符时进行+1操作
- poll应付大数目的文件描述符时更快,select()需要内核检查大量描述符对应的fd_set中的每一个比特位
- select()监控的文件描述符的数目是固定的,poll可以创建特定大小的数组来保存监控的描述符,不受文件描述符值的大小的影响,poll可以监控的文件描述符的数量远大于select
- fd_set会在select()返回之后变化,在下一次进入select之前需要重新初始化需要监控的fd_set。poll将被监控的输入和输出分开,允许被监控的文件组数被复用而不需要重新初始化
- select函数的超时参数在返回时也是未定义的,每次超时进入到select之前都需要重新设置超时参数
- select优点:select可移植性很好,具有较好的超时值精度
- epoll优点:支持进程打开大数目的socket描述符,IO效率不随socket描述符的增加而线性下降,因为epoll只对活跃的socket进行操作,使用mmp加速内核与用户空间的信息传递,内核通过内核与用户空间mmap处于同一块内存实现的
- poll将用户传入的pollfd数组拷贝到内核空间的拷贝操作和数组长度相关,是O(n)操作,像用户空间拷贝数据和剥离等待队列等操作的时间也是O(0)
I/O复用
- I/O复用的典型网络应用场景:
- 客户处理多个描述符
- 一个客户处理多个套接字
- TCP服务器既要处理监听套接字,又要处理已连接套接字
- 服务器既要处理TCP又要处理UDP
- 服务器要处理多个服务或者多个协议