2007年11月12日星期一

正则表达式30分钟入门教程

正则表达式30分钟入门教程

版本:v2.21 (2007-8-3) 作者:deerchao 来源:unibetter大学生社区 转载请注明来源

目录

  1. 本文目标
  2. 如何使用本教程
  3. 正则表达式到底是什么?
  4. 入门
  5. 测试正则表达式
  6. 元字符
  7. 字符转义
  8. 重复
  9. 字符类
  10. 反义
  11. 替换
  12. 分组
  13. 后向引用
  14. 零宽断言
  15. 负向零宽断言
  16. 注释
  17. 贪婪与懒惰
  18. 处理选项
  19. 平衡组/递归匹配
  20. 还有些什么东西没提到
  21. 联系作者
  22. 一些我认为你可能已经知道的术语的参考
  23. 网上的资源及本文参考文献
  24. 更新说明

本文目标

30分钟内让你明白正则表达式是什么,并对它有一些基本的了解,让你可以在自己的程序或网页里使用它。

如何使用本教程

最重要的是——请给我30分钟,如果你没有使用正则表达式的经验,请不要试图在30内入门。当然,如果你是超人,那自然得另当别论。

别被下面那些复杂的表达式吓倒,只要跟着我一步一步来,你会发现正则表达式其实并没有你 想像中的那么困难。当然,如果你看完了这篇教程之后,发现自己明白了很多,却又几乎什么都记不得,那也是很正常的——我认为,没接触过正则表达式的人在看 完这篇教程后,能把提到过的语法记住80%以上的可能性为零。这里只是让你明白基本的原理,以后你还需要多练习,多使用,才能熟练掌握正则表达式。

除了作为入门教程之外,本文还试图成为可以在日常工作中使用的正则表达式语法参考手册。就作者本人的经历来说,这个目标还是完成得不错的——你看,我自己也没能把所有的东西记下来,不是吗?

文本格式约定:专业术语 元字符/语法格式 正则表达式 正则表达式中的一部分(用于分析) 用于在其中搜索的字符串 对正则表达式或其中一部分的说明清除格式

正则表达式到底是什么?

在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

很可能你使用过Windows/Dos下用于文件查找的通配符(wildcard),也就是*?。如果你想查找某个目录下的所有的Word文档的话,你会搜索*.doc。在这里,*会被解释成任意的字符串。和通配符类似,正则表达式也是用来进行文本匹配的工具,只不过比起通配符,它能更精确地描述你的需求——当然,代价就是更复杂——比如你可以编写一个正则表达式,用来查找所有以0开头,后面跟着2-3个数字,然后是一个连字号“-”,最后是7或8位数字的字符串(像010-123456780376-7654321)。

正则表达式是用于进行文本匹配的工具,所以本文里多次提到了在字符串里搜索/查找,这种说法的意思是在给定的字符串中,寻找与给定的正则表达式相匹配的部分。有可能字符串里有不止一个部分满足给定的正则表达式,这时每一个这样的部分被称为一个匹配。匹配在本文里可能会有三种意思:一种是形容词性的,比如说一个字符串匹配一个表达式;一种是动词性的,比如说在字符串里匹配正则表达式;还有一种是名词性的,就是刚刚说到的“字符串中满足给定的正则表达式的一部分”。

入门

学习正则表达式的最好方法是从例子开始,理解例子之后再自己对例子进行修改,实验。下面给出了不少简单的例子,并对它们作了详细的说明。

假设你在一篇英文小说里查找hi,你可以使用正则表达式hi

这是最简单的正则表达式了,它可以精确匹配这样的字符串:由两个字符组成,前一个字符是h,后一个是i。通常,处理正则表达式的工具会提供一个忽略大小写的选项,如果选中了这个选项,它可以匹配hi,HI,Hi,hI这四种情况中的任意一种。

不幸的是,很多单词里包含hi这两个连续的字符,比如him,history,high等等。用hi来查找的话,这里边的hi也会被找出来。如果要精确地查找hi这个单词的话,我们应该使用\bhi\b

\b是正则表达式规定的一个特殊代码(好吧,某些人叫它元字符,metacharacter),代表着单词的开头或结尾,也就是单词的分界处。虽然通常英文的单词是由空格或标点符号或换行来分隔的,但是\b并不匹配这些单词分隔符中的任何一个,它只匹配一个位置。(如果需要更精确的说法,\b匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w

假如你要找的是hi后面不远处跟着一个Lucy,你应该用\bhi\b.*\bLucy\b

这里,.是另一个元字符,匹配除了换行符以外的任意字符*同样是元字符,不过它代表的不是字符,也不是位置,而是数量——它指定*前边的内容可以连续重复出现任意次以使整个表达式得到匹配。因此,.*连在一起就意味着任意数量的不包含换行的字符。现在\bhi\b.*\bLucy\b的意思就很明显了:先是一个单词hi,然后是任意个任意字符(但不能是换行),最后是Lucy这个单词

如果同时使用其它的一些元字符,我们就能构造出功能更强大的正则表达式。比如下面这个例子:

0\d\d-\d\d\d\d\d\d\d\d匹配这样的字符串:以0开头,然后是两个数字,然后是一个连字号“-”,最后是8个数字(也就是中国的电话号码。当然,这个例子只能匹配区号为3位的情形)。

这里的\d是一个新的元字符,匹配任意的数字(0,或1,或2,或……)-不是元字符,只匹配它本身——连字号。

为了避免那么多烦人的重复,我们也可以这样写这个表达式:0\d{2}-\d{8}。 这里\d后面的{2}({8})的意思是前面\d必须连续重复匹配2次(8次)

测试正则表达式

如果你不觉得正则表达式很难读写的话,要么你是一个天才,要么,你不是地球人。正则表达式的语法很令人头疼,即使对经常使用它的人来说也是如此。由于难于读写,容易出错,所以很有必要创建一种工具来测试正则表达式。

由于在不同的环境下正则表达式的一些细节是不相同的,本教程介绍的是Microsoft .Net 2.0下正则表达式的行为,所以,我向你介绍一个.Net下的工具Regex Tester。首先你确保已经安装了.Net Framework 2.0,然后下载Regex Tester。这是个绿色软件,下载完后打开压缩包,直接运行RegexTester.exe就可以了。

下面是Regex Tester运行时的截图:

Regex Tester运行时的截图

元字符

现在你已经知道几个很有用的元字符了,如\b,.,*,还有\d.当然还有更多的元字符可用,比如\s匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等\w匹配字母或数字或下划线或汉字等

下面来试试更多的例子:

\ba\w*\b匹配以字母a开头的单词——先是某个单词开始处(\b),然后是字母a,然后是任意数量的字母或数字(\w*),最后是单词结束处(\b)(好吧,现在我们说说正则表达式里的单词是什么意思吧:就是几个连续的\w。不错,这与学习英文时要背的成千上万个同名的东西的确关系不大)。

\d+匹配1个或更多连续的数字。这里的+是和*类似的元字符,不同的是*匹配重复任意次(可能是0次),而+则匹配重复1次或更多次

\b\w{6}\b 匹配刚好6个字母/数字的单词

表1.常用的元字符
代码 说明
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线或汉字
\s 匹配任意的空白符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

元字符^(和数字6在同一个键位上的符号)以及$\b有点类似,都匹配一个位置。^匹配你要用来查找的字符串的开头,$匹配结尾。这两个代码在验证输入的内容时非常有用,比如一个网站如果要求你填写的QQ号必须为5位到12位数字时,可以使用:^\d{5,12}$

这里的{5,12}和前面介绍过的{2}是类似的,只不过{2}匹配只能不多不少重复2次{5,12}则是重复的次数不能少于5次,不能多于12次,否则都不匹配。

因为使用了^$,所以输入的整个字符串都要用来和\d{5,12}来匹配,也就是说整个输入必须是5到12个数字,因此如果输入的QQ号能匹配这个正则表达式的话,那就符合要求了。

和忽略大小写的选项类似,有些正则表达式处理工具还有一个处理多行的选项。如果选中了这个选项,^$的意义就变成了匹配行的开始处和结束处

字符转义

如果你想查找元字符本身的话,比如你查找.,或者*,就出现了问题:你没法指定它们,因为它们会被解释成其它的意思。这时你就必须使用\来取消这些字符的特殊意义。因此,你应该使用\.\*。当然,要查找\本身,你也得用\\.

例如:www\.unibetter\.com匹配www.unibetter.comc:\\Windows匹配c:\Windows

重复

你已经看过了前面的*,+,{2},{5,12}这几个匹配重复的方式了。下面是正则表达式中所有的限定符(指定数量的代码,例如*,{5,12}等):

表2.常用的限定符
代码/语法 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

下面是一些使用重复的例子:

Windows\d+匹配Windows后面跟1个或更多数字

13\d{9}匹配13后面跟9个数字(中国的手机号)

^\w+匹配一行的第一个单词(或整个字符串的第一个单词,具体匹配哪个意思得看选项设置)

字符类

要想查找数字,字母或数字,空白是很简单的,因为已经有了对应这些字符集合的元字符,但是如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办?

很简单,你只需要在中括号里列出它们就行了,像[aeiou]就匹配任何一个英文元音字母[.?!]匹配标点符号(.或?或!)(英文语句通常只以这三个标点结束)。

我们也可以轻松地指定一个字符范围,像[0-9]代表的含意与\d就是完全一致的:一位数字,同理[a-z0-9A-Z_]也完全等同于\w(如果只考虑英文的话)。

下面是一个更复杂的表达式:\(?0\d{2}[) -]?\d{8}

这个表达式可以匹配几种格式的电话号码,像(010)88886666,或022-22334455,或02912345678等。我们对它进行一些分析吧:首先是一个转义字符\(,它能出现0次或1次(?),然后是一个0,后面跟着2个数字(\d{2}),然后是)-空格中的一个,它出现1次或不出现(?),最后是8个数字(\d{8})。不幸的是,它也能匹配010)12345678(022-87654321这样的“不正确”的格式。要解决这个问题,请在本教程的下面查找答案。

反义

有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义

表3.常用的反义代码
代码/语法 说明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

例子:\S+匹配不包含空白符的字符串

]+>匹配用尖括号括起来的以a开头的字符串

替换

好了,现在终于到了解决3位或4位区号问题的时间了。正则表达式里的替换指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用|把不同的规则分隔开。听不明白?没关系,看例子:

0\d{2}-\d{8}|0\d{3}-\d{7}这个表达式能匹配两种以连字号分隔的电话号码:一种是三位区号,8位本地号(如010-12345678),一种是4位区号,7位本地号(0376-2233445)

\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}这个表达式匹配3位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔。你可以试试用替换|把这个表达式扩展成也支持4位区号的。

\d{5}-\d{4}|\d{5}这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字,或者用连字号间隔的9位数字。之所以要给出这个例子是因为它能说明一个问题:使用替换时,顺序是很重要的。如果你把它改成\d{5}|\d{5}-\d{4}的话,那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配替换时,将会从左到右地测试每个分枝条件,如果满足了某个分枝的话,就不会去管其它的替换条件了。

Windows98|Windows2000|WindosXP这个例子是为了告诉你替换不仅仅能用于两种规则,也能用于更多种规则。

分组

我们已经提到了怎么重复单个字符(直接在字符后面加上限定符就行了);但如果想要重复多个字符又该怎么办?你可以用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了,你也可以对子表达式进行其它一些操作(后面会有介绍)。

(\d{1,3}\.){3}\d{1,3}是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:\d{1,3}匹配1到3位的数字(\d{1,3}\.){3}匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,最后再加上一个一到三位的数字(\d{1,3})。

不幸的是,它也将匹配256.300.888.999这 种不可能存在的IP地址(IP地址中每个数字都不能大于255。题外话,好像反恐24小时第三季的编剧不知道这一点,汗...)。如果能使用算术比较的 话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

理解这个表达式的关键是理解2[0-4]\d|25[0-5]|[01]?\d\d?,这里我就不细说了,你自己应该能分析得出来它的意义。

后向引用

使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。

后向引用用于重复搜索前面某个分组匹配的文本。例如,\1代表分组1匹配的文本。难以理解?请看示例:

\b(\w+)\b\s+\1\b可以用来匹配重复的单词,像go go, kitty kitty。首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b),然后是1个或几个空白符(\s+),最后是前面匹配的那个单词(\1)。

