IO多路复用

IO模型

阻塞IO只能阻塞一个IO操作,IO复用模型能阻塞多个IO操作,所以才叫多路复用
读数据

  1. 等待数据到达
  2. 将到达的数据拷贝到kernel的buffer,再从kernel buffer拷贝到User Space的buffer

Blocking IO

阻塞IO

直到数据全拷贝至User Space后才返回

Non-Blocking IO

非阻塞IO

不断去Kernel做Polling,询问比如读操作是否完成,没完成则read()操作会返回EWOUDBLOCK,需要过一会再尝试执行一次read()。该模式会消耗大量CPU

IO Multiplexing

IO多路复用

之前等待时间主要消耗在等数据到达上。IO Multiplexing则是将等待数据到来和读取实际数据两个事情分开,好处是通过select()等IO Multiplexing的接口一次可以等待在多个Socket上。select()返回后,处于Ready状态的Socket执行读操作时候也会阻塞,只是只阻塞将数据从Kernel拷贝到User的时间

Signal-Driven I/O

信号驱动IO

首先注册处理函数到SIGIO信号上,在等待数据到来过程结束后,系统触发SGIO信号,之后可以在信号处理函数中执行读数据操作,再唤醒Main Thread或直接唤醒Main Thread让它完成数据读取。整个过程没有一次阻塞。
问题:TCP下,连接断开/可读/可写等都会产生Signal,并且Signal没有提供好的方法去区分这些Signal到底为什么被触发

Asynchronous I/O

异步IO

AIO是注册一个读任务,直到读任务完全完成后才会通知应用层。AIO是由内核通知IO操作什么时候完成,信号驱动IO是由内核告知何时启动IO操作
也存在挺多问题,比如如何去cancel一个读任务

IO模型比较

模型比较

除了AIO是异步IO,其他全是同步IO

IO多路复用接口

select

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
  • nfds是readfds、writefds、exceptfds中编号最大的那个文件描述符加1。
  • readfds 监听读操作的文件描述符列表,当被监听的文件描述符有可以不阻塞就读取的数据时,select会返回并将读就绪的描述符放在readfds指向的数组内
  • writefds 监听写操作,当被监听的文件描述符中能可以不阻塞就写数据时,select会返回
  • exceptfds 监听出现异常的文件描述符类别
  • timeout select最大阻塞时间,精度为毫秒
    select返回条件:
  • 有文件描述符就绪,可读/可写/异常
  • 线程被interrupt;
  • timeout

fd_set: 一个long类型的数组,每一位可以表示一个文件描述符

# 简化结构  32=1024/8/4
# unsigned long int “无符号长整型”数据
typedef struct{
    unsigned long int fds_bits[32];
}fd_set;

问题

  • 监听的文件描述符有上限FD_SETSIZE,一般是1024。因为fd_set是个bitmap,它为最多nfds个描述符都用一个bit去表示是否监听
  • 用户侧,select返回传入的所有的描述符列表集合,包括ready和非ready的描述符,用户侧需要去遍历所有readfds、writefds、exceptfds去看哪个描述符是ready状态,再做接下来的处理。还要清理这个ready状态,做完IO操作后再塞给select准备执行下一轮IO操作
  • Kernel侧,select执行后每次都要陷入内核遍历三个描述符集合数组为fd注册监听,即在描述符指向的Socket或文件等上面设置处理函数,从而在文件ready时能调用处理函数。等有文件描述符ready后,在select返回退出之前,kernel还需要再次遍历描述符集合,将设置的这些处理函数拆除再返回
  • 惊群问题。假设一个fd被多个进程或线程注册在自己的select描述符集合内,当这个文件描述符ready后会将所有监听它的进程或线程全部唤醒
  • 无法动态添加描述符,比如一个线程已经在执行select,突然想写数据到某个新描述符上,就只能等前一个select返回后重新设置fd set重新执行select

poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

返回条件与select一样。
fds还是关注的描述符列表。poll将events和reevents分开了,所以如果关注的events没有发生变化就可以重用fds,poll只修改rents不会动events。fds是个数组,不是fds_set,没有了上限。
相对于select,poll解决了fds长度上限问题,解决了监听描述符无法复用问题,但仍需在poll返回后遍历fds去找ready的描述符,也要清理ready描述符对应的revents,Kernel也同样是每次poll调用需要去遍历fds注册监听,poll返回时拆除监听,也仍有惊群问题,无法动态修改描述符的问题。

epoll

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

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 */
};

使用步骤:

  1. 用epoll_create创建epoll的描述符;
  2. 用epoll_ctl将一个个需要监听的描述符以及监听的事件类型用epoll_ctl注册在epoll描述符上;
  3. 执行epoll_wait等着被监听的描述符Ready,epoll_wait返回后遍历Ready的描述符,根据Ready的事件类型处理事件
  4. 如果某个被监听的描述符不再需要了,需要用epoll_ctl将它与epoll的描述符解绑
  5. 当epoll描述符不再需要时需要主动close,像关闭文件一样释放资源

