linux网络报文接收发送浅析

对于linux内核来说,网络报文由网络设备来进行接收。设备驱动程序从网络设备中读取报文,通过内核提供的网络接口函数,将报文传递到内核中的网络协议栈。报文经过协议栈的处理,或转发、或丢弃、或被传送给某个进程。
网络报文的发送与之相反,进程通过系统调用将数据送入网络协议栈,或者由网络协议栈自己发起报文的发送,然后协议栈通过调用网络接口函数来调度驱动程序,使其将报文传送给网络设备,从而发送出去。
本文讨论的是网络接口层,它是网络设备驱动程序与网络协议栈交互的纽带。见下图中红色部分的netif。

报文的接收
网络报文的接收源自网络设备。网络设备在接收到一个报文之后,通过中断告知CPU。网卡驱动程序需要注册对该中断事件的处理函数(参见《linux中断处理浅析》),以处理接收到的报文。
在中断处理函数中,网络驱动程序有两种方法对报文进行处理(老式的方法,和新式的方法),我们先介绍老式的处理方式。在这种方式下,中断处理函数主要完成以下工作:
分配一个skb结构(该结构用于保存一个报文)。操作设备,将设备收到的数据拷贝到这个skb结构对应的缓冲区中。设置skb的协议类型skb->protocol,该类型表明了网络协议栈的上层协议(下面我们将会看到)。然后调用内核提供的网络接口函数netif_rx;

netif_rx(skb);
netif_rx函数对skb的如时间戳这样的附加信息进行初始化以后,将这个skb结构放入当前CPU的softdate_net结构的input_pkt_queue队列中。netif_rx会根据队列的长度,对设备的拥塞状况进行判断(队列过长则代表报文接收过快,以致于上层来不及处理)。如果设备已陷入拥塞,则收到的报文可能直接被丢弃。
如果一切正常,netif_rx会调用网络接口函数netif_rx_schedule,以触发对接收报文的进一步处理;

netif_rx_schedule(dev);
netif_rx使用softdate_net结构中内嵌的backlog_dev作为dev来调用netif_rx_schedule,后者将其加入到softdate_net结构的poll_list队列中(如果这个dev不在队列中的话),以使其等待被调度。

相比老式的处理方式,新式的处理方式(称为NAPI)在中断处理函数中仅仅是以对应设备的dev结构为参数调用netif_rx_schedule函数即可。
最后netif_rx_schedule函数会触发NET_RX_SOFTIRQ软中断,于是接下来对应的软中断处理函数net_rx_action将被调用;

net_rx_action();
对于当前CPU对应的softdate_net结构的poll_list队列中的所有dev,调用dev->poll方法。该方法是由对应dev的驱动程序实现的,用于接收及处理报文(前面提到的backlog_dev除外)。
net_rx_action每次运行都有一定的限度,并不一定要将所有报文都处理完。在处理完一定数量的报文配额、或处理过程超过一定时间后,net_rx_action便会返回。返回前触发一次NET_RX_SOFTIRQ软中断,等待下一次中断到来的时候继续被调度。

以上过程如图所示(摘自ULNI):

上面提到的softdate_net结构是用于进行报文收发调度的结构,内核为每个CPU维护一个这样的结构。在报文接收过程中用到了其中的三个成员:
1、poll_list,网络设备dev的队列。其中的设备接收到了报文,需要被处理;
2、input_pkt_queue,skb报文结构的队列,保存了已接收并需要被处理的报文;
3、backlog_dev,一个虚拟的网络设备dev结构;
后两个成员是专门为支持老式的处理方式而设置的,在这种方式下,接收到的skb被放入input_pkt_queue队列,然后backlog_dev被加入poll_list。而最后,自然backlog_dev->poll函数将对input_pkt_queue队列中的skb进行处理。backlog_dev->poll等于process_backlog函数;