你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?\w+)(或者把尖括号换成'也行:(?'Word'\w+)),这样就把\w+的组名指定为Word了。要反向引用这个分组捕获的内容,你可以使用\k,所以上一个例子也可以写成这样:\b(?\w+)\b\s+\k\b

使用小括号的时候,还有很多特定用途的语法。下面列出了最常用的一些:

表4.分组语法
捕获
(exp) 匹配exp,并捕获文本到自动命名的组里
(?exp) 匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp)
(?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号
零宽断言
(?=exp) 匹配exp前面的位置
(?<=exp) 匹配exp后面的位置
(?!exp) 匹配后面跟的不是exp的位置
(? 匹配前面不是exp的位置
注释
(?#comment) 这种类型的组不对正则表达式的处理产生任何影响,用于提供注释让人阅读

我们已经讨论了前两种语法。第三个(?:exp)不会改变正则表达式的处理方式,只是这样的组匹配的内容不会像前两种那样被捕获到某个组里面

零宽断言

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧:

(?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。比如\b\w+(?=ing\b),匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找I'm singing while you're dancing.时,它会匹配singdanc

(?<=exp)也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如(?<=\bre)\w+\b会匹配以re开头的单词的后半部分(除了re以外的部分),例如在查找reading a book时,它匹配ading

假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了),你可以这样查找需要在前面和里面添加逗号的部分:((?<=\d)\d{3})*\b,用它对1234567890进行查找时结果是234567890

下面这个例子同时使用了这两种断言:(?<=\s)\d+(?=\s)匹配以空白符间隔的数字(再次强调,不包括这些空白符)

负向零宽断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果我们想查找这样的单词--它里面出现了字母q,但是q后面跟的不是字母u,我们可以尝试这样:

\b\w*q[^u]\w*\b匹配包含后面不是字母u的字母q的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词的结尾的话,像Iraq,Benq,这个表达式就会出错。这是因为[^u]总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[^u]将会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的\w*\b将会匹配下一个单词,于是\b\w*q[^u]\w*\b就能匹配整个Iraq fighting负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题:\b\w*q(?!u)\w*\b

零宽度负预测先行断言(?!exp)断言此位置的后面不能匹配表达式exp。例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词

同理,我们可以用(?,零宽度正回顾后发断言断言此位置的前面不能匹配表达式exp(?匹配前面不是小写字母的七位数字

一个更复杂的例子:(?<=<(\w+)>).*(?=<\/\1>)匹配不包含属性的简单HTML标签内里的内容()指定了这样的前缀被尖括号括起来的单词(比如可能是),然后是.*(任意的字符串),最后是一个后缀(?=<\/\1>)。注意后缀里的\/,它用到了前面提过的字符转义;\1则是一个反向引用,引用的正是捕获的第一组,前面的(\w+)匹配的内容,这样如果前缀实际上是的话,后缀就是了。整个表达式匹配的是之间的内容(再次提醒,不包括前缀和后缀本身)。

注释

小括号的另一种用途是能过语法(?#comment)来包含注释。例如:2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)

要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。

例如,我们可以前面的一个表达式写成这样:

      (?<=    # 断言要匹配的文本的前缀
<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
) # 前缀结束
.* # 匹配任意文本
(?= # 断言要匹配的文本的后缀
<\/\1> # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
) # 后缀结束

贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。考虑这个表达式:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aabab(为什么第一个匹配是aab而不是ab?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配最有最大的优先权——The Match That Begins Earliest Wins)。

表5.懒惰限定符
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

处理选项

上面介绍了几个选项如忽略大小写,处理多行等,这些选项能用来改变处理正则表达式的方式。下面是.Net中常用的正则表达式选项:

表6.常用的处理选项
名称 说明
IgnoreCase(忽略大小写) 匹配时不区分大小写。
Multiline(多行模式) 更改^$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
Singleline(单行模式) 更改.的含义,使它与每一个字符匹配(包括换行符\n)。
IgnorePatternWhitespace(忽略空白) 忽略表达式中的非转义空白并启用由#标记的注释。
RightToLeft(从右向左查找) 匹配从右向左而不是从左向右进行。
ExplicitCapture(显式捕获) 仅捕获已被显式命名的组。
ECMAScript(JavaScript兼容模式) 使表达式的行为与它在JavaScript里的行为一致。

一个经常被问到的问题是:是不是只能同时使用多行模式和单行模式中的一种?答案是:不是。这两个选项之间没有任何关系,除了它们的名字比较相似(以至于让人感到疑惑)以外。

平衡组/递归匹配

注意:这里介绍的平衡组语法是由.Net Framework支持的;其它语言/库不一定支持这种功能,或者支持此功能但需要使用不同的语法。

有时我们需要匹配像( 100 * ( 50 + 15 ) )这样的可嵌套的层次性结构,这时简单地使用\(.+\)则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论的是贪婪模式,懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等,比如( 5 / ( 3 + 2 ) ) ),那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的,配对的括号之间的内容呢?

为了避免(\(把你的大脑彻底搞糊涂,我们还是用尖括号代替圆括号吧。现在我们的问题变成了如何把xx aa> yy这样的字符串里,最长的配对的尖括号内的内容捕获出来?

这里需要用到以下的语法构造:

  • (?'group') 把捕获的内容命名为group,并压入堆栈
  • (?'-group') 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
  • (?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
  • (?!) 零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败

如果你不是一个程序员(或者你是一个对堆栈的概念不熟的程序员),你就这样理解上面的三种语法吧:第一个就是在黑板上写一个 "group",第二个就是从黑板上擦掉一个"group",第三个就是看黑板上写的还有没有"group",如果有就继续匹配yes部分,否则就匹配 no部分。

我们需要做的是每碰到了左括号,就在黑板上写一个"group",每碰到一个右括号,就擦掉一个,到了最后就看看黑板上还有没有--如果有那就证明左括号比右括号多,那匹配就应该失败。

<                         #最外层的左括号
[^<>]* #最外层的左括号后面的不是括号的内容
(
(
(?'Open'<) #碰到了左括号,在黑板上写一个"Open"
[^<>]* #匹配左括号后面的不是括号的内容
)+
(
(?'-Open'>) #碰到了右括号,擦掉一个"Open"
[^<>]* #匹配右括号后面不是括号的内容
)+
)*
(?(Open)(?!)) #在遇到最外层的右括号前面,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败
> #最外层的右括号

平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的

标签:]*>[^<>]*(((?'Open']*>)[^<>]*)+((?'-Open'
)[^<>]*)+)*(?(Open)(?!))
.

还有些什么东西没提到

我已经描述了构造正则表达式的大量元素,还有一些我没有提到的东西。下面是未提到的元素的列表,包含语法和简单的说明。你可以在网上找到更详细的参考资料来学习它们--当你需要用到它们的时候。如果你安装了MSDN Library,你也可以在里面找到关于.net下正则表达式详细的文档。

表7.尚未详细讨论的语法
\a 报警字符(打印它的效果是电脑嘀一声)
\b 通常是单词分界位置,但如果在字符类里使用代表退格
\t 制表符,Tab
\r 回车
\v 竖向制表符
\f 换页符
\n 换行符
\e Escape
\0nn ASCII代码中八进制代码为nn的字符
\xnn ASCII代码中十六进制代码为nn的字符
\unnnn Unicode代码中十六进制代码为nnnn的字符
\cN ASCII控制字符。比如\cC代表Ctrl+C
\A 字符串开头(类似^,但不受处理多行选项的影响)
\Z 字符串结尾或行尾(不受处理多行选项的影响)
\z 字符串结尾(类似$,但不受处理多行选项的影响)
\G 当前搜索的开头
\p{name} Unicode中命名为name的字符类,例如\p{IsGreek}
(?>exp) 贪婪子表达式
(?-exp) 平衡组
(?im-nsx:exp) 在子表达式exp中改变处理选项
(?im-nsx) 为表达式后面的部分改变处理选项
(?(exp)yes|no) 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式;否则使用no
(?(exp)yes) 同上,只是使用空表达式作为no
(?(name)yes|no) 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用no
(?(name)yes) 同上,只是使用空表达式作为no

联系作者

好吧,我承认,我骗了你,读到这里你肯定花了不止30分钟.相信我,这是我的错,而不是因为你太笨.我之所以说"30分钟",是为了让你有信心,有耐心继续下去.既然你看到了这里,那证明我的阴谋成功了.上这种当的滋味还不错吧?

要投诉我,或者觉得我其实可以做得更好,或者有任何其它问题,欢迎来我的博客进行讨论.

一些我认为你可能已经知道的术语的参考

字符
程序处理文字时最基本的单位,可能是字母,数字,标点符号,空格,换行符,汉字等等。
字符串
0个或更多个字符的序列。
文本
文字,字符串。
匹配
符合规则,检验是否符合规则,符合规则的部分。
断言
声明一个应该为真的事实。只有当断言为真时才会对正则表达式继续进行匹配。

网上的资源及本文参考文献

2007年10月23日星期二

LINUX网络

Linux和网络几乎是同义词。实际上Linux是Internet或WWW的产物。它的开发者和用户使用web交换信息、想法、代码,而Linux自身 也常用于支持一些组织的联网需求。众所周知,TCP/IP 协议是 Internet 的标准协议,同时也是事实上的工业标准。Linux 的网络实现支持 BSD 套接字,支持完整的 TCP/IP 协议。本章描述了Linux如何支持统称为TCP/IP的网络协议。


10.1 TCP/IP网络概览(An Overview of TCP/IP Networking)
本节简要描述 TCP/IP 协议,这并不是一个详尽的描述,有关该协议的详细内容。请阅读第10本参考书(附录)。
TCP/IP 协议是 Internet 网的基本协议,它实际由许多协议组成,并以协议组的形式存在,其中的主要协议有:传输控制协议(TCP)、用户数据报协议(UDP)、网际协议(IP)、网际信报控制协议(ICMP)和地址解析协议(ARP)等。
设计TCP/IP协议的目的是用来支持连接在ARPANET上的计算机之间的通讯。ARPANET是由美国政府投资的一个美国的研究网络。ARPANET 是一些网络概念的先驱,例如报文交换和协议分层(让一种协议利用其它协议提供的服务)。ARPANET于1988年退出,但是它的后继者(NSF NET和Internet)发展得更大。现在所知的World Wide Web是在ARPANET中发展的,它本身也是由TCP/IP协议支持的。Unix在ARPANET上有大量的使用,第一个发布的网络版的Unix是 4.3BSD。Linux的网络实现基于4.3BSD模型,它支持BSD socket(和一些扩展)和全系列的TCP/IP网络功能。选择这种编程接口是因为它非常流行,可以帮助程序在Linux和其它Unix平台之间移植。
在一个IP网络中,每一台主机都必须具有唯一的地址,这一地址称为“IP 地址”。IP地址是一个32位的数字,它唯一地标识一台机器。例如,WWW是一个非常巨大、不断增长的IP网络,每一个连接在上面的机器都分配了一个独一 无二的IP地址。IP地址用由点分隔的四个数字表示,每个数字的取值范围一般在 0 ~ 255 之间,例如,16.42.0.9。在 Internet 上,主机的 IP 地址分为五类,分别是 A、B、C、D和E五类。主机的唯一 IP 地址一般从 A、B或C类地址中派生;D类地址用来将计算机组织成一个功能组;而E类地址是试验性的,当前不可用。另外,一些特殊的 IP 地址被保留用于特殊目的,例如,127.0.0.1 就是用来特指本地主机的回环地址。
IP地址实际上分为两个部分:网络地址和主机地址。这些地址的大小(尺寸)可能不同,用来分割这两部分的是机器的子网掩码。以16.42.0.9为例,如 果其子网掩码是255.255.0.0,则该IP地址表示的网络地址是16.42,主机地址是0.9。TCP/IP 软件利用子网掩码判断数据传输的目标主机是否和源主机处于同一子网中。例如,某主机的 IP 地址为 192.1.1.1,而子网掩码为 255.255.255.0,如果目标主机的 IP 地址为 192.1.1.4,则说明目标主机和源主机处于同一子网中,而如果目标主机的 IP 地址为 192.1.2.1,则说明不在同一子网中。上述判断通过利用子网掩码计算两台主机所在的子网地址而实现。主机 IP 地址和子网掩码的二进制与运算的结果称为“子网地址”,例如,主机 IP 地址 192.1.1.1 和子网掩码 255.255.255.0 的二进制与运算的结果为 192.1.1.0,即该主机所在子网的地址为 192.1.1.0,对地址为 192.1.1.4 的主机来说,可计算该主机所在子网地址为 192.1.1.0,于是说明目标主机和源主机处于同一子网中,而192.1.2.1 却不在同一子网中。
主机地址可以再进一步划分成为子网(subnetwork)和主机地址。再次以16.42.0.9为例,子网地址可以是16.42.0,主机地址为 16.42.0.9。将IP地址进一步划分,允许各个组织划分它们自己的网络。例如,假设16.42是ACME计算机公司的网络地址,16.42.0是它 的子网0,16.42.1是它的子网1。这些子网可以在分离的大楼里,它们也许通过电话专线或者甚至通过微波连接。IP地址由网络管理员分配,使用IP子 网是分散网络管理任务的一个好办法。IP子网的管理员可以自由地分配他们自己子网内的IP地址。
子网掩码也同时定义了子网中主机的最大数目。如果子网掩码为 255.255.255.0,在上面的例子中,IP 地址从 192.1.1.1 到 192.1.1.254 的主机均可出现在同一子网中(IP 地址 192.1.1.0 和 192.1.1.255 分别作为子网地址和子网中的广播地址),因此,该子网中的主机数目最多为 254 台。
因为数字式的 IP 地址非常难于记忆,因而通常利用主机域名标识主机,例如,bbs.tsinghua.edu.cn 是清华大学 BBS 服务器的域名,其 IP 地址为 202.112.58.200。但在利用域名标识主机的同时,也需要能够将域名转换为 IP 地址的机制,这种机制称为“域名解析”。在一般的 TCP/IP 主机中,这一名称可通过静态的 hosts 文件指定,也可通过 DNS (分布式名称服务器)服务器动态获得,这种情况下,本地主机必须知道一个或多个DNS服务器的IP地址。在 Linux 中,/etc/hosts 文件指定静态的主机名称,而 etc/resolv.conf 文件指定 DNS 服务器的 IP 地址。
不管何时,每当连接另外一台机器时,比如读取一个web page,都要使用它的IP地址(和那台机器交换数据)。这种数据包括在IP报文(packet)中,每一个报文都有一个IP头(包括源和目标机器的IP 地址、一个校验和和其它有用的信息)。这个校验和是从IP报文的数据中通过计算得来的,IP报文的接收者可以用它来判断IP报文在传输过程中是否被损坏 (可能是一个噪音很大的电话线)。应用程序传输的数据可能被分解成容易处理的更小的报文。IP数据报文的大小依赖于连接的介质而变化:以太网报文通常大于 PPP报文。目标主机必须重新装配这些数据报文,然后才能交给接收程序。如果通过一个相当慢的串行连接访问一个包括大量图形图像的web页,就可以用图形 的方式看出数据的分段和重组。
一般而言,逻辑上处于同一子网的两台主机处于同一局域网中,如果目标主机和源主机处于同一子网,就可通过某种机制获得目标主机的网卡物理地址,从而利用局 域网技术实现数据传输。从主机的 IP 地址获得物理地址的机制称为“地址解析”,在 TCP/IP 协议中,地址解析可由专门的协议(地址解析协议)完成。如果目标主机和源主机不在同一子网中,这时的数据传输就要通过其他计算机完成,如果目标主机和源主 机跨越大的地理距离,则可能要通过许多计算机的参与才能实现数据的传输。跨越不同子网的数据传输通过网关或路由器实现。网关(或路由器)连接在多于一个的 子网上,它们会把一个子网上接收的IP报文重新发送到另一个子网。当 TCP/IP 软件发现数据传输的目标主机处于其他子网时,它首先将数据发送到网关,然后由网关选择适当的路径传输,直到数据到达目标主机为止。例如,如果子网 16.42.1.0和16.42.0.0通过一个网关连接,那么所有从子网0发送到子网1的报文必须先发送到网关,以便网关转发它们。Linux 维护一个路由表,每个目标 IP 地址均对应一个路由表项。利用路由表项,Linux 可将每个跨子网的 IP 数据包发送到一个适当的主机(路由器)。系统中的路由表实际是动态的,并随着应用程序的网络使用情况和网络拓扑结构的变化而变化。

图10.1 TCP/IP协议层次结构

如前所述,TCP/IP 实际是以协议组的形式存在的,图 10-1 是 TCP/IP 协议层次结构。从图中可看出,TCP/IP映射为四层的结构化模型。这一模型也称为网际协议组(Internet Protocol Suit),可划分为网络接口、网际、传输和应用四层。
网络接口层(Network Interface Layer)负责和网络的直接通讯。它必须理解正在使用的网络结构,诸如令牌环和以太网等,并且还要提供允许网际层与之通讯的接口。网际层负责和网络接口层之间的直接通讯。
网际层(Internet Layer)主要完成利用网际协议(IP)的路由和数据包传递。传输层上的所有协议均要使用IP发送数据。网际协议定义如下规则:如何寻址和定向数据包;如何处理数据包的分段和重新组装;如何提供安全性信息;以及如何识别正在使用的服务类型等。
但是,由于IP不是基于连接的协议,因此它不能保证在线路中传输的数据不会丢失、破坏、重复或颠倒次序。这由网络模型中的高层,即传输层或应用层负责。网际层中还有一些其他的协议:网际信报控制协议(ICMP),网际组管理协议(IGMP)以及地址解析协议(ARP)等。
传输层(Transport Layer)负责提供应用程序之间的通讯。这种通讯可以是基于连接的,也可以是非基于连接的。这两种连接类型的主要差别在于是否跟踪数据以及是否确保数据 发送到目标等。传输控制协议(Transmission Control Protocol, TCP)是基于连接的协议,能提供可靠的数据传输;而用户数据报协议(User Datagram Protocol, UDP)是非基于连接的协议,不能确保数据的正确传输。
Internet协议组的应用层(Application Layer)作为应用程序和网络组件之间的接口而存在,其中存在大量的协议,包括简单网络管理协议(Simple Network Management Protocol,SNMP)、文件传输协议(File Transfer Protocol,FTP)、简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)等。
图10.2 TCP数据包的传输

图 14-2 给出了 TCP 数据包的在网际协议组中的传输情况。TCP 利用 IP 数据包传输它自己的数据包,这时,IP 数据包中的数据是 TCP 数据包本身。UDP 也利用 IP 数据包进行数据的传输,在这种情况下,接收方的 IP 层必须能够知道接收到的 IP 数据包要发送给传输层中的哪个协议。为此,每个 IP 数据包头中包含一个字节,专门用作协议标识符。接收方的 IP 层利用这一标识符决定将数据包发送给传输层的哪一个协议处理。和上面的情况类似,同一台主机上利用同一协议进行通讯的应用程序可能有许多,因此,也需要一 种机制来标识应由哪一个应用程序处理同一种数据包。为此,应用程序利用 TCP/IP 协议进行通讯时,不仅要指定目标 IP 地址,还要指定应用程序的“端口”地址。端口可唯一标识应用程序,标准的网络应用程序使用标准的端口地址,例如 Web 服务器的标准端口为 80。在网络接口中,IP 地址和端口地址合称为“套接字”。
IP 协议层可利用许多不同的物理介质传输 IP 数据包,图 14-2 中,IP 数据包进一步包装在以太网数据帧中传输。除以太网外,IP 数据包还可以在令牌环网等其他物理介质上传输。以太网数据帧头中包含了数据帧的目标以太网地址,以太网地址实际就是以太网卡的硬件地址或物理地址,一般由 6 位整数组成,如 00-A0-0C-13-CC-78。以太网卡的物理地址是唯一的,一些特殊的物理地址保留用于广播等目的。因为以太网数据帧和 IP 数据包一样,可以传输不同的协议数据,因此,数据帧中也包含一个标识协议的整数。
为了通过多种连接协议(例如通过以太网)来传输IP报文,IP层必须找出目标IP主机的以太网地址。这是因为IP地址只是一个寻址的概念,以太网设备自己 有自己的物理地址。IP地址可以由网络管理员根据需要分配和再分配,而网络硬件则只响应具有它自己物理地址的以太网帧,或者特殊的多点广播地址(所有的机 器都必须接收)。在以太网中,数据的传输是通过物理地址或硬件地址实现的,而 IP 地址实际只是一种概念性的逻辑地址,因此,在类似以太网这样的网络中,必须采用地址解析协议(ARP)将 IP 地址翻译为实际的硬件地址。ARP 负责为 IP 所请求的任意一个本地 IP 地址找出其本地物理地址。为了得到一个IP地址所关联的硬件地址,主机会发送一个ARP请求包,其中包含它希望转换的IP地址,该包被作为一个多点广播包 发送,网络上所有的点都可以收到它。具有这个IP地址的目标主机用一个ARP回应来应答,这中间包括了它的物理硬件地址。ARP 在内存的高速缓冲区中维护最近所映射的物理地址,以备后用。如果目标 IP 地址是本地地址,ARP 可发送一个本地广播请求获取目标 IP 主机的物理地址,并将物理地址返回给 IP。如果 IP 发现目标 IP 地址处于远程子网中,则数据包必须发送到路由器,这时,ARP 可替 IP 找到路由器的物理地址。APR不仅仅限制在以太网设备,它也可以解析其它物理介质的IP地址,例如FDDI。不能进行ARP的设备会有标记,这样 Linux就不需要试图对它们进行ARP。
还有一个相反的功能,反向ARP,或RARP,用来把物理地址转换到IP地址。
IP协议是网络层协议,它被其它协议使用,来传送它们的数据。传输控制协议(TCP)是一个可靠的端到端的协议,它使用IP协议传送和接收它的报文。象 IP报文有自己的头一样,TCP报文也有自己的头。TCP是一个面向连接的协议,两个网络应用程序通过一个虚拟的连接连在一起,甚至它们中间可能会有许多 子网、网关和路由器。TCP在两个应用程序之间可靠地传送和接收数据,并且保证不会有数据的丢失和重复。当TCP使用IP传送它的报文时,在IP报文中包 含的数据就是TCP报文本身。每一个通讯主机上的IP层都负责传送和接收IP报文。用户数据报协议(UDP)也使用IP层传送它的报文,但是不象TCP, UDP不是一个可靠的协议,它只提供数据报服务。其它协议也可以使用IP,这意味着当接收到IP报文时,接收的IP层必须知道要把这个IP报文中包含的数 据交给哪一个上层协议。为此,每一个IP报文的头中都有一个字节,包含一个协议标识符。当TCP请求IP层传输一个IP报文的时候,IP报文的头就说明它 包含一个TCP报文。IP层的接收者使用这个协议标识符来决定把接收到的数据向上传递给哪一个协议,在这种情况下是TCP层。当应用程序通过TCP/IP 通讯时,它们不但必须要指定目的地的IP地址,还要指定目的地应用程序的端口(port)地址。一个端口地址唯一地标识一个应用程序,标准的网络应用程序 使用标准的端口地址:例如web服务器使用端口80。这些已经注册的端口地址可以在文件/etc/services中查到。
协议分层并没有停留在TCP、UDP和IP上。IP协议本身使用许多不同的物理介质,将IP报文传输到其它的IP主机。这些介质自己也可能增加它们自己的 协议头。这样的例子有以太网层、PPP和SLIP。一个以太网允许许多主机同时连接在一根物理电缆上。每一个传送的以太帧都可以被所有连接的主机看到,所 以每一个以太网设备都有一个独一无二的地址。传送到指定地址的每一个以太网帧都会被那个地址的主机接收,而连接到这个网络的其它主机都会忽略掉该太网帧。 当每一个以太网设备制造的时候,这个独一无二的地址就内建在设备里边,通常保存在以太网卡的SROM中。以太地址由6个字节长,例如,可能的地址是08- 00-2b-00-49-4A。一些以太网地址被保留用于多点广播,用这种目标地址发送的以太网帧会被网络上所有的主机接收。因为以太网帧中可能运载许多 不同的协议(作为数据),和IP报文一样,它们的头中也都包含一个协议标识符。这样以太网层可以正确地接收IP报文并把数据传输到IP层。

10.2 Linux TCP/IP网络分层


图10.3 Linux的网络分层

如图10.3所示,象网络协议本身一样, Linux对于internet 协议地址族的实现就象一系列连接的软件层。
? BSD socket由只和它相关的通用的socket管理软件来支持。
? 支持BSD socket的是INET socket层,它管理以IP为基础的协议(TCP和UDP)的通讯端点。UDP是一个无连接的协议,而TCP是一个可靠的端到端的协议。
? 当传送UDP报文的时候,Linux不知道也不关心它们是否安全到达了目的地。相反地,TCP对其报文进行了编号,TCP连接的每一端都要确保传送的数据被正确地接收到。
? IP层包括了网际协议(Internet Protocol)的代码实现。这种代码在传送的数据前增加IP头,而且知道如何把进来的IP报文转送到TCP或者UDP层。
? 在IP层之下,支持Linux联网的是网络设备,例如PPP和以太网。网络设备并非总是物理设备,其中一些(比如loopback设备)就是纯粹的软件设 备。不象标准的用mknod命令创建的Linux设备,网络设备只有在底层的软件找到并且初始化它们之后才出现。只有当你把适当的以太网设备驱动程序建到 内核中以后,才能看到设备文件/dev/eth0。
? ARP协议位于IP层和支持ARP的协议之间。
上面的层次模型可以再进一步抽象为三个层次,即:套接字层、网络协议层和网络设备层。在具体实现中,每个层次被抽象为一个对象,它们是:
? 套接字(socket)。一个套接字就是网络中的一个连接,它向用户提供了基于文件I/O(read、write等)的网络数据传输。Socket通过网 络协议实现自身,它与网络协议密切相关,体现了网络和文件系统、进程管理之间的关系,它是网络传输的入口。
? 网络协议(protocol)。网络协议是一种网络语言,它规定了通信双方交换信息的一种规范,是网络传输的基础。
? 设备接口(device and interface)。网络设备接口控制着网络数据由软件到硬件和由硬件到软件的过程,体现了网络和设备的关系,是网络传输的桥梁。
? 套接字缓冲区(network buffer)。套接字缓冲区是一块保存网络数据的内存区域,是网络各层之间交换数据的地方。要在网络上发送的数据保存在套接字缓冲区中,协议的各层对其 处理并增加自己的包装;从网络上接收的数据也保存在套接字缓冲区中,协议的各层对其处理并去掉自己的包装。套接字缓冲区与网络的各层都有密切关系,是网络 传输的灵魂。

10.3 BSD socket 接口
套接字(Socket)既可看成是支持多种网络操作形式的接口,也可看成是一种进程间通讯接口。在一条通讯连接中,每个参与通讯的进程有一个套接字 (Socket)描述。一个socket描述了通讯连接的一端,两个通讯进程每一个都会有一个socket,描述它们之间通讯连接中自己端的情况。可以将 套接字(Socket)看成是某种特殊类型的管道,但和管道不同的是,套接字并不限制其中可以包含的数据数量。Linux 支持多种套接字种类,不同的套接字种类称为“地址族” (address families)或协议族(Protocol families)或域(domain),这是因为每种套接字种类拥有自己的通讯寻址方法。Linux 所支持的套接字地址族见下表10-1。
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
#define AF_AX25 3 /* Amateur Radio AX.25 */
#define AF_IPX 4 /* Novell IPX */
#define AF_APPLETALK 5 /* AppleTalk DDP */
#define AF_NETROM 6 /* Amateur Radio NET/ROM */
#define AF_BRIDGE 7 /* Multiprotocol bridge */
#define AF_ATMPVC 8 /* ATM PVCs */
#define AF_X25 9 /* Reserved for X.25 project */
#define AF_INET6 10 /* IP version 6 */
#define AF_ROSE 11 /* Amateur Radio X.25 PLP */
#define AF_DECnet 12 /* Reserved for DECnet project */
#define AF_NETBEUI 13 /* Reserved for 802.2LLC project */
#define AF_SECURITY 14 /* Security callback pseudo AF */
#define pseudo_AF_KEY 15 /* PF_KEY key management API */
#define AF_NETLINK 16
#define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */
#define AF_PACKET 17 /* Packet family */
#define AF_ASH 18 /* Ash */
#define AF_ECONET 19 /* Acorn Econet */
#define AF_ATMSVC 20 /* ATM SVCs */
#define AF_SNA 22 /* Linux SNA Project (nutters!) */
#define AF_IRDA 23 /* IRDA sockets */
#define AF_MAX 32 /* For now.. */

和虚拟文件系统类似,Linux将上述套接字地址族抽象为统一的 BSD 套接字接口,应用程序关心的只是 BSD 套接字接口,而 BSD 套接字由各地址族专有的软件支持(实现)。一般而言,BSD 套接字可支持多种套接字类型,不同的套接字类型提供的服务不同,Linux 所支持的 BSD 套接字类型见表 10-2,表 10-1 中的套接字地址族并不一定全部支持这些套接字类型。但同一协议族可以提供多种服务类型,如TCP/IP协议族提供虚电路和数据报两种服务。
Linux BSD socket支持以下socket类型:
#define SOCK_STREAM 1 /* stream (connection) socket */
#define SOCK_DGRAM 2 /* datagram (conn.less) socket */
#define SOCK_RAW 3 /* raw socket */
#define SOCK_RDM 4 /* reliably-delivered message */
#define SOCK_SEQPACKET 5 /* sequential packet socket */
#define SOCK_PACKET 10 /* linux specific way of */
/* getting packets at the dev */
/* level. For writing rarp and */
/* other similar things on the */
/* user level. */
其中:
Stream
这种socket提供了可靠的、双向连续的数据流,保证传输过程中数据不会丢失、损坏或重复。Stream socket由INET address family中的TCP协议支持。
Datagram
这种socket也提供了双向的数据传输,但是和stream socket不同,它不保证消息会到达。甚至当消息确实到达了,也不保证它们会按顺序到达或没有重复或没有损坏。这种类型的socket由Internet address family中的UDP协议支持。
RAW
这允许进程直接(所以叫“raw”)访问底层的协议。例如,可以打开一个以太网设备的raw socket,观察raw IP数据流。
Reliable Delivered Messages (RDM)
可靠的消息递交,它很象数据报但是可以保证数据到达。
Sequenced Packets
定序分组socket,象stream socket但是数据报文大小是固定的。
Packet
这不是标准的BSD socket类型,它是Linux特定的扩展,允许进程直接在设备层访问报文。
使用socket通讯的进程采用客户服务器模型。服务器提供服务,而客户使用这种服务。Web 服务器是这样的一个例子,web服务器提供web page, web 客户(或浏览器)读取这些页。使用socket的服务器,首先创建一个socket,然后为它bind一个名字。这个名字的格式和socket的 address family有关,对INET地址族,名字是服务器的本地地址(IP地址+端口号)。Socket的名字或地址用sockaddr数据结构指定。
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
一个INET socket会绑定一个IP端口地址。注册的端口编号可以在/etc/services中看到:例如,web服务器的端口号是80。在socket上绑定 一个地址后,服务器就listen进来的对于绑定地址的连接请求。请求的发起者(客户),创建一个socket,并在上面执行一个连接请求,在请求中指定 服务器的目标地址。对于一个INET socket,服务器的地址是它的IP地址和它的端口号。这些进来的请求必须通过大量的协议层,找到它的路径,然后在服务器的监听端口等待。一旦服务器接 收到了进来的请求,它可以接受(accept)或者拒绝(reject)它。如果要接受进来的请求,服务器必须创建一个新的socket来接受它。因为, 一个已经用于监听进来的连接请求的socket,不能再用于支持一个连接。连接建立之后,两端都可以自由地发送和接收数据。最后,当一个连接不再需要的时 候,它可以被关闭。双方都必须小心,以保证正确地处理正在传送的数据报文。
在一个BSD socket上,操作的确切意义依赖于它底层的地址族。建立一个TCP/IP连接和建立一个业余无线电X.25连接有很大的不同。象虚拟文件系统一样, Linux 用BSD socket层作为应用程序所关心的一个抽象的BSD socket接口,该层由独立的与地址族相关的软件(如INET Socket 层)提供支持。当内核初始化的时候,建立在内核的地址族就向BSD socket层登记自己。稍后,当应用程序创建和使用BSD socket的时候,在BSD socket和它的支撑地址族之间就会建立一个联系。这种联系是通过交叉的数据结构和地址族支持例程表实现的。例如,当应用程序创建一个新的socket 的时候,BSD socket层就使用地址族相关的socket创建例程。
当配置内核的时候,它所支持的地址族和协议都建立到了protocols向量表中。该向量表的每一项都由地址族的名称(例如“INET”)和它的初始化例程的地址组成。
struct net_proto {
const char *name; /* Protocol name */
void (*init_func)(struct net_proto *); /* Bootstrap */
};
struct net_proto protocols[] = {
#ifdef CONFIG_PACKET
{ "PACKET", packet_proto_init },
#endif

#ifdef CONFIG_UNIX
{ "UNIX", unix_proto_init }, /* Unix domain socket family */
#endif

#ifdef NEED_802
{ "802.2", p8022_proto_init }, /* 802.2 demultiplexor */
{ "SNAP", snap_proto_init }, /* SNAP demultiplexor */
#endif

#ifdef NEED_LLC
{ "802.2LLC", llc_init }, /* 802.2 LLC */
#endif

#ifdef CONFIG_INET
{ "INET", inet_proto_init }, /* TCP/IP */
#ifdef CONFIG_IPV6
{ "INET6", inet6_proto_init}, /* IPv6 */
#endif
#endif
…………
}
当系统启动的时候,socket接口初始化,每一个协议的初始化代码都要被调用。对于socket地址族,这会导致一系列协议操作的登记。
Linux支持的所有的协议都记录在数组net_families[]中。该数组的定义如下:
#define NPROTO 32 /* should be enough for now.. */
struct net_proto_family *net_families[NPROTO];

这是一个指针数组,每个指针都指向一个net_proto_family数据结构。数据结构net_proto_family的定义为:
struct net_proto_family
{
int family;
int (*create)(struct socket *sock, int protocol);
/* These are counters for the number of different methods of each
we support */
short authentication;
short encryption;
short encrypt_net;
};
这里主要有两项内容:
family是协议地址族编码,定义在表1.1中。该编码是协议在数组net_families[]中的下标。
Create是协议创建函数,在创建相应协议的Socket时调用。函数create根据协议类型type,为所建立的Socket指定一个与之对应的操作集struct proto_ops。该操作集定义了协议对基本的Socket操作的实现。定义如下:
此后,对Socket的所有操作都会转化为和地址族相关的特殊操作,并经过这些操作完成Socket操作。

10.4 TCP/IP协议初始化
在执行系统初始化的sock_init()时,要调用定义在数组protocols[]中的每个协议的初始化函数,实现该协议的初始化。协议初始化函数完成的工作大致相同,下面以TCP/IP协议为例,介绍该初始化过程。
TCP/IP协议的初始化函数是inet_proto_init,它定义在net/ipv4/af_inet.c中。
该函数的定义为:
void inet_proto_init(struct net_proto *pro)
其中pro是一个net_proto数据结构,即protocols[]数组中的当前元素。该数据结构定义了两个元素:协议名和协议初始化函数。
函数inet_proto_init所做的工作如下:

一、 调用函数sock_register(&inet_family_ops)向BSD Socket注册。
函数sock_register十分简单,它把自己的协议族注册到数组net_families[]中。
int sock_register(struct net_proto_family *ops)
{
if (ops->family >= NPROTO) {
printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n",
ops->family, NPROTO);
return -ENOBUFS;
}
net_families[ops->family]=ops;
return 0;
}
这里ops是具体协议的协议族。INET协议的协议族定义为:
struct net_proto_family inet_family_ops = {
PF_INET,
inet_create
};
其中:PF_INET是INET协议族的编号,其值为2。
inet_create是INET协议Socket的创建函数。
net_proto_family结构的其余三个短整数(short)此处未定义。
函数调用sock_register(&inet_family_ops)的意思是给数组net_families[2]赋值inet_family_ops。
每个协议的初始化函数都要调用函数sock_register,将描述自己协议族的数据结构net_proto_family放入数组net_families[]的相应位置。

二、 处理inet协议
一个inet协议由一个inet_protocol数据结构描述。该结构的定义如下:
struct inet_protocol
{
int (*handler)(struct sk_buff *skb, unsigned short len);
void (*err_handler)(struct sk_buff *skb, unsigned char *dp, int len);
struct inet_protocol *next;
unsigned char protocol;
unsigned char copy:1; /* 同协议编号的协议是否已经存在 */
void *data;
const char *name;
};
初始化开始时,描述INET的几个协议的inet_protocol结构连成了一个链表,其表头指针为inet_protocol_base。









为了以后对协议查找的方便,将INET所支持协议的inet_protocol数据结构放到一个Hash表中。该Hash表为:
#define MAX_INET_PROTOS 32
struct inet_protocol *inet_protos[MAX_INET_PROTOS] =
{
NULL
};
将链表inet_protocol_base中的各元素按其协议编号(protocol)插入到Hash表中。
下面是已定义的各INET协议的编号:
/* Standard well-defined IP protocols. */
enum {
IPPROTO_IP = 0, /* Dummy protocol for TCP */
IPPROTO_ICMP = 1, /* Internet Control Message Protocol */
IPPROTO_IGMP = 2, /* Internet Group Management Protocol */
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */
IPPROTO_TCP = 6, /* Transmission Control Protocol */
IPPROTO_EGP = 8, /* Exterior Gateway Protocol */
IPPROTO_PUP = 12, /* PUP protocol */
IPPROTO_UDP = 17, /* User Datagram Protocol */
IPPROTO_IDP = 22, /* XNS IDP protocol */
IPPROTO_RSVP = 46, /* RSVP protocol */
IPPROTO_GRE = 47, /* Cisco GRE tunnels (rfc 1701,1702) */

IPPROTO_IPV6 = 41, /* IPv6-in-IPv4 tunnelling */
IPPROTO_PIM = 103, /* Protocol Independent Multicast */
IPPROTO_RAW = 255, /* Raw IP packets */
IPPROTO_MAX
};

三、 ARP协议初始化:arp_init()
在linux/net/ipv4/arp.c。
1、当Linux网络层初始化的时候,每一个协议都登记自己:在ptype_all链表或者ptype_base hash table中增加一个packet_type的数据结构。当网络设备接收到一个数据包时,底层软件分析该包,确定其类型,而后查找ptype_all链表 和ptype_base hash table,在其中寻找与接收包协议匹配的packet_type数据结构,并据此确定处理该包的协议和相应的处理例程。
packet_type的定义如下:
struct packet_type
{
unsigned short type; /* This is really htons(ether_type). */
struct device *dev; /* NULL is wildcarded here */
int (*func) (struct sk_buff *, struct device *,
struct packet_type *);
void *data; /* Private to the packet type */
struct packet_type *next;
};
这个packet_type数据结构包括协议类型(定义在include/linux/if_ether.h中)、一个指向网络驱动设备的指针、一个指向 协议的数据接收处理例程的指针和一个指向这个列表或者hash table中下一个packet_type数据类型的指针。
ptype_all是一个packet_type结构的链表,用于探听(snoop)从任意网络设备上接收到的所有的数据报文(其type为ETH_P_ALL,即处理所有类型的包),通常不使用。
ptype_base是一个hash table ,其定义如下:
struct packet_type *ptype_base[16];
ptype_base Hash table使用协议标识符(type域)作hash值,用于确定进来的网络报文应该由哪一种协议接收。网络的bottom half把进来的sk_buff中的协议类型和上述任一表中的一个或多个packet_type条目进行匹配。协议可能会匹配一个或多个条目,例如当探听 (snoop)所有的网络通信的时候,这时,这个sk_buff会被克隆。最后,这个sk_buff被传递到匹配的协议的处理例程。
当然,ARP协议要注册其协议类型和协议处理例程。其packet_type数据结构是:
static struct packet_type arp_packet_type =
{
__constant_htons(ETH_P_ARP),
NULL, /* All devices */
arp_rcv,
NULL,
NULL
};
从中可见,ARP协议的包处理例程是arp_rcv。

2、注册ARP协议的/PROC文件系统操作集。
static struct proc_dir_entry proc_net_arp = {
PROC_NET_ARP, 3, "arp",
S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_net_inode_operations,
arp_get_info
};
3、创建一个neigh_sysctl_table数据结构
neigh_sysctl_register(NULL, &arp_tbl.parms, NET_IPV4, NET_IPV4_NEIGH, "ipv4");

四、 IP协议初始化:ip_init()
在linux/net/ipv4/ip_output.c。
1、与ARP协议相似,IP协议也要注册其接收的包的类型和处理函数。其注册的packet_type数据结构定义为:
static struct packet_type ip_packet_type =
{
__constant_htons(ETH_P_IP),
NULL, /* All devices */
ip_rcv,
NULL,
NULL,
};
可见,IP包的处理函数是ip_rcv。
2、IP路由初始化:ip_rt_init()
在linux/net/ipv4/router.c
3、注册IP协议的/PROC文件系统操作集。

五、 TCP V4初始化:tcp_v4_init(&inet_family_ops)
在linux/net/ipv4/tcp_ipv4.c
1、 初始化一个inode:tcp_inode;
2、 初始化inode tcp_inode中的socket部分。

六、 TCP初始化:tcp_init()
在linux/net/ipv4/tcp.c
1、为数据结构open_request建立一个slab cache。
struct open_request {
struct open_request *dl_next; /* Must be first member! */
__u32 rcv_isn;
__u32 snt_isn;
__u16 rmt_port;
__u16 mss;
__u8 retrans;
__u8 __pad;
unsigned snd_wscale : 4,
rcv_wscale : 4,
tstamp_ok : 1,
sack_ok : 1,
wscale_ok : 1;
/* The following two fields can be easily recomputed I think -AK */
__u32 window_clamp;/* window clamp at creation time */
__u32 rcv_wnd; /* rcv_wnd offered first time */
__u32 ts_recent;
unsigned long expires;
struct or_calltable *class;
struct sock *sk;
union {
struct tcp_v4_open_req v4_req;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct tcp_v6_open_req v6_req;
#endif
} af;
#ifdef CONFIG_IP_TRANSPARENT_PROXY
__u16 lcl_port; /* LVE */
#endif
};

2、为数据结构tcp_bind_bucket建立一个slab cache。
struct tcp_bind_bucket {
unsigned short port;
unsigned short flags;
#define TCPB_FLAG_LOCKED 0x0001
#define TCPB_FLAG_FASTREUSE 0x0002
#define TCPB_FLAG_GOODSOCKNUM 0x0004

struct tcp_bind_bucket *next;
struct sock *owners;
struct tcp_bind_bucket **pprev;
};

3、 为数据结构tcp_tw_bucket建立一个slab cache。
struct tcp_tw_bucket {
/* These _must_ match the beginning of struct sock precisely.
* XXX Yes I know this is gross, but I'd have to edit every single
* XXX networking file if I created a "struct sock_header". -DaveM
*/
struct sock *sklist_next;
struct sock *sklist_prev;
struct sock *bind_next;
struct sock **bind_pprev;
__u32 daddr;
__u32 rcv_saddr;
__u16 dport;
unsigned short num;
int bound_dev_if;
struct sock *next;
struct sock **pprev;
unsigned char state,
zapped;
__u16 sport;
unsigned short family;
unsigned char reuse,
nonagle;

/* And these are ours. */
__u32 rcv_nxt;
struct tcp_func *af_specific;
struct tcp_bind_bucket *tb;
struct tcp_tw_bucket *next_death;
struct tcp_tw_bucket **pprev_death;
int death_slot;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct in6_addr v6_daddr;
struct in6_addr v6_rcv_saddr;
#endif
};

七、 ICMP初始化:icmp_init(&inet_family_ops)
在linux/net/ipv4/icmp.c
1、 初始化一个inode:icmp_inode;
2、 初始化inode icmp_inode中的socket部分。

八、 如果定义了IP隧道,则初始化IP隧道:ipip_init()
在linux/net/ipv4/ipip.c
IP隧道是一个设备,需要建立device数据结构并注册。这部分可以作为模块。
1、 注册设备。
2、 在inet_protos[] hash 表中增加表示IPIP协议的inet_protocol数据结构。

九、 如果定义了GRE over IPv4 tunneling driver,则初始化它:ipgre_init()
在linux/net/ipv4/ip_gre.c
GRE是一个设备,需要建立device数据结构并注册。这部分可以作为模块。
1、 注册设备。
2、 在inet_protos[] hash 表中增加表示GRE协议的inet_protocol数据结构。

十、 如果定义了IP防火墙,则初始化防火墙:ip_fw_init()
在linux/net/ipv4/ip_fw.c
1、 初始化几个chain :ip_init_chain()。
2、 注册该防火墙操作。
struct firewall_ops
{
struct firewall_ops *next;
int (*fw_forward)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_input)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_output)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
/* Data falling in the second 486 cache line isn't used directly
during a firewall call and scan, only by insert/delete and other
unusual cases
*/
int fw_pf; /* Protocol family */
int fw_priority; /* Priority of chosen firewalls */
};
注册的防火墙操作为:
struct firewall_ops ipfw_ops=
{
NULL,
ipfw_forward_check,
ipfw_input_check,
ipfw_output_check,
PF_INET,
0 /* We don't even allow a fall through so we are last */
};
3、 注册防火墙对应的/PROC文件系统操作。

十一、 如果定义了IP伪装(ip masquerading),则初始化它:ip_masq_init()
定义在linux/net/ipv4/ip_masq.c

十二、 如果定义了多点传送路由,则初始化它:ip_mr_init()
在linux/net/ipv4/ipmr.c

十三、 注册有关的/PROC操作

10.5 The INET Socket Layer
整个Socket系统只提供了一个系统调用接口:sys_socketcall,在sys_call_table中只占一项。该函数的定义如下:
asmlinkage int sys_socketcall(int call, unsigned long *args)
这里:call是Socket系统调用号,args是给该调用的参数。
Linux向用户提供的系统调用包括:
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
所以,Socket接口大致包含17个操作。对这些操作的调用有一个大致的顺序,如下图所示。
























函数sys_socketcall的定义非常简单,它根据参数call的值,分别调用相应的处理函数。具体的工作在处理函数中实现。
参见include/linux/net.h,linux/net/socket.c
描述BSD Socket接口的是一个数据结构,其定义如下:
struct socket
{
socket_state state;

unsigned long flags;
struct proto_ops *ops;
struct inode *inode;
struct fasync_struct *fasync_list; /* Asynchronous wake up list */
struct file *file; /* File back pointer for gc */
struct sock *sk;
struct wait_queue *wait;

short type;
unsigned char passcred;
unsigned char tli;
};
其中:
struct proto_ops {
int family;

int (*dup)(struct socket *newsock, struct socket *oldsock);
int (*release)(struct socket *sock, struct socket *peer);
int (*bind)(struct socket *sock, struct sockaddr *umyaddr,
int sockaddr_len);
int (*connect)(struct socket *sock, struct sockaddr *uservaddr,
int sockaddr_len, int flags);
int (*socketpair)(struct socket *sock1, struct socket *sock2);
int (*accept)(struct socket *sock, struct socket *newsock,
int flags);
int (*getname)(struct socket *sock, struct sockaddr *uaddr,
int *usockaddr_len, int peer);
unsigned int (*poll)(struct file *file, struct socket *sock,
struct poll_table_struct *wait);
int (*ioctl) (struct socket *sock, unsigned int cmd,
unsigned long arg);
int (*listen)(struct socket *sock, int len);
int (*shutdown)(struct socket *sock, int flags);
int (*setsockopt)(struct socket *sock, int level, int optname,
char *optval, int optlen);
int (*getsockopt)(struct socket *sock, int level, int optname,
char *optval, int *optlen);
int (*fcntl) (struct socket *sock, unsigned int cmd, unsigned long arg);
int (*sendmsg)(struct socket *sock, struct msghdr *m, int total_len,
struct scm_cookie *scm);
int (*recvmsg)(struct socket *sock, struct msghdr *m, int total_len,
int flags, struct scm_cookie *scm);
};

但该结构并不独立存在,实际上,它是inode结构的一部分(inode结构中描述具体文件系统信息的联合中的一个成员)。因此分配socket结构实际 是分配一个inode。也就是说,BSD socket实际是VFS的一个具体实现,虽然BSD socket本身也是虚拟的,也需要具体的协议实现(如INET协议)对它的支持。所以对socket的操作与文件系统操作是一致的。
INET socket层支持包含TCP/IP协议的internet address family。
在socket初始化时,BSD socket的各个支持协议都向BSD socket层登记其address family,即在数组net_families[]登录其net_proto_family数据结构。该数据结构中包含有协议族编号和一个socket 创建函数。当要创建一个具体的socket时,系统创建一个socket数据结构(实际是inode数据结构),但该结构中不包含具体协议的细节。一个具 体协议的细节信息保存在协议专门的数据结构中,如INET socket层使用它自己的数据结构sock。为了访问的方便,两个数据结构中都有指针互相指向对方。
Sock数据结构是一个很大的结构,其定义如下:
struct sock {
/* This must be first. */
struct sock *sklist_next;
struct sock *sklist_prev;

/* Local port binding hash linkage. */
struct sock *bind_next;
struct sock **bind_pprev;

/* Socket demultiplex comparisons on incoming packets. */
__u32 daddr; /* Foreign IPv4 addr */
__u32 rcv_saddr; /* Bound local IPv4 addr */
__u16 dport; /* Destination port */
unsigned short num; /* Local port */
int bound_dev_if; /* Bound device index if != 0 */

/* Main hash linkage for various protocol lookup tables. */
struct sock *next;
struct sock **pprev;

volatile unsigned char state, /* Connection state */
zapped; /* In ax25 & ipx means not linked */
__u16 sport; /* Source port */

unsigned short family; /* Address family */
unsigned char reuse, /* SO_REUSEADDR setting */
nonagle; /* Disable Nagle algorithm? */

atomic_t sock_readers; /* User count */
int rcvbuf; /* Size of receive buffer in bytes*/

struct wait_queue **sleep; /* Sock wait queue */
struct dst_entry *dst_cache; /* Destination cache */
atomic_t rmem_alloc; /* Receive queue bytes committed */
struct sk_buff_head receive_queue; /* Incoming packets */
atomic_t wmem_alloc; /* Transmit queue bytes committed*/
struct sk_buff_head write_queue; /* Packet sending queue */
atomic_t omem_alloc; /* "o" is "option" or "other" */
__u32 saddr; /* Sending source */
unsigned int allocation; /* Allocation mode */
int sndbuf; /* Size of send buffer in bytes*/
struct sock *prev;

/* Not all are volatile, but some are, so we might as well say they
* all are. XXX Make this a flag word -DaveM
*/
volatile char dead,
done,
urginline,
keepopen,
linger,
destroy,
no_check,
broadcast,
bsdism;
unsigned char debug;
int proc;
unsigned long lingertime;

int hashent;
struct sock *pair;

/* Error and backlog packet queues, rarely used. */
struct sk_buff_head back_log, error_queue;

struct proto *prot;

unsigned short shutdown;

#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
union {
struct ipv6_pinfo af_inet6;
} net_pinfo;
#endif

union {
struct tcp_opt af_tcp;
#if defined(CONFIG_INET) || defined (CONFIG_INET_MODULE)
struct raw_opt tp_raw4;
#endif
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct raw6_opt tp_raw;
#endif /* CONFIG_IPV6 */
#if defined(CONFIG_SPX) || defined (CONFIG_SPX_MODULE)
struct spx_opt af_spx;
#endif /* CONFIG_SPX */

} tp_pinfo;

int err, err_soft; /* Soft holds errors that don't
* cause failure but are the cause
* of a persistent failure not just
* 'timed out' */
unsigned short ack_backlog;
unsigned short max_ack_backlog;
__u32 priority;
unsigned short type;
unsigned char localroute; /* Route locally only */
unsigned char protocol;
struct ucred peercred;

#ifdef CONFIG_FILTER
/* Socket Filtering Instructions */
struct sk_filter *filter;
#endif /* CONFIG_FILTER */

/* This is where all the private (optional) areas that don't
* overlap will eventually live.
*/
union {
void *destruct_hook;
struct unix_opt af_unix;
#if defined(CONFIG_ATALK) || defined(CONFIG_ATALK_MODULE)
struct atalk_sock af_at;
#endif
#if defined(CONFIG_IPX) || defined(CONFIG_IPX_MODULE)
struct ipx_opt af_ipx;
#endif
#if defined (CONFIG_DECNET) || defined(CONFIG_DECNET_MODULE)
struct dn_scp dn;
#endif
#if defined (CONFIG_PACKET) || defined(CONFIG_PACKET_MODULE)
struct packet_opt *af_packet;
#endif
#if defined(CONFIG_X25) || defined(CONFIG_X25_MODULE)
x25_cb *x25;
#endif
#if defined(CONFIG_AX25) || defined(CONFIG_AX25_MODULE)
ax25_cb *ax25;
#endif
#if defined(CONFIG_NETROM) || defined(CONFIG_NETROM_MODULE)
nr_cb *nr;
#endif
#if defined(CONFIG_ROSE) || defined(CONFIG_ROSE_MODULE)
rose_cb *rose;
#endif
#ifdef CONFIG_NETLINK
struct netlink_opt af_netlink;
#endif
#if defined(CONFIG_ECONET) || defined(CONFIG_ECONET_MODULE)
struct econet_opt *af_econet;
#endif
#if defined(CONFIG_IRDA) || defined(CONFIG_IRDA_MODULE)
struct irda_sock *irda;
#endif
} protinfo;

/* IP 'private area' or will be eventually. */
int ip_ttl; /* TTL setting */
int ip_tos; /* TOS */
unsigned ip_cmsg_flags;
struct ip_options *opt;
unsigned char ip_hdrincl; /* Include headers ? */
__u8 ip_mc_ttl; /* Multicasting TTL */
__u8 ip_mc_loop; /* Loopback */
__u8 ip_recverr;
__u8 ip_pmtudisc;
int ip_mc_index; /* Multicast device index */
__u32 ip_mc_addr;
struct ip_mc_socklist *ip_mc_list; /* Group array */

/* This part is used for the timeout functions (timer.c). */
int timeout; /* What are we waiting for? */
struct timer_list timer; /* This is the sock cleanup timer. */
struct timeval stamp;

/* Identd */
struct socket *socket;

/* RPC layer private data */
void *user_data;

/* Callbacks */
void (*state_change)(struct sock *sk);
void (*data_ready)(struct sock *sk,int bytes);
void (*write_space)(struct sock *sk);
void (*error_report)(struct sock *sk);

int (*backlog_rcv)(struct sock *sk, struct sk_buff *skb);
void (*destruct)(struct sock *sk);
};
struct proto {
/* These must be first. */
struct sock *sklist_next;
struct sock *sklist_prev;

void (*close)(struct sock *sk, long timeout);
int (*connect)(struct sock *sk,struct sockaddr *uaddr,
int addr_len);

struct sock * (*accept) (struct sock *sk, int flags);
void (*retransmit)(struct sock *sk, int all);
void (*write_wakeup)(struct sock *sk);
void (*read_wakeup)(struct sock *sk);

unsigned int (*poll)(struct file * file, struct socket *sock,
struct poll_table_struct *wait);

int (*ioctl)(struct sock *sk, int cmd, unsigned long arg);
int (*init)(struct sock *sk);
int (*destroy)(struct sock *sk);
void (*shutdown)(struct sock *sk, int how);
int (*setsockopt)(struct sock *sk, int level,
int optname, char *optval, int optlen);
int (*getsockopt)(struct sock *sk, int level,
int optname, char *optval, int *option);
int (*sendmsg)(struct sock *sk, struct msghdr *msg,int len);
int (*recvmsg)(struct sock *sk, struct msghdr *msg,
int len, int noblock, int flags, int *addr_len);
int (*bind)(struct sock *sk, struct sockaddr *uaddr,
int addr_len);

int (*backlog_rcv) (struct sock *sk, struct sk_buff *skb);

/* Keeping track of sk's, looking them up, and port selection methods.
*/
void (*hash)(struct sock *sk);
void (*unhash)(struct sock *sk);
void (*rehash)(struct sock *sk);
unsigned short (*good_socknum)(void);
int (*verify_bind)(struct sock *sk, unsigned short snum);

unsigned short max_header;
unsigned long retransmits;
char name[32];
int inuse, highestinuse;
};

数据结构的关系如下图所示:


在上述结构中有三个主要的操作集:
1、 在file结构中的文件操作集,这是一个file_operations数据结构。BSD Socket对该操作集的实现是socket_file_ops。这组文件操作集由定义在socket上的proto_ops操作集实现。
2、 在socket结构中的协议操作集,这是一个proto_ops数据结构。INET协议对它有两种实现:inet_stream_ops和inet_dgram_ops。这两个操作集中的函数又由sock结构上的操作集proto实现。
3、 在sock结构中的协议操作集,这是一个proto数据结构。传输层对它的实现有tcp_prot、udp_prot、raw_prot。
4、 传输层和网络层(即TCP/IP)与网络设备驱动程序之间的接口是device数据结构。其中定义了网络设备驱动程序需要实现的函数集。各种网络设备对device中的函数集有不同的实现。
5、 而每个底层协议都定义一个struct inet_protocol数据结构,用于接收并处理来自底层的该协议的数据包。
在建立一个socket时,至少要确定上述三个操作集。下面分别讨论socket的系统调用。

10.5.1 创建一个BSD Socket(Creating a BSD Socket)
创建新socket的系统调用是sys_socket,其定义如下:
int sys_socket(int family, int type, int protocol)
该函数需要三个参数:地址族标识符、socket类型和协议。
该函数所做工作如下:
1、 检查协议族是否在1到NPROTO范围内,不是则报错返回。
2、 如果协议族没有注册(net_families[family]==NULL)并且内核支持模块,则请求模块装入进程装入指定的协议族,并初始化它。
request_module(module_name),其中module_name的格式为net-pf-%d,%d的内容是协议族的标识符。
如果装入进程执行后,协议族仍然没有注册,则出错返回。
3、 检查类型type的值是否正确,即其值是否为Linux支持的BSD Socket类型。这些类型包括:SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW、SOCK_RDM、 SOCK_PACKET、SOCK_WEB。支持SOCK_PACKET的是专门的一个地址族:AF_PACKET(Packet family),该地址族与AF_INET并列。
4、 申请一个socket数据结构并填入相应的值。
由于socket是inode数据结构的一部分,所以申请一个socket实际上就是申请一个空白inode。如果申请inode成功,其socket结构为:
inode->u.socket_i
设置inode的相应域:i_mode域为socket,存取权限为完全许可(0777);i_sock域为1;i_uid和i_gid为当前进程的fsuid和fsgid。
设置socket的相应域:
sock->inode = inode;
init_waitqueue(&sock->wait);
sock->fasync_list = NULL;
sock->state = SS_UNCONNECTED; /* 当前状态为未连接 */
sock->flags = 0;
sock->ops = NULL;
sock->sk = NULL;
sock->file = NULL;
sock->type = type;
5、 调用协议族相关的socket创建函数。
协议族相关的socket创建函数在描述该协议族的net_proto_family数据结构中,而该net_proto_family数据结构已经注册 到了net_families[]数组中。以family为下标,查找数组net_families[]即可找到指定协议族的socket创建函数。执行 该函数完成与具体协议相关的初始化工作。
显然,不同的协议族其socket创建函数是不同的。INET协议族的socket创建函数是inet_create(在linux/net/ipv4/af_inet.c中),该函数的定义如下:
static int inet_create(struct socket *sock, int protocol)
其中:sock是前面已分配的socket数据结构;protocol是协议编号。
该函数所做工作如下:
1) 如果socket的类型是SOCK_PACKET,而且该协议族还没有注册,则申请插入该协议族对应的模块。而后调用协议族SOCK_PACKET的socket创建函数。
2) 将socket的状态改为SS_UNCONNECTED。
3) 申请sock数据结构。
在初始化时,已经为sock数据结构建立的slab cache,从中申请一个sock数据结构。将申请到的sock结构清0。
sk->family = family;
4) 根据socket的类型分别处理:
? SOCK_STREAM:支持该类型的协议必须是TCP。
protocol = IPPROTO_TCP;
prot = &tcp_prot;
sock->ops = &inet_stream_ops;
? SOCK_SEQPACKET:
未实现。
? SOCK_DGRAM:
protocol = IPPROTO_UDP;
prot=&udp_prot;
sock->ops = &inet_dgram_ops;
? SOCK_RAW:
prot = &raw_prot;
sock->ops = &inet_dgram_ops;
5) 初始化socket和sock数据结构的域。由函数sock_init_data(在net/core/sock.c)完成。包括:
sk->state = TCP_CLOSE;
以及sock数据结构中的几个处理函数,如state_change、data_ready、write_space、error_report、destruct等。
6) 初始化sock的其余各域。如:
sk->family = PF_INET;
sk->protocol = protocol;
sk->prot = prot;
…………………………
6)如果在sk->prot中定义了初始化函数init,则执行该函数。对TCP协议来说,其初始化函数是tcp_prot-> tcp_v4_init_sock。
6、 在当前进程的文件描述符表current->files->fd[]中找一个空闲的文件描述符,作上相应的标记,表示它已被分配。
7、 找一个空闲的file数据结构,初始化其中的各域,将其填入当前进程的文件描述符表中。
file->f_op = &socket_file_ops;
file->f_mode = 3;
file->f_flags = O_RDWR;
file->f_pos = 0;
socket是对VFS的一种实现,因此socket也要实现文件操作集file_operations,这个实现就是socket_file_ops。 对所有的socket,该文件操作集都是一样的。有了这组对文件操作集的实现,就可以将对socket的操作看成普通的文件操作了。
8、 sock->file = fcheck(retval); 该指针指向该打开socket对应的file数据结构。
9、 返回文件描述符,以后即可利用该文件描述符和普通的文件操作来处理socket通信了。

