select与epoll
    文件描述符是什么:内核用来标识某些进程正在访问文件的一个标志

    http1.0
    客户端向服务端发送一个预先定义好的文本(Http request),然后服务端处理一下(通常是从硬盘取一个后缀名是html的文件),然后把文件通过文本方式发回去(http request)

    http下面连接TCP连接通道,所有的文本数据都通过tep通道接收和发送,通道建立要用到socket
    此时单进程监听80端口,接收请求,进行处理,返回响应
    可以工作,但接收请求的速度很慢,因为操作系统要从网卡读数据,读完以后再复制给进程。

    http2.0:多进程
    当accept连接后,对于新的socket,不在主进程处理,而是新创建子进程接管,主进程不会阻塞,可以继续接受新的连接
    请求比较少的时候非常顺畅,但请求多了以后,操作系统不堪重负,每个进程都得耗费大量的系统资源,而且进程切换也很耗费资源

    http3.0 使用select
    阻塞的本质:客户端(浏览器)还没有把数据发过来,但服务端进程又迫切得想读,但又读不到,只能阻塞住
    解决问题:

    • 每次主进程接收请求后,不是迫切地去读,而是将每个socket都对应一个文件描述符,将文件描述符告诉操作系统,再去阻塞等待。
    • 操作系统在后台检查这些编号得socket,如果发现这些socket准备好了,会把对应得socket做个标记,再把进程唤醒去处理socket数据。

    总结来说,这是进程和操作系统得一种通信方式,进程告诉操作系统他要等什么东西,然后阻塞,如果东西准备好了,操作系统把进程唤醒,让操作系统做事
    但是还有一个缺陷,每次进程被唤醒之后,要把所有得文件描述符都查看一遍,看到底是哪个状态修改了,总感觉很麻烦

    http4.0 epoll
    每个进程最多只能创建有限个(1024个)文件描述符,同时很多socket并不活跃,一段时间内浏览器并没有发送数据过来, 1000多个socket可能只有几十个需要处理,但是要全都查看。而且每次进程都要把文件描述符从内核态拷贝到用户态进行遍历。
    这时出现了epoll,不需要遍历全部文件描述符集合,只要处理有变化得文件描述符即可。

    select、poll、epoll本质都是同步io,因为他们都需要在读写时间就绪后自己负责读写。
    异步io无需自己负责读写,异步io的实现会负责把数据从内核拷贝到用户空间

    select工作过程(一个select函数)
    image.png

    select的缺点

    1. 单个进程能够监视文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024
    2. 内核到用户空间内存拷贝浪费大量资源
    3. 每次需要遍历所有文件描述符
    4. 触发方式为水平触发,应用程序如果没有完成对一个已经就绪的文件描述符io操作,每次select调用还会将这些文件描述符通知进程。

    poll的改善(一个poll函数)
    select使用数组保存文件描述符表,poll使用链表保存文件描述符,没有了监视文件数量的限制。但还有其他三个缺点

    epoll的改善
    select和poll比较差的根源:在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大

    epoll怎么解决的(三个函数)

    1. - 调用epoll_create()建立一个epoll对象(在内核当中)
    2. - 调用epoll_ctlepoll对象中添加套接字对应的文件描述符(从用户态拷贝到内核态),所有文件描述符被维护在一棵红黑树上,通过回调函数内核可以将io准备好的描述符放到一个链表里面,该链表存在于内核空间和用户空间共享的内存当中。
    3. - 进程调用epoll_wait收集活跃的连接

    epoll_wait向进程返回活跃的socket进行处理
    epoll水平触发:每次调用epoll_wait()检测到文件描述符状态修改时,将此事件通知进程,进程可以不立即处理该时间,下次调用epoll_wait()会再次通知进程。(默认方式)支持阻塞和非阻塞io
    epoll边缘出发(特殊也是特色的方式):调用epoll_wait()后进程必须立即处理时间,下次再调用时不会得到原来就绪事件的通知。只支持非阻塞io,否则容易处理一个较长的操作时,把其他任务饿死。