网络IO理解
首先服务端将本机地址和端口bind在listensock上,再用listen()去将listensock套接字设置为listen状态,然后调用accept,进入阻塞状态。如果此时有客户端请求连接,就是第一次握手的开始。
客户端会先调用connect来申请连接:
connect调用时是发送SYN,然后服务端返回SYN+ACK,然后客户端的connect返回,发送最后一个ACK,服务端接收到以后accept返回,开始进行read。
这里的过程,服务端会被阻塞两次,一次是accept,一次是后续的read。
read具体的读取过程就是从sock读取数据到内核缓冲区(网络设备->内核缓冲区),然后read将数据从内核缓冲区拷贝到用户缓冲区(内核缓冲区->用户缓冲区):
对阻塞IO的理解
这个就是经典的阻塞IO,那么此时就要从read解决问题。
这里要明确一个问题,就绪事件是在数据从网卡拷贝到内核缓冲区以后就产生了,也就是说内核缓冲区有数据了就是就绪事件了。
对select的理解
如果有多个客户端连接,每个客户端对应一个线程去按部就班处理是不理想的方式。我们可以想到的是把这些fd放在一个容器,然后用一个线程不停地遍历,然后对于有就绪事件的fd进行处理。
对poll的理解
poll相对于select其实就是没有了文件描述符数量的限制,
对epoll的理解
epoll解决了三个问题:
- select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
- select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)
- select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
优化为:
- 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。
- 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
- 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。