10.5.2 为一个INET BSD socket绑定一个地址(Binding an Address to an INET BSD Socket)
为了监听进来的网际连接请求,每一个服务器都必须创建一个INET BSD socket并把自己的地址绑定到它上面。绑定IP地址的系统调用是sys_bind,定义在(net/socket.c中)。其定义为:
int sys_bind(int fd, struct sockaddr *umyaddr, int addrlen)
这里:fd是打开的socket的文件描述符;umyaddr是一个socket地址,其定义如下:
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
addrlen是该地址的长度。
该函数所做的工作如下:
1、 根据socket的文件描述符fd,找到它对应的file、inode、socket数据结构。
2、 将socket地址从用户空间拷贝到内核。
3、 执行sock->ops->bind (sock, (struct sockaddr *)address, addrlen);
因此,bind的主要工作在INET socket 层完成,并需要底层的TCP和UDP协议层提供一些支持。一个已经绑定了地址的socket不能再用于其它通讯。通常,绑定的地址是分配给支持INET地址族的网络设备的地址,而且接口必须是开启的并能够使用。
INET socket 层完成bind工作的函数是inet_bind(定义在net/ipv4/af_inet.c中),该函数的定义如下:
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
该函数所做工作如下:
1、 将socket地址转化为Internet地址。Uaddr是一个由16个字节组成的socket地址,它不带格式,适用于所有的地址族。在使用时,要根 据地址族将这个地址转化成相应格式的地址,如此处将该地址转化为Internet地址。一个Internet地址是一个数据结构,其定义如下:
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */

