CategoryResourceRepost/极客时间专栏/趣谈Linux操作系统/核心原理篇:第八部分 网络系统/47 | 接收网络包(上):如何搞明白合作伙伴让我们做什么?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

499 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="47 | 接收网络包(上):如何搞明白合作伙伴让我们做什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/9d/d400e8c1fb0a87fd4e600ca7d5b39d9d.mp3"></audio>
前面两节,我们分析了发送网络包的整个过程。这一节,我们来解析接收网络包的过程。
如果说网络包的发送是从应用层开始层层调用一直到网卡驱动程序的话网络包的结束过程就是一个反过来的过程我们不能从应用层的读取开始而应该从网卡接收到一个网络包开始。我们用两节来解析这个过程这一节我们从硬件网卡解析到IP层下一节我们从IP层解析到Socket层。
## 设备驱动层
网卡作为一个硬件,接收到网络包,应该怎么通知操作系统,这个网络包到达了呢?咱们学习过输入输出设备和中断。没错,我们可以触发一个中断。但是这里有个问题,就是网络包的到来,往往是很难预期的。网络吞吐量比较大的时候,网络包的到达会十分频繁。这个时候,如果非常频繁地去触发中断,想想就觉得是个灾难。
比如说CPU正在做某个事情一些网络包来了触发了中断CPU停下手里的事情去处理这些网络包处理完毕按照中断处理的逻辑应该回去继续处理其他事情。这个时候另一些网络包又来了又触发了中断CPU手里的事情还没捂热又要停下来去处理网络包。能不能大家要来的一起来把网络包好好处理一把然后再回去集中处理其他事情呢
网络包能不能一起来这个我们没法儿控制但是我们可以有一种机制就是当一些网络包到来触发了中断内核处理完这些网络包之后我们可以先进入主动轮询poll网卡的方式主动去接收到来的网络包。如果一直有就一直处理等处理告一段落就返回干其他的事情。当再有下一批网络包到来的时候再中断再轮询poll。这样就会大大减少中断的数量提升网络处理的效率这种处理方式我们称为**NAPI**。
为了帮你了解设备驱动层的工作机制我们还是以上一节发送网络包时的网卡drivers/net/ethernet/intel/ixgb/ixgb_main.c为例子来进行解析。
```
static struct pci_driver ixgb_driver = {
.name = ixgb_driver_name,
.id_table = ixgb_pci_tbl,
.probe = ixgb_probe,
.remove = ixgb_remove,
.err_handler = &amp;ixgb_err_handler
};
MODULE_AUTHOR(&quot;Intel Corporation, &lt;linux.nics@intel.com&gt;&quot;);
MODULE_DESCRIPTION(&quot;Intel(R) PRO/10GbE Network Driver&quot;);
MODULE_LICENSE(&quot;GPL&quot;);
MODULE_VERSION(DRV_VERSION);
/**
* ixgb_init_module - Driver Registration Routine
*
* ixgb_init_module is the first routine called when the driver is
* loaded. All it does is register with the PCI subsystem.
**/
static int __init
ixgb_init_module(void)
{
pr_info(&quot;%s - version %s\n&quot;, ixgb_driver_string, ixgb_driver_version);
pr_info(&quot;%s\n&quot;, ixgb_copyright);
return pci_register_driver(&amp;ixgb_driver);
}
module_init(ixgb_init_module);
```
在网卡驱动程序初始化的时候我们会调用ixgb_init_module注册一个驱动ixgb_driver并且调用它的probe函数ixgb_probe。
```
static int
ixgb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev = NULL;
struct ixgb_adapter *adapter;
......
netdev = alloc_etherdev(sizeof(struct ixgb_adapter));
SET_NETDEV_DEV(netdev, &amp;pdev-&gt;dev);
pci_set_drvdata(pdev, netdev);
adapter = netdev_priv(netdev);
adapter-&gt;netdev = netdev;
adapter-&gt;pdev = pdev;
adapter-&gt;hw.back = adapter;
adapter-&gt;msg_enable = netif_msg_init(debug, DEFAULT_MSG_ENABLE);
adapter-&gt;hw.hw_addr = pci_ioremap_bar(pdev, BAR_0);
......
netdev-&gt;netdev_ops = &amp;ixgb_netdev_ops;
ixgb_set_ethtool_ops(netdev);
netdev-&gt;watchdog_timeo = 5 * HZ;
netif_napi_add(netdev, &amp;adapter-&gt;napi, ixgb_clean, 64);
strncpy(netdev-&gt;name, pci_name(pdev), sizeof(netdev-&gt;name) - 1);
adapter-&gt;bd_number = cards_found;
adapter-&gt;link_speed = 0;
adapter-&gt;link_duplex = 0;
......
}
```
在ixgb_probe中我们会创建一个struct net_device表示这个网络设备并且netif_napi_add函数为这个网络设备注册一个轮询poll函数ixgb_clean将来一旦出现网络包的时候就是要通过它来轮询了。
当一个网卡被激活的时候我们会调用函数ixgb_open-&gt;ixgb_up在这里面注册一个硬件的中断处理函数。
```
int
ixgb_up(struct ixgb_adapter *adapter)
{
struct net_device *netdev = adapter-&gt;netdev;
......
err = request_irq(adapter-&gt;pdev-&gt;irq, ixgb_intr, irq_flags,
netdev-&gt;name, netdev);
......
}
/**
* ixgb_intr - Interrupt Handler
* @irq: interrupt number
* @data: pointer to a network interface device structure
**/
static irqreturn_t
ixgb_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct ixgb_adapter *adapter = netdev_priv(netdev);
struct ixgb_hw *hw = &amp;adapter-&gt;hw;
......
if (napi_schedule_prep(&amp;adapter-&gt;napi)) {
IXGB_WRITE_REG(&amp;adapter-&gt;hw, IMC, ~0);
__napi_schedule(&amp;adapter-&gt;napi);
}
return IRQ_HANDLED;
}
```
如果一个网络包到来触发了硬件中断就会调用ixgb_intr这里面会调用__napi_schedule。
```
/**
* __napi_schedule - schedule for receive
* @n: entry to schedule
*
* The entry's receive function will be scheduled to run.
* Consider using __napi_schedule_irqoff() if hard irqs are masked.
*/
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&amp;softnet_data), n);
local_irq_restore(flags);
}
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&amp;napi-&gt;poll_list, &amp;sd-&gt;poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
```
__napi_schedule是处于中断处理的关键部分在他被调用的时候中断是暂时关闭的但是处理网络包是个复杂的过程需要到延迟处理部分所以____napi_schedule将当前设备放到struct softnet_data结构的poll_list里面说明在延迟处理部分可以接着处理这个poll_list里面的网络设备。
然后____napi_schedule触发一个软中断NET_RX_SOFTIRQ通过软中断触发中断处理的延迟处理部分也是常用的手段。
上一节我们知道软中断NET_RX_SOFTIRQ对应的中断处理函数是net_rx_action。
```
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&amp;softnet_data);
LIST_HEAD(list);
list_splice_init(&amp;sd-&gt;poll_list, &amp;list);
......
for (;;) {
struct napi_struct *n;
......
n = list_first_entry(&amp;list, struct napi_struct, poll_list);
budget -= napi_poll(n, &amp;repoll);
}
......
}
```
在net_rx_action中会得到struct softnet_data结构这个结构在发送的时候我们也遇到过。当时它的output_queue用于网络包的发送这里的poll_list用于网络包的接收。
```
struct softnet_data {
struct list_head poll_list;
......
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
......
}
```
在net_rx_action中接下来是一个循环在poll_list里面取出网络包到达的设备然后调用napi_poll来轮询这些设备napi_poll会调用最初设备初始化的时候注册的poll函数对于ixgb_driver对应的函数是ixgb_clean。
ixgb_clean会调用ixgb_clean_rx_irq。
```
static bool
ixgb_clean_rx_irq(struct ixgb_adapter *adapter, int *work_done, int work_to_do)
{
struct ixgb_desc_ring *rx_ring = &amp;adapter-&gt;rx_ring;
struct net_device *netdev = adapter-&gt;netdev;
struct pci_dev *pdev = adapter-&gt;pdev;
struct ixgb_rx_desc *rx_desc, *next_rxd;
struct ixgb_buffer *buffer_info, *next_buffer, *next2_buffer;
u32 length;
unsigned int i, j;
int cleaned_count = 0;
bool cleaned = false;
i = rx_ring-&gt;next_to_clean;
rx_desc = IXGB_RX_DESC(*rx_ring, i);
buffer_info = &amp;rx_ring-&gt;buffer_info[i];
while (rx_desc-&gt;status &amp; IXGB_RX_DESC_STATUS_DD) {
struct sk_buff *skb;
u8 status;
status = rx_desc-&gt;status;
skb = buffer_info-&gt;skb;
buffer_info-&gt;skb = NULL;
prefetch(skb-&gt;data - NET_IP_ALIGN);
if (++i == rx_ring-&gt;count)
i = 0;
next_rxd = IXGB_RX_DESC(*rx_ring, i);
prefetch(next_rxd);
j = i + 1;
if (j == rx_ring-&gt;count)
j = 0;
next2_buffer = &amp;rx_ring-&gt;buffer_info[j];
prefetch(next2_buffer);
next_buffer = &amp;rx_ring-&gt;buffer_info[i];
......
length = le16_to_cpu(rx_desc-&gt;length);
rx_desc-&gt;length = 0;
......
ixgb_check_copybreak(&amp;adapter-&gt;napi, buffer_info, length, &amp;skb);
/* Good Receive */
skb_put(skb, length);
/* Receive Checksum Offload */
ixgb_rx_checksum(adapter, rx_desc, skb);
skb-&gt;protocol = eth_type_trans(skb, netdev);
netif_receive_skb(skb);
......
/* use prefetched values */
rx_desc = next_rxd;
buffer_info = next_buffer;
}
rx_ring-&gt;next_to_clean = i;
......
}
```
在网络设备的驱动层有一个用于接收网络包的rx_ring。它是一个环从网卡硬件接收的包会放在这个环里面。这个环里面的buffer_info[]是一个数组存放的是网络包的内容。i和j是这个数组的下标在ixgb_clean_rx_irq里面的while循环中依次处理环里面的数据。在这里面我们看到了i和j加一之后如果超过了数组的大小就跳回下标0就说明这是一个环。
ixgb_check_copybreak函数将buffer_info里面的内容拷贝到struct sk_buff *skb从而可以作为一个网络包进行后续的处理然后调用netif_receive_skb。
## 网络协议栈的二层逻辑
从netif_receive_skb函数开始我们就进入了内核的网络协议栈。
接下来的调用链为netif_receive_skb-&gt;netif_receive_skb_internal-&gt;__netif_receive_skb-&gt;__netif_receive_skb_core。
在__netif_receive_skb_core中我们先是处理了二层的一些逻辑。例如对于VLAN的处理接下来要想办法交给第三层。
```
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
struct packet_type *ptype, *pt_prev;
......
type = skb-&gt;protocol;
......
deliver_ptype_list_skb(skb, &amp;pt_prev, orig_dev, type,
&amp;orig_dev-&gt;ptype_specific);
if (pt_prev) {
ret = pt_prev-&gt;func(skb, skb-&gt;dev, pt_prev, orig_dev);
}
......
}
static inline void deliver_ptype_list_skb(struct sk_buff *skb,
struct packet_type **pt,
struct net_device *orig_dev,
__be16 type,
struct list_head *ptype_list)
{
struct packet_type *ptype, *pt_prev = *pt;
list_for_each_entry_rcu(ptype, ptype_list, list) {
if (ptype-&gt;type != type)
continue;
if (pt_prev)
deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
*pt = pt_prev;
}
```
在网络包struct sk_buff里面二层的头里面有一个protocol表示里面一层也即三层是什么协议。deliver_ptype_list_skb在一个协议列表中逐个匹配。如果能够匹配到就返回。
这些协议的注册在网络协议栈初始化的时候, inet_init函数调用dev_add_pack(&amp;ip_packet_type)添加IP协议。协议被放在一个链表里面。
```
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
list_add_rcu(&amp;pt-&gt;list, head);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt-&gt;type == htons(ETH_P_ALL))
return pt-&gt;dev ? &amp;pt-&gt;dev-&gt;ptype_all : &amp;ptype_all;
else
return pt-&gt;dev ? &amp;pt-&gt;dev-&gt;ptype_specific : &amp;ptype_base[ntohs(pt-&gt;type) &amp; PTYPE_HASH_MASK];
}
```
假设这个时候的网络包是一个IP包则在这个链表里面一定能够找到ip_packet_type在__netif_receive_skb_core中会调用ip_packet_type的func函数。
```
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};
```
从上面的定义我们可以看出接下来ip_rcv会被调用。
## 网络协议栈的IP层
从ip_rcv函数开始我们的处理逻辑就从二层到了三层IP层。
```
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
const struct iphdr *iph;
struct net *net;
u32 len;
......
net = dev_net(dev);
......
iph = ip_hdr(skb);
len = ntohs(iph-&gt;tot_len);
skb-&gt;transport_header = skb-&gt;network_header + iph-&gt;ihl*4;
......
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
......
}
```
在ip_rcv中得到IP头然后又遇到了我们见过多次的NF_HOOK这次因为是接收网络包第一个hook点是NF_INET_PRE_ROUTING也就是iptables的PREROUTING链。如果里面有规则则执行规则然后调用ip_rcv_finish。
```
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
const struct iphdr *iph = ip_hdr(skb);
struct net_device *dev = skb-&gt;dev;
struct rtable *rt;
int err;
......
rt = skb_rtable(skb);
.....
return dst_input(skb);
}
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)-&gt;input(skb);
```
ip_rcv_finish得到网络包对应的路由表然后调用dst_input在dst_input中调用的是struct rtable的成员的dst的input函数。在rt_dst_alloc中我们可以看到input函数指向的是ip_local_deliver。
```
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
struct net *net = dev_net(skb-&gt;dev);
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb-&gt;dev, NULL,
ip_local_deliver_finish);
}
```
在ip_local_deliver函数中如果IP层进行了分段则进行重新的组合。接下来就是我们熟悉的NF_HOOK。hook点在NF_INET_LOCAL_IN对应iptables里面的INPUT链。在经过iptables规则处理完毕后我们调用ip_local_deliver_finish。
```
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
__skb_pull(skb, skb_network_header_len(skb));
int protocol = ip_hdr(skb)-&gt;protocol;
const struct net_protocol *ipprot;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
int ret;
ret = ipprot-&gt;handler(skb);
......
}
......
}
```
在IP头中有一个字段protocol用于指定里面一层的协议在这里应该是TCP协议。于是从inet_protos数组中找出TCP协议对应的处理函数。这个数组的定义如下里面的内容是struct net_protocol。
```
struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly;
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
......
return !cmpxchg((const struct net_protocol **)&amp;inet_protos[protocol],
NULL, prot) ? 0 : -1;
}
static int __init inet_init(void)
{
......
if (inet_add_protocol(&amp;udp_protocol, IPPROTO_UDP) &lt; 0)
pr_crit(&quot;%s: Cannot add UDP protocol\n&quot;, __func__);
if (inet_add_protocol(&amp;tcp_protocol, IPPROTO_TCP) &lt; 0)
pr_crit(&quot;%s: Cannot add TCP protocol\n&quot;, __func__);
......
}
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
static struct net_protocol udp_protocol = {
.early_demux = udp_v4_early_demux,
.early_demux_handler = udp_v4_early_demux,
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
```
在系统初始化的时候网络协议栈的初始化调用的是inet_init它会调用inet_add_protocol将TCP协议对应的处理函数tcp_protocol、UDP协议对应的处理函数udp_protocol放到inet_protos数组中。
在上面的网络包的接收过程中会取出TCP协议对应的处理函数tcp_protocol然后调用handler函数也即tcp_v4_rcv函数。
## 总结时刻
这一节我们讲了接收网络包的上半部分,分以下几个层次。
- 硬件网卡接收到网络包之后通过DMA技术将网络包放入Ring Buffer。
- 硬件网卡通过中断通知CPU新的网络包的到来。
- 网卡驱动程序会注册中断处理函数ixgb_intr。
- 中断处理函数处理完需要暂时屏蔽中断的核心流程之后通过软中断NET_RX_SOFTIRQ触发接下来的处理过程。
- NET_RX_SOFTIRQ软中断处理函数net_rx_actionnet_rx_action会调用napi_poll进而调用ixgb_clean_rx_irq从Ring Buffer中读取数据到内核struct sk_buff。
- 调用netif_receive_skb进入内核网络协议栈进行一些关于VLAN的二层逻辑处理后调用ip_rcv进入三层IP层。
- 在IP层会处理iptables规则然后调用ip_local_deliver交给更上层TCP层。
- 在TCP层调用tcp_v4_rcv。
<img src="https://static001.geekbang.org/resource/image/a5/37/a51af8ada1135101e252271626669337.png" alt="">
## 课堂练习
我们没有仔细分析对于二层VLAN的处理请你研究一下VLAN的原理然后在代码中看一下对于VLAN的处理过程这是一项重要的网络基础知识。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">