一、彻底解密:Select,Poll底层系统调用的核心思想原理
1.1、在我们学习高并发的时候,epoll是个基础,但是也要对比先了解下select,poll的底层系统调用的核心思想,核心原理。
1.2、epoll作为linux下高性能网络服务器的必备技术至关重要,Java的nio,nginx,redis,skynet和大部分游戏服务器都使用到这一多路复用技术。
1.3、epoll的重要性,也是大厂的面试必备,不少大厂在招聘服务端的时候,可能会问到epoll相关的问题。
比如:epoll和select的区别是什么?epoll高效率的原因是什么?
二、首先看下计算机的系统结构
数据的读取过程:网卡会把接收到的数据写入到socket内核缓冲区。
①阶段:网卡收到网线传来的数据:在上面的图中对应的是IO南桥芯片上的DMA(CPU的助理)通过网络接口收集从网线传来的数据。
②阶段:硬件电路的传输;数据的传输是在硬件的电路上传输的。
③阶段:最终将数据写入到内存中的某个地址上。比如IO南桥芯片上,DMA控制程序把数据写到IO北桥芯片上,然后最终通过存储器总线以及存储器接口把数据写入到socket的内核缓冲区上。
三、网络编程
下面是一段最基础的网络编程代码,先创建socket对象,依次调用bind,listen,accept,最后调用recv接收数据
4、操作系统的工作队列
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到revc时,程序会从运行状态变为等待状态、接收到数据后又变回到运行状态,操作系统会分时获取到时间片执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。
下图中的计算机中运行着A,B,C三个进程,其中进程A执行着上述基础网络程序,一开始,这三个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。
4.1、socket对象的结构
当进程A执行到创建socket时,操作系统会创建一个由文件系统管理的socket对象。这个socket对象包含了发送缓冲区;接收缓冲区;等待队列、socket的等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。
4.2、进程A从工作队列移动端该socket的等待队列中
当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的的等待队列中,线程将阻塞。
由于工作队列只剩下了进程B和C,依据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序,所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。
4.3、Cpu的工作队列,多核cpu都有自己的工作队列
5、内核接收数据的全过程
当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码,也由于socket的接收缓冲区已经有了数据,recv可以返回接收到的数据
①步骤:计算机收到了对端传送的数据。
②步骤:数据经由网卡传送到内存
③步骤:然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序
④步骤:中断程序先将网络数据写入到对应的socket的接收缓冲区里面
⑤步骤:再唤醒进程A,将进程A重新放入到工作队列中。
6、唤醒进程的过程如下图所示:
首先在上面的步骤中,产出几个问题
①、内核如何知道接收的网络数据是属于哪个socket的呢?
答案:socket的数据包格式(源ip,源端口,协议,目的ip,目的端口)
一般通过目的ip,目的端口,就可以识别出来接收到的网络数据是属于哪个socket。
②、如果目的ip,目的端口是相同的那怎么办呢?
其实多个客户端与同一个服务端建立了连接,这个时候内核就会有多个socket。并且为它们分配多个fd文件描述符,它们收到网络数据后无法通过目的端口来直接匹配socket的,还需要再通过源ip和端口来确定是属于哪个socket。
③、内核如何同时监控多个socket?
内核负责轮训所有的socket,当某个socket有数据到达了,就通知用户进程。目前经典的解决办法:I/O多路复用。
多路复用在linux内核代码迭代过程中依次支持了三种调用:
SELECT;POLL;EPOLL
二、select操作:Linux内核,windows内核都提供了select操作,可以把1024个文件描述符的IO事件轮询,简化为一次轮询,轮询发生在内核空间中。
在如下的代码中:
先准备一个数据fds存放着所有需要监视的socket,然后调用select。
如果fds中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。
用户程序可以遍历fds数组,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。
例子:假如程序同时监视如下图的Sock1,Sock2,Sock3三个Socket,那么在调用Select之后,操作系统把进程A分别加入这三个Socket的等待队列中。
当任何一个socket收到数据后,中断程序将唤起进程,下图展示了sock2接收到了数据的处理流程
所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面,如下图所示:
对于调用了select的进程A而言:
①、A存在于多个socket的等待队列中。
②、当某个socket被写入数据时,A也被唤醒并从多个socket的等待队列中移除后加入内核的工作队列中。
③、但是此时A并不知道是哪个socket被写入了数据,所以只能遍历所有的socket。
④、在A处理完成任务后移出内核的工作队列,但是此时却需要遍历所有的socket并加入它们的等待队列中。
Select的不足:两次socket列表遍历
第一次:每次调用select都需要将fds列表传递给内核,有一定的开销,进程加入socket的等待队列时,需要遍历所有的socket.
第二次:当进程A被唤醒后,进程只知道至少有一个socket接收到了数据,不知道是谁,程序需要遍历一遍socket列表后,才可以得到就绪的socket,唤醒后需要从等待队列中移除。
正是因为遍历操作开销大,出于效率的考量,才会规定socket的最大监视数量,默认只能监视1024个socket。
三、poll的出现
1997年,出现了poll作为select的替代者,最大的区别就是poll不再限制socket的数量。
3.1、poll系统调用
poll其实内部实现基本跟select一样,区别在于它们底层组织fd[]的数据结构不太一样,从而实现了poll的最大文件句柄数量限制去除了。
poll的描述符fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。
3.2、pollfd的结构
成员变量说明:
fd:每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。
events:表示要告诉操作系统需要监视fd的事件:如输入,输出,错误,每一个事件有多个取值
revents:revents域是文件描述符的操作结果事件,内核在调用返回时设置这个域,events域中请求的任何事件都可能在revents域中返回。
3.3、events和revents的取值如下:
3.4、socket读就绪条件(读事件)
①、该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记SO_RCVLOWAT。对于TCP和UDP的套接字而言,缓冲区低水位的值默认为1,那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的,我们可以通过使用SO_RCVLOWAT套接字选项(setsockopt函数)来设置该套接字的低水位大小,此种描述符就绪(可读)的情况下,当我们使用read/recv等对该套接字执行读操作的时候,套接字不会阻塞,而是成功返回一个大于0的值。即可读数据的大小。
②、该连接的读半部关闭(tcp的连接都是双工模式),也就是接收到FIN的TCP连接,对于这样的套接字的读操作,将不会阻塞,而是返回0也就是EOF。
③、该套接字是一个listen的监听套接字。并且目前已经完成的连接数不为0,对这样的套接字进行accept操作通常不会阻塞。
④、有一个错误套接字待处理,对这样的套接字的读操作将不阻塞并返回-1,同时把errno设置成确切错误条件,这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
3.5、socket写就绪条件(写事件)
①、socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SENDLOWAT,此时可以无阻塞的写,并且返回值大于0.对于TCP和UDP而言,这个低水位SO_SENDLOWAT的值默认是2048,而套接字的发送缓冲区的大小是8k,这意味着一般一个套接字连接成功后,就是处于可以写的装态的。我们可以通过SO_SENDLOWAT套接字选项来设置这个低水位,此种情况下,我们设置该套接字为非阻塞。对该套接字进行写操作如write,send等。将不阻塞,并返回一个正值。例如:由传输层接受到的字节数,即发送的数据大小。
②、该连接的写半部关闭(主动发送的FIN包的TCP连接),对这样的套接字的写操作将会产生SIGPIPE信号,所以我们的网络程序基本都要自定义处理SIGPIPE信号,因为SIGEPIPE的信号的默认处理方式都是退出程序。
③、使用非阻塞的connect的套接字已建立连接,或者connect已经以失败告终,即connect有结果了。
④、有一个错误套接字待处理,对这样的套接字的读操作将不阻塞并返回-1,同时把errno设置成确切错误条件,这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
poll()系统调用的本质
select和poll系统调用的本质一样
poll的机制与select在本质上没有多个差别,每次调用时,都需要把fd集合从用户态拷贝到内核态。
二者管理多个描述符也是进行轮询,根据描述符的状态进行处理。