/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
2、 如果sock目前的状态不是TCP_CLOSE则错误返回。
3、 如果addr_len过短或该sock已经指定了端口号(num!=0)则错误返回。
4、 调用函数inet_addr_type(定义在net/ipv4/fib_frontend.c)确定地址类型。
? 前8位为0的IP地址是广播地址RTN_BROADCAST;
? 前4位为F的IP地址是广播地址RTN_BROADCAST;
? 前4位为E的IP地址是多目地址RTN_MULTICAST;
? 如果local_table不空,则调用它的tb_lookup函数确定IP地址的类型。
5、 检查地址及其类型的合法性。要么地址为0,要么地址类型为RTN_LOCAL、RTN_MULTICAST、RTN_BROADCAST、RTN_UNICAST,否则错误返回。
6、 将绑定的IP地址保存在sock数据结构中。在sock中保存两个地址:
rcv_saddr用于hash lookups,此处的地址就是要绑定的地址。
saddr用于transmit,此处的地址一般情况下与rcv_saddr相同,但对多目地址和广播地址,saddr为0。
7、 取出绑定地址的端口号。如果该端口号为0,则调用sock操作集中的函数good_socknum找一个空闲的端口号。对TCP协议,该函数是tcp_good_socknum。如果找到的端口号<1024而且用户不是超级用户,则错误返回。
8、 调用sock操作集中的函数verify_bind检查端口号的合法性。对TCP协议,该函数是tcp_v4_verify_bind。
9、 将要绑定的端口号保存在sock数据结构的num(Local port)和sport(Source port)域中。
10、 目标地址和目标端口号设为0。
11、 善后处理,如:
sk->prot->rehash(sk);
add_to_prot_sklist(sk);
dst_release(sk->dst_cache);
sk->dst_cache=NULL;

