深入解析Linux TCP/IP栈工作原理:Socket系统调用与内核数据结构交互详解

本文会从上层来阐述 Linux 上的 TCP/IP 栈的工作方式,具体而言,包括 socket 系统调用与内核数据结构的交互,以及内核与实际网络的交互。撰写这篇文章的部分缘由是对监听队列溢出是如何运作进行解释,因为它与我在工作中一直钻研的一个问题存在关联。

深入解析Linux TCP/IP栈工作原理:Socket系统调用与内核数据结构交互详解

建好的连接怎么工作

先从建好的连接开始介绍,稍后将解释新建连接是如何工作的。

内核管理的每一个 TCP 文件描述符都是一个 struct 。它会记录 TCP 相关的信息,像序列号、当前窗口大小等等。同时,它还拥有一个接收缓冲区(receive buffer,也叫 receive queue)和一个写缓冲区(write buffer,也叫 write queue)。后面我会交替使用 buffer 和 queue 这两个术语。你若对更多细节感兴趣,那么能够在 Linux 内核的 net/sock.h 里看到 socket 结构的实现。

当一个新的数据包进入网络接口(NIC)时,内核可通过被 NIC 中断或者轮询 NIC 的方式来获取数据。通常情况下,内核是以中断驱动还是处于轮询模式,这取决于网络通信量。若 NIC 非常繁忙,那么内核采用轮询的效率会更高;而如果 NIC 不繁忙,就可以使用中断,这样能节省 CPU 周期和电源。Linux称这种技术为NAPI,字面意思是“新的api”。

内核从 NIC 获取数据包后,会对其进行解码。接着,它会依据源 IP、源端口、目标 IP 和目标端口,找出与该数据包相关联的 TCP 连接。此信息可用于查找与该连接关联的内存中的 struct sock。若数据包是按顺序到来的,那么数据有效负载会被复制到套接字的接收缓冲区中。此时,内核会执行 read(2)。同时,内核也会使用诸如 select(2) 这样的 I/O 多路复用方式系统调用,或者使用 epoll_wait(2) 这样的 I/O 多路复用方式系统调用,以此来唤醒等待此套接字的进程。

用户态的进程实际调用文件描述符上的 read(2)时,会使内核从其接收缓冲区中删除数据,并且会把该数据复制到进程调用 read(2)所提供的缓冲区中。

发送数据的工作原理类似。应用程序调用 write(2)时,会把数据从用户提供的缓冲区复制到内核写入队列里。接着,内核会把数据从写队列复制到 NIC 中,并且实际发送数据。如果网络处于繁忙状态,倘若 TCP 发送窗口已满,又或者存在流量整形策略等情况,从用户实际发起 write(2)这个操作开始,到将数据传输至 NIC 的实际时间可能会出现延迟。

这种设计会有一个结果,那就是当应用程序读取速度过慢或者写入速度过快时,内核的接收队列和写入队列有可能会被填满。所以,内核为读写队列设定了最大大小。这样做能够保证行为无法控制的应用程序使用的内存量是有限制的。比如,内核可能会把每个接收队列和写入队列的大小限制在 100KB 。每个 TCP 套接字能够使用的最大内核内存量大概是 200KB 。因为其他 TCP 数据结构的大小相比于队列的大小而言可以忽略不计 。

读语义

直到有数据可以使用。

如果接收缓冲区不是空的,用户调用了 read(2),那么系统调用会立刻返回那些可用的数据。如果读取队列中准备好的数据量比用户提供的缓冲区的大小要小,就可能会出现部分读取的情况。调用方能够通过检查 read(2)的返回值来察觉到这一情况。

如果接收缓冲区已满,TCP 连接的另一端又尝试发送更多数据,那么内核会拒绝对数据包进行 ACK。这属于常规的 TCP 拥塞控制情况。

写语义

如果写入队列未被填满,同时用户发起写入操作,那么系统调用就会成功。若写入队列有充足的空间,就会复制所有数据。倘若写入队列仅有部分数据的空间,就会进行部分写入,并且只有部分数据会被复制到缓冲区。调用方通过查看 write(2)的返回值来确认这一情况。

如果写入队列已满,同时用户调用了写入(write(2)),那么系统调用将会被阻塞。

新建连接的工作机制

在之前的一节里,我们看到已建立的连接能够利用接收队列和写入队列来对为每个连接所分配的内核内存量进行限制。类似的技术也被用于对为新连接预留的内核内存量进行限制。

从用户态角度而言,新建立的 TCP 连接是通过在监听套接字上进行 accept(2)的调用而创建的。并且,监听套接字是使用 listen(2)系统调用所形成的套接字。

accept(2)的原型通过一个套接字以及两个字段来对另一端套接字的信息进行存储。accept(2)所返回的值为一个整数,该整数表示新建立连接的文件描述符。

int 函数用于接受连接,它接受三个参数,一个是套接字描述符 sockfd,一个是指向结构体 sockaddr 的指针 addr,还有一个是指向 socklen_t 类型变量的指针 addrlen,用于获取地址的长度。

