首页 > 系统相关 >Linux 内核源码分析---NIC 数据包接收与发送

Linux 内核源码分析---NIC 数据包接收与发送

时间:2024-08-25 09:21:41浏览次数:10  
标签:调用 函数 NIC skb 网卡 源码 IP 数据包

网络接口控制器(network interface controller,NIC),又称网络接口控制器,网络适配器(network adapter),网卡(network interface card),或局域网接收器(LAN adapter),是一块被设计用来允许计算机在计算机网络上进行通信的计算机硬件

由于其拥有 MAC 地址,因此属于 OSI 模型的第 2 层。它使得用户可以通过电缆或无线相互连接。每一个网卡都有一个被称为 MAC 地址的独一无二的 48 位串行号,它被写在卡上的一块 ROM 中。在网络上的每一个计算机都必须拥有一个独一无二的 MAC 地址。没有任何两块被生产出来的网卡拥有同样的地址。这是因为电气电子工程师协会(IEEE)负责为网络接口控制器销售商分配唯一的 MAC 地址。

NIC 网卡

网卡上面装有处理器和存储器(包括RAM和ROM)。网卡和局域网之间的通信是通过电缆或双绞线以串行传输方式进行的。而网卡和计算机之间的通信则是通过计算机主板上的 I/O 总线以并行传输方式进行。因此,网卡的一个重要功能就是要进行串行/并行转换
由于网络上的数据率和计算机总线上的数据率并不相同,因此在网卡中必须装有对数据进行缓存的存储芯片。

NIC 主要功能
1.它是主机与介质的桥梁设备;
2.实现主机与介质之间的电信号匹配;
3.提供数据缓冲能力;
4.控制数据传送的功能(网卡一方面负责接收网络上传过来的数据包,解包后,将数据通过主板上的总线传输给本地计算机;另一方面它将本地计算机上的数据打包后送入网络。)

工作工程:
一是将电脑的数据封装为帧,并通过网线(对无线网络来说就是电磁波)将数据发送到网络上去;
二是接收网络上传过来的帧,并将帧重新组合成数据,发送到所在的电脑中。
网卡接收所有在网络上传输的信号,但只接受发送到该电脑的帧和广播帧,将其余的帧丢弃。然后,传送到系统CPU做进一步处理。当电脑发送数据时,网卡等待合适的时间将分组插入到数据流中。接收系统通知电脑消息是否完整地到达,如果出现问题,将要求对方重新发送。

网卡驱动:END设备驱动程序的装载、启动END设备、网络数据包的接收及网络数据包的发送。

END设 备驱动程序的装载主要就是完成 END 设备驱动程序与驱动功能抽象层的挂接,使得网络协议栈实现对 END设备的控制。
VxWorks 中的网络协议栈叫作SENS(Scalable Enhanced Network Stack),即可裁减增强性网络协议栈。在 SENS 中,网络接口的驱动程序是叫做 END(Enhanced Network Driver),即增强型网络驱动程序,它处于数据链路层。 IP,TCP以及UDP等其它协议合称为网络协议层。 在数据链路层和网络协议层之间存在一个接口,这个接口在SENS中叫作MUX(Multiplexer)接口,也称为MUX层。
VxWorks 是美国Wind River System公司(风河公司)推出的一个运行在目标机上的高性能、可裁减的嵌入式实时操作系统。

NIC分类:根据网卡所支持的物理层标准与主机接口的不同,网卡可以分为不同的类型,如以太网卡和令牌环网卡等。根据网卡与主板上总线的连接方式、网卡的传输速率和网卡与传输介质连接的接口的不同,网卡分为不同的类型。
NIC 可根据接口类型大致分类:有线或无线。
有线网卡: 这些通常是使用以太网电缆连接到网络的以太网NIC。 它们以其强大而稳定的连接而闻名。
无线网卡: 它们使用Wi-Fi 连接到网络。 它们具有移动性和易于安装的优点,但可能会受到干扰。

数据包接收

一个 UDP 数据包在物理网卡上处理流程:

1.从网卡到内存
每个网络设备(网卡)有驱动才能工作,驱动在内核启动时需要加载到内核中

从逻辑上看,驱动是负责衔接网络设备和内核网络栈的中间模块,每当网络设备接收到新的数据包时,就会触发中断,而对应的中断处理程序正是加载到内核中的驱动程序。

在这里插入图片描述

(1)数据包进入物理网卡,如果目的地址不是该网络设备,且该网络设备没有开启混杂模式,该包会被该网络设备丢弃;

混杂模式是指一台机器的网卡能够接收所有经过它的数据流,而不论其目的地址是否是它。一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据。 当网卡工作在混杂模式下时,网卡将来自接口的所有数据都捕获并交给相应的驱动程序。