当底层的网络设备接收到报文时,这些报文必须被转到正确的INET和BSD socket处处理。为此,UDP和TCP都维护一个hash table,用于查找进来的IP信息的地址,从而把它们转到正确的socket/sock对。TCP是一个面向连接的协议,所以正处理的TCP报文中要比 正处理的UDP报文中包含更多的信息。
UDP维护一个已分配的UDP端口的hash table:udp_table。该表用一个基于端口号的hash函数作索引,包含一个指向sock数据结构的指针。因为UDP hash table比允许的端口号要小得多(udp_hash只有128,或UDP_HTABLE_SIZE),所以表中的一些条目指向的是一个sock数据结构 链表,该链表用每一个sock的next 指针连接在一起。
TCP更加复杂,因为它维护了几个hash table 。但是,在绑定操作中,TCP实际上并不把绑定的sock数据结构加到它的hash table中,它只是检查请求的端口当前没有被使用。在listen操作中,sock数据结构才加到TCP的hash table中。

10.5.3 Listening on an INET BSD Socket
一旦一个socket拥有了一个绑定的地址,它就可以监听进来的连接请求。如果进来的连接请求指定了这个绑定地址,该socket就会响应。一个网络应用 程序也可以不绑定地址而直接在一个socket上监听,这种情况下,INET socket层会找到一个未用的端口号(对于这种协议而言),并自动把它绑定到这个socket上。
socket的listen函数把socket变成TCP_LISTEN的状态,并且执行所需的和网络相关的工作,以便允许进来的连接。
对于UDP socket,改变socket的状态已经足够,但是TCP要把socket的sock数据结构加到它的两个hash table中,原因是该socket已经激活。这两个hash表分别是tcp_bound_hash和tcp_listening_hash 表。这两个表都通过一个基于IP端口号的hash函数进行索引。
完成listen功能的是函数sys_listen,其定义如下:
int sys_listen(int fd, int backlog)
其中backlog是请求队列的长度,listen以此参数限制排队请求的个数,Linux允许的最大个数是128(SOMAXCONN)。
该函数所做工作如下:
1、 根据socket对应文件描述符fd,找到该socket对应的file、inode、socket数据结构。
2、 调用socket协议操作集上的listen函数完成真正的listen工作。
3、 返回。
所以真正的listen在协议操作集中完成。INET提供的listen函数是inet_listen。该函数的定义如下:
int inet_listen(struct socket *sock, int backlog)
他所完成的工作如下:
1、 找到与该socket关联的sock数据结构。
2、 如果socket的状态不是SS_UNCONNECTED或socket的类型不是SOCK_STREAM,则出错返回。
3、 如果sock结构上没有绑定端口(sock->num==0),则调用sock操作集中的函数good_socknum为其找一个空闲的端口号。如果还不能成功,则错误返回。
4、 设置sock的max_ack_backlog域的值为backlog(参数)。
5、 如果sock的状态不是TCP_LISTEN,则:
sk->ack_backlog = 0;
sk->state = TCP_LISTEN;
dst_release(xchg(&sk->dst_cache, NULL));
sk->prot->rehash(sk);
add_to_prot_sklist(sk);
6、 socket->flags |= SO_ACCEPTCON;
7、 返回。