listen 的原型使用了一个套接字文件描述符以及一个 backlog 参数。

函数 int listen 接收两个参数,一个是 sockfd,另一个是 backlog;它的功能是进行监听相关的操作。

backlog 是一个参数。如果用户没有足够快地调用 accept(2),那么它会控制内核为新连接保留多少内存。

假设您拥有一个存在阻塞的单线程 HTTP 服务器,每个 HTTP 请求大概需要 100 毫秒。在此情形下,HTTP 服务器处理每个请求会花费 100 毫秒,之后才能再次调用 accept(2)。这表明在最多 10 个 rps 时不会出现排队现象。倘若内核中的 rps 超过 10 个,那就有两种选择。

内核的第一个选择是不接受连接。比如,内核能够拒绝对传入的 SYN 包进行 ACK。较为常见的是,内核会完成 TCP 三次握手,接着使用 RST 来终止连接。无论哪种情况,其结果都是相同的:倘若连接被拒绝,就无需分配接收或写入缓冲区。这样做的理由在于,倘若用户空间进程未能足够迅速地接受连接,那么恰当的做法便是让新请求失败。反对这样做的理由为,这种做法过于粗暴,尤其在新的连接呈现爆发性的时候。

内核的第二个选择是接受连接,接着为其分配一个套接字结构,这个结构包含接收缓冲区和写入缓冲区。然后把套接字对象进行排队,以便以后使用。当下次用户调用 accept(2)时,就能够立即获得已分配好的套接字,而不会出现阻塞系统调用的情况。

支持第二种方式的理由在于,当处理速率或连接速率有趋向于爆发的情况时,它显得过于“宽宏大量”。比如,在刚才所描述的服务器里,倘若有 10 个新连接同时出现,接着在这一秒内不再有更多连接出现。要是内核将新连接进行排队,那么在这一秒当中所有的请求都能够被处理。内核采用拒绝新连接的策略时,即便进程原本能够满足请求速率,也只有一个连接能够成功。

有两个反对排队的论点。其一,过多排队会致使分配大量内核内存。倘若内核在分配带有大接收缓冲区的数千个套接字,那么内存使用量便可能迅速增加,并且用户空间进程甚至有可能无法处理所有这些请求。其二,排队会让应用程序在连接的另一端(即客户机)显得很缓慢。客户机能够看到它具备建立新 TCP 连接的能力。然而,当它尝试使用这些新连接时,服务器的响应显得极为缓慢。鉴于此情况,建议让新的连接失败,因为这样能更明显地反馈出服务器的异常。另外,如果服务器严重破坏了新的连接,客户机便可以知晓要进行退让;这属于另一种拥塞控制形式。

监听队列(listen queue)和溢出

您可能会怀疑,内核实际上是将这两种方法结合起来的。内核会对新连接进行排队,不过只是一定数量的连接。内核排队的连接数量是由 listen(2)的 backlog 参数来控制的。通常,这个值会被设置为相对较小的数值。在 Linux 系统中,socket.h 把 somaxconn 的值设定为 128 。在 kernel 2.4.25 以前,这就是被允许的最大值。而现在的最大值处于……

在 /proc/sys/net/core/somaxconn 中进行了指定,然而通常情况下,您会发现程序会使用 somaxconn(或者是更小的硬编码值)。

当监听队列被填满时,新的连接会遭到拒绝。这种情况被称作监听队列溢出。你能够通过读取/proc/net/netstat 并对 ListenOverflows 的值进行检查,以此来观察相关情况。这是整个内核所具有的全局计数器。就我所了解的情况而言,你没有办法获取到每个监听套接字的监听溢出统计信息。

编写网络服务器时,监控监听溢出是很重要的。因为监听溢出不会从服务器角度触发用户可见的行为。服务器会愉快地进行 accept(2)操作,每日接收连接,且不会返回连接被丢弃的迹象。例如,若您为 Python 应用程序使用 Nginx 作为代理服务器。

如果 python 应用程序的运行速度比较慢,那么就有可能引发 nginx listen 套接字的溢出情况。在这种情况出现的时候,您在 nginx 日志里不会看到任何与之相关的指示。并且,您会一直看到 200 这样的状态代码,就和往常一样。所以,如果您仅仅只是对应用程序的 HTTP 状态代码进行监视,那么您是无法察觉到那些阻止请求转发到应用程序的 TCP 错误的。

你可能想看:
« 上一篇
下一篇 »

相关推荐

python爬虫教程PDF,Python爬虫书PDF

2026年09月02日

32阅读

外国教程python,python编程教学视频

2026年09月02日

27阅读

python简明教程沈洁元,python详细基础教程

2026年09月02日

21阅读

python库包教程,python 包

2026年09月02日

27阅读

python教程大全集,python教程600集

2026年09月02日

27阅读

python入门教程完整版,python零基础入门全套教程视频

2026年09月02日

23阅读