Alan Cox:单向链表中prev指针的妙用
之前发过一篇二级指针操作单向链表的例子,显示了C语言指针的灵活性,这次再探讨一个指针操作链表的例子,而且是一种完全不同的用法。
这个例子是linux-1.2.13网络协议栈里的,关于链表遍历&数据拷贝的一处实现。源文件是/net/inet/dev.c,你可以从kernel.org官网上下载。
从最早的0.96c版本开始,linux网络部分一直采取TCP/IP协议族实现,这是最为广泛应用的网络协议,整个架构就是经典的OSI七层模型的描述,其中dev.c是属于链路层实现。从功能上看,其位于网络设备驱动程序和网络层协议实现模块之间,作为二者之间的数据包传输通道,一种接口模块而存在——对驱动层的接口函数netif_rx, 以及对网络层的接口函数net_bh。前者提供给驱动模块的中断例程调用,用于链路数据帧的封装;后者作为驱动中断例程底半部(buttom half),用于对数据帧的解析处理并向上层传送。
为了便于理解,这里补充一下网络通信原理和linux驱动中断机制的背景知识。从最底层的物理层说起,当主机和路由器相互之间进行通信的时候,在物理介质上(同轴、光纤等)以电平信号进行传输。主机或路由器的硬件接口(网卡)负责收发这些信号,当信号发送到接口,再由内置的调制解调器(modem)将数字信号转换成二进制码,这样才能驻留在主机的硬件缓存中。这时接口(网卡)设备驱动程序将通过硬中断来获取硬件缓存中的数据,驱动程序是操作系统中负责直接同硬件设备打交道的模块,硬中断的触发是初始化时通过设置控制寄存器实现的,用于通知驱动程序硬件缓存中有新的数据到来。linux卡设备驱动就是在中断处理例程(ISR)中将硬件缓存数据拷贝到内核缓存中,打包成数据链路帧进行解析处理,再向上分发到各种协议层。由于ISR上下文是原子性的、中断屏蔽的,整个步骤又较为繁琐,因此全部放在ISR中处理会影响到其它中断响应实时性,于是linux有实现一种bottom half的软中断处理机制,将整个ISR一分为二,前半部上下文屏蔽所有中断,专门处理紧急的、实时性强的事务,如拷贝硬件缓存并打包封装,后半部上下文没有屏蔽中断(但代码不可重入),用于处理比较耗时且非紧急事务,包括数据帧的解析处理和分发。下面要讲的net_bh就属于后半部。
我们主要关心的是将链路帧分发到协议层那一段逻辑,下面摘自net_bh函数中的一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
526 void net_bh( void *tmp) 527 { ... 577 578 /* 579 * We got a packet ID. Now loop over the "known protocols" 580 * table (which is actually a linked list, but this will 581 * change soon if I get my way- FvK), and forward the packet 582 * to anyone who wants it. 583 * 584 * [FvK didn't get his way but he is right this ought to be 585 * hashed so we typically get a single hit. The speed cost 586 * here is minimal but no doubt adds up at the 4,000+ pkts/second 587 * rate we can hit flat out] 588 */ 589 pt_prev = NULL; 590 for (ptype = ptype_base; ptype != NULL; ptype = ptype->next) 591 { 592 if ((ptype->type == type || ptype->type == htons(ETH_P_ALL)) && (!ptype->dev || ptype->dev==skb->dev)) 593 { 594 /* 595 * We already have a match queued. Deliver 596 * to it and then remember the new match 597 */ 598 if (pt_prev) 599 { 600 struct sk_buff *skb2; 601 skb2=skb_clone(skb, GFP_ATOMIC); 602 /* 603 * Kick the protocol handler. This should be fast 604 * and efficient code. 605 */ 606 if (skb2) 607 pt_prev->func(skb2, skb->dev, pt_prev); 608 } 609 /* Remember the current last to do */ 610 pt_prev=ptype; 611 } 612 } /* End of protocol list loop */ 613 /* 614 * Is there a last item to send to ? 615 */ 616 if (pt_prev) 617 pt_prev->func(skb, skb->dev, pt_prev); 618 /* 619 * Has an unknown packet has been received ? 620 */ 621 else 622 kfree_skb(skb, FREE_WRITE); 623 ... 640 } |
在此稍稍解说一下数据结构,skb就是内核缓存中sock数据封装,协议栈里从链路层到传输层都会用到,只不过封装格式不同,主要是对协议首部(header)的由下而上层层剥离(反之由上而下是层层创建),在此你只需理解为一个链路数据帧即可。这段代码的逻辑是解析skb中的协议字段,从协议类型链表(由ptype_base维护)中查询对应的协议节点进行函数指针func回调,以便将数据帧分发到相应的协议层(如ARP、IP、8022、8023等)。
第一眼看上去是不是有点奇怪?这段代码竟然用一个pt_prev指针去维护ptype链表中前一个节点,从而产生了额外的条件分支判断,咋一看是否多了很多“余”了?回顾一下那篇二级指针操作单向链表的博文,简直完全是反其道而行之的。如果把pt_prev去掉,代码可以精简为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
for (ptype = ptype_base; ptype != NULL; ptype = ptype->next) { if ((ptype->type == type || ptype->type == htons(ETH_P_ALL)) && (!ptype->dev || ptype->dev==skb->dev)) { /* * We already have a match queued. Deliver * to it and then remember the new match */ struct sk_buff *skb2; skb2=skb_clone(skb, GFP_ATOMIC); /* * Kick the protocol handler. This should be fast * and efficient code. */ if (skb2) pt_prev->func(skb2, skb->dev, pt_prev); } } /* End of protocol list loop */ kfree_skb(skb, FREE_WRITE); |
咋看一下“干净”了很多,不是吗?但我们要记住一点,凡是网上发布的linux内核源代码,都是都是经过众多黑客高手们重重检视并验证过的,人家这么写肯定有十分充足的理由,所以不要太过于相信自己的直觉了,让我们再好好review一下代码吧!看看这段循环里做了什么事情?特别是第592~611行。
由于从网络上拷贝过来skb是唯一的,而分发的协议对象可能是多个,所以在回调之前要做一次clone动作(注意这里是深度拷贝,相当于一次kmalloc)。分发之后还需要调用kfree_skb释放掉原始skb数据块,它的历史使命到此完成了,没有保留的必要(第622行)。注意,这两个动作都是存在内核开销的。
然而这里为啥要pt_prev维护一个后向节点呢?这是有深意的,它的作用就是将当前匹配协议项的回调操作延时了。举个例子,如果链表遍历中找到某个匹配项,当前循环仅仅用pt_prev去记录这个匹配项,除此之外不做任何事情,待到下一次匹配项找到时,才去做上一个匹配项pt_prev的回调操作,直到循环结束,才会去做最后的匹配项的回调(当然pt_prev==NULL表示没有一次匹配,直接释放掉),所以这是一种拖延战术。有什么好处呢?就是比原先节省了很多不必要的操作。那么哪些操作是不必要的呢?这里我们逆向思考一下,我们看到clone是在协议字段匹配并且pt_prev!=NULL的前提条件下执行的,而kfree是在pt_prev==NULL的前提条件下执行的。在此可以假设一下,如果ptype链表中存在N项协议与之匹配,那么这段代码只会执行N-1次clone,而没有pt_prev时将会执行N次clone和1次kfree,再如果ptype链表中有且仅有一项协议与之匹配,那么整个循环既不会执行到第601行的clone,也不会执行到第622行的kfree。
也就是说,当整个链表至少有一项匹配的一般情况下,pt_prev存在比没有时减少了一次clone和一次kfree的开销;只有全部不匹配的最差情况下,两者都只做一次kfree动作,持平。这就是延迟策略产生的效益。
熟悉TCP/IP协议族的开发人员应该知道MTU(最大传输单元)这个概念,遵循不同协议的MTU值是不同的。比如以太网帧MTU是1500个字节,802.3帧MTU是1492字节,PPP链路帧MTU是269字节,而超通道MTU理论上是65535字节。要知道在一个高速吞吐量通信网络环境下,在大块数据分片传输线路里,在内核级别代码中,减少一处系统开销意味着什么?
其实我们完全可以抛开一切网络协议相关知识,这不过是一段极其普通的单向链表操作而已,逻辑并不复杂。但是看看人家顶级黑客是怎么思考和coding的,对比一下自己写过的代码,多少次数据处理是用一个简单的for循环匆匆敷衍了事而没有进一步思考其中的粗陋和不合理之处?面对真正的编程高手这种“心计”与“城府”,你是不是有种莫名不安感?你会怀疑你真的了解怎么去使用和操作C语言中基本的链表数据结构么?如果答案是肯定的,那就开始颤抖吧(哈,别误会,其实上面这段话不过是笔者的自我告解罢了)~~~
最后,让我们感谢尊敬的Alan Cox大大对Linux社区卓越精细、无与伦比的贡献!(Alan是图中中部戴红帽子的那位)
附注:
最新的Linux-2.6.x版本中协议栈实现部分变动很大,但/net/core/dev.c的netif_receive_skb函数里仍然保留了pt_prev这种用法,目的是一样的,都是为了减少一次系统开销的优化操作。
关于Alan,他在斯旺西大学工作时,在学校服务器上安装了一个早期的linux版本,供学校使用。他修正了许多的问题,重写了网络系统中的许多部份。随后成为linux内核开发小组中的重要成员。Alan Cox负责维持2.2版,在2.4版上拥有自己的分支(在版本号上会冠上ac,如 2.4.13-ac1)。他的分支版本非常稳定,修正许多错误,许多厂商都使用他的版本。在他去进修工商管理硕士之前,涉入许多linux内核开发的事务,在社群中有很高的地位,有时会被视为是Linus之下的第二号领导者。
不过,今年1月28日的时候,Alan因为家庭原因宣布退出Linux项目了,下面是他Google+的声明:
“I’m leaving the Linux world and Intel for a bit for family reasons, I’m aware that ‘family reasons’ is usually management speak for ‘I think the boss is an asshole’ but I’d like to assure everyone that while I frequently think Linus is an asshole (and therefore very good as kernel dictator) I am departing quite genuinely for family reasons and not because I’ve fallen out with Linus or Intel or anyone else. Far from it I’ve had great fun working there.”
Alan Cox:单向链表中prev指针的妙用的更多相关文章
- C语言:将ss所指字符串中所有下标为奇数位上的字母转换成大写,若不是字母,则不转换。-删除指针p所指字符串中的所有空白字符(包括制表符,回车符,换行符)-在带头结点的单向链表中,查找数据域中值为ch的结点,找到后通过函数值返回该结点在链表中所处的顺序号,
//将ss所指字符串中所有下标为奇数位上的字母转换成大写,若不是字母,则不转换. #include <stdio.h> #include <string.h> void fun ...
- java笔试之从单向链表中删除指定值的节点
输入一个单向链表和一个节点的值,从单向链表中删除等于该值的节点,删除后如果链表中无节点则返回空指针. 链表的值不能重复 构造过程,例如 1 -> 2 3 -> 2 5 -> 1 4 ...
- 输出单向链表中倒数第k个结点
描述 输入一个单向链表,输出该链表中倒数第k个结点,链表的倒数第0个结点为链表的尾指针. 链表结点定义如下: struct ListNode { int m_nKey; ListNode* ...
- [华为]输出单向链表中倒数第k个结点
输入一个单向链表,输出该链表中倒数第k个结点,链表的倒数第1个结点为链表的尾指针. 链表结点定义如下: struct ListNode { int m_nKey; ListNode* m_ ...
- 线性数据结构案例1 —— 单向链表中获取倒数k个节点
一.介绍 先遍历整个链表获取链表长度length,然后通过 (length-index) 方式得到我们想要节点在链表中的位置. 二.代码 public Node findLastIndexNode( ...
- 13:在O(1)时间内删除单向链表中的一个节点
思路:如果从首部开始依次查找,那么时间是O(n). 既然我们知道要删除的结点i,那么我们就知道它指向的下一个结点j,那么我们可以将j的内容复制到i,然后将i的指针指向j的下一个结点,这样虽然看起来我们 ...
- Python3玩转单链表——逆转单向链表pythonic版
[本文出自天外归云的博客园] 链表是由节点构成的,一个指针代表一个方向,如果一个构成链表的节点都只包含一个指针,那么这个链表就是单向链表. 单向链表中的节点不光有代表方向的指针变量,也有值变量.所以我 ...
- Q:判断链表中是否存在环的相关问题
问题:如何判断一个单向链表中是否存在环? 例如: 链表中存在环(B-->D): <-- <--^ | | v | A-->B-->C-->D 链表中不存在环: A- ...
- Java-链表(单向链表、双向链表)
Java-链表 1.什么是链表? 2.链表的特点是什么? 3.链表的实现原理? 4.如何自己写出一个链表? 1.什么是链表? 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过 ...
随机推荐
- 获取sevlet response值
调用: PrintWriter out = new PrintWriter(new OutputStreamWriter(response.getOutputStream(), "UTF-8 ...
- 【Linux】好玩的linux命令
Linux里面有很多有趣的东西,这篇文章整理了一些.摘录一下: 1. sl 命令 你会看到一辆火车从屏幕右边开往左边...... 安装 $ sudo apt-get install sl 运行 $ ...
- MySql 常见错误代码大全
B.. 服务器错误代码和消息 服务器错误信息来自下述源文件: · 错误消息信息列在share/errmsg.txt文件中.“%d”和“%s”分别代表编号和字符串,显示时,它们将被消息值取代. · 错误 ...
- 局域网内主机ssh访问服务器宿主下VMWare 虚拟机(Ubuntu 12.04.1)并且实现虚拟机能上网的那点事
(1)首先虚拟机已安装ssh服务 1) 自动安装 ssh 服务 apt-get install openssh-server 安装完成后,将自动开启 ssh 服务. 2) 查看 ssh 服务是否已开启 ...
- VB6 获取和设置默认打印机
Private Declare Function GetProfileString Lib "kernel32" Alias "GetProfileStringA&quo ...
- (二)用控制器controller给模型数据赋初始值
之前博客,非常easy的就实现了模型数据和页面显示的自己主动绑定.如今我们使用控制器,给模型赋初始值. 假设使用jquery来实现变量赋初值,须要在页面载入完毕后运行$("#target&q ...
- soapUI pro :INFO:Error getting response for []; javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
need to configure two for the https address: Step 1 export the certificate from the IE settings opti ...
- Selenium简单测试页面加载速度的性能(Page loading performance)
利用selenium的可以执行javascript脚本的特性,我写了一个java版本的获得页面加载速度的代码,这样你就可以在进行功能测试的同时进行一个简单的测试页面的加载速度的性能测试. 我现在的项目 ...
- django之创建第5个项目-条件语句
1.index <!DOCTYPE html> <html lang="en"> <head> <meta charset="U ...
- excel怎么在插入的方框上打勾
本例主要介绍如何在excel中插入带对勾的方框. 工具/原料 Excel 操作步骤: 在编辑Excel表格模板时,比如说简历.人力信息登记表等,经常需要有一些可选项,如下例的婚姻状况就包括“有配 ...