process_backlog(backlog_dev, budget);
既然net_rx_action每次运行都有一个配额,它在调用dev->poll时也会传递当前剩余的配额值,即budget。
process_backlog会遍历input_pkt_queue队列中的skb,调用netif_receive_skb函数对其进行处理。
process_backlog函数有两种结局,一个是配额到或时间到,直接返回;另一个是处理完input_pkt_queue队列中的所有skb,此时需要将backlog_dev从poll_list中删除。

新式的NAPI处理方式所要做的事跟老的处理方式其实是很类似的。在其对应的dev->poll函数中,需要分配skb结构、从设备读取报文、调用netif_receive_skb让网络协议栈的上层来处理报文。
这种方式最大的好处是:在dev->poll函数中,不一定只处理一个报文。具体怎么处理可以由驱动程序灵活控制。比如说,假设现在网络负载非常大,如果网络设备每接收一个报文都通过一次中断来告知内核,这样做效率并不理想。而此时dev->poll可以做一些轮询的工作,如果网络设备已经接收了多个报文,可以一次性都处理了。并且,就算设备此刻所接收到的报文都已经处理完了,驱动程序也可以根据某种方式预判设备在很短的一段时间内还将收到报文,于是依然将自己对应的dev结构留在poll_list中,等待下一次继续被调度。
当dev仍结构留在poll_list中时,设备驱动程序可以关闭设备接收到报文时的中断通知,因为目前处于轮询状态。而当驱动程序认为在将来的一段时间以内无报文可收时,则可以将其dev从poll_list中移除,然后开启设备接收到报文时的中断通知。等待下一次报文接收的中断到来时,这个dev再重新被放入poll_list。

netif_receive_skb(skb);
该函数会将skb提交给抓包程序进行处理、还会触发数据链路层的桥接功能(见《linux网桥浅析》)、然后将报文提交给网络协议栈的上层(网络层)进行处理。
网络层的协议有IP、ARP等等很多种,在这里怎么知道这个skb该提交给哪种协议呢?在报文的数据链路层报头中保存着三个重要信息,发送者和接收者的Mac地址、和上层协议标识。回想一下之前的流程,在skb接收完成之后我们就已经设置了skb->protocol(从报头中得到),上层协议就由它来指定。比如,0x0800代表IP协议、0x0806代表ARP协议,这是由协议规定的。
netif_receive_skb并不是用一个switch-case来匹配skb->protocol,以选择网络层处理函数的。系统中有一个名为ptype_base的hash表,各种网络层的协议在其初始化时都会在这个hash表中注册一个类型为packet_type的表项(以协议类型为key),如下图所示(摘自ULNI):

netif_receive_skb要做的就是在这个hash表中遍历所有type与skb->protocol匹配的packet_type结构(packet_type结构的dev可用于限定skb->dev,NULL表示不限),然后调用其func回调函数。(可见,一个报文有可能被多种协议所处理。)
至此报文被提交到了网络层,在这里就不继续深入了。

报文的发送
报文的发送是由网络协议栈的上层发起的。网络协议栈上层构造一个需要发送的skb结构后(该skb已经包含了数据链路层的报头),调用dev_queue_xmit函数进行发送;

dev_queue_xmit(skb);
该函数先会处理一些缓冲区重组、计算校验和之类的杂事,然后开始处理报文的发送。
发送报文有两种策略,有队列或无队列。这是由网络设备驱动程序在定义其对应的dev结构时指定的,一般的设备都会使用队列。
dev->qdisc指向一个队列的实例,里面包含了队列本身以及操作队列的方法(enqueue、dequeue、requeue)。这些方法的集合组成了一种队列规则(skb将以某种规则入队、以某种规则出队,并不一定是简单的先进先出),这样的规则可用于流量控制。
网络设备驱动程序可以选择自己的设备使用什么样的队列,或是不使用队列。
对于有队列的设备,dev_queue_xmit调用dev->qdisc->enqueue方法将skb加入队列,然后调用qdisc_run函数。而qdisc_run会调用qdisc_restart来对队列进行处理。

