系统调用:用户态和内核态的切换。用户态进程通过系统调用申请使用操作系统提供的程序。本质是操作系统为用户开放了一个中断。
文件描述符(file descriptor):
IO多路复用(事件驱动):多路是指网络连接,复用的是同一个线程。是一种同步IO模型在epoll等系统调用时是阻塞的,read/write等io操作时是非阻塞的。
IO多路复用:指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如通过select和poll函数等系统调用。一个线程可以监视多个文件描述符的读写就绪状况,某个socket有可读或者可写的时候,可以给一个通知,才去做读写操作。文件句柄就绪,通知应用程序进行读写操作;没有就绪文件句柄阻塞应用程序并交出cpu。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行,比如使用线程池。在启动上千个连接时,一个线程监控就绪状态,就绪的每个连接开一个新的线程处理即可。
多路复用的三种实现方式:select、poll、epoll。
select模型:每次调用select都需要将fd集合从用户态拷贝到内核。遍历所有fd判断有无读写事件发生效率较低。单个进程打开的fd有限制默认1024。
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,直到有数据可读、可写、出异常、超时就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理(bitmap)。
poll模型:每次调用需要将fd从用户态拷贝到内核态。遍历所有fd判断有无读写事件发生效率较低。不限制最大文件描述符采用链表的方式存储fd。
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间。然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。
epoll模型(仅Linux支持):
epoll使用事件的就绪通知方式,通过epoll_ctl注册fd,一旦fd就绪内核就会采用callback的回调机制来激活此fd,epoll_wait便可以收到通知,进行相关的io操作。
epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,则用户态拷贝到内核态只需要一次。
结构体:红黑树:节点存储epoll需要监控的事件
双链表:就绪事件,没有则阻塞进程。
函数:
epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd
epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数
epoll_wait() 轮训所有的callback集合,并完成对应的IO操作
优点:
没有并发限制:支持fd上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄。
效率提高:使用回调通知,只有活跃的fd才会调用callback函数,即只负责活跃的连接,跟连接总数无关。
内存拷贝:利用mmap(mmap是一种内存映射文件的方法,即将一个文件映射到进程的地址空间)映射文件至内存,内核和用户空间mmap同一块内存实现,减少复制的开销。
EpollLT模式(水平触发):默认模式,只要fd还有数据可读,每次epoll_wait都会返回它的事件,提醒程序操作。
EpollET模式(边缘触发):高速模式,fd变为有绪状态,只会通知一次。无论fd中是否还有数据可读,直到下次有数据流入才会再次通知。在ET模式下,读fd时需要将buffer读完或者或者遇到EAGAIN错误。必须使用非阻塞IO,避免由于一个文件句柄阻塞导致其他的也阻塞。
区别:一个进程支持的最大句柄数量/fd剧增后的io效率问题/消息传递方式
为什么采用非阻塞io:多路复用只能返回有可读但是不会返回多少可读。当第二次调用,socket里面有没有数据是不确定的,要是贸然调用,read可能就阻塞了。
惊群现象:多个进程或者线程通过 select 或者 epoll 监听一个 listen socket,当有一个新连接完成三次握手之后,所有进程都会通过 select 或者 epoll 被唤醒,但是最终只有一个进程或者线程 accept 到这个新连接,若是采用了阻塞 I/O,没有accept 到连接的进程或者线程就 block 住了。
同步阻塞io:当accept一个请求后,在recv或send调用阻塞时,将无法accept其他请求(必须等上一个recv或send处理完),无法并发处理。服务端可以采用多线程,但是大量的线程会占用内存,线程切换也需要开销。
同步非阻塞io:服务器端当accept一个请求后,加入fds集合,每次轮询一遍fds集合recv(非阻塞)数据,没有数据则立即返回错误,但是每次轮询所有fd(包括没有发生读写事件fd)会很浪费cpu。
\
\
阻塞:数据没有到达时进程会等待直到数据到达。
非阻塞:数据没有到达时进程会返回并过段时间再次尝试调用。
同步io:数据到达时需要等待并处理数据。引起请求进程阻塞,直到IO操作完成。
异步io:调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。由内核启动操作,并在整个操作完成后通知进程。不引起请求进程阻塞。
io复用:进程先调用epoll等系统调用(阻塞)获取是否有数据到达,有数据再进行IO操作(非阻塞)。
信号驱动io模型:内核在描述字准备好时用信号SIGIO通知我们(非阻塞)。首先允许套接口进行信号驱动IO,然后通过系统调用sigacation安装信号处理程序。此系统调用立即返回,进程继续工作,是非阻塞的。当数据报准备好被读时,为该进程生成一个SIGIO信号,随后在信号处理程序中调用IO操作读取数据报。