(2)物理网卡将数据包通过 DMA 的方式写入到指定的内存地址【Ring Buffer,环形缓冲区】,该地址由网卡驱动分配并初始化;

DMA传输可以在不占用CPU的情况下,直接将数据从外设传输到内存或从内存传输到外设。由于DMA传输不需要CPU的干预,因此它大大减轻了CPU的负担。

(3)物理网卡通过硬件中断(IRQ)通知 CPU,有新的数据包到达物理网卡需要处理;

IRQ (Interrupt ReQuest) 指来自设备的中断请求。 目前,它们可以通过一个引脚或通过一个数据包进入。 多个设备可以连接到同一个引脚,从而共享一个IRQ。 IRQ编号是用来描述硬件中断源的内核标识符。

(4)接下来 CPU 根据中断表,调用已经注册了的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数;
(5)驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉物理网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停地被中断;
(6)启动软中断继续处理数据包。这样做的原因是硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致 CPU 没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理;

中断处理程序的上部分和下半部可以理解为:
上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行;

2.内核处理数据包
网络设备驱动程序会通过触发内核网络模块中的软中断处理函数,内核处理数据包:
在这里插入图片描述
(7)对上一步中驱动发出的软中断,内核中的 ksoftirqd 进程会调用网络模块的相应软中断所对应的处理函数,确切地说,这里其实是调用 net_rx_action 函数;
(8)接下来 net_rx_action 调用网卡驱动里的 poll 函数来一个个地处理数据包;

net_rx_action() 方法的功能是 ring buffer 取出数据包,然后对其进行进入协议栈之前的大量处理。et_rx_action() 从处理 ring buffer 开始处理。

(9)而 poll 函数会让驱动程序读取网卡写到内存中的数据包,事实上,内存中数据包的格式只有驱动知道;
(10)驱动程序将内存中的数据包转换成内核网络模块能识别的 skb(socket buffer) 格式,然后调用 napi_gro_receive 函数;
(11)napi_gro_receive 函数会处理 GRO 相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈,然后判断是否开启了 RPS;如果开启了,将会调用 enqueue_to_backlog 函数;

GRO(Generic Receive Offloading):是一种较老的硬件特性(LRO)的软件实现,功能是对分片的包进行重组然后交给更上层,以提高吞吐。 GRO 给协议栈提供了一次将包交给网络协议栈之前,对其检查校验和 、修改协议头和发送应答包(ACK packets)的机会。

  • 如果 GRO 的 buffer 相比于包太小了,它可能会选择什么都不做;
  • 如果当前包属于某个更大包的一个分片,调用 enqueue_backlog() 将这个分片放到某个 CPU 的包队列;当包重组完成后,会交会协议栈网上送;
  • 如果当前包不是分片包,往上送。

RPS(Receive Packet Steering,接收包控制,接收包引导) 是 RSS(Receive Side Scaling,多队列分发)的一种软件实现。
RPS 的工作原理:

  • 对 packet 做 hash,以此决定分到哪个 CPU 处理;然后 packet 放到每个 CPU 独占的 backlog 队列;
  • 从当前 CPU 向对端 CPU 发起一个进程间中断(IPI,Inter-processor Interrupt)。如果当时对端 CPU 没有在处理 acklog 队列收包,这个 IPI 会触发它开始从 backlog 收包。

(12)enqueue_to_backlog 函数会将数据包放入 input_pkt_queue 结构体中,然后返回;

如果 input_pkt_queue 满了的话,该数据包将会被丢弃,这个队列的大小可以通过 net.core.netdev_max_backlog 来配置;

(13)接下来 CPU 会在软中断上下文中处理自己 input_pkt_queue 里的网络数据,实际上是调用 __netif_receive_skb_core 函数来处理;
(14)如果没开启 RPS,napi_gro_receive 函数会直接调用 __netif_receive_skb_core 函数来处理网络数据包;

__netif_receive_skb_core 完成将数据送到协议栈这一繁重工作。这里面做的事情非常多, 按顺序包括:

  • 处理 skb 时间戳;
  • Generic XDP:软件执行 XDP 程序(XDP 是硬件功能,本来应该由硬件网卡来执行);
  • 处理 VLAN header;
  • TAP 处理:例如 tcpdump 抓包、流量过滤;
  • TC:TC 规则或 TC BPF 程序;
  • Netfilter:处理 iptables 规则等。

(15)紧接着 CPU 会根据是不是有 AF_PACKET 类型的 socket(原始套接字),如果有的话,拷贝一份数据给它(tcpdump 所抓的包就是这个包);
(16)将数据包交给内核 TCP/IP 协议栈处理;
(17)当内存中的所有数据包被处理完成后(poll函数执行完成),重新启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知 CPU。

