深入理解Linux内核网络(一):内核接收数据包的过程
在应用层执行read调用后就能很方便地接收到来自网络的另一端发送过来的数据,其实在这一行代码下隐藏着非常多的内核组件细节工作。在本节中,将详细讲解数据包如何从内核到应用层,以intel igb网卡为例。
部分内容来源于 《深入理解Linux网络》、《Linux内核源码分析TCP实现》
网络收包总览
Linux内核以及网卡驱动主要实现链路层、网络层和传输层这三层上的功能,内核为更上面的应用层提供socket连接来支持用户进程访问。分层图如下:
当网络设备有数据到达时,它会通过中断信号通知CPU。这种通知是通过电压变化实现的,目的是让CPU立即处理数据。如果在中断处理函数中完成所有的工作,会导致CPU长时间被占用,从而无法响应其他重要的设备输入(如鼠标和键盘)。这会影响系统的整体响应能力。
为了解决上述问题,Linux将中断处理分为“上半部”和“下半部”:
- 上半部:负责进行快速、简单的工作,比如确认中断的发生,读取基本数据等。这个部分的目标是尽快释放CPU,以便可以处理其他中断。
- 下半部:将大部分复杂的处理工作放在这里,这样可以在更低的优先级下、在CPU空闲时进行处理。
软中断:在Linux 2.4版本及以后的版本中,下半部的处理主要通过软中断实现。软中断不依赖于物理电压变化,而是通过内存中的变量来标记是否有软中断需要处理。ksoftirqd是一个内核线程,专门负责处理这些软中断。
Linux启动
创建ksoftirqd内核线程
Linux系统中软中断在ksoftirqd内核线程中处理,Linux会创建和CPU核数相等的ksoftirqd线程。
static struct smp_hotplug_thread softirq_threads = {.store = &ksoftirqd,.thread_should_run = ksoftirqd_should_run,.thread_fn = run_ksoftirqd,.thread_comm = "ksoftirqd/%u",
};
static _init int spawn_ksoftirqd(void) {register_cpu_notifier(&cpu_nfb);BUG_ON(smpboot_register_percpu_thread(&softirq_threads));return 0;
}
early_initcall(spawn_ksoftirqd);
网络子系统初始化
网络子系统初始化会为每个CPU初始化softnet_data
,也会为RX_SOFTIRQ(接收软终端)和TX_SOFTIRQ(发送软中断)注册处理函数。
Linux通过调用subsys_initcall
来初始化子系统。subsys_initcall(net_dev_init)
; net_dev_init
是初始化网络子系统的函数。
//file: net/core/dev.c
static int __init net_dev_init(void) {// 为每个CPU初始化soft_netfor_each_possible_cpu(i) {struct work_struct *flush = per_cpu_ptr(&flush_works, i);struct softnet_data *sd = &per_cpu(softnet_data, i);INIT_WORK(flush, flush_backlog);skb_queue_head_init(&sd->input_pkt_queue);skb_queue_head_init(&sd->process_queue);......}......// 注册TX,RX软中断处理函数open_softirq(NET_TX_SOFTIRQ, net_tx_action);open_softirq(NET_RX_SOFTIRQ, net_rx_action);
}
sortnet_data
数据结构中的poll_list
等待驱动程序将其poll函数注册进来。open_softirq
函数用于为每种软中断类型注册一个处理函数。将特定软中断编号(如NET_TX_SOFTIRQ
和NET_RX_SOFTIRQ
)与其对应的处理函数(如net_tx_action
和net_rx_action
)关联起来。这些处理函数负责处理发送(TX)和接收(RX)网络数据包的逻辑。
通过softirq_vec
变量注册软中断处理函数,ksoftirqd线程通过该变量查找中断处理函数处理对应软中断。
// file: kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *)) {softirq_vec[nr].action = action;
}
协议栈注册
内核实现了ip_rcv
,tcp_rcv
,udp_rcv
这些网络协议处理函数,由内核进行注册使用。
//file: net/ipv4/af_inet.cstatic struct packet_type ip_packet_type __read_mostly = {.type = cpu_to_be16(ETH_P_IP),.func = ip_rcv,.list_func = ip_list_rcv,
};/* thinking of making this const? Don't.* early_demux can change based on sysctl.*/
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,
};/* thinking of making this const? Don't.* early_demux can change based on sysctl.*/
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,
};static const struct net_protocol icmp_protocol = {.handler = icmp_rcv,.err_handler = icmp_err,.no_policy = 1,.netns_ok = 1,
};
//file: net/ipv4/af_inet.c
static int __init inet_init(void) {// 注册TCP,UDP,ICMP网络层之上协议if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)pr_crit("%s: Cannot add ICMP protocol\n", __func__);if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)pr_crit("%s: Cannot add UDP protocol\n", __func__);if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)pr_crit("%s: Cannot add TCP protocol\n", __func__);
#ifdef CONFIG_IP_MULTICASTif (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)pr_crit("%s: Cannot add IGMP protocol\n", __func__);
#endif
}
从上面的代码中可以看到,udp_protocol
结构体中的handler是udp_rcv
,tcp_protocol
结构体中的handler是tcp_v4_rcv
,它们通过inet_add_protocol
函数被初始化进来。
// file: net/ipv4/protocol.c
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{if (!prot->netns_ok) {pr_err("Protocol %u is not namespace aware, cannot register.\n",protocol);return -EINVAL;}return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],NULL, prot) ? 0 : -1;
}// 导出符号,供其他模块使用
// 注册协议后,其他模块就可以通过该变量相应协议的处理函数了
struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly;
EXPORT_SYMBOL(inet_protos);
inet_add_protocol
函数用于将TCP和UDP协议对应的处理函数注册到inet_protos
数组中,从而使内核能够在接收到这些协议的数据包时找到相应的处理逻辑。同时,调用 dev_add_pack(&ip_packet_type)
将ip_packet_type
结构体中的协议名称和处理函数 ip_rcv
注册到 ptype_base
哈希表中。
void dev_add_pack(struct packet_type *pt)
{struct list_head *head = ptype_head(pt);......
}static inline struct list_head *ptype_head(const struct packet_type *pt)
{if (pt->type == htons(ETH_P_ALL))return pt->dev ? &pt->dev->ptype_all : &ptype_all;elsereturn pt->dev ? &pt->dev->ptype_specific :&ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;
这样inet_protos
记录着UDP,TCP协议处理函数地址,ptype_base
记录ip协议处理函数地址。
网卡驱动初始化
驱动程序会使用module_init
向内核注册一个函数,驱动程序被加载时,内核会调用这个函数。以igb网卡为例:
// drivers/net/ethernet/intel/igb/igb_main.c
static struct pci_driver igb_driver = {.name = igb_driver_name,.id_table = igb_pci_tbl,.probe = igb_probe,.remove = igb_remove,....sriov_configure = igb_pci_sriov_configure
}static int __init igb_init_module(void)
{...ret = pci_register_driver(&igb_driver);return ret;
}
module_init(igb_init_module);
驱动调用pci_register_driver
后Linux内核可以获取该驱动相关信息,比如name
和probe
。网卡被识别后,内核会调用驱动提供的probe
函数(在这里即igb_probe
)。驱动probe
函数执行的目的是让设备处于ready状态。
probe函数主要作用:
- 获取网卡MAC地址。
- DMA初始化。
- 注册ethtool实现函数。
- 注册
net_device_ops
,netdev
等变量。 - 初始化NAPI,注册poll函数到napi数据结构。
// file: drivers/net/ethernet/intel/igb/igb_main.c
static const struct net_device_ops igb_netdev_ops = {.ndo_open = igb_open,.ndo_stop = igb_close,.ndo_start_xmit = igb_xmit_frame,.ndo_get_stats64 = igb_get_stats64,.ndo_set_rx_mode = igb_set_rx_mode,.ndo_set_mac_address = igb_set_mac,.ndo_change_mtu = igb_change_mtu,.ndo_do_ioctl = igb_ioctl,// ...
};
也就是实现了图中的4-7步。在第5步中,网卡驱动实现了ethtool所需要的接口,也在这里完成函数地址的注册。当ethtool发起一个系统调用之后,内核会找到对应操作的回调函数。也就是ethtool 命令最后调用的都是网卡驱动函数。
NAPI(New API)是 Linux 内核中的网络设备处理机制,通过结合中断驱动和轮询模式来处理数据包。它在接收到数据包`时首先触发中断,随后禁用中断并通过软中断轮询处理多个数据包,减少中断频率和 CPU 开销,提高处理效率。
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{// 设置DMApci_using_dac = 0;err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));if (!err) {pci_using_dac = 1;} else {err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));if (err) {dev_err(&pdev->dev,"No usable DMA configuration, aborting\n");goto err_dma;}}// 获取MAC地址if (eth_platform_get_mac_address(&pdev->dev, hw->mac.addr)) {/* copy the MAC address out of the NVM */if (hw->mac.ops.read_mac_addr(hw))dev_err(&pdev->dev, "NVM Read Error\n");}memcpy(netdev->dev_addr, hw->mac.addr, netdev->addr_len);// 设置netdev_ops,设置ethool实现函数// igb_netdev_ops中包含igb_open等函数,网卡启动时会使用netdev->netdev_ops = &igb_netdev_ops;igb_set_ethtool_ops(netdev);// 其中会调用alloc_q_vector/* setup the private structure */err = igb_sw_init(adapter);
}static int igb_alloc_q_vector(struct igb_adapter *adapter,...)
{....../* initialize NAPI */netif_napi_add(adapter->netdev, &q_vector->napi,igb_poll, 64); ......
}void netif_napi_add(struct net_device *dev, struct napi_struct *napi,int (*poll)(struct napi_struct *, int), int weight)
{...napi->poll = poll;napi->weight = weight;napi->dev = dev;...
}
启动网卡
启用网卡时会调用上面网卡驱动初始化中 net_device_ops
提供的open函数(即igb_open
)。其包括网卡启用、发包、设置MAC地址等回调函数(函数指针)。当启用一个网卡时(例如,通过ifconfig eth0 up
) ,net_device_ops
变量中定义的ndo_open
方法会被调用。这是一个函数指针,对于igb网卡来说,该指针指向的是igb_open
方法。
// igb_main.c
static int __igb_open(struct net_device *netdev, bool resuming)
{// 分配TX描述符队列/* allocate transmit descriptors */err = igb_setup_all_tx_resources(adapter);if (err)goto err_setup_tx;// 分配RX描述符队列/* allocate receive descriptors */err = igb_setup_all_rx_resources(adapter);if (err)goto err_setup_rx;// 注册中断处理函数err = igb_request_irq(adapter);if (err)goto err_req_irq;// 启用NAPIfor (i = 0; i < adapter->num_q_vectors; i++)napi_enable(&(adapter->q_vector[i]->napi));}
以上代码中,_igb_open
函数调用了igb_setup_all_tx_resources
和igb_setup_all_x_resources
。在调用igb_setup_all_x_resources
这一步操作中,分配了RingBuffer,并建立内存和Rx队列的映射关系。(Rx和Tx队列的数量和大小可以通过ethtool进行配置。)
这里以分配RX队列代码为例:
// file: igb_main.c
static int igb_setup_all_rx_resources(struct igb_adapter *adapter)
{int i, err = 0;// 分配多RX队列for (i = 0; i < adapter->num_rx_queues; i++) {err = igb_setup_rx_resources(adapter->rx_ring[i]);if (err) {for (i--; i >= 0; i--)igb_free_rx_resources(adapter->rx_ring[i]);break;}}return err;
}
在上面的源码中,通过循环创建了若干个接收队列,下面看一下每个接收队列咱们创建出来的:
// 分配每个队列
int igb_setup_rx_resources(struct igb_ring *rx_ring)
{struct device *dev = rx_ring->dev;int size;// 1. 分配igb_rx_buffer数组内存size = sizeof(struct igb_rx_buffer) * rx_ring->count;rx_ring->rx_buffer_info = vmalloc(size);if (!rx_ring->rx_buffer_info)goto err;// 2. 分配网卡使用DMA数组内存/* Round up to nearest 4K */rx_ring->size = rx_ring->count * sizeof(union e1000_adv_rx_desc);rx_ring->size = ALIGN(rx_ring->size, 4096);rx_ring->desc = dma_alloc_coherent(dev, rx_ring->size,&rx_ring->dma, GFP_KERNEL);if (!rx_ring->desc)goto err;// 3. 初始化队列成员rx_ring->next_to_alloc = 0;rx_ring->next_to_clean = 0;rx_ring->next_to_use = 0;......
}
从上述源码可以看到,实际上一个RingBuffer的内部不是仅有一个环形队列数组,而是有两个,如图2.9所示。
- igb_rx_buffer数组:这个数组是内核使用的,通过vzalloc申请的。
- e1000_adv_rx desc数组:这个数组是网卡硬件使用的,通过dma_alloc_coherent分配。
接下来看中断处理函数注册部分,会先检查是否支持MSIX中断,如果不支持或设置失败则使用MSI中断。MSIX情况下注册的硬中断处理函数为igb_msix_ring。
MSI(Message Signaled Interrupts)和 MSI-X 是用于替代传统中断(如线性中断)的机制。MSI 通过写入特定内存地址来触发中断,减少了对物理线路的依赖,提供更高效的中断处理。MSI-X 是 MSI 的扩展版本,允许设备配置多个中断向量,从而支持更复杂的多队列和多核处理,进一步提升性能和灵活性。
static int igb_request_irq(struct igb_adapter *adapter)
{struct net_device *netdev = adapter->netdev;struct pci_dev *pdev = adapter->pdev;int err = 0;if (adapter->flags & IGB_FLAG_HAS_MSIX) {err = igb_request_msix(adapter);if (!err)goto request_done;/* fall back to MSI */...}// MSI...
request_done:return err;
}static int igb_request_msix(struct igb_adapter *adapter)
{// 为每个队列注册中断for (i = 0; i < adapter->num_q_vectors; i++) {struct igb_q_vector *q_vector = adapter->q_vector[i];vector++;q_vector->itr_register = adapter->io_addr + E1000_EITR(vector);err = request_irq(adapter->msix_entries[vector].vector,igb_msix_ring, 0, q_vector->name,q_vector);}
}
可以看到MSI-X方式下可以为网卡每个接收队列都注册中断,从而可以在网卡中断层面设置让收到的包由不同CPU处理,即修改中断的CPU亲和性,指定中断由特定CPU集处理。
小结
Linux启动中涉及网络的大致过程如下:
- 创建了
ksoftirqd
内核线程来处理软中断 - 初始化网络子系统为每个cpu初始化收发包使用数据结构
soft_net
,并且注册RX,TX软中断处理函数 - 协议栈注册将ip协议处理函数注册到
ptype_base
数据结构中,tcp,udp协议处理函数注册到inet_protos
数据结构中 - 网卡驱动初始化使网卡ready,注册了ethtool实现函数,初始化NAPI
- 启动网卡则分配RX和TX队列内存,注册硬中断处理函数。可以接收数据包了
接收数据
硬中断处理
数据帧到达网卡,网卡将数据帧DMA到分配给它的内存中,发起硬中断通知CPU数据包到达。
当RingBuffer满的时候,新来的数据包将被丢弃。使用ifconfig命令查看网卡的时候,可以看到里面有个overruns,表示因为环形队列满被丢弃的包数。如果发现有丢包,可能需要通过ethtool命令来加大环形队列的长度。
static irqreturn_t igb_msix_ring(int irq, void *data)
{struct igb_q_vector *q_vector = data;/* Write the ITR value calculated from the previous interrupt. */igb_write_itr(q_vector);napi_schedule(&q_vector->napi);return IRQ_HANDLED;
}
igb_write_itr
记录硬件中断频率,追踪napi_schedule
调用可以发现会调用___napi_schedule(this_cpu_ptr(&softnet_data), n)
。将驱动传来的poll_list添加到cpu变量softnet_data
中的poll_list
。
// file: net/core/dev.cstatic inline void ____napi_schedule(struct softnet_data *sd,struct napi_struct *napi)
{list_add_tail(&napi->poll_list, &sd->poll_list);__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
_raise_softirq_irqoff
触发了一个软中断NET_RX_SOFTIRQ
,这个所谓的触发过程只是对一个变量进行了一次或运算而已。
// file: kernel/softirq.c
void __raise_softirq_irqoff(unsigned int nr)
{trace_softirq_raise(nr);or_softirq_pending(1UL << nr);
}// file: include/linux/interrupt.h
// 设置当前CPU软中断
#define or_softirq_pending(x) (__this_cpu_or(local_softirq_pending_ref, (x)))
Linux在硬中断里只完成简单必要的工作,剩下的大部分的处理都是转交给软中断的。通过以上代码可以看到,硬中断处理过程非常短,只是记录了一个寄存器,修改了一下CPU的poll_list
,然后发出一个软中断。
ksoftirqd内核线程处理软中断
网络包的接收处理过程主要都在ksoftirqd内核线程中完成,软中断都是在这里处理的:
检测软中断标记时使用ksoftirqd_should_run函数。
// file: kernel/softirq.c
static int ksoftirqd_should_run(unsigned int cpu)
{return local_softirq_pending();
}// 读取当前CPU软中断标记
#define local_softirq_pending() (__this_cpu_read(local_softirq_pending_ref))
检测到软中断标记,会执行对应处理程序:
// file: kernel/softirq.cstatic void run_ksoftirqd(unsigned int cpu)
{local_irq_disable();if (local_softirq_pending()) {/** We can safely run softirq on inline stack, as we are not deep* in the task stack here.*/__do_softirq();local_irq_enable();cond_resched();return;}local_irq_enable();
}asmlinkage __visible void __softirq_entry __do_softirq(void)
{h = softirq_vec;while ((softirq_bit = ffs(pending))) {......trace_softirq_entry(vec_nr);// 执行对应处理函数h->action(h);trace_softirq_exit(vec_nr);h++;pending >>= softirq_bit;}
}
硬中断中注册软中断时是修改当前cpu的相关变量,而内核线程处理软中断时也是通过读取当前cpu相应变量,所以硬中断在哪个cpu上被处理,软中断也会在对应cpu上被处理。
如果发现软中断集中在一个核上,应该考虑通过修改硬中断亲和性将其打散到不同cpu上。
处理RX软中断的函数是在网络子系统初始化时注册的net_rx_action
函数,获取当前cpu的softnet_data
成员,获取其poll_list
进行处理。time_limit
(时间限制),budget
(处理数据包数量限制)用来控制主动退出,防止net_rx_action
占用cpu过长时间。
// file: net/core/dev.cstatic __latent_entropy void net_rx_action(struct softirq_action *h)
{struct softnet_data *sd = this_cpu_ptr(&softnet_data);unsigned long time_limit = jiffies +usecs_to_jiffies(netdev_budget_usecs);int budget = netdev_budget; // 最多处理多少数据包LIST_HEAD(list);LIST_HEAD(repoll);local_irq_disable();list_splice_init(&sd->poll_list, &list);local_irq_enable();for (;;) {struct napi_struct *n;n = list_first_entry(&list, struct napi_struct, poll_list);// napi_poll中会删除节点budget -= napi_poll(n, &repoll);/* If softirq window is exhausted then punt.* Allow this to run for 2 jiffies since which will allow* an average latency of 1.5/HZ.*/if (unlikely(budget <= 0 ||time_after_eq(jiffies, time_limit))) {sd->time_squeeze++;break;}}...
}
net_rx_action
遍历poll_list
并执行napi_poll
函数,其中会调用驱动注册到napi
数据结构的poll
函数(igb_poll
函数)。
static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{...if (test_bit(NAPI_STATE_SCHED, &n->state)) {// 调用驱动注册的poll函数work = n->poll(n, weight);trace_napi_poll(n, work, weight);}...
}
igb_poll
函数中重点是对igb_clean_rx_irq
的调用,读取RX描述符队列,根据RX描述符信息将数据帧从RingBuffer中取下,放入skb
并释放对应内存(之后会重新分配)。
static int igb_poll(struct napi_struct *napi, int budget)
{......if (q_vector->tx.ring)clean_complete = igb_clean_tx_irq(q_vector, budget);if (q_vector->rx.ring) {int cleaned = igb_clean_rx_irq(q_vector, budget);work_done += cleaned;if (cleaned >= budget)clean_complete = false;}......
}
static int igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
{......while (likely(total_packets < budget)) {union e1000_adv_rx_desc *rx_desc;struct igb_rx_buffer *rx_buffer;unsigned int size;rx_desc = IGB_RX_DESC(rx_ring, rx_ring->next_to_clean);size = le16_to_cpu(rx_desc->wb.upper.length);rx_buffer = igb_get_rx_buffer(rx_ring, size);/* retrieve a buffer from the ring */if (skb)igb_add_rx_frag(rx_ring, rx_buffer, skb, size);...igb_put_rx_buffer(rx_ring, rx_buffer);cleaned_count++;/* fetch next buffer in frame if non-eop */if (igb_is_non_eop(rx_ring, rx_desc))continue;/* verify the packet layout is correct */if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {skb = NULL;continue;}/* populate checksum, timestamp, VLAN, and protocol */igb_process_skb_fields(rx_ring, rx_desc, skb);// 在该函数中送入协议栈napi_gro_receive(&q_vector->napi, skb);...}// 将释放的内存重新分配,放回ring_bufferif (cleaned_count)igb_alloc_rx_buffers(rx_ring, cleaned_count);
}
上文代码中igb_clean_rx_irq
函数的while循环中,igb_is_non_eop
检测是否到数据帧结尾,数据帧有可能较大占用多个数据块,由多个描述符描述,将同一数据帧放入同一skb_buff
。之后接下来进入napi_gro_receive
函数进行GRO处理。
// File: net/core/dev.c
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb) {skb_gro_reset_offset(skb);return napi_skb_finish(dev_gro_receive(napi, skb), skb);
}
GRO允许将多个小的接收数据包合并为一个较大的数据包。这种合并操作发生在内核网络栈中,可以减少每个数据包的处理开销。
最后在napi_skb_finish
中,数据包被送到协议栈。
static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{switch (ret) {case GRO_NORMAL:if (netif_receive_skb_internal(skb))ret = GRO_DROP;break;......
}
网络协议栈处理
netif_receive_skb
函数会根据包的协议进行处理,最终会调用__netif_receive_skb_core
函数处理。,假如是UDP包,将包依次送到 ip_rcv
、udp_rcv
等协议处理函数中进行处理。
tcpdump用到的协议会注册到
ptype_all
上,在这里会将数据包(sk_buff
)传递给所有注册的协议,tcpdump就是这样抓包的。之后交由注册到ptype_base的对应协议处理函数处理。
// file: net/core/dev.c
int netif_receive_skb(struct sk_buff *skb) {// RPS处理逻辑,先忽略return netif_receive_skb(skb);
}static int _netif_receive_skb(struct sk_buff *skb) {......ret = ___netif_receive_skb_core(skb, false); return ret;
}static int _netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc) {......struct ptype *pt_prev = NULL;int ret;// 遍历所有抓包类型list_for_each_entry_rcu(ptype, &ptype_all, list) {// 检查设备是否匹配if (!ptype->dev || ptype->dev == skb->dev) {// 如果有前一个ptype,交付skbif (pt_prev) {ret = deliver_skb(skb, pt_prev, orig_dev);}pt_prev = ptype; // 更新前一个ptype}}......// 根据类型遍历抓包类型list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {// 检查类型和设备if (ptype->type == type &&(!ptype->dev || ptype->dev == skb->dev || ptype->dev == orig_dev)) {// 如果有前一个ptype,交付skbif (pt_prev) {ret = deliver_skb(skb, pt_prev, orig_dev);}pt_prev = ptype; // 更新前一个ptype}}return ret; // 返回结果
}
tcpdump是通过虚拟协议的方式工作的,它会将抓包函数以协议的形式挂到ptype_all上。设备层遍历所有的“协议”,这样就能抓到数据包来供我们查看了。tcpdump会执行到packet_create
。
// file: net/packet/af_packet.cstatic int packet_create(struct net *net, struct socket *sock, ...) {po->prot_hook.func = packet_rcv;if (sock->type == SOCK_PACKET)po->prot_hook.func = packet_rcv_spkt;po->prot_hook.af_packet_priv = sk;register_prot_hook(sk);
}
register_prot_hook
函数会把tcpdump用到的“协议”挂到ptype_all
上。
接着_netif_receive_skb_core
函数取出protocol
,它会从数据包中取出协议信息,然后遍历注册在这个协议上的回调函数列表。ptype_base
是一个哈希表,在前面的“协议栈注册”部分提到过。ip_rcv
函数地址就是存在这个哈希表中的。
// file: net/core/dev.cstatic inline int deliver_skb(struct sk_buff *skb,struct packet_type *pt_prev,struct net_device *orig_dev)
{......return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
在其中会调用到在协议栈注册时的函数,对于ip包来讲会进入到ip_rcv
。
IP层处理
ip_rcv
函数如下,其中NF_HOOK
是钩子函数,是netfilter的过滤点,而iptables就是基于netfilter的,iptables设置的规则就是在这些地方被执行。
// file: net/ipv4/ip_input.c/** IP receive entry point*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev)
{......return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,net, NULL, skb, dev, NULL,ip_rcv_finish);
}
执行完钩子函数后会执行最后一个参数提供的函数ip_rcv_finish
。其中一般情况最后调用dst_input()
函数,将数据包从网络层传输到传输层。
static int ip_rcv_finish(struct sk_buff *skb) {......if (!skb_dst(skb)) {int err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, skb->dev);}......return dst_input(skb);
}
跟踪ip_route_input_noref
后看到它又调用了ip_route_input_mc
。在ip_route_input_mc
中,函数ip_local_deliver
被赋值给了dst.input
。
// File: net/ipv4/route.cstatic int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,u8 tos, struct net_device *dev, int our) {......if (our) {rth->dst.input = ip_local_deliver;rth->rt_flags |= RTCF_LOCAL;}......
}
然后回到ip_rcv_finish
的dst_input
,其中调用的input方法就是ip_route_input_mc
中赋值的ip_local_deliver
// file: include/net/dst.h
static inline int dst_input(struct sk_buff *skb) {return skb_dst(skb)->input(skb);
}
ip_local_deliver
函数将接收到的 IP 数据包传递给本地协议栈的上层处理。inet_protos
中保存着tcp_v4_rcv
和udp_rcv
的函数地址。这里将会根据包中的协议类型选择分发,在这里skb包将会进一步被派送到更上层的协议中,UDP和TCP。
// file: net/ipv4/ip_input.c
int ip_local_deliver(struct sk_buff *skb) {if (ip_is_fragment(ip_hdr(skb))) {if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))return 0;}return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);
}// file: net/ipv4/ip_input.c
static int ip_local_deliver_finish(struct sk_buff *skb) {......int protocol = ip_hdr(skb)->protocol;const struct net_protocol *ipprot;ipprot = rcu_dereference(inet_protos[protocol]); //确定函数if (ipprot != NULL) {return ipprot->handler(skb);}
}
小结
Linux内核从网卡接收数据的过程:
- 网卡将数据帧DMA到内存的RingBuffer中,然后向CPU发起中断通知。
- CPU响应中断请求,调用网卡启动时注册的中断处理函数。
- 中断处理函数几乎没干什么,只发起了软中断请求。
- 内核线程ksoftirqd发现有软中断请求到来,先关闭硬中断。ksoftirqd线程开始调用驱动的poll函数收包。
- poll函数将收到的包送到协议栈注册的
ip_rcv
函数中。 ip_rcv
函数将包送到udp_rcv函数中(对于TCP包是送到tcp_rcv_v4 ) 。
疑问点
RingBuffer到底是什么,RingBuffer为什么会丢包?
RingBuffer,或称为环形缓冲区,以循环方式管理内存,从而实现高效的数据存储和读取。网卡在收到数据的时候以DMA的方式将包写到RingBuffer中。软中断收包的时候来这里把skb取走,并申请新的skb重新挂上去。
网络相关的硬中断、软中断都是什么?
在网卡将数据放到RingBuffer中后,接着就发起硬中断,通知CPU进行处理。在硬中断的上下文中,CPU进行的工作相对较少。它主要负责将接收到的数据包所在的设备信息添加到一个特定的数据结构中,这个结构用于管理待处理的网络设备列表。这个设备列表是一个双向链表,包含所有待处理的数据包来源的设备。
一旦硬中断处理完成,系统会触发一个软中断。这种中断通常用于执行更复杂的任务,因为软中断的优先级低于硬中断,可以在稍后进行处理。数据将被传递到协议栈的处理函数中,进行进一步的处理,如解包、路由等。
Linux里的ksoftirqd内核线程是干什么的?
在Linux中,ksoftirqd是一个内核线程,用于处理软中断(softirqs)。软中断是一种轻量级的中断机制,当某些事件需要处理但不需要立即响应时,系统可以将这些事件作为软中断进行延迟处理。
在多核系统中,会有多个ksoftirqd线程,每个CPU核心都会有一个对应的ksoftirqd。这样可以提高软中断处理的并行性,充分利用多核CPU的优势。
tcpdump能否抓到netfilter封禁的包?
收报阶段可以,tcpdump工作在设备层,将相关协议注册到ptype_all表中,网络层处理前传递给其处理。
netfilter通过钩子函数过滤包,大多数钩子函数位于网络层。
发包阶段则不能抓到,应为在网络层被过滤掉的包不能在设备层抓到。
相关文章:

深入理解Linux内核网络(一):内核接收数据包的过程
在应用层执行read调用后就能很方便地接收到来自网络的另一端发送过来的数据,其实在这一行代码下隐藏着非常多的内核组件细节工作。在本节中,将详细讲解数据包如何从内核到应用层,以intel igb网卡为例。 部分内容来源于 《深入理解Linux网络》…...
mysql学习教程,从入门到精通,SQL LIKE 运算符(28)
1、SQL LIKE 运算符 在SQL中,LIKE运算符主要用于在WHERE子句中搜索列中的指定模式。它通常与通配符一起使用,如%(代表零个、一个或多个字符)和_(代表单个字符),以执行模糊匹配。下面是一个使用…...

uniapp微信小程序使用ucharts遮挡自定义tabbar的最佳解决方案
如图所示: 使用的ucharts遮挡住了我自定义的tabbar(如果不是提需求的有病,我才不会去自定义tabbar) 查阅了不少文档,说是开启 ucharts 的 canvas2d 即可: 官网文档地址: uCharts官网 - 秋云…...

C初阶(八)选择结构(分支结构)--if、else、switch
前言: C语言是用来解决问题的,除了必要的数据输入与输出(见前文),还要有逻辑结构。其中基本可以归为三类:顺序结构、选择结构、循环结构。今天,杰哥提笔写的是关于选择结构(又叫“分…...

基于Springboot vue应急物资供应管理系统设计与实现
博主介绍:专注于Java(springboot ssm 等开发框架) vue .net php python(flask Django) 小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设,从业十五余年开发设计教学工作☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆不然下次找…...

区块链+Web3学习笔记
学习资料来源于B站: 17小时最全Web3教程:ERC20,NFT,Hardhat,CCIP跨链_哔哩哔哩_bilibili 该课程提供的Github代码地址,相关资料详见README.md: Web3_tutorial_Chinese/README.md at main sm…...
Redis: 集群高可用之节点与插槽管理
概述 Redis Cluster 集群模式,它使用的是分片来存储数据的,数据都存在多个节点上。而且使用了哈希槽这样的机制,它内部维护了 16384 个插槽那就是说每一个节点其实都具体的分布了一些槽,如果我们添加一个节点的话,槽总…...

HUAWEI New4.9G 与 2.6G 无法正常切换问题处理案例
HUAWEI New4.9G 与 2.6G 无法正常切换问题处理案例 在某地市的 XX 音乐节保障准备期间,为确保活动期间的网络质量,现场新开了 4.9G HUAWEI 室外基站。在网络优化和测试中,发现UE无法实现从 2.6G 到 4.9G 的正常切换。虽然现场具备 4.9G信号覆…...
Qt C++设计模式->责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它允许多个对象有机会处理请求,而不需要明确指定哪个对象处理。通过将这些对象连成一条链,请求沿着链传递,直到有对象处理它为止。该模式…...
paypal支付v2.0(php)支付代码
第一步:获取access_token: <?php$clientId ; // 替换为你的 PayPal Client ID $clientSecret ; // 替换为你的 PayPal Client Secret// PayPal API 请求的 URL $url "https://api-m.sandbox.paypal.com/v1/oauth2/token";// 初始化 cURL $ch …...
基于Python的自然语言处理系列(23):DrQA
在本篇文章中,我们将实现 DrQA 模型,该模型最初由论文 Reading Wikipedia to Answer Open-Domain Questions 提出。DrQA 是一种用于开放域问答系统的端到端解决方案,最初包括信息检索模块和深度学习模型。本次实现中,我们主要探讨 DrQA 的深度学习模型部分。 1. 数据加载 …...

誉天Linux云计算课程学什么?为什么保障就业?
一个IT工程师相当于干了哪些职业? 其中置顶回答生动而形象地描绘道: 一个IT工程师宛如一个超级多面手,相当于——加班狂程序员测试工程师实施工程师网络工程师电工装卸工搬运工超人。 此中酸甜苦辣咸,相信很多小伙伴们都深有体会。除了典…...

无人机控制和飞行、路径规划技术分析
无人机控制和飞行、路径规划技术是现代无人机技术的核心组成部分,它们共同决定了无人机的性能和应用范围。以下是对这些技术的详细分析: 一、无人机控制技术 无人机控制技术主要涉及飞行控制系统的设计、传感器数据的处理以及指令的发送与执行。飞行控…...

【C++】模拟实现红黑树
🦄个人主页:修修修也 🎏所属专栏:实战项目集 ⚙️操作环境:Visual Studio 2022 目录 一.了解项目功能 二.逐步实现项目功能模块及其逻辑详解 📌实现RBTreeNode类模板 🎏构造RBTreeNode类成员变量 🎏实现RBTreeNode类构…...

离线安装docker
背景描述 项目需要在研发环境虚拟机上安装docker部署应用。 所在的服务器是一个内网,无法访问到外网环境。 服务器OS版本是 麒麟V10 linux 安装docker 安装包下载 获取所需版本的docker binary包,官方链接https://download.docker.com/linux/stati…...

MySQL高阶2066-账户余额
目录 题目 准备数据 分析数据 总结 题目 请写出能够返回用户每次交易完成后的账户余额. 我们约定所有用户在进行交易前的账户余额都为0, 并且保证所有交易行为后的余额不为负数。 返回的结果请依次按照 账户(account_id), 日期( day ) 进行升序排序…...

《RabbitMQ篇》Centos7安装RabbitMQ
安装RabbitMQ 安装包网盘下载地址 链接:https://pan.baidu.com/s/1bG_nP0iCdAejkctFp1QztQ?pwd4mlw 先上传安装包到服务器(erlang-23.3.4.11-1.el7.x86_64.rpm和rabbitmq-server-3.9.16-1.el7.noarch.rpm)然后使用指令安装 # 安装 erlang r…...

昇思学习打卡营第31天|深度解密 CycleGAN 图像风格迁移:从草图到线稿的无缝转化
1. 简介 图像风格迁移是计算机视觉领域中的一个热门研究方向,其中 CycleGAN (循环对抗生成网络) 在无监督领域取得了显著的突破。与传统需要成对训练数据的模型如 Pix2Pix 不同,CycleGAN 不需要严格的成对数据,只需两类图片域数据,…...
跟我学C++中级篇——空值的定义
一、空值 在提到c/c的空值时,先扯远一些。谈一谈数学中的0,0的出现要晚于其它的数,而0的出现却引发了数学的极大的发展和进步。而在计算机科学中,在使用一个变量时,它的值的可能性有很多,其中,…...
(三)Mysql 数据库系统全解析
一、Mysql 数据库 数据库的作用和优势 作用:集中化存储结构性的数据。优势: 减小数据冗余,避免数据的重复存储。保证数据的真实有效和唯一性,提高数据的质量。方便数据共享访问,使得不同的用户和应用可以方便地获取所需…...

第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...

GC1808高性能24位立体声音频ADC芯片解析
1. 芯片概述 GC1808是一款24位立体声音频模数转换器(ADC),支持8kHz~96kHz采样率,集成Δ-Σ调制器、数字抗混叠滤波器和高通滤波器,适用于高保真音频采集场景。 2. 核心特性 高精度:24位分辨率,…...
SQL Server 触发器调用存储过程实现发送 HTTP 请求
文章目录 需求分析解决第 1 步:前置条件,启用 OLE 自动化方式 1:使用 SQL 实现启用 OLE 自动化方式 2:Sql Server 2005启动OLE自动化方式 3:Sql Server 2008启动OLE自动化第 2 步:创建存储过程第 3 步:创建触发器扩展 - 如何调试?第 1 步:登录 SQL Server 2008第 2 步…...

【堆垛策略】设计方法
堆垛策略的设计是积木堆叠系统的核心,直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法,涵盖基础规则、优化算法和容错机制: 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则: 大尺寸/重量积木在下…...

Vue3 PC端 UI组件库我更推荐Naive UI
一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用,前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率,还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库(Naive UI、Element …...
前端调试HTTP状态码
1xx(信息类状态码) 这类状态码表示临时响应,需要客户端继续处理请求。 100 Continue 服务器已收到请求的初始部分,客户端应继续发送剩余部分。 2xx(成功类状态码) 表示请求已成功被服务器接收、理解并处…...
写一个shell脚本,把局域网内,把能ping通的IP和不能ping通的IP分类,并保存到两个文本文件里
写一个shell脚本,把局域网内,把能ping通的IP和不能ping通的IP分类,并保存到两个文本文件里 脚本1 #!/bin/bash #定义变量 ip10.1.1 #循环去ping主机的IP for ((i1;i<10;i)) doping -c1 $ip.$i &>/dev/null[ $? -eq 0 ] &&am…...
Vue 实例的数据对象详解
Vue 实例的数据对象详解 在 Vue 中,数据对象是响应式系统的核心,也是组件状态的载体。理解数据对象的原理和使用方式是成为 Vue 专家的关键一步。我将从多个维度深入剖析 Vue 实例的数据对象。 一、数据对象的定义方式 1. Options API 中的定义 在 Options API 中,使用 …...
MySQL用户远程访问权限设置
mysql相关指令 一. MySQL给用户添加远程访问权限1. 创建或者修改用户权限方法一:创建用户并授予远程访问权限方法二:修改现有用户的访问限制方法三:授予特定数据库的特定权限 2. 修改 MySQL 配置文件3. 安全最佳实践4. 测试远程连接5. 撤销权…...