IO多路复用解析
Socket
就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口;
FD:file descriptor
文件描述符,非负整数.”一切皆文件”,Linux中的一切资源都可以通过文件的方式访问和管理,而FD就类似于文件的索引(符号),指向某个资源,
内核(kernel)利用FD来访问和管理资源;
用户态和内核态
现在的Linux操作系统会分为用户空间和内核空间;
用户态:当进程或者线程运行在用户空间的时候就处于用户态,在用户空间下只能执行一些相对安全的cpu指令;
内核态:当进程或者线程运行在内核空间的时候就处于内核态,在内核态可以执行一些比较危险的特权指令,这样做的目的是为了整个系统的安全考虑,避免应用程序瞎搞,把整个操作系统给搞挂了;但是内核空间它也将一些socket的相关能力进行封装,会通过像read和write这些函数提供出来供用户空间使用,这些函数也称为系统调用函数;当我们在用户态调用这些函数后,系统会切换到内核态,由内核来完成相应的socket操作,最终将执行的结果返回给我们;
系统调用函数read和write的核心流程
read:当我们在用户空间调用read函数的时候,数据会先从网卡拷贝到内核空间,也就是socket缓冲区,然后再从内核空间拷贝到用户空间;
write:与read函数相反,由用户空间拷贝到内核空间,然后再拷贝到网卡上;
同步阻塞
线程阻塞等待与就绪socket建立连接,严格的按照与建立Socket建立连接顺序来进行数据的读写;
单线程:如果某个socket发生了阻塞,会影响到其他socket的处理;
多线程:客户端较多时,会造成资源的浪费,全部socket中每个时刻可能只有几个就绪。同时线程的调度,上下文的切换甚至线程栈内存资源的占用都可能成为系统的瓶颈;
同步非阻塞
同步非阻塞IO需要我们在用户空间下不断的去遍历所有监听的socket的FD,调用read函数来检查数据是否到来;
优点:相对于同步阻塞,单个socket的阻塞不会影响到其他socket;
缺点:同步非阻塞它需要不断去遍历调用read函数来检查是否有数据到来,调用read函数会有一个用户态切换到内核态的开销,如果socket较多的情况下,这个开销是很大的;
select
当用户空间调用select函数时,会先将所有监听socket的FD拷贝一份到内核空间,然后阻塞当前用户空间调用select函数的线程程,内核空间会检查当前所有FD中没有就绪的,如果有就会返回就绪的FD集合,没有则阻塞当前检查进程,当有数据包到达后,socket接收到中断信号,会检查当前socket对应的等待队列是否有进程正在阻塞等待,如果有的话则会唤醒阻塞的内核进程,内核进程则会去遍历一遍所有的FD,然后检查到某些FD就绪之后,然后将相应的FD集合返回给用户空间,用户空间根据集合,遍历一遍所有的FD那些是就绪的,然后对就绪的FD进行数据的读写;
优点:select相对于同步非阻塞,将检查所有监听的socket对应的FD是否就绪下沉到了操作系统层面,从而避免了在用户空间进行大量的read函数造成用户态与内核态切换的系统开销;
缺点:每个进程打开的FD有一定的上限,由FD_SETSIZE设置,默认值是1024;
每次调用select需要将所有的FD从用户态拷贝到内核态;如果这个FD集合是比较大的,这个也是会造成一定的开销的;
用户空间接受到返回FD集合需要遍历一遍所有的FD获取就绪的FD,这里会造成一个O(n)遍历的开销;
epoll(select的增强版本)
epoll有三个系统调用函数:
1.epoll_create(int size):用于创建一个给定size大小的epoll;
2.epoll_ctl:用于注册一个FD的读事件或者写事件到epoll;
3.epoll_wait:类似与select,用于获取就绪FD事件;
首先我们会在我们用户空间调用epoll_create函数来创建epoll,epoll中主要有三个数据结构,分别是用一个红黑树结构来维护所有的FD来达到一个快速查找FD的效果,第二个就是一个List结构用来维护所有就绪的FD列表,第三个就是一个队列结构来维护遍历所有FD发现没有FD就绪而阻塞的进程,方便后续有Socket接收到数据包来唤醒进程;
接着调用epoll_ctl函数,会将FD添加到内核空间的红黑树上;FD添加完成后;
接着调用epoll_wait函数,用户空间调用epoll_wait的线程会被阻塞,内核空间会检查所有的FD是否有就绪的,有则添加就绪事件
到就绪列表,没有,则进程让出cpu,将进程添加到阻塞队列;当有数据包到达后,socket接收到中断信号,会将对应事件添加到就序列表,并唤醒阻塞的进程,然后会将就绪列表中的事件集合返回给用户空间,用户空间进行相应的事件处理;
优点:直接将FD维护在内核空间的红黑树上,避免了从用户态将FD拷贝到内核态;
通过就绪列表直接就可以获取到那些FD是就绪的,避免了遍历所有的FD来获取就绪的FD;
缺点:跨平台性不够好,目前只支持Linux,像macOS等操作系统不支持;
在监听连接数和事件较少的场景下,select可能会更好,epoll所解决的问题是FD较多的情况下,在FD较少的情况下,由于select的实现更加简单,其他的性能是可能会更好的;
水平触发和边缘触发
LT:水平触发,默认。epoll_wait检测到事件后,如果该事件没有被处理完毕,后续每次epoll_wait调用都会返回该事件,水平触发相对来说更注重
安全性一点,但是他每次都需要检查事件是否处理完毕;
ET:边缘触发,epoll_wait检测到事件后,只会在当次返回该事件,不管该事件是否被处理完毕,边缘触发相对来说更注重性能一点;