3.内核网络协议栈
内核 TCP/IP 协议栈此时接收到的数据包其实是三层(网络层)数据包,因此,数据包首先会首先进入到 IP 网络层,然后进入传输层处理。
IP 网络层
在这里插入图片描述
(18)ip_rcv 是函数是 IP 网络层处理模块的入口函数,该函数首先判断属否需要丢弃该数据包(目的 mac 地址不是当前网卡,并且网卡设置了混杂模式),如果需要进一步处理就调用注册在 netfilter 中的 NF_INET_PRE_ROUTING 这条链上的处理函数;
(19)NF_INET_PRE_ROUTINGnetfilter 放在协议栈中的钩子函数,可以通过 iptables 来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走;
(20)routing 进行路由处理,如果目的 IP 不是本地 IP,且没有开启 ip 转发功能,那么数据包将被丢弃;否则进入 ip_forward 函数处理;
(21)ip_forward 函数会先调用 netfilter 注册在 NF_INET_FORWARD 链上的处理函数,如果数据包没有被丢弃,那么将继续往后调用 dst_output_sk 函数;
(22)dst_output_sk 函数会调用 IP 网络层的相应函数将该数据包发送出去;
(23)ip_local_deliver 如果上面路由处理发现发现目的 IP 是本地 IP,那么将会调用 ip_local_deliver 函数,该函数先调用 NF_INET_LOCAL_IN 链上的相关处理函数,如果通过,数据包将会向下发送到传输层;

传输层
在这里插入图片描述
(24)udp_rcv 函数是 UDP 处理层模块的入口函数,它首先调用 __udp4_lib_lookup_skb 函数,根据目的 IP 和端口查找对应的 socket(所谓 socket 基本就是 ip+port 组成的结构体),如果没有找到相应的 socket,那么该数据包将会被丢弃,否则继续;
(25)sock_queue_rcv_skb 该函数的职责一是检查 socket 的接收缓存是不是满了,如果满了的话就丢弃该数据包;二是调用 sk_filter 检查这个数据包是否是满足条件的包,如果当前 socket 上设置了 filter,且该包不满足条件的话,这个数据包也将被丢弃;
(26)__skb_queue_tail 函数将数据包放入 socket 接收队列的末尾;
(27)sk_data_ready 通知 socket 数据包已经准备好;
(28)调用完 sk_data_ready 之后,一个数据包处理完成,等待应用层程序来读取;

(29)应用层一般有两种方式接收数据,一种是 recvfrom;二种是 epoll/select 监听相应的socket。

数据包发送

Linux 网络数据包的发送过程和接收过程正好相反

1.应用层
应用层处理过程的起点是应用程序调用 Linux 网络接口创建 socket:
在这里插入图片描述
(1)socket(...) 调用该函数来创建一个 socket 结构体,并初始化相应的操作函数;
(2)sendto(sock, ...) 应用层程序调用该函数开始发送数据包,该函数会调用后面的 inet_sendmsg 函数;
(3)inet_sendmsg 该函数主要是检查当前 socket 有没有绑定源端口,如果没有的话,调用 inet_autobind 函数分配一个,然后调用 UDP 层的函数进行传输;
(4)inet_autobind 函数会调用 get_port 函数获取一个可用的端口;

2.传输层
在这里插入图片描述

(5)udp_sendmsg 函数是 UDP 传输层模块发送数据包的入口。该函数中先调用 ip_route_output_flow 函数获取路由信息(主要包括源 IP 和网卡),然后调用 ip_make_skb 构造 skb 结构体,最后将网卡信息和该 skb 关联起来;
(6)ip_route_output_flow 函数主要处理路由信息,它会根据路由表和目的 IP,找到这个数据包应该从哪个网络设备发送出去,如果该 socket 没有绑定源 IP,该函数还会根据路由表找到一个最合适的源 IP 给它。 如果该 socket 已经绑定了源 IP,但根据路由表,从这个源 IP 对应的网卡没法到达目的地址,则该包会被丢弃,于是数据发送失败将返回错误。该函数最后会将找到的网络设备和源 IP 塞进 flowi4 结构体并返回给 udp_sendmsg 函数;
(7)ip_make_skb 函数的功能是构造 skb 包,构造好的 skb 包里面已经分配了 IP 包头(包括源 IP 信息),同时该函数会调用 __ip_append_dat 函数对数据包进行分片,同时还会在该函数中检查 socket 的发送缓存是否已经用光,如果被用光的话,返回 ENOBUFS 错误信息;
(8)udp_send_skb(skb, fl4) 函数主要是往 skb 里面填充 UDP 的包头,同时处理校验值,然后交给 IP 网络层的相应函数处理;

