Linux 驱动开发之网络设备分析1(基于Linux6.6)---网络分层结构介绍
一、概述
Linux内核采用分层结构处理网络数据包。分层结构与网络协议的结构匹配,既能简化数据包处理流程,又便于扩展和维护。
1.1、Linux 内核网络栈的分层结构
Linux 内核网络栈的处理结构大致分为以下几个层次,每一层负责不同的网络协议和任务。每个层次处理的数据包逐渐向上或向下传递,直到达到目标。
1. 数据链路层 (Link Layer)
- 作用:数据链路层负责在物理介质上发送和接收原始数据帧。它是网络通信的基础,直接与硬件接口打交道。
- 功能:包括数据帧的封装与解封装、MAC 地址的处理、链路的建立和维护等。
- 协议:常见的协议包括以太网(Ethernet)、Wi-Fi、PPP(点对点协议)等。
- 相关模块:例如,
eth0
代表一个以太网设备,驱动程序负责将网络层传送的数据转换为物理帧。
2. 网络层 (Network Layer)
- 作用:网络层主要处理 IP 地址和数据包的路由问题。它负责将数据包从源节点转发到目的节点,管理网络层的地址(如 IP 地址),以及数据包的路由选择。
- 功能:包括 IP 数据包的封装与解封装、路由选择、IP 地址的管理等。
- 协议:主要是 IPv4 和 IPv6。这层还处理数据包的分段和重组(尤其是对于较大的数据包)。
- 相关模块:
net/ipv4
目录下的代码负责处理 IPv4 协议的相关操作;net/ipv6
目录下的代码负责处理 IPv6 协议。
3. 传输层 (Transport Layer)
- 作用:传输层负责端到端的数据传输,确保数据从源主机到目标主机的完整性和可靠性。
- 功能:包括端口号管理、数据流控制、差错检测与恢复、连接管理等。
- 协议:
- TCP(传输控制协议):面向连接、可靠的协议,确保数据包按顺序并完整到达目的地。
- UDP(用户数据报协议):无连接、不保证可靠性的协议,适用于对时延要求较高的应用,如视频流和实时通信。
- 相关模块:
net/ipv4/tcp
和net/ipv4/udp
目录下的代码负责处理 TCP 和 UDP 协议的实现。
4. 应用层 (Application Layer)
- 作用:应用层是用户与网络交互的接口,它通过上层协议与传输层协议交互,执行具体的应用任务。
- 功能:应用层提供的服务包括 Web 服务、文件传输、邮件传输等。
- 协议:HTTP、FTP、SMTP、DNS 等应用层协议。
- 相关模块:应用层协议的实现通常是在用户空间中运行的应用程序(如 Web 服务器、邮件服务器等),但内核也提供了某些应用层协议的支持(如 DNS 解析、Socket API 等)。
1.2、网络数据包的处理流程
网络数据包从一个设备发送到另一个设备的过程中,会通过上述分层结构,每一层对数据包进行处理或修改。
1. 数据包从上层传递到下层
当应用程序通过网络发送数据时,数据首先会从应用层通过套接字(Socket)传递给传输层。在传输层,数据会被封装成 TCP 或 UDP 数据包。接着,这些数据包会传递给网络层,在那里它们会被封装成 IP 数据包,最终通过数据链路层被转换成适合物理传输的帧。
2. 数据包从下层传递到上层
当数据包到达目标设备时,数据会从数据链路层上交给网络层,网络层解封装并根据目标 IP 地址进行路由选择。接着,数据包会通过传输层进行进一步处理(如对 TCP 数据包进行重组和错误检查)。最终,数据会到达应用层,供相应的应用程序使用。
1.3、各层的相互交互
-
数据链路层与网络层:数据链路层负责通过物理介质传输数据帧,而网络层负责为数据包分配 IP 地址、选择路径并进行路由。数据链路层与网络层相互独立,但又密切配合。例如,在发送一个 IP 数据包时,网络层会将数据包交给数据链路层,而数据链路层再将其封装成帧并通过物理层发送出去。
-
网络层与传输层:传输层负责将网络层的功能扩展为端到端的可靠传输。通过 TCP 和 UDP 协议,传输层可以保证数据包的顺序、完整性、流量控制等。网络层的 IP 数据包到达目的主机后,传输层接管并根据目的端口将数据传递给相应的应用。
-
传输层与应用层:传输层通过套接字接口与应用层进行交互,保证了数据的可靠传输。应用层可以通过编程接口(如 TCP/IP 套接字)向传输层发送数据,传输层则通过底层协议进行传输。
二、内核网络结构
在Linux内核中,对网络部分按照网络协议层、网络设备层、设备驱动功能层和网络媒介层的分层体系设计。
在Linux内核,所有的网络设备都被抽象为一个接口处理,该接口提供了所有的网络操作。
net_device结构表示网络设备在内核中的情况,也就是网络设备接口。网络设备接口既包括软件虚拟的网络设备接口,如环路设备,也包括了网络硬件设备,如以太网卡。
Linux内核有一个dev_base的全局指针,指向一个设备链表,包括了系统内的所有网络设备。该设备链表每个节点是一个网络设备。
在net_device结构中提供了许多供系统访问和协议层调用的设备方法,包括初始化、打开关闭设备、数据包发送和接收等。
在 Linux 内核中,net_device
结构体是网络设备的核心数据结构,它表示网络接口设备,并包含与该设备相关的各种信息。无论是物理设备(如网卡)还是虚拟设备(如桥接接口、虚拟网卡等),它们都通过 net_device
结构体进行管理和配置。
2.1、net_device
结构体
net_device
结构体定义在 include/linux/netdevice.h
中,它包含了网络设备的所有关键信息,包括设备的状态、属性、操作函数、硬件地址、队列等。以下是 net_device
结构体的核心字段和解释:
struct net_device {
struct device dev; // 设备基础信息,继承自设备模型
struct net_device_ops *netdev_ops; // 指向网络设备操作函数的指针
struct rtnl_link_stats64 stats; // 网络设备的统计信息(如接收字节、发送字节等)
unsigned long state; // 设备的状态
unsigned long flags; // 设备的标志位(如启用或禁用)
struct ifreq *ifrq; // 指向接口请求的指针(如 ioctl 操作时用到)
struct vlan_group *vlan_group; // 支持 VLAN 的虚拟局域网组
struct net_device *next; // 指向下一个网络设备,用于链表结构
// 其他字段省略...
};
2.2、核心字段说明
1. dev
(设备基础信息)
dev
是一个设备结构体,继承自通用的设备模型 struct device
。该字段包含设备的基本信息,如设备的名称、ID、状态等。
dev
包含了设备的属性,如name
(设备名称),bus
(设备所在总线),以及设备的注册信息。dev
使得网络设备可以与其他硬件设备一样,参与到 Linux 的设备模型中,支持动态加载、卸载等操作。
2. netdev_ops
(网络设备操作函数)
netdev_ops
是一个指向 struct net_device_ops
结构体的指针,它定义了与网络设备相关的操作函数。这些操作函数包括网络设备的初始化、数据发送、数据接收、IOCTL 操作等。
struct net_device_ops
中常见的操作函数有:
ndo_open
:启动设备,初始化硬件,启动接收和发送数据。ndo_stop
:停止设备,释放硬件资源。ndo_start_xmit
:将数据包从内核发送到网络设备。ndo_set_rx_mode
:设置接收模式(例如,接收所有数据包,或只接收多播数据包)。ndo_get_stats
:获取网络设备的统计信息(如流量统计)。
3. stats
(设备统计信息)
stats
是 struct rtnl_link_stats64
类型,记录了网络设备的各种统计信息,如传输的字节数、接收的字节数、丢包数、错误数等。
struct rtnl_link_stats64 {
u64 rx_bytes; // 接收的字节数
u64 tx_bytes; // 发送的字节数
u64 rx_packets; // 接收的包数
u64 tx_packets; // 发送的包数
u64 rx_errors; // 接收错误数
u64 tx_errors; // 发送错误数
u64 rx_dropped; // 接收丢包数
u64 tx_dropped; // 发送丢包数
// 其他统计信息...
};
4. state
(设备状态)
state
是一个标志位,用于表示设备的当前状态。常见的状态包括:
IFF_UP
:设备已启用。IFF_RUNNING
:设备正在运行(数据传输正常)。IFF_PROMISC
:设备处于混杂模式,接收所有经过的数据包。
5. flags
(设备标志位)
flags
用于标记设备的一些附加特性。例如,是否支持多播,是否启用了硬件加速等。
6. vlan_group
(VLAN 组)
对于支持 VLAN 的网络设备,这个字段指向该设备所属的 VLAN 组。一个设备可以绑定多个 VLAN,vlan_group
提供了 VLAN 配置的信息。
7. next
(链表指针)
next
是一个指针,指向链表中下一个 net_device
结构体。通常,内核通过这种方式管理系统中所有的网络设备。
2.3、网络设备的生命周期
-
初始化设备
- 在设备驱动程序中,内核通过
alloc_netdev()
函数分配一个新的net_device
结构体,并初始化它。 netdev_ops
中的ndo_open
函数会被调用来启动设备,初始化硬件和设置网络接口。
- 在设备驱动程序中,内核通过
-
注册设备
- 使用
register_netdev()
函数将net_device
结构体注册到内核中,成为一个可用的网络接口。 - 设备注册后,内核会为该设备分配一个名称(如
eth0
、wlan0
)。
- 使用
-
操作设备
- 在运行过程中,设备会根据应用程序的需求(如发送数据包、接收数据包等)执行相应的操作。例如,
ndo_start_xmit
被调用来处理数据包的发送。 ndo_stop
用于关闭设备,释放资源。
- 在运行过程中,设备会根据应用程序的需求(如发送数据包、接收数据包等)执行相应的操作。例如,
-
卸载设备
- 使用
unregister_netdev()
函数注销网络设备,设备会被从内核网络栈中移除,并释放相应的内存和资源。
- 使用
2.4、net_device
与网络协议栈
net_device
结构体不仅仅是一个硬件抽象层,它还与网络协议栈的其他部分紧密集成。例如:
- 网络层会通过
net_device
转发数据包。每个网络设备都有一个对应的 IP 地址和路由信息。 - 传输层通过套接字接口与
net_device
交互,发送和接收数据。
Linux 网络栈中的数据包传输、路由、流量控制等都与 net_device
密切相关。
三、与网络有关的数据结构
内核对网络数据包的处理都是基于sk_buff结构的,该结构是内核网络部分最重要的数据结构。
网络协议栈中各层协议都可以通过对该结构的操作实现本层协议数据的添加或者删除。使用sk_buff结构避免了网络协议栈各层来回复制数据导致的效率低下。
sk_buff结构可以分为两个部分,一部分是存储数据包缓存,在图中表示为PackertData,另一部分是由一组用于内核管理的指针组成。
3.1、主要指针的作用与数据流动
-
数据接收:
- 网络设备接收到数据包后,会通过
dev
字段关联到相应的网络设备。sk_buff
会被创建并分配内存,data
指向数据包的有效数据部分。 sk_buff
会被加入到接收队列,链表中的next
和prev
指针会将多个数据包连接在一起。
- 网络设备接收到数据包后,会通过
-
协议处理:
- 网络协议栈会通过
protocol
字段检查数据包的协议类型(如 IPv4、IPv6、ARP 等)。根据协议类型,内核会调用相应的协议栈来进一步解析或转发数据包。 - 在 TCP/UDP 等协议中,
sk
字段将指向对应的套接字,用于将数据交给应用程序。
- 网络协议栈会通过
-
数据转发:
- 在数据包转发过程中,
sk_buff
会通过协议栈层层传递,并根据目标地址决定数据包的转发路径。最终数据包会通过网络设备发送出去。
- 在数据包转发过程中,
-
数据包释放:
- 一旦数据包被完全处理,内核会释放与
sk_buff
相关的内存。next
和prev
指针会帮助内核管理sk_buff
链表,确保内存得到正确释放。
- 一旦数据包被完全处理,内核会释放与
3.2、sk_buff
管理的内存
在 Linux 内核中,sk_buff
的内存分配和释放是网络子系统的重要部分。每个 sk_buff
通常会包含一个动态分配的内存区域,用于存储数据包内容。内存分配和释放通过以下几种方式进行:
alloc_skb()
:分配新的sk_buff
实例并初始化。dev_alloc_skb()
:为sk_buff
分配内存,并初始化相关字段。kfree_skb()
:释放sk_buff
占用的内存。consume_skb()
:减少sk_buff
的引用计数,直到它被释放。
数据包的大小在内核网络协议栈的处理过程中会发生改变,因此data和tail指针也会不断变化,而head和tail指针是不会发生改变的。
对于一个TCP数据包为例,sk_buff还提供了几个指针直接指向各层协议头。mac指针指向数据的mac头;nh指针指向网络协议头,一般是IP协议头;h指向传输层协议头,在本例中是TCP协议。
对各层设置指针的是方便了协议栈对数据包的处理。
四、数据包接收流程
在Linux内核中,一个网络数据包从网卡接收到用户空间需要经过链路层、传输层和socket的处理,最终到达用户空间。
以DM9000网卡为例,当网卡收到数据包以后,调用中断处理函数 dm9000_interrupt(),该函数检查中断处理类型,如果是接收数据包中断,则调用 dm9000_rx()函数接收数据包到内核空间。
dm9000_rx()函数收到数据包完成后,内核会继续调用 netif_rx()函数,函数的作用是把网卡接收到数据提交给协议栈处理。
协议栈使用 net_rx_action()函数处理接收数据包队列,该函数处理数据包后如果是 IP数据包则提交给ip_recv()函数处理。ip_recv()函数主要是检查一个数据包IP头的合法性,检查通过后交给 ip_local_deliver()和 ip_local_deliver_finish()函数处理,之所以分开处理是因为内核中有防火墙相关的代码需要动态加载到此处。
IP头处理完毕后,以UDP数据包为例将交由 udp_recv()函数处理,与 ip_recv()函数类亿,该函数检查 UDP头的合法性,然后交给 udp_queue_recv()函数处理,最后提交给 sock_queue_recv()函数处理。
数据包进入 socket部分的第一个函数是 skb_recv_datagram(),该函数从内核的 socket队列取出数据包,交给 socket部分的 udp_recvmsg()函数,该函数负责处理UDP的数据,sock_recvmsg()处理提交给 sock_read()函数。
sock_read()函数读取接收到的数据缓冲,把数据返回给 sys_read()系统调用。sys_read()函数调用最终把数据复制到用户空间,供用户使得。
五、数据包发送流程
以UDP数据包发送流程为例,在DM9000网卡上如何发送一个数据包。
当用户空间的应用程序通过 socket函数 sento()发送一个UDP数据后,会调用内核空间的 sock_writev()函数,然后通过 sock_sendmsg()函数处理。sock_sendmsg()函数调用 inet_sendmsg()函数处理,inet_sendmsg()函数会把要发送的数据交给传输层的 udp_sendmsg()函数处理。
udp_sendmsg()函数在数据前加入UDP头,然后把数据交给 ip_build_xmit()函数处理,该函数根据 socket提供的目的 IP和端口信息构造IP头,然后调用 output_maybe_reroute()函数处理。out_maybe_reroute()函数检查数据包是否需要经过路由,最后交给 ip_output()函数写入到发送队列,写入完成后由 ip_finish_output()函数处理后续工作。
链路层的 dev_queue_xmit()函数处理发送队列,调用 DM9000网卡的发送数据包函数 dm9000_xmit()发送数据包,发送完毕后,调用 dm9000_xmit_done函数处理发送结果。