Linux 内核网络栈
网络栈的架构总览
Table of Contents
这篇文章主要是从高层视角粗略地讲一下 Linux 内核网络栈(Linux Network Stack)的运行过程,同时也会涉及一部分与网卡之间的交互方式。这篇文章大部分源自于《Anatomy of the Linux networking stack》 ,但是这篇文章过于远古已经在 IBM Developer 的网站中 404 了,所以我参阅了很多其他的文章,都统一追加到文章末尾。
综述
我们在学习计算机网络的时候一直接触的都是 OSI 七层协议模型或者面向工业界的 TCP/IP 四层模型,在 Linux 中只有内核态、用户态和物理网卡,而且网络栈不仅支持 TCP/IP 协议,还支持很多其他的协议,比如 UDP 协议等。那么问题来了,TCP/IP 四层模型与内核网络栈的关系是什么?
左侧是 TCP/IP 协议的四层结构,应用层的代表协议是 HTTP 协议等等,传输层就是 TCP、UDP 协议,提供点对点的通讯功能,网络层代表是 IP 协议,主要的职责是将数据包引导到目的地,链路层的作用是访问物理层的设备驱动程序,把数据包发送到物理链路上。
右侧是网络栈的结构,他们之间的关系是协议只是网络栈的一部分,主要由 network protocols layer 实现。从上面的图上就能看出来,TCP/IP 协议也好,UDP 协议也好都是内核栈的一层中实现的不同模块而已。
内核网络栈
内核网络栈太长了,后续都简称为网络栈。
网络栈是一个五层的结构,为了适配不同的协议和网络设备,这里面有协议无关接口(procotol agnostic interface)和设备无关结构(device agnostic interface)两个抽象层,一个实现多种网络协议的网络协议(network protocols)层,一个向用户暴露的系统调用接口(syscall interface)层以及一个与设备对接的设备驱动(device drivers)层。
在网络栈的最上层就是系统调用接口(syscall interface)层,为用户提供了操作网络的一系列操作 socket 的系统调用,这里面就有我们熟知的 bind
、listen
、accept
以及 connect
,用户通过网络收发数据就像操作一个正常文件一样。在 Linux 系统的网络操作大概率是离不开 socket 编程,socket 本就是网络栈的一部分。
在系统调用接口层的下面是协议无关接口(protocol agnostic interface)层,也是 sockets 层,系统调用操作的正是 socket。这个协议无关接口实际上是把系统调用和网络协议解耦,不管用户使用什么协议,都只需要通过 socket 操作网络。在 Linux 中只有一个 socket 结构定义(struct socket
),但是对于不同的 protocols 有不同的结构体定义。在这一层提供了有关 socket 的一系列操作,比如怎么创建一个 socket,怎么建立 socket 连接等等。
在协议无关接口层的下面是网络协议(network protocols)层,是我们最关注的一层。这一层既可以实现 TCP、UDP 协议,也可以实现 IP、raw Ethernet 协议,当然还有很多其他协议。
我这时候就有个疑问,为什么四层结构(TCP/IP)、三层结构(IP)甚至二层结构(raw Ethernet)的协议都能在网络协议层实现呢?我们不要被四层协议限制了想象空间,我们只看网络栈的结构,网络协议层的直接下层是设备驱动层,可以理解为过了这层就是直接将数据包发送给物理设备了,但是这个数据包的具体协议是什么物理网络设备并不关心。理论上,只要是能在物理链路上传播的协议的数据包都可以在网络协议层被构造出来。
在网络协议层的下面是设备无关接口(device agnostic interface)层,与上面的协议无关接口层一样也是一个解耦方案。这层提供了一些通用的函数和接口,能让不同的设备驱动操作上层的网络栈,主要包括三种接口:
- 注册/反注册网络设备:以注册为例,做一些完整性检查(sanity checks),创建一个 sysfs entry,将其添加到内核维护的设备链表中。
- 发送数据到网络设备:将数据包结构体(
sk_buff
,稍后再仔细去介绍)入队到设备待发送数据的队列中。 - 从网络设备接收数据:设备驱动构造一个数据包结构体放到内核的队列中。
最下面的一层就是设备驱动(device drivers)层了,这个就不再展开介绍了。
重要数据结构
我们一般是使用 my_sock = socket(AF_INET, SOCK_STREAM, 0);
来创建 socket,这表明使用 AF_INET
Internet 地址簇(Address Family)与 socket 通讯,同时使用流式 socket(SOCK_STREAM
)。
在 inetsw_array
中保存了所有可能的 socket 类型的 protocol 类型组合,比如 Internet & SOCK_STREAM 这两个组合,如下图所示(来自《Linux网络栈解剖(Anatomy of the Linux networking stack)》,有一处 typo:"&top_prot" -> "&tcp_prot")。除此之外,我们还能看到 proto_ops
定义了 socket 的通用操作,proto
定义了 TCP 特殊的操作。
Socket buffer (sk_buff
) 是数据在 socket 移动的重要结构体,sk_buff 包括了数据包的数据以及与协议相关的状态数据,一个数据包等于一个 sk_buff。Sk_buff 有链表结构,一个连接的包的 sk_buff 可能会被连接在一起,每个 sk_buff 还包含一个网络设备的结构体 (net_device),表示这个包从哪个设备上发送或从哪个设备上接收。(来自《Linux网络栈解剖(Anatomy of the Linux networking stack)》)
网卡与网络栈
这节探索一个数据包到底是如何通过网络栈从网卡到用户手中的,整理流程如下图所示(来自《图解Linux网络包接收过程》)。
这是一个支持 DMA 技术的网卡和开启 NAPI(New Application Program Interface),在第二步之前都是由网卡将数据写入内存中,完全不需要 CPU 参与,直到 DMA 操作结束后,网卡会向 CPU 发起一个硬中断,通知 CPU 有数据到达。Linux 在硬中断里只完成简单必要的工作,剩下的大部分的处理都是转交给软中断。软中断就调用网卡驱动接受数据帧,随后数据帧被从 buffer 中取出封装为 sk_buff 结构体,后面的操作就与内核的网络栈接上了。
八股时间
不错的文章集合:
- 阿里二面:没有 accept,能建立 TCP 连接吗?:对 TCP 三次握手、socket 全连接和半连接等知识有很结构化的总结,推荐一看。
Socket 与 TCP 三次握手
说到网络栈和 socket 了,我们在来简单说下 TCP 的三次握手。Socket 提供的 syscall 都是一样的,但是不同协议的实现不同,对于 TCP/IP 协议来说,TCP 的三次握手是发生在 client 调用 connect
系统调用的,如下图所示(来自《TCP握手与socket通信细节》)。