3.IP 网络层
在这里插入图片描述
(9)ip_send_skb 是 IP 网络层模块发送数据包的入口函数,该函数主要是调用后面的一系列的函数来发送网络层数据包;
(10)__ip_local_out_sk 函数用来设置 IP 报文头的长度和校验值,然后调用下面 netfilter 钩子链 NF_INET_LOCAL_OUT 上注册的处理函数;
(11)NF_INET_LOCAL_OUT 是 netfilter 钩子关卡,可以通过 iptables 来配置链上的处理函数;如果该数据包没被丢弃,则继续往下走;
(12)dst_output_sk 该函数根据 skb 里面的信息,调用相应的 output 函数 ip_output;
(13)ip_output 函数将上一层 udp_sendmsg 得到的网卡信息写入 skb 然后调用 netfilter 钩子链 NF_INET_POST_ROUTING 上注册的处理函数;
(14)NF_INET_POST_ROUTING 是 netfilter 钩子关卡,可以通过 iptables 来配置链上的处理函数;在这一步主要是配置了原地址转换(SNAT),从而导致该 skb 的路由信息发生变化;
(15)ip_finish_output 函数判断经过了上一步的处理之后路由信息是否发生变化,如果发生变化的话,需要重新调用 dst_output_sk 函数(重新调用这个函数时,可能就不会再走到 ip_output 函数调用的分支,而是走到被 netfilter 指定的 output 函数,这里有可能是 xfrm4_transport_output),否则接着往下走;
(16)ip_finish_output2 函数根据目的 IP 到路由表里面找到下一跳(nexthop)的地址,然后调用 __ipv4_neigh_lookup_noref 函数去 arp 表里面找下一跳的 neigh 信息,没找到的话会调用 __neigh_create 函数构造一个空的 neigh 结构体;
(17)dst_neigh_output 函数调用 neigh_resolve_output 函数获取 neigh 信息,并将信息里面的 mac 地址填到 skb 中,然后调用 dev_queue_xmit 函数发送数据包;

4.内核处理数据包

在这里插入图片描述

(18)dev_queue_xmit 函数是内核模块开始处理发送数据包的入口,该函数会先获取设备对应的 qdisc,如果没有的话(如 loopback 或者 IP tunnels),就直接调用 dev_hard_start_xmit 函数,否则数据包将经过 traffic control 模块进行处理;

要实现对数据包接收和发送的这些控制行为,需要使用队列结构来临时保存数据包。在Linux 实现中,把这种包括数据结构和算法实现的控制机制抽象为结构队列规程:Queuing discipline,简称为qdisc。
qdisc 对外暴露两个回调接口 enqueue 和 dequeue 分别用于数据包入队和数据包出队,而具体的排队算法实现则在 qdisc 内部隐藏。不同的qdisc实现在Linux内核中实现为不同的内核模块。

(19)traffic control 模块主要对数据包进行过滤和排序,如果队列满了的话,数据包会被丢掉;

流量控制Traffic Control简称TC,表示网络设备接收和发送数据包的排队机制。比如,数据包的接收速率、发送速率、多个数据包的发送顺序等。
Traffic Control的作用包括以下几种:

  1. 调整(Shaping): 通过推迟数据包发送来控制发送速率,只用于网络出方向(egress);
  2. 时序(Scheduling):调度不同类型数据包发送顺序,比如在交互流量和批量下载类型数据包之间进行发送顺序的调整。只用于网络出方向(egress);
  3. 监督(Policing): 根据到达速率决策接收还是丢弃数据包,用于网络入方向(ingress) ;
  4. 丢弃(Dropping):根据带宽丢弃数据包,可以用于出入两个方向;

(20)dev_hard_start_xmit 函数先拷贝一份 skb 给“packet taps”(tcpdump 命令的数据就从来自于此),然后调用 ndo_start_xmit 函数来发送数据包。如果 dev_hard_start_xmit 函数返回错误的话,调用它的函数会把 skb 放到一个地方,然后抛出软中断 NET_TX_SOFTIRQ 交给软中断处理程序 net_tx_action 函数稍后重试处理;

dev_hard_start_xmit 是将数据包交给网卡驱动进行发送的接口

(21)ndo_start_xmit 函数绑定到具体驱动发送数据的处理函数;这一步之后,数据包发送任务就交给网络设备驱动程序了,不同的网络设备驱动有不同的处理方式,但是大致流程基本一致:

  1. 将 skb 放入网卡自己的发送队列;
  2. 通知网卡发送数据包;
  3. 网卡发送完成后发送中断给 CPU;
  4. 收到中断后进行 skb 的清理工作。

https://www.vxworks.net/support/86-linux-network-programming
内容大多来自:Linux 数据包的接收与发送过程
Linux 网络栈接收数据(RX):原理及内核实现(2022)
Linux流量控制(Traffic Control)介绍

标签:调用,函数,NIC,skb,网卡,源码,IP,数据包
From: https://blog.csdn.net/FDS99999/article/details/141311259

相关文章