1. 基础概念
1.1 Socket
套接字。对网络中不同主机上的应用进程之间进行双向通讯的端点的抽象。
例子:客户端将数据通过网线发送到服务端,客户端需要一个出口,服务端需要一个入口,这两个“口子”就是Socket
1.2 FD: file descriptor
文件描述符,非负整数。“一切皆是文件”,linux中的一切资源都可以通过文件的方式访问和管理。而FD就是类似文件的索引(符号),指向某个资源。内核(kernel)利用FD来访问和管理资源。
1.3 socket通讯流程图
2 IO模型
例子: 你是一个老师,让学生做作业,学生做完作业后收作业
- 同步阻塞::逐个收作业,先收A,在收B,接着收C/D,如果有一个学生还未做完,则你会等到他写完 ,然后才继续收下一个
- 同步非阻塞:逐个收作业,先收A,在收B,接着收C/D,**如果有一个学生还未做完,则你会跳过该学生,**继续收下一个
(相对于同步阻塞这已经是一个很好的方法,不会因为某个学生未就绪而阻塞住,减少了对后续学生的影响,但这个访问也有个问题, 就是如果你下去收作业的时候 ,全部学生都还没做完,则你可能会白收一圈。)
- select/poll:学生写完了作业会举手,但是你不知道是谁举手,需要一个个的去询问
- epoll: 学生写完了作业会举手,你知道是谁举手,你直接去收作业
2.1 同步阻塞
三次握手之后
客户端write的时候,也会阻塞。write完成以后:
接下来会处理第二个请求
同步阻塞总结:
- 单线程: 某个socket阻塞,会影响到其他socket处理
- 多线程: 客户端较多时,会造成资源浪费,全部socket中可能每个时刻只有几个就绪。同时,线程的调度、上下文切换乃至它们占用的内存,可能成为瓶颈。
2.2 同步非阻塞
2.2.1 从操作底层看IO
现在的linux操作系统底层会分为用户空间和内核空间,当进程或者线程运行在内核空间的时候就处于内核态,在用户空间的时候,则处于用户态。在用户空间下只能执行一些相对安全的一些cpu指令。一些危险的特权指令值能在内核空间下运行。
涉及到socket的操作都需要在内核空间下完成的,用户空间下无法完成这些操作的。但是内核将socket的这些相关能力封装后,通过像read 、write这些函数提供出来,供用户空间使用。这些函数也成为系统调用函数。当我们调用read、write这些函数时,系统会切换到内核态,由内核来执行相应的socket操作,最终将执行的结果返回给我们。
当我们在用户空间调用read函数读取数据的时候,数据会从网卡先拷贝到内核空间,也就是socket缓冲区,然后从内核空间拷贝到用户空间。而write函数则相反,需要从用户空间拷贝到内核空间,最后在拷贝到网卡上。
例子:
假设服务端现在与四个客户端建立了四个socket连接。 会产生对应的四个fd。
同步非阻塞总结:
从操作系统层面解决了阻塞问题。
- 优点:单个socket阻塞,不会影响到其他socket
- 缺点:需要在用户空间不断的去遍历调用read函数来检查数据是否到来。这个系统调用,会涉及到用户态和内核态的一个切换, 当socket的比较多的时候,这会有很大的开销。
2.3 Select
select是如何解决同步非阻塞IO下系统调用频繁的一个问题。
继续按上面的例子分析select调用过程:
例子:
假设服务端现在与四个客户端建立了四个socket连接。 会产生对应的四个fd
当我们在用户空间调用select函数的时候,我们首先会将这四个fd拷贝一份到内核空间,然后接着内核空间这个时候会来遍历这四个fd。就是会检查这个fd上对应的socket有没有数据到来,也就是有没有就绪,如果没有就绪,他就继续往下检查,直到他如果检查到某个fd已经就绪了,就是说已经有数据可以读了。这个时候他会将这个fd打上一个标记,然后最终返回。(返回的fd列表里就绪fd的数量)
返回完以后,此时用户空间知道有事件就绪了,但是它并不知道具体是哪个fd上的socket就绪,所以这个时候用户空间需要去遍历这个fd的集合,找到最终的就绪的fd,然后去对这个fd进行数据的处理。
select遍历完一次, 它发现没有fd就绪,这个时候其实可能会将当前的一个用户进程给阻塞起来。然后当客户端向服务端发送数据时,这个数据会通过网络传输来到达服务端的这个网卡。然后网卡会通过DMA的方式将这个数据包写入到指定的内存中。然后处理完成之后,会通过中断信号告诉cpu有新的数据包到达。cpu收到中断信号后会进行响应中断,然后调用中断的处理程序进行处理。首选根据这个数据包的ip跟端口找到对应的这个socket,然后将这个数据保存到这个socket的一个接收队列,然后再检查这个socket对应的一个等待队列里面会否有进程正在阻塞等待。如果有的话,则会唤醒该进程,我们的用户进程唤醒以后,则会再继续检查一遍这个fd集合。
fd_set
使用long类型数组实现位图:1个long可以表示64位,则16个long可以表示1024位
select总结:
将socket是否就绪检查逻辑下沉到操作系统层面,避免了大量系统调用。告诉你有时间就绪,但是没告诉你具体是哪个fd
- 优点:不需要每个fd都进行一次系统调用,解决了频繁用户态、内核态切换问题
- 缺点:单个进程监听的fd存在限制,默认1024;每次调用需要将fd从用户态拷贝到内核态;不知道具体是哪些文件描述符就绪,需要遍历全部文件描述符;入参的三个fd_set 集合每次调用都需要重置
2.4 epoll
epoll总结:
高效的处理高并发下的大量连接,同时有非常优异的性能
- 优点:解决了上述的缺点
- 缺点:跨平台性不够好,只支持linux,macOS等操作系统不支持;相较于epoll,select更轻量可移植性强;在监听连接数和事件较少的场景下,select可能更优