10.5.4 Making a Connection to an INET BSD Socket
一旦创建了一个socket,如果它没有被用于监听进来的连接请求,它就可以用于建立向外的连接请求。对于无连接的协议,比如UDP,这个socket操作不需要做许多,但是对于面向连接的协议,如TCP,则需要在两个应用程序之间建立一个虚拟电路。
一个向外的连接只能在一个正确状态的INET BSD socket上进行:就是说,在该socket上还没有建立起连接,而且该socket也没有用于监听进来的连接。这意味着这个BSD socket数据结构必须在SS_UNCONNECTED状态。
完成连接的系统调用是函数sys_connect,其定义为:
int sys_connect(int fd, struct sockaddr *uservaddr, int addrlen)
该函数完成的工作如下:
1、 根据socket的文件描述符fd,找到它对应的file、inode、socket数据结构。
2、 将地址uservaddr由用户空间拷贝到内核空间。
3、 调用socket操作集上的connect函数完成真正的连接工作。
不同的协议族对connect函数的实现是不同的,甚至同一个协议族对它的实现也不尽相同。如INET协议族对它的实现就有两种:inet_stream_connect和inet_dgram_connect。
inet_stream_connect的定义如下:
int inet_stream_connect(struct socket *sock, struct sockaddr * uaddr,
int addr_len, int flags)
它所完成的工作如下:
1、 找出socket对应的sock数据结构。
2、 如果socket的状态不是SS_UNCONNECTED而且不是SS_CONNECTING则错误返回。
3、 如果socket的状态是SS_CONNECTING 。
如果sock的状态是TCP_ESTABLISHED、TCP_CLOSE_WAIT、TCP_FIN_WAIT1、TCP_FIN_WAIT2、TCP_SYN_RECV中之一,则将socket的状态改为SS_CONNECTED,成功返回。
if (sk->zapped || sk->err)
goto sock_error;
if (flags & O_NONBLOCK)
return -EALREADY;
4、 如果socket的状态不是SS_CONNECTING,即如果socket的状态是SS_UNCONNECTED。
如果该socket上还没有绑定端口(num==0),则为其找一个空闲端口。如果还不能成功,则错误返回。
调用sock操作集中的connect函数。如果成功,则将socket的状态改为SS_CONNECTING;否则,错误返回。
5、 如果sock的状态是TCP_TIME_WAIT、TCP_CLOSE、TCP_CLOSE_WAIT、TCP_LAST_ACK、 TCP_LISTEN、TCP_CLOSING且socket的状态是SS_CONNECTING,则错误返回。因为,当socket的状态是 SS_CONNECTING时,sock不应该出现上述状态。
6、 如果sock的状态不是TCP_ESTABLISHED,而且此次连接建立不许阻塞(flags & O_NONBLOCK不等于0),则返回(-EINPROGRESS )。
7、 如果sock的状态是TCP_SYN_SENT或TCP_SYN_RECV,则调用函数inet_wait_for_connect(sk),将当前进程挂起(将当前进程的状态改为TASK_INTERRUPTIBLE,重新调度)。
8、 将socket的状态改为SS_CONNECTED。成功返回。
9、 如果因为各种原因导致此次连接不能成功,则将sock的状态改为TCP_CLOSE,将socket的状态改为SS_UNCONNECTED,并释放必要的资源,而后返回错误代码。
显然,主要的连接工作是在sock操作集的connect函数函数中完成的。TCP完成该任务的函数是tcp_v4_connect,UDP完成该任务的函数是udp_connect。

UDP协议不在两个应用程序之间建立虚拟连接,所有发送的消息都是数据报,发出的消息可能到达也可能没有到达它的目的地。但是,它也支持BSD socket的connect操作。在一个UDP INET BSD socket上的一个连接操作只是简单地建立远程应用程序的地址:它的IP地址和它的IP端口号。另外,它也要建立一个路由表条目的缓存区,这样,在这个 BSD socket上发送的UDP数据报就不需要再检查路由表数据库(除非这个路由变成无效)。这个缓存的路由信息由INET sock数据结构中的ip_route_cache指针指出。如果没有给出地址信息,这个BSD socket发送的消息就自动使用这个缓存的路由和IP地址信息。UDP把sock的状态改变成为TCP_ESTABLISHED。
对于在一个TCP BSD socket上进行的连接操作,TCP必须建立一个包括连接信息的TCP消息,并把它发送到给定的IP目标。这个TCP消息中包含连接所需要的信息:一个 独一无二的起始消息顺序编号、发起主机可以管理的消息的最大尺寸、发送和接收的窗口大小等等。在TCP中,所有的消息都编了号,初始顺序编号用作第一个消 息编号。Linux选择一个合理的随机数以避免恶意的协议攻击。每一个从TCP连接的一端发送、被另一端成功接收的消息都要被确认,告诉对方消息已成功地 到达、而且没有被损坏。没有确认的消息会被重发。发送和接收窗口的大小是在收到确认前允许发送的消息的数目。最大消息尺寸来源于网络设备,该设备是请求的 发起方正在使用的网络设备。如果接收端的网络设备支持的最大消息尺寸比较小,则这个连接会使用两者中间最小的一个。执行向外的TCP连接请求的应用程序现 在必须等待目标应用程序的响应,是接受还是拒绝这个连接请求。对于期望进来消息的TCP sock,它被加到了tcp_listening_hash,以便进来的TCP消息可以定向到这个sock数据结构。TCP也启动计时器,这样以来,如果 目标应用程序不响应请求,向外的连接请求就会超时。

10.5.5 Accepting Connection Requests
UDP不支持连接的概念,接受对INET socket的连接请求只适用于TCP协议。在一个监听的socket上进行接受(accept)操作会从原来的监听的socket克隆出一个新的 socket数据结构。然后,这个accept操作被传递给支撑的协议层,在这种情况下,是INET去接受任何进来的连接请求。如果底层的协议,比如 UDP,不支持连接,INET协议层的accept操作会失败。否则,accept操作会通过真正的协议(在这里是TCP)传递。
accept操作可能是阻塞,也可能是非阻塞的。在非阻塞的情况下,如果没有可接受(accept)的进来的连接,这个accept操作会失败,而新创建 的socket数据结构将会被废弃。在阻塞的情况下,执行accept操作的网络应用程序会被加到等待队列,然后挂起,直到接收到一个TCP的连接请求。
不论何时,当接收到一个进来的、对于激活的、正在监听的socket的TCP连接请求时,TCP都要建立一个新的sock数据结构来表示它。当连接最终被 接受时,该sock数据结构将成为这个TCP连接的buttom half。它也克隆包含连接请求的、进来的sk_buff,并把该sk_buff排在监听的、sock数据结构的receive_queue队列中 (receive_queue是sock数据结构的一个域)。这个克隆的sk_buff包括一个指针,指向这个新创建的sock数据结构。
一旦接收到一个连接请求,包含这个请求的sk_buff会被废弃,这个sock数据结构被返回到INET socket层,在这里它被连接到先前创建的新的socket数据结构。这个新的socket的文件描述符(fd)被返回给网络应用程序,应用程序就可以 用这个文件描述符对这个新创建的INET BSD socket进行socket操作。
实现accept操作的是系统调用sys_accept,其定义如下:
int sys_accept(int fd, struct sockaddr *upeer_sockaddr, int *upeer_addrlen)
它所做的工作如下:
1、 根据socket的文件描述符fd,找到它对应的file、inode、socket数据结构。
2、 申请一个新的socket(inode)。
3、 新socket的类型(type)等于老socket的类型。
4、 调用socket操作集中的dup函数,复制一个socket。INET中完成该操作的函数是sock_no_dup,实际是调用协议集的create函数重新创建一个socket。
5、 调用新socket操作集中的accept操作。
6、 创建新的file数据结构,填写其内容(如文件操作集是socket_file_ops),将其插入到当前进程的files数组中。
7、 如果参数upeer_sockaddr不空,则:
调用新socket操作集中的getname函数,获得本次连接的目的地址和端口号(远程主机的地址和端口号)。
? 如果函数getname执行不成功(连接还没有建立),则关掉新建立的socket,转2,重新开始。
? 如果函数getname执行成功(连接已经建立),则将获得的地址拷贝到用户地址空间,即拷贝到upeer_sockaddr中。返回新socket的文件描述符fd。
显然,accept的主要工作在第5步完成。INET对该函数有两个实现:inet_accept和sock_no_accept,前者用于stream类型,后者用于dgram类型。
inet_accept的定义如下:
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
其中:sock是老socket,newsock是新socket,flags是老socket对应的file数据结构中的f_flags标志。
它完成以下工作:
1、 根据新、老socket数据结构,分别找到它们对应的sock数据结构。新的sock数据结构也是刚刚创建的。
2、 如果老socket的状态不是SS_UNCONNECTED,或老socket结构的flags 标志中没有定义SO_ACCEPTCON,则错误返回。listen操作设置flags的SO_ACCEPTCON标志位,因此,如果一个socket没 有设置此标志,表示它没有执行过listen操作,所以不能在其上accept。
3、 如果老sock的pair域(一个执行sock结构的指针)不空,暂存该域的值到变量sk2中,将老sock的pair域清空。
否则,即如果老sock的pair域为空,则执行老sock的操作集中的accept操作,完成accept动作。TCP中实现的accept函数是 tcp_accept,UDP中没有实现accept函数。accept函数返回一个sock数据结构,将该结构暂存到变量sk2中。
4、 新socket的sock结构就是sk2所指的sock数据结构,在新socket和新sock数据结构之间建立连接。
5、 如果参数flags中指明O_NONBLOCK,则释放原来为新socket创建的sock数据结构。成功返回。
6、 如果新sock的状态是TCP_ESTABLISHED,则释放原来为新socket创建的sock数据结构,将新socket的状态改为SS_CONNECTED。成功返回。
7、 如果其间出现了错误,或sk2所指的sock的状态为TCP_CLOSE,则释放sk2所指的sock数据结构,错误返回。
真正的accept动作在第3步中由sock操作集中的accept函数完成。
TCP中完成accept动作的函数是tcp_accept(定义在net/ipv4/tcp.c中)。该函数的定义为struct sock *tcp_accept(struct sock *sk, int flags),它所完成的工作如下:
1、 根据sock *sk找到它对应的tcp_opt数据结构:sk->tp_pinfo.af_tcp。
2、 如果sock的状态不是TCP_LISTEN,则出错返回。
3、 在tcp_opt的syn_wait_queue队列上排列的是所有到来的TCP请求,查找该队列,看是否有连接请求(sock状态为 TCP_SYN_SENT或TCP_SYN_RECV的请求)。每个请求都用一个数据结构open_request表示,其中有个指针指向sock数据结 构。
4、 如果没有找到请求,而且参数flags表示不能阻塞(O_NONBLOCK),则返回。
5、 如果没有找到请求,而且参数flags表示可以阻塞,则调用函数wait_for_connect,将当前进程挂起,等待连接请求的到来。
6、 接到连接请求,从中取出sock数据结构,将该请求从其队列中摘下,释放它的数据结构open_request。
7、 返回从连接请求中摘下的sock数据结构。

