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函数)
select的缺点
- 单个进程能够监视文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024
- 内核到用户空间内存拷贝浪费大量资源
- 每次需要遍历所有文件描述符
- 触发方式为水平触发,应用程序如果没有完成对一个已经就绪的文件描述符io操作,每次select调用还会将这些文件描述符通知进程。
poll的改善(一个poll函数)
select使用数组保存文件描述符表,poll使用链表保存文件描述符,没有了监视文件数量的限制。但还有其他三个缺点
epoll的改善
select和poll比较差的根源:在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大
epoll怎么解决的(三个函数)
- 调用epoll_create()建立一个epoll对象(在内核当中)
- 调用epoll_ctl向epoll对象中添加套接字对应的文件描述符(从用户态拷贝到内核态),所有文件描述符被维护在一棵红黑树上,通过回调函数内核可以将io准备好的描述符放到一个链表里面,该链表存在于内核空间和用户空间共享的内存当中。
- 进程调用epoll_wait收集活跃的连接
epoll_wait向进程返回活跃的socket进行处理
epoll水平触发:每次调用epoll_wait()检测到文件描述符状态修改时,将此事件通知进程,进程可以不立即处理该时间,下次调用epoll_wait()会再次通知进程。(默认方式)支持阻塞和非阻塞io
epoll边缘出发(特殊也是特色的方式):调用epoll_wait()后进程必须立即处理时间,下次再调用时不会得到原来就绪事件的通知。只支持非阻塞io,否则容易处理一个较长的操作时,把其他任务饿死。