先从建好的连接开始介绍,稍后将解释新建连接是如何工作的。
当一个新的数据包进入网络接口(NIC)时,通过被NIC中断或通过轮询NIC的方式通知内核获取数据。通常内核是由中断驱动还是处于轮询模式取决于网络通信量;当NIC非常繁忙时,内核轮询效率更高,但如果NIC不繁忙,则可以使用中断来节省CPU周期和电源。Linux称这种技术为NAPI,字面意思是“新的api”。
当用户态的进程实际调用文件描述符上的read(2)时,它会导致内核从其接收缓冲区中删除数据,并将该数据复制到此进程调用read(2)所提供的缓冲区中。
这种设计的一个结果是,如果应用程序读取速度太慢或写入速度太快,内核的接收和写入队列可能会被填满。因此,内核为读写队列设置最大大小。这样可以确保行为不可控的应用程序使用有限制的内存量。例如,内核可能会将每个接收和写入队列的大小限制在100KB。然后每个TCP套接字可以使用的最大内核内存量大约为200KB(因为与队列的大小相比,其他TCP数据结构的大小可以忽略不计)。
读语义
如果接收缓冲区是非空的,并且用户调用read(2),系统调用将立即返回这些可用的数据。如果读取队列中准备好的数据量小于用户提供的缓冲区的大小,则可能发生部分读取。调用方可以通过检查read(2)的返回值来检测到这一点。
如果写入队列未满,并且用户调用写入,则系统调用将成功。如果写入队列有足够的空间,则将复制所有数据。如果写入队列只有部分数据的空间,那么将发生部分写入,并且只有部分数据将被复制到缓冲区。调用方通过检查write(2)的返回值来检查这一点。
在上一节中,我们看到了已建立的连接如何使用接收和写入队列来限制为每个连接分配的内核内存量。使用类似的技术也用来限制为新连接保留的内核内存量。
accept(2)的原型采用一个套接字和两个字段来存储另一端套接字的信息。accept(2)返回的值是一个整数,表示新建立连接的文件描述符:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);backlog是一个参数,当用户没有足够快地调用accept(2)时,它控制内核将为新连接保留多少内存。
内核的第一个选择是根本不接受连接。例如,内核可以拒绝对传入的SYN包进行ACK。更常见的情况是,内核将完成TCP三次握手,然后使用RST终止连接。不管怎样,结果都是一样的:如果连接被拒绝,就不需要分配接收或写入缓冲区。这样做的理由是,如果用户空间进程没有足够快地接受连接,那么正确的做法是使新请求失败。反对这样做的理由是,这太粗暴(aggressive),尤其是如果新的连接爆发(bursty)的时候。
支持第二种方式的理由是,当处理速率或连接速率趋向于爆发时,它过于“宽宏大量”。例如,在我们刚才描述的服务器中,假设有10个新连接同时出现,然后这一秒中没有更多的连接出现。如果内核将新连接排队,那么在第这一秒中所有的请求都会被处理。如果内核采用拒绝新的连接的策略,那么即使进程本来能够满足请求速率的,也只有一个连接会成功。
正如您可能怀疑的那样,内核实际上结合了这两种方法。内核将会对新连接进行排队,但只是一定数量的连接。内核将排队的连接数量由listen(2)的backlog参数控制。通常此值设置为相对较小的值。在Linux上,socket.h 将 somaxconn 的值设置为128,在kernel 2.4.25之前,这是允许的最大值。现在最大值是在/proc/sys/net/core/somaxconn中指定的,但是通常您会发现程序使用somaxconn(或更小的硬编码值)。
在编写网络服务器时,监控监听溢出非常重要,因为监听溢出不会从服务器的角度触发任何用户可见的行为。服务器将愉快地accept(2)每日的连接,而不返回任何连接被丢弃的迹象。例如,假设您为Python应用程序使用Nginx作为代理服务器。
如果python应用程序太慢,则可能导致nginx listen套接字溢出。当发生这种情况时,您将在nginx日志中看不到任何关于这一点的指示,您将一直看到200状态代码,像往常一样。因此,如果您只是监视应用程序的HTTP状态代码,您将无法看到阻止请求转发到应用程序的TCP错误。