10.5.6 Shutdown a socket
当socket不再使用时,应该将其关掉,完成该项工作的系统调用是sys_shutdown,其定义如下:
int sys_shutdown(int fd, int how)
其中:how是关闭的方式,共有两种:RCV_SHUTDOWN和SEND_SHUTDOWN。
该函数完成如下工作:
1、 根据参数fd,找到要关掉的socket的file、inode、socket数据结构。
2、 调用socket操作集中的shutdown函数。
INET注册的shutdown函数是inet_shutdown,该函数的定义如下:
int inet_shutdown(struct socket *sock, int how)
该函数完成如下工作:
1、 根据socket参数找到与之关联的sock数据结构。
2、 检查参数how的合法性。
3、 如果socket的状态是SS_CONNECTING并且sock的状态是TCP_ESTABLISHED,则将socket的状态改为SS_CONNECTED。
4、 如果sock为空或sock的状态表示还没有在其上建立连接,则出错返回。
5、 执行sock操作集中的shutdown函数。
6、 执行sock结构上的state_change函数。
7、 成功返回。
TCP注册的用于完成shutdown的函数是tcp_shutdown,该函数的定义如下:
void tcp_shutdown(struct sock *sk, int how)
它完成如下工作:
1、 如果参数how中没有指明SEND_SHUTDOW,则说明不需要向对方发送信息,因此,简单返回。
2、 如果sock的状态是TCP_ESTABLISHED、TCP_SYN_SENT、TCP_SYN_RECV、TCP_CLOSE_WAIT,则向对方发送一个FIN包。
3、 释放sock数据结构。

10.5.7 Read data from a socket
两个socket建立起来连接以后,就可以在其上发送和接收数据包。BSD Socket对数据包的发送和接收采用的方法与操作普通文件相同,即对socket描述符fd的读写操作。在socket建立时,要生成一个file数据 结构,在该结构中注册的文件操作集是socket_file_ops,其定义为:
struct file_operations socket_file_ops = {
sock_lseek,
sock_read,
sock_write,
NULL, /* readdir */
sock_poll,
sock_ioctl,
NULL, /* mmap */
sock_no_open, /* special open code to disallow open via /proc */
NULL, /* flush */
sock_close,
NULL, /* no fsync */
sock_fasync
};
以后在socket描述符上所使用的文件操作就都按上述操作集,定位到了socket函数上。Socket操作集中最重要的是它的读和写函数,这两个函数完成socket数据包的发送和接收。
完成socket读操作的是函数sock_read,其定义如下:
ssize_t sock_read(struct file *file, char *ubuf, size_t size, loff_t *ppos)
它所做的工作如下:
1、 检查参数的合法性(包括size和ppos)。
2、 根据参数file找到它对应的socket数据结构。
3、 填写一个msghdr数据结构,用于信息的接收。Msghdr数据结构的定义如下:
struct msghdr {
void * msg_name; /* Socket name */
int msg_namelen; /* Length of name */
struct iovec * msg_iov; /* Data blocks */
__kernel_size_t msg_iovlen; /* Number of blocks */
void * msg_control; /* Per protocol magic (eg BSD file
* descriptor passing) */
__kernel_size_t msg_controllen; /* Length of cmsg list */
unsigned msg_flags;
};
struct iovec
{
void *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
对消息头msghdr的各个域赋如下初值:
msg.msg_name=NULL;
msg.msg_namelen=0;
msg.msg_iov=&iov;
msg.msg_iovlen=1;
msg.msg_control=NULL;
msg.msg_controllen=0;
iov.iov_base=ubuf;
iov.iov_len=size;
4、 调用函数sock_recvmsg,从底层接收一个消息,其返回值是实际接收的消息长度。
显然,完成socket读操作的主要是函数sock_recvmsg。该函数定义在net/socket.c中,其定义如下:
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int size, int flags)
该函数完成如下工作:
1、定义一个scm_cookie数据结构(Socket level control messages),该数据结构的定义为(在include/net/scm.h):
struct scm_cookie
{
struct ucred creds; /* Skb credentials */
struct scm_fp_list *fp; /* Passed files */
unsigned long seq; /* Connection seqno */
};
其中:

struct scm_fp_list
{
int count;
struct file *fp[SCM_MAX_FD];
};
struct ucred {
__u32 pid;
__u32 uid;
__u32 gid;
};
将该scm_cookie结构清空。
2、调用socket操作集上的recvmsg函数,从底层接收消息。其返回值是实际接收到的消息的长度。
3、 调用函数scm_recv对接收到的消息做进一步的处理。
因此,主要的接收工作在第二步中由socket操作集上的recvmsg函数完成。INET实现的recvmsg函数是inet_recvmsg。该函数的定义如下:
int inet_recvmsg(struct socket *sock, struct msghdr *msg, int size,
int flags, struct scm_cookie *scm)
1、




10.5 IP层(The IP Layer)
10.5.1 Socket Buffers
Linux将网络协议分成许多层,每一层都使用其它层提供的服务。但这样的网络协议会有一个问题:每一个协议都要在传送的时候在数据上增加协议头和尾,而 在处理接收数据的时候删除协议头和尾。这使得在协议之间传送数据缓冲区相当困难,因为每一层都需要找出它的特定的协议头和尾在哪里。一个解决方法是在每一 层都拷贝缓冲区,但是这样会非常低效。替代的,Linux使用socket 缓冲区或者说sk_buffs在协议层之间,或协议层与网络设备驱动程序之间传输数据。sk_buffs包括指针和长度域,允许每一协议层使用标准的函数 或方法操纵应用程序数据。
struct sk_buff {
struct sk_buff * next; /* Next buffer in list */
struct sk_buff * prev; /* Previous buffer in list */
struct sk_buff_head * list; /* List we are on */
struct sock *sk; /* Socket we are owned by */
struct timeval stamp; /* Time we arrived */
struct device *dev; /* Device we arrived on/are leaving by */

/* Transport layer header */
union
{
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct spxhdr *spxh;
unsigned char *raw;
} h;

/* Network layer header */
union
{
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
struct ipxhdr *ipxh;
unsigned char *raw;
} nh;

/* Link layer header */
union
{
struct ethhdr *ethernet;
unsigned char *raw;
} mac;

struct dst_entry *dst;

char cb[48];

unsigned int len; /* Length of actual data */
unsigned int csum; /* Checksum */
volatile char used; /* Data moved to user and not MSG_PEEK */
unsigned char is_clone, /* We are a clone */
cloned, /* head may be cloned (check refcnt to be sure). */
pkt_type, /* Packet class */
pkt_bridged, /* Tracker for bridging */
ip_summed; /* Driver fed us an IP checksum */
__u32 priority; /* Packet queueing priority */
atomic_t users; /* User count - see datagram.c,tcp.c */
unsigned short protocol; /* Packet protocol from driver. */
unsigned short security; /* Security level of packet */
unsigned int truesize; /* Buffer size */

unsigned char *head; /* Head of buffer */
unsigned char *data; /* Data head pointer */
unsigned char *tail; /* Tail pointer */
unsigned char *end; /* End pointer */
void (*destructor)(struct sk_buff *); /* Destruct function */
#ifdef CONFIG_IP_FIREWALL
__u32 fwmark; /* Label made by fwchains, used by pktsched */
#endif
#if defined(CONFIG_SHAPER) || defined(CONFIG_SHAPER_MODULE)
__u32 shapelatency; /* Latency on frame */
__u32 shapeclock; /* Time it should go out */
__u32 shapelen; /* Frame length in clocks */
__u32 shapestamp; /* Stamp for shaper */
__u16 shapepend; /* Pending */
#endif

#if defined(CONFIG_HIPPI)
union{
__u32 ifield;
} private;
#endif
};


图10.4显示了sk_buff数据结构:每一个sk_buff都有和它关联的一块数据。sk_buff有四个数据指针,用于操纵和管理socket缓冲区的数据:
参见include/linux/skbuff.h
head 指向内存中的数据区的起始位置。它在sk_buff以及和它相关的数据块被分配的时候确定。
data 指向协议数据的当前起始位置。这个指针随着当前拥有这个sk_buff 的协议层的不同而变化。
tail 指向协议数据的当前结尾位置。同样,这个指针也随拥有这个sk_buff的协议层不同而变化。
end 指向内存中数据区域的结尾。这是在这个sk_buff分配的时候确定的。
另有两个长度字段len和truesize,分别描述当前协议报文的长度和数据缓冲区的总长度。sk_buff处理代码提供了标准的机制用于在应用程序数据上增加和删除协议头和尾。这种代码安全地操纵sk_buff中的data、tail和len字段。处理程序包括:
push 把data 指针向数据区域的起始位置移动,并增加len字段。用于在要传送的数据前面增加数据或协议头。
参见include/linux/skbuff.h skb_push()
pull 把data指针从数据区域起始向结尾移动,并减少len字段。用于从接收数据的起始位置删除数据或协议头。
参见include/linux/skbuff.h skb_pull()
put 把tail指针向数据区域的结尾移动并增加len字段,用于在传输的数据尾部增加数据或协议信息。
参见include/linux/skbuff.h skb_put()
trim 把tail指针向数据区域的开始移动并减少len字段。用于从接收的数据中删除数据或协议尾。
参见include/linux/skbuff.h skb_trim()
sk_buff数据结构也包括一些指针,使用这些指针,在处理过程中这个数据结构可以存储在sk_buff数据结构的双向环形链表中。有通用的sk_buff例程,在这些列表的头和尾中增加sk_buff和删除其中的sk_buff。

10.5.2 Receiving IP Packets
第8 章描述了Linux的网络设备驱动程序如何建立到内核以及如何被初始化。这产生了一系列device数据结构,它们在dev_base列表中被链接在一 起。每一个device数据结构都描述了它的设备,并提供了一组回调例程,当需要网络驱动程序工作的时候,网络协议层可以调用这些例程。这些函数中的大多 数和传输数据以及网络设备的地址有关。当一个网络设备从它的网络上接收到数据报文的时候,它必须把接收到的数据转换到sk_buff数据结构中。这些 sk_buff数据结构又被网络驱动程序加到backlog队列。如果backlog队列增长得太大,那么刚接收的sk_buff就会被废弃。如果有工作 要执行,这个网络的button half就被标记成准备运行。
参见net/core/dev.c netif_rx()
当网络的bottom half处理程序被调度程序调度执行时,它首先处理任何等待传送的网络报文,然后才处理sk_buff的backlog队列,确定接收到的报文需要传送到那个协议层。
当Linux网络层初始化的时候,每一个协议都登记自己:在ptype_all列表或者ptype_base hash table中增加一个packet_type的数据结构。packet_type的定义如下:
struct packet_type {
unsigned short type; /* This is really htons(ether_type). */
struct device * dev;
int (*func) (struct sk_buff *, struct device *,
struct packet_type *);
void *data;
struct packet_type *next;
};

这个packet_type数据结构包括协议类型、一个指向网络驱动设备的指针、一个指向协议的数据接收处理例程的指针和一个指向这个列表或者hash table中下一个packet_type数据类型的指针。ptype_all链表用于探听(snoop)从任意网络设备上接收到的所有的数据报文,通常 不使用。ptype_base hash table 的定义如下:
struct packet_type *ptype_base[16];
ptype_base hash table使用协议标识符作hash值,用于确定进来的网络报文应该由哪一种协议接收。网络的bottom half把进来的sk_buff中的协议类型和上述任一表中的一个或多个packet_type条目进行匹配。协议可能会匹配一个或多个条目,例如当探听 (snoop)所有的网络通信的时候,这时,这个sk_buff会被克隆。最后,这个sk_buff被传递到匹配的协议的处理例程。
参见net/core/dev.c net_bh()
参见net/ipv4/ip_input.c ip_recv()

10.5.3 Sending IP Packets
报文在应用程序交换数据的过程中传送,或者也可能是为了支持已经建立的连接或为了建立连接而由网络协议产生。不管数据用什么方式产生,都建立一个包含数据的sk_buff,并在它通过协议层的时候增加许多协议头。
这个sk_buff需要传递到进行传输的网络设备。但是首先,协议(例如IP)需要决定使用哪一个网络设备。这依赖于这个报文的最佳路由。对于通过 modem连接到一个网络的计算机,比如通过PPP协议,这种路由选择比较容易。报文应该要么通过loopback设备传送给本地主机,要么传送到PPP modem连接的另一端的网关。对于连接到以太网的计算机而言,这种选择比较困难,因为网络上连接了许多计算机。
对于传送的每一个IP报文,IP协议使用路由表来解析目标IP地址的路由。对于每一个IP目标,如果在路由表中查找成功,会返回一个描述要使用路由的rtable数据结构。
Rtable数据结构的定义如下:
struct rtable
{
struct rtable *rt_next;
__u32 rt_dst;
__u32 rt_src;
__u32 rt_gateway;
atomic_t rt_refcnt;
atomic_t rt_use;
unsigned long rt_window;
atomic_t rt_lastuse;
struct hh_cache *rt_hh;
struct device *rt_dev;
unsigned short rt_flags;
unsigned short rt_mtu;
unsigned short rt_irtt;
unsigned char rt_tos;
};
其中包括使用的源IP地址、网络device数据结构的地址,有时候还会有一个预先建立的硬件头。这个硬件头和网络设备相关,包含源和目的物理地址和其它 同介质相关的信息。如果网络设备是以太网设备,硬件头如图10.1中所示,其中的源和目的地址会是物理的以太网地址。硬件头和路由缓存在一起,因为在这个 路由传送的每一个IP报文都需要追加这个头,而建立这个头需要时间。硬件头可能包含必须使用ARP协议才能解析的物理地址。这时,发出的报文会暂停,直到 地址解析成功。一旦硬件地址被解析,并建立了硬件头,这个硬件头就被缓存,这样以后使用这个接口的IP报文就不需要再进行ARP。
参见include/net/route.h