qdisc_restart(dev);
该函数主要的工作就是不断调用dev->qdisc->dequeue方法从队列中取出待发送的报文,然后调用dev->hard_start_xmit方法进行发送。该方法是由设备驱动程序实现的,会直接和网络设备去打交道,将报文发送出去。
如果报文发送失败,qdisc_restart会调用dev->qdisc->requeue方法将skb重新放回队列。同时,还将调用netif_schedule函数将dev加入softdate_net的output_queue队列中(其中的设备都是有报文等待发送的,将在稍后被处理)。然后触发一次NET_TX_SOFTIRQ软中断。于是在下一个中断到来时,对应的软中断处理函数net_tx_action将被调用。
而如果dev->hard_start_xmit方法发送报文成功,则表示报文已经送到了网络设备的发送缓冲区,设备会自动将报文发送出去。并且在报文发送完成时,设备会通过中断通知驱动程序。对应的中断处理函数也会触发NET_TX_SOFTIRQ软中断。此外,已发送完成的skb将被加入softdate_net的completion_queue队列中,等待被释放。

软中断NET_TX_SOFTIRQ被触发,将使得net_tx_action函数被调用。该函数主要做了两件事:
1、从softdate_net的completion_queue队列中取出每一个skb,将其释放;
2、对于softdate_net的output_queue队列中的dev,调用qdisc_run继续尝试发送其qdisc队列中的报文;

对于有队列的设备,其队列主要用于流量控制以及发送失败时的缓冲;对于没有队列的设备(比如lo,环回设备),dev_queue_xmit函数则会直接调用dev->hard_start_xmit进行发送,如果失败报文就会被丢弃。

以上过程如图所示:

qdisc_restart函数在执行过程中还会关心dev是否被暂停(就如接收报文时要关心网络是否拥塞一样),如果被暂停则结束处理流程并返回。
而dev的暂停与否是由设备驱动程序来设置的,在dev->hard_start_xmit函数中,驱动程序如果发现设备当前的发送缓冲区太小(比如小到无法再容纳一个报文。这表示报文发送过快,以致于设备来不及处理),则会让设备暂停。而当网络设备在完成报文的发送后会产生中断,对应的中断处理程序又可以根据设备当前的发送缓冲区大小,决定是否让设备从暂停中恢复。

而如果网络设备出现问题,无法发送报文了,则可能设备上的发送缓冲区一直处于被占满的状态,导致设备一直被暂停。另一方面,报文发不出去,也就不会有通知发送完成的中断产生,设备也就不会从暂停状态恢复,于是网络就瘫痪了。
为了检测这种情况,驱动程序可以为设备设置一个看门狗定时器。如果发现设备正在暂停状态,并且距离最后一次发送报文已经过去一定的时间,而发送完成的中断还没有收到,则认为该设备出现问题。此时看门狗定时器将触发驱动程序提供的相关函数,将设备复位,以试图让其恢复正常工作。

时间: 2024-08-07 01:29:32

linux网络报文接收发送浅析的相关文章

linux IPv4报文处理浅析

在<linux网络报文接收发送浅析>一文中介绍了数据链路层关于网络报文的处理. 对于接收到的报文,如果不被丢弃.不被网桥转发,会调用netif_receive_skb()提交给IP层: 而对于IP层向外发送的报文,则通过调用dev_queue_xmit()提交给数据链路层. 本文就以netif_receive_skb()和dev_queue_xmit()为起始,简要介绍一下报文在IP层的处理过程. 先来一张图: 报文接收(图中橙色箭头所指) netif_receive_skb()对每一种已注册

linux网络实现分析(1)——数据包的接收(从网卡到协议栈)