优点

  • 监听的描述符没有上限
  • epoll_wait每次只会返回Ready的描述符,不用完整遍历所有被监听的描述符
  • 监听的描述符被注册到epoll后会与epoll的描述符绑定,维护在内核,不主动通过epoll_ctl执行删除不会自动被清理,所以每次执行epoll_wait后用户侧不用重新配置监听,Kernel侧在epoll_wait调用前后也不会反复注册和拆除描述符的监听
  • 可以通过epoll_ctl动态增减监听的描述符,即使有另一个线程已经在执行epoll_wait
  • epoll_ctl在注册监听的时候还能传递自定义的event_data,一般是传描述符
  • 即使没线程等在epoll_wait上,Kernel因为知道所有被监听的描述符,所以在这些描述符Ready时候就能做处理,等下次有线程调用epoll_wait时候直接返回。这也帮助epoll去实现IO Edge Trigger,即IO Ready时候Kernel就标记描述符为Ready,之后在描述符被读空或写空前不再去监听它
  • 多个不同的线程能同时调用epoll_wait等在同一个epoll描述符上,有描述符Ready后它们就去执行

缺点

  • epoll_ctl是个系统调用,每次修改监听事件,增加监听描述符的时候都是一次系统调用,没有批量操作的方法
  • 对于服务器上大量连上又断开的连接处理效率低,即accept()执行后生成一个新的描述符需要执行epoll_ctl去注册新Socket的监听,之后epoll_wait又是一次系统调用,如果Socket立即断开了epoll_wait会立即返回,又需要再用epoll_ctl把它删掉
  • 依然有惊群问题,需配合使用方式避免

kqueue

int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, 
           const struct timespec *timeout);

struct kevent {
         uintptr_t  ident;       /* identifier for this event */
         short     filter;       /* filter for event */
         u_short   flags;        /* action flags for kqueue */
         u_int     fflags;       /* filter flag value */
         int64_t   data;         /* filter data value */
         void      *udata;       /* opaque user data identifier */
         uint64_t  ext[4];       /* extensions */
};

changelist用于传递关心的event
nchanges用于传递changelist的大小
eventlist用于当有事件产生后,将产生的事件放在这里
nevents用于传递eventlist大小
timeout 超时时间
kqueue高级的地方在于,它监听的不一定非要是Socket,不一定非要是文件,可以是一系列事件,所以struct kevent内参数叫filter,用于过滤出关心的事件。
kqueue有epoll所有优点,还能通过changelist一次注册多个关心的event,不需要像epoll那样每次调用epoll_ctl去配置

更多Epoll

epoll综合的执行过程

当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里。
如此,一棵红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。


对比

Edge-Trigger, Level-Trigger

Epoll有两种触发模式,一种Edge Trigger简称ET,一种Level Trigger简称LT。每个使用epoll_ctl注册在epoll描述符上的被监听的描述符都能单独配置自己的触发模式。
从使用角度的区别:ET模式下当一个文件描述符Ready后,需要以Non-Blocking方式一直操作这个FD直到操作返回EAGAIN错误位置,期间Ready这个事件只会触发epoll_wait一次返回。LT模式,如果FD上的事件一直处在Ready状态没处理完,则每次调用epoll_wait都会立即返回
场景:

  1. 一个Socket注册在epoll fd上,监听它的读事件
  2. Socket另一端发送了2KB数据到这个Socket
  3. epoll_wait返回,并带着这个Socket的fd说它读Ready
  4. Socket的reader只从Socket读了1KB的数据
  5. 再次执行epoll_wait
    如果这个Socket注册在epoll fd上时待着EPOLLET标志,即ET模式,及时Socket还有1KB数据没读,第5步epoll_wait执行时也不会立即返回,会一直阻塞直到再有新数据到达这个Socket。因为这个Socket上的数据一直没有读完,Ready状态在上一次触发epoll_wait返回后一直没被清理。需要等这个Socket上所有可读的数据全部被读干净,read()操作返回EAGAIN后,再次执行epoll_wait,如果再有新数据到达Socket,epoll_wait才会立即因为Socket读Ready而返回。
    如果LT模式,Socket还剩1KB数据没读,第5步执行epoll_wait后它也会带着这个Socket的fd立即返回,event列表会记录这个Socket读Ready。
    ET模式下如果数据分好几个部分到来,则即使处于读Ready状态且Socket还未读空情况下,每个新到达的数据部分都会触发一次epoll_wait返回,除非Socket的fd在注册到epoll fd的时候设置EPOLLONESHOT标志,这个Socket只要触发过一次epoll_wait返回后,不管再有多少数据到来,Socket有没有读空,都不会再触发epoll_wait返回,必须主动带着EPOLL_CTL_MOD再执行一次epoll_ctl把Socket的fd重新设置到epoll的fd上

Java的Selector

Java的NIO提供了Selector类,用于跨平台的实现Socket Polling,即IO多路复用。BSD系统上对应的是Kqueue,Window上对应的是Select,Linux上对应的是LT的Epoll(为了跨平台统一,Windows上背后是Select,是LT的)
Selector的使用:

  1. 先通过Selector.open()创建出来Selector;
  2. 创建出来SelectableChannel(可以理解为Socket),配置Channel为Non-Blocking
  3. 通过Channel下的register接口注册Channel到Selector,注册时可以带上关系的事件比如OP_READ, OP_ACCEPT, OP_WRITE等
  4. 调用Selector上的select()等待有Channel上有Event产生
  5. select()返回后说明有Channel有Event产生,通过Selector获取SelectionKey,即哪些Channel有什么事件产生了
  6. 遍历所有获取的SelectionKey检查产生了什么事件,是OP_READ还是OP_WRITE等,之后处理Channel上的事件
  7. 从select()返回的Iterator中移除处理完的SelectionKey
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容