10.5.4 Data Fragmentation
每一个网络设备都有一个最大的报文尺寸,它无法传送或接收比最大的报文尺寸更大的数据报文。IP协议允许使用大报文,但为了适应网络设备的处理能力,IP协议会把大数据报分割成网络设备能处理的小数据报。IP协议头中有一个分片域,其中包含一个标记和一个分片偏移量。
当要传输一个IP报文的时候,IP查找用来发送IP报文的网络设备。通过IP路由表来查找这个设备。每一个设备(device数据结构)都有一个字段描述 它的最大传输单元(字节)的大小,这个字段是mtu。如果设备的mtu比等待传送的IP报文的报文尺寸小,那么这个IP报文就必须被分割到更小的片段 (mtu大小)。每一个片段用一个sk_buff代表:它的IP头标记了它是一个片段,以及这个IP报文包含的片段在整个数据中的偏移量。最后一个报文被 标记为最后一个IP片段。如果在分割成片段的过程中,IP无法分配sk_buff,这次传送就会失败。
接收IP片段比发送更难,因为IP片段可能以任意顺序被接收,而且它们必须在重组之前全部接收到。每一次一个IP报文被接收的时候,都要检查看它是不是一 个IP片段。收到一个消息的第一个片段时,IP就建立一个新的ipq数据结构,并将其连接到等待组装的IP片段的ipqueue列表中。当更多的IP片段 到来时,找到正确的ipq数据结构并建立一个新的ipfrag数据结构来描述这个新来的片段。每一个ipq数据结构都唯一描述了一个成为碎片的IP接收 帧,包括它的源和目标IP地址、上层协议标识符和这个IP帧的标识符。当所有的片段都被接收到时,它们被组装在一起成为一个单一的sk_buff,然后再 传递到下一个协议层去处理。每一个ipq都包括一个计时器,每接收到一个有效的碎片时,它就重新启动。如果这个计时器过期,这个ipq数据结构和它的 ipfrag就被去除,并假设这个消息在传输过程中丢失了。然后由高层的协议负责重新传输这个消息。
static struct ipq *ipqueue = NULL; /* IP fragment queue */

/* Describe an IP fragment. */
struct ipfrag
{
int offset; /* offset of fragment in IP datagram */
int end; /* last byte of data in datagram */
int len; /* length of this fragment */
struct sk_buff *skb; /* complete received fragment */
unsigned char *ptr; /* pointer into real fragment data */
struct ipfrag *next; /* linked list pointers */
struct ipfrag *prev;
};

/* Describe an entry in the "incomplete datagrams" queue. */
struct ipq
{
unsigned char *mac; /* pointer to MAC header */
struct iphdr *iph; /* pointer to IP header */
int len; /* total length of original datagram */
short ihlen; /* length of the IP header */
short maclen; /* length of the MAC header */
struct timer_list timer; /* when will this queue expire? */
struct ipfrag *fragments; /* linked list of received fragments */
struct ipq *next; /* linked list pointers */
struct ipq *prev;
struct device *dev; /* Device - for icmp replies */
};
参见net/ipv4/ip_input.c ip_rcv()

10.6 The Address Resolution Protocol (ARP)
地址解析协议的任务是提供IP地址到物理硬件地址(例如以太网地址)的转换。IP在把数据(以sk_buff的形式)传送到设备驱动程序进行传送的时候才 需要这种转换。它首先进行一些检查,看这个设备是否需要一个硬件头,如果是,这个报文的硬件头是否需要重建。Linux缓存硬件头以免频繁地重建。如果硬 件头需要重建,它就调用和设备相关的硬件头重建例程。所有的以太设备使用相同的通用头重建例程,然后使用ARP服务把目标IP地址转换到物理地址。
参见net/ipv4/ip_output.c ip_build_xmit()
参见net/ethernet/eth.c rebuild_header()
ARP协议本身非常简单,包含两种消息类型:ARP请求和ARP应答。ARP请求包括需要转换的IP地址,应答(希望)包括转换的IP地址,即硬件地址。 ARP请求被广播到连接到网络的所有主机,所以,对于一个以太网,所有连在以太网上的机器都可以看到这个ARP请求。IP地址与这个请求中包括的IP地址 相同的机器会回应这个ARP请求,用包含它自己物理地址的ARP作应答。
Linux中的ARP协议层围绕着一个arp_table数据结构的表而建立。该表中的每一项描述一个IP和物理地址的对应。这些条目在IP地址需要转换的时候创建,而随着时间的推移当其变得陈旧的时候被删除。每一个arp_table数据结构都包含以下域:
Last used 这个ARP条目上一次使用的时间;
Last update 这个ARP条目上一次更新的时间;
Flags 描述这个条目的状态:它是否完成等等;
IP address 这个条目描述的IP地址;
Hardware address 转换(翻译)后的硬件地址;
Hardware header 指向一个缓存的硬件头的指针;
Timer 这是一个timer_list的条目,用于让没有回应的ARP请求超时;
Retries 这个ARP请求重试的次数;
Sk_buff queue 等待解析这个IP地址的sk_buff条目的列表;
ARP表的定义如下:
struct neigh_table
{
struct neigh_table *next;
int family;
int entry_size;
int key_len;
int (*constructor)(struct neighbour *);
int (*pconstructor)(struct pneigh_entry *);
void (*pdestructor)(struct pneigh_entry *);
void (*proxy_redo)(struct sk_buff *skb);
struct neigh_parms parms;
/* HACK. gc_* shoul follow parms without a gap! */
int gc_interval;
int gc_thresh1;
int gc_thresh2;
int gc_thresh3;
unsigned long last_flush;
struct timer_list gc_timer;
struct timer_list proxy_timer;
struct sk_buff_head proxy_queue;
int entries;
atomic_t lock;
unsigned long last_rand;
struct neigh_parms *parms_list;
struct neigh_statistics stats;
struct neighbour *hash_buckets[NEIGH_HASHMASK+1];
struct pneigh_entry *phash_buckets[PNEIGH_HASHMASK+1];
};
struct neighbour
{
struct neighbour *next;
struct neigh_table *tbl;
struct neigh_parms *parms;
struct device *dev;
unsigned long used;
unsigned long confirmed;
unsigned long updated;
__u8 flags;
__u8 nud_state;
__u8 type;
__u8 probes;
unsigned char ha[MAX_ADDR_LEN];
struct hh_cache *hh;
atomic_t refcnt;
int (*output)(struct sk_buff *skb);
struct sk_buff_head arp_queue;
struct timer_list timer;
struct neigh_ops *ops;
u8 primary_key[0];
};
struct neigh_table arp_tbl =
{
NULL,
AF_INET,
sizeof(struct neighbour) + 4,
4,
arp_constructor,
NULL,
NULL,
parp_redo,
{ NULL, NULL, &arp_tbl, 0, NULL, NULL, 30*HZ, 1*HZ, 60*HZ, 30*HZ, 5*HZ,
3, 3, 0, 3, 1*HZ, (8*HZ)/10, 64, 1*HZ },
30*HZ, 128, 512, 1024,
};



struct arp_table *arp_tables[FULL_ARP_TABLE_SIZE]
arp_table数据结构的定义如下:
struct arp_table
{
struct arp_table *next; /* Linked entry list */
unsigned long last_used; /* For expiry */
unsigned long last_updated; /* For expiry */
unsigned int flags; /* Control status */
u32 ip; /* ip address of entry */
u32 mask; /* netmask - used for generalised
proxy arps (tridge) */
unsigned char ha[MAX_ADDR_LEN]; /* Hardware address */
struct device *dev; /* Device the entry is tied to */
struct hh_cache *hh; /* Hardware headers chain */

/* The following entries are only used for unresolved hw addresses. */
struct timer_list timer; /* expire timer */
int retries; /* remaining retries */
struct sk_buff_head skb; /* list of queued packets */
};
ARP表是一个由指针组成的表,是一个arp_table向量表或hash表,用于把arp_table条目链接在一起。这些条目被缓存,以加速对它们的 访问。查找缓存的条目时,首先根据它的IP地址的最后两个字节生成一个索引,用该索引在ARP中获得一个arp_table链表,然后搜索该链表,直到找 到正确的条目。Linux还在arp_table外缓存预先建立的硬件头,该硬件头用hh_cache数据结构表示,arp_table的指针hh指向缓 存的硬件头。
当请求一个IP地址转换而又没有相应的arp_table条目的时候,ARP必须发送一个ARP请求消息。它首先在ARP表中创建一个新的 arp_table条目,而后把包含需要地址转换的网络报文的sk_buff加入到这个新arp_table条目的sk_buff队列中。上述一切作完以 后,它发出一个ARP请求并启动ARP过时计时器。如果没有回应,ARP会重试几次。如果仍旧没有回应,ARP会删除这个arp_table条目。任何排 队等待这个IP地址转换的sk_buff数据结构都会被通知,由传输它们的上层协议负责处理这种失败。UDP不关心丢失的报文,但是TCP会在一个建立的 TCP连接上试图重新发送。如果这个IP地址的属主用它的硬件地址作了应答,这个arp_table条目就被标记为完成,任何排队的sk_buff会被从 对队列中删除,硬件地址被写到每一个sk_buff的硬件头中,从而可以继续它们的传送。
ARP协议层也必须回应指明它的IP地址的ARP请求。ARP在初始化时登记它的协议类型:ETH_P_ARP,产生一个packet_type数据结 构。这意味着网络设备接收到的所有的ARP报文都会传给它。象ARP应答一样,这也包括ARP请求。它使用接收设备的device数据结构中的硬件地址产 生ARP应答。
device数据结构定义在linux/include/linux/netdevice.h中,其中有硬件地址和硬件地址长度等域。
网络拓扑结构不断变化,IP地址也可能被重新分配给不同的硬件地址。例如,一些拨号服务为它建立的每一个连接分配一个IP地址。为了让ARP表中包括最新 的条目,ARP运行一个定期的计时器,检查所有的arp_table条目,看哪一个超时了。它非常小心,不删除包含一个或多个缓存的硬件头的条目。因为删 除这些条目比较危险,可能有其它数据结构依赖它们。一些arp_table条目是永久的,并被标记,所以它们不会被释放。ARP表不能增长得太大:每一个 arp_table条目都要消耗一些内核内存。每当需要分配一个新的条目而ARP表又达到了它的最大尺寸的时候,就要查找最旧的条目并删除它们,从而为新 条目让出空间。

10.7 IP Routing
IP路由功能确定发向一个特定IP地址的IP报文应该向哪里转发。当传送IP报文的时候,会有许多选择。目的地是否可以到达?如果可以,应该使用哪一个网 络设备来发送?是不是有不止一个可以到达目的地的网络设备?哪一个最好?IP路由数据库维护的信息可以回答这些问题。有两个数据库,最重要的是转发信息数 据库(Forwarding Information Database)。这个数据库是个详尽的列表,其中列出了已知的IP目标地址和到达该地址的最佳路由。另一个是更小但更快的数据库:路由缓存 (route cache)用于快速查找IP目标的路由。象所有缓存一样,它必须只包含最常访问的路由,它的内容是从转发信息数据库(FIDB)中得来的。
通过BSD socket接口的IOCTL请求增加和删除路由。这些请求被传递到具体的协议去处理。INET协议层只允许具有超级用户权限的进程增加和删除IP路由。 这些路由可以是固定的,也可以是动态的,随着时间不断变化。多数系统使用固定路由,除非它们本身是路由器。路由器运行路由协议,它不断地检查所有已知IP 目标路由的可用性。不是路由器的系统叫做末端系统(end system)。路由协议用守护进程的形式来实现,例如GATED,它们也使用BSD socket接口的IOCTL来增加和删除路由。

10.7.1 The Route Cache
不论何时,当要查找一个IP路由时,都首先在路由缓存中检查匹配的路由。如果路由缓存中没有匹配的路由,才查找转发信息数据库。如果在FIDB中也找不到 路由,IP报文发送失败,应用程序被告知。如果路由在转发信息数据库而不在路由缓存中,就要为这个路由生成一个新的条目并将其增加到路由缓存中。路由缓存 是一个表(ip_rt_hash_table),其中包括指向rtable数据结构链的指针。路由缓存表的索引是基于IP地址最小两字节的hash 函数。在通常的目标地址中,这两个字节有很大不同,以它们作hash value可以提供最好的分散。
ip_rt_hash_table hash表和rtable数据结构的定义如下:
#define RT_HASH_DIVISOR 256

struct rtable *ip_rt_hash_table[RT_HASH_DIVISOR];

typedef int atomic_t;

struct rtable
{
struct rtable *rt_next;
__u32 rt_dst;
__u32 rt_src;
__u32 rt_gateway;
atomic_t rt_refcnt;
atomic_t rt_use;
unsigned long rt_window;
atomic_t rt_lastuse;
struct hh_cache *rt_hh;
struct device *rt_dev;
unsigned short rt_flags;
unsigned short rt_mtu;
unsigned short rt_irtt;
unsigned char rt_tos;
};
每一个rtable条目中都包含路由的信息:目标IP地址、到达这个IP地址要使用的网络设备(device结构)、可以使用的最大的信息尺寸等等。其中 还有一个引用计数器(refrence count)、一个使用计数器(usage count)和上次使用的时间戳(在jiffies中)。每一次使用这个路由的时候,它的引用计数器就会增加,显示利用这个路由的网络连接的数目;当应用 程序停止使用这个路由的时候,它的引用计数器就会减少。每一次查找路由的时候,它的使用计数器就会增加,利用该值对这个hash表的rtable条目链排 序。路由缓存中所有条目的最后使用时间戳用于定期检查这个rtable是否太老。如果这个路由最近没有使用,它就会被从路由表中废弃。如果路由保存在路由 缓存中,它们就被排序,最常用的条目排在hash链的最前面。这意味着当查找路由的时候会更快地找到它们。
参见net/ipv4/route.c check_expire()

10.7.2 The Forwarding Information Database
转发信息数据库(图10.5显示)包含了当时系统中可用路由的IP视图。它是非常复杂的数据结构,虽然已经对它进行了合理有效的安排,但是就对它的查询而 言,它并不是一个快速的数据库。特别是,如果每一个要传输的IP报文都在这个数据库中查找目标,那么其转发速度会非常慢。这也是为什么要使用路由缓存的原 因:加速已经知道最佳路由的IP报文的传送。路由缓存中的数据来源于这个转发信息数据库,表示了它最常用的条目。
每一个IP子网用一个fib_zone数据结构表示。所有的fib_zone被放在一个hash表中,该hash表为fib_zones hash表,其中的每一个元素是一个指向fib_zone数据结构的指针。hash索引取自IP子网的掩码。通向同一子网的每一个路由都用一个 fib_node、fib_info数据结构对来描述,这种数据结构对排在fib_zone数据结构中的fz_list队列中。如果这个子网的路由数目变 得太大,就要再生成一个hash table,让对fib_node数据结构的查找更容易。
对于同一个IP子网,可能存在多个路由,而且这些路由又可能穿过多个网关。IP路由层不允许使用同一个网关的子网有多于一个的路由。换句话说,如果对于一 个子网有多个路由,那么要保证每一个路由都用不同的网关。和每一个路由关联的是它的量度(metric),这是用来衡量这个路由的益处。事实上,一个路由 的量度是它在到达目标子网之前必须跳过的子网数目。这个量度越高,路由越差。