linux网络实现分析(1)--数据包的接收(从网卡到协议栈) --lvyilong316 说明:源码参考2.6.32 从网卡到协议栈的skb接收有两种方式:NAPI和非NAPI.其中有公共逻辑,也有区别.首先看下用到的基本数据结构. 1. 基本数据结构 l   softnet_data    每cpu数据 struct softnet_data {     struct Qdisc        *output_queue;  //发送帧队列     struct sk_buff_head

Windows 和 Linux下使用socket下载网页页面内容(可设置接收/发送超时)的代码

主要难点在于设置recv()与send()的超时时间,具体要注意的事项,请看代码注释部分,下面是代码:   [cpp] view plaincopyprint?   #include <stdio.h>   #include <sys/types.h>   #include <stdlib.h>   #include <string.h>   #include <errno.h>   #include <string.h>     

linux网络实现分析(2)——数据包的接收(从链路层到ip层)

linux网络实现分析(2)--数据包的接收(从链路层到ip层) --lvyilong316 任何数据包在由驱动接收进入协议栈都会经过netif_receive_skb函数,可以说这个函数是协议栈的入口.在分析这个函数前,首先介绍下三层协议在内核中的组织方式.     在Linux内核中,有两种不同目的的3层协议: (1) ptype_all管理的协议主要用于分析目的,它接收所有到达第3层协议的数据包. (2) ptype_base管理正常的3层协议,仅接收具有正确协议标志符的数据包,例如,In

信号截获-Linux关机时无法发送网络请求?linux 关机时还能发送网络请求吗?

问题描述 Linux关机时无法发送网络请求?linux 关机时还能发送网络请求吗? 通过截获系统关机给进程发送的SIGTERM信号,然后给服务端发送网络请求:此时网络服务已然关闭,无法发送成功. 有没有什么办法在linux关机前发送网络通信请求? 解决方案 由于linux关机命令的优先级别是很高,你要实现这个功能,理论上需要设置一个更高级别的进程来监听关机进程.住:本人没有试验过,请楼主实践求知 解决方案二: 关机时,你的程序已经对系统没有多少控制了.处理时机不受控制.你应该服务器设置超时等判断

Linux网络文件系统的数据备份、恢复及同步机制

本文将详细介绍针对该网络文件系统的数据备份.恢复及同步机制在内核的具体实现,给广大系统管理员和研发人员提供技术参考.网络文件系统(NFS)协议是由 Sun MicroSystem 公司在 20 世纪 80 年代为了提供对共享文件的远程访问而设计和实现的,它采用了经典的客户机/服务器模式提供服务.为了达到如同 NFS 协议通过使用 Sun 公司开发的远在本机上使用本地文件系统一样便捷的效果,NFS 通过使用远程过程调用协议(RPC Protocol)来实现运行在一台计算机上的程序来调用在另一台远程

你必须了解的基础的Linux网络命令

摘要:有抱负的 Linux 系统管理员和 Linux 狂热者必须知道的.最重要的.而且基础的 Linux 网络命令合集. 在 It's FOSS 我们并非每天都谈论 Linux 的"命令行方面".基本上,我更专注于 Linux 的桌面端.但你们读者中的一些人在内部调查(仅面向 It's FOSS newsletter 订阅者)中指出,你们也想学些命令行技巧.速查表也受大部分读者所喜欢和支持. 为此,我编辑了一个 Linux 中基础网络命令的列表.它并不是一个教你如何使用这些命令的教程,

你必须了解的基础的 Linux 网络命令

23在 It's FOSS 我们并非每天都谈论 Linux 的"命令行方面".基本上,我更专注于 Linux 的桌面端.但你们读者中的一些人在内部调查(仅面向 It's FOSS newsletter 订阅者)中指出,你们也想学些命令行技巧.速查表也受大部分读者所喜欢和支持. 为此,我编辑了一个 Linux 中基础网络命令的列表.它并不是一个教你如何使用这些命令的教程,而是一个命令合集和他们的简短解释.所以,如果你已经使用过这些命令,你可以用它来快速记住命令. 你可以把这个网页添加为书

Linux网络编程入门

(一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍 客户端和服务端          网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户端         在网络程序中,如果一个程序主动和外面的程序通信,那么我们把这个程序称为客户端程序. 比如我们使用ftp程序从另外一         个地方获取文件的时候,是我们的ftp程序主动同外面进行通信(获取文件), 所以这个地方我们的ftp程序就是客户端程序.  服务端