文件描述符(File Descriptor):简称FD,是一个从O开始递增的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。
I0多路复用︰是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
下文中经常同时出现fd和socket,fd为笼统说法,便于理解,详细解释流程时使用socket
select是Linux中最早的IO多路复用实现方案
int select (int maxfdp,
fd_set *readset,
fd_set *writeset,
fd_set *exceptest,
struct timeval *timeout);
//参数详解
nfds:select中监视的文件句柄数,一般设为要监视的文件中的最大文件号加一。
readfds:检测读是否就绪的文件描述符集合
writefds :检测写是否就绪的文件描述符集合
exceptfds:检测异常情况是否发生的文件描述符集合
(1、信包模式下伪终端主设备上从设备状态发生改变;2、流式套接字接收到了带外数据)
timeout: 设为NULL,等待直到某个文件描述符发生变化;设为0秒0毫秒,不等待直接返回;
设为大于0的值,有描述符变化或超时时间到才返回。
select返回值:负数表示有错误发生;大于等于0,表示有n个描述符就绪。具体是哪个,
还需要用FD_ISSET遍历判定。
FD_CLR(inr fd,fd_set* set);将描述符fd从fdset所指向的集合中移除
FD_SET(int fd,fd_set*set);将描述符fd添加到fdset所指向的集合中
FD_ZERO(fd_set *set); 将fdset指向的集合初始化为空
FD_ISSET(int fd,fd_set *set);测试描述词组set中相关fd 的位是否为真
需注意,每次调用 select 都需要将 Socket 列表由用户进程拷贝到内核,当 Socket 列表比较大时,拷贝操作是一个不可忽视的开销,因此 Select 会限制监听 Socket 的最大数量
需要注意,进程A 还需要被添加到它所监听的每一个 Socket 的等待列表中,也就是说这里存在遍历 Socket 列表并操作 Socket 的开销
此处依然存在遍历 Socket 列表并操作 Socket 的开销
此时进程A 知道它所监听的 Socket 列表中存在可以读写操作的 Socket,但是并不知道到底是哪一个 Socket,此处仍然需要遍历列表才能进行下一步操作
poll模式对select模式做了简单改进,但性能提升不明显
//pollfd 中的事件类型
#define POLLIN //可读事件
#define POLLOUT //可写事件
#define POLLERR //错误事件
#define POLLNVAL //fd未打开
//pollfd结构
struct pollfd {
int fd;//错误事件
short int events; //要监听的事件类型:读、写、异常
short int revents;//实际发生的事件类型
};
//poll函数
int poll(
struct pollfd *fds,//pollfd数组,可以自定义大小
nfds_t nfds,//数组元素个数
int timeout //超时时间
);
将pollfd数组拷贝到内核空间
,转链表存储,无上限拷贝pollfd数组到用户空间
,返回就绪fd数量nstruct eventpoll {
//...
struct rb_root rbr; //一颗红黑树,记录要监听的FD
struct list_head rdlist; //一个链表,记录就绪的FD
//...
};
//1.会在内核创建eventpoll结构体,返回对应的句柄epfd
int epoll_create(int size);
//2.将一个FD添加到epoll的红黑树中,并设置ep_poll_callback
//callback触发时,就把对应的FD加入到rdlist这个就绪列表中
int epoll_ctl(
int epfd,//epoll实例的句柄
int op,//要执行的操作,包括:ADD、MOD、DEL
int fd,//要监听的FD
struct epoll_event *event //要监听的事件类型:读、写、异常等
);
//3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量
int epoll_wait(
int epfd,// eventpoll实例的句柄
struct epoll_event *events,// 空event数组,用于接收就绪的FD
int maxevents,//events数组的最大长度
int timeout//超时时间,-1用不超时;0不阻塞;大于0为阻塞时间
);
rbr红黑树
结构中,也就是说进程A 监听的 Socket 集合交由内核维护。这样每次只需要将新的 Socket 对象单独拷贝到内核,开销很小,另外这个过程中也会设置回调函数到 Socket 的等待列表中
rdlist就绪列表
,如果没有任何 Socket 就绪,进程A 就需要在此处阻塞,也就是被从工作队列中移除。需要注意,进程A 此时只要被添加到 epoll 的wq等待列表中即可,不需要被添加到每一个 Socket 的等待列表
因为进程A 未被直接添加到所有 Socket 的等待列表,所以此处也就不需要将其从每个 Socket 的等待列表移除
此时进程A 只需要读取 epoll 的rdlist就绪列表就能知道哪些 Socket 读写就绪,epoll中的events数组发挥作用,用于存储就绪的fd,直接操作就绪列表中 Socket 的即可,不需要再遍历查找
select和poll的共同缺陷:select/poll 低效的原因之一是将 “添加 / 维护待检测任务” 和 “阻塞进程 / 线程” 两个步骤合二为一。每次调用 select 都需要这两步操作,然而大多数应用场景中,需要监视的 socket 个数相对固定,并不需要每次都修改。
epoll的优势:
epoll 将“添加 / 维护待检测任务” 和 “阻塞进程 / 线程” 这两个操作分开,先用 epoll_ctl() 维护等待队列,再调用 epoll_wait() 阻塞进程(解耦),效率得到了提升
基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听的FD数量增多而下降
每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
内核会将就绪的FD直接拷贝到用户空间的指定位置,用户进程无需遍历所有FD就能知道就绪的FD是谁
当FD有数据可读时,我们调用epoll_wait就可以得到通知。但是事件通知的模式有两种:
例:
假设一个客户端socket对应的FD已经注册到了epoll实例中
客户端socket发送了2kb的数据
服务端调用epoll_wait,得到通知说FD就绪
服务端从FD读取了1kb数据命
回到步骤3(再次调用epoll_wait,形成循环)
阻塞IO就是两个阶段都必须阻塞等待
非阻塞IO的用户应用会频繁发起请求等待返回成功
信号驱动IO是与内核建立SIGlO的信号关联并设置回调,当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待
当有大量I0操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出而且内核空间与用户空间的频繁信号交互性能也较低。
异步lO的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其它事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。
由于异步IO完全不阻塞,需要控制内核并发量,并发过高容易导致崩溃