相信自己,快乐每一天
分类: LINUX
2013-10-30 11:46:12
原文地址:sk_buff属性详解 作者:chenshko
第一部分:
skb - Linux network buffers
skb——Linux网络缓存
Harald Welte laforge@gnumonks.org
1.3, 2000/10/14 21:27:02
---------------------------------------------------------------------
Short description about the linux network buffers (skb's)
---------------------------------------------------------------------
1. Introduction
At the time I wanted to know more about the Linux network stack, I always wanted a document like this to exist. But unfortunately I never found one. After I gained some basic knowledge about the Linux network stack internals, I wrote one.
I'm happy if this document is of any use for other people trying to learn about the Linux kernel.
Please let me know of any bugs in this document. It should resemble kernel revision 2.4.0-test4
2. skbuff's
skbuffs are the buffers in which the linux kernel handles network packets. The packet is received by the network card, put into a skbuff and then passed to the network stack, which uses the skbuff all the time.
skbuffs是那些linux内核处理网络分组的缓存。网卡收到分组后,将它们放进skbuff,然后再传送给网络堆栈。网络堆栈一直要用到skbuff。
2.1 struct sk_buff
The
struct sk_buff is defined in
在
next
next buffer in list
链表中下一个缓存
prev
previous buffer in list
链表中前一个缓存
list
list we are on
当前链表
sk
socket we belong to
所属socket
stamp
timeval we arrived at
(分组)到达的时间
dev
device we are leaving by
分组离开的设备
rx_dev
device we arrived at
分组到达的设备
h
transport layer header (tcp,udp,icmp,igmp,spx,raw)
传输层头标(tcp,udp,icmp,igmp,spx,raw)
nh
network layer header (ip,ipv6,arp,ipx,raw)
网络层头标(ip,ipv6,arp,ipx,raw)
mac
link layer header
链路层头标
dst
FIXME:
(目的地??)
cb
control buffer, used internally
控制缓存,后台使用
len
length of actual data
实际数据长度
csum
checksum
校验和
used
FIXME: data moved to user and not MSG_PEEK
已经传递给用户的数据,未经MSG_PEEK
is_clone
we are a clone
为克隆副本
cloned
head may be cloned
头标可被克隆
pkt_type
packet class
分组等级分类
ip_summed
driver fed us ip checksum
驱动器将IP校验和反馈给我们
priority
packet queuing priority
分组排队优先级
users
user count
用户计数
protocol
packet protocol from driver
驱动器的分组协议
security
security level of packet
分组的安全级别
truesize
real size of the buffer
缓存的真实尺寸
head
pointer to head of buffer
缓存头指针
data
data head pointer
数据头指针
tail
tail pointer
尾指针
end
end pointer
结束指针
destructor
destructor function
拆除功能
nfmark
netfilter mark
网络过滤器标志
nfcache
netfilter internal caching info
网络过滤器内在高速缓存信息
nfct
associated connection, if any
相关联的连接
tc_index
traffic control index
流量控制索引
2.2 skb support functions
skb支持的功能函数
There are a bunch of skb support functions provided by the sk_buff layer. I briefly describe the most important ones in this section.
主要描述一下sk_buff层最重要的一些功能函数。
allocation / free / copy / clone and expansion functions
分配、释放、复制、克隆、扩展等功能函数
struct sk_buff *alloc_skb(unsigned int size, int gfp_mask)
This function allocates a new skb. This is provided by the skb layer to initialize some private data and do memory statistics. The returned buffer has no headroom and a tailroom of /size/ bytes.
此函数分配了一个新的skb,skb层提供此函数来初始化一些私有数据,同时作内存统计。返回的缓存没有头空间,有一个尺寸/字节尾空间;
《Linux设备驱动程序》一书描述为:
struct sk_buff *alloc_skb(unsigned int len,int priority)
struct sk_buff *dev_alloc_skb(unsigned int len)
分配一个缓冲区。alloc_skb分配一个缓冲区并初始化skb->data和skb->tail到skb->head。
dev_alloc_skb函数是以GFP_ATOMIC优先级调用alloc_skb,并保存skb->head和skb->data之间16个字节的一个快捷方式。这个数据空间可以用来“推(push)”硬件包头。
void kfree_skb(struct sk_buff *skb)
Decrement the skb's usage count by one and free the skb if no references left.
skb使用计数减一,如果没有任何可参照的说明,释放skb;
《Linux设备驱动程序》一书描述为:
释放一个缓冲区。kfree_skb被内核内部使用。kree_skb被内核内部使用。
struct sk_buff *skb_get(struct sk_buff *skb)
Increments the skb's usage count by one and returns a pointer to it.
skb使用计数加一,返回一个指向skb的指针。这个skb估计是新使用的skb。
struct sk_buff *skb_clone(struct sk_buff *skb, int gfp_mask)
This function clones a skb. Both copies share the packet data but have their own struct sk_buff. The new copy is not owned by any socket, reference count is 1.
这个函数克隆一个skb。两份拷贝共享分组数据,但分别拥有自己的sk_buff结构。新拷贝不属于任何socket,参考计数为1。
struct sk_buff *skb_copy(const struct sk_buff *skb, int gfp_mask)
Makes a real copy of the skb, including packet data. This is needed, if You wish to modify the packet data. Reference count of the new skb is 1.
产生一个实实在在的skb拷贝,包含分组数据。但你想要修改分组数据时,需要这么做。新拷贝的参考计数为1。
struct skb_copy_expand(const struct sk_buff *skb, int new_headroom, int new_tailroom, int gfp_mask)
Make a copy of the skb, including packet data. Additionally the new skb has a headroom of /new_headroom/ bytes size and a tailroom of /new_tailroom/ bytes.
产生一个skb拷贝,包含分组数据。另外,新skb还拥有一个/new_headroom/字节大小的头空间,和一个/new_tailroom/字节大小的尾空间。
anciliary functions
~l
辅助函数:
int skb_cloned(struct sk_buff *skb)
Is the skb a clone?
判断skb是否是克隆
int skb_shared(struct sk_Buff *skb)
Is this skb shared? (is the reference count > 1)?
判断此skb是否共享(参考计数是不是大于1)。
operations on lists of skb's
对skb链表的操作。
struct sk_buff *skb_peek(struct sk_buff_head *list_)
peek a skb from front of the list; does not remove skb from the list
偷窥链表起始的一个skb;不把skb从链表中移除。
struct sk_buff *skb_peek_tail(struct sk_buff_head *list_)
peek a skb from tail of the list; does not remove sk from the list
偷窥链表尾部的一个skb;不把skb从链表中移除。
__u32 skb_queue_len(sk_buff_head *list_)
return the length of the given skb list
返回给定skb链表的长度
void skb_queue_head(struct sk_buff_head *list_, struct sk_buff *newsk)
enqueue a skb at the head of a given list
将一个skb排入给定链表头部。
void skb_queue_tail(struct sk_buff_head *list_, struct sk_buff *newsk)
enqueue a skb at the end of a given list.
将一个skb排入给定链表尾部。
struct sk_buff *skb_dequeue(struct sk_buff_head *list_)
dequeue a skb from the head of the given list.
将一个skb从给定链表头部移除。
struct sk_buff *sbk_dequeue_tail(struct sk_buff_head *list_)
dequeue a skb from the tail of the given list
将一个skb从给定链表尾部移除。
operations on skb data
对skb数据的操作
unsigned char *skb_put(struct sk_buff *sbk, int len)
extends the data area of the skb. if the total size exceeds the size of the skb, the kernel will panic. A pointer to the first byte of new data is returned.
扩展skb的数据域。如果总长度超出了skb的尺寸大小,内核将陷入慌乱。此函数将返回一个指向新数据第一个字节的指针。
unsigned char *skb_push(struct sk_buff *skb, int len)
extends the data area of the skb. if the total size exceeds the size of the skb, the kernel will panic. A pointer to the first byte of new data is returned.
扩展skb的数据域。如果。。。。与上一个函数解释一样,不知是不是发布者的错误。
unsigned char *skb_pull(struct sk_buff *skb, int len)
remove data from the start of a buffer, returning the bytes to headroom. A pointr to the next data in the buffer is returned.
将数据从缓存起始处移除,回收头空间字节。返回一个指向缓存中下一个数据的指针。
int skb_headroom(struct sk_buff *skb)
return the amount of bytes of free space at the head of skb
返回skb头部空闲空间的字节数。
int skb_tailroom(struct sk_buff *skb)
return the amount of bytes of free space at the end of skb
返回skb尾部空闲空间的字节数。
struct sk_buff *skb_cow(struct sk_buff *skb, int headroom)
if the buffer passed lacks sufficient headroom or is a clone it is copied and additional headroom made available.
如果缓存缺乏头空间或者是一个克隆,则缓存将被拷贝,激活附加头空间。
第二部分:
标题: Linux网络代码导读v0.2
作者: yawl
主页: www.nsfocus.com
时间: 11/2000
1 前言
许多人在分析linux代码时对网络部分(主要是src/linux/net,src/linux/include/net及
src/linux/include/linux目录下的文件)比较感兴趣,确实,尽管已经从书本上学到了大
量的TCP/IP原理,不读源码的话,头脑中还是建立不起具体的印象。而分析这部分代码的
一个问题便是代码众多而资料很少。这篇文章的目的就是勾勒出一个框架,让读者能够大致
能够了解TCP/IP究竟是怎么工作的。以前见到的许多代码分析都是基于2.0内核的,在新的
内核中许多函数变了名字,这尤其给初学者带来了困难,本文是以2.4.0-test9的代码作例子,
这样对照代码时可能更清晰些。
其实网络部分的代码我只对防火墙部分一行行仔细分析过,其他许多地方也只是一知半解,
如果理解有误,欢迎指正。
建议在看本文的同时,用source insight()建立一个项目,同时看代码,
这样可能效果更好点。我也用过其他的一些工具,但在分析大量的代码的时候,没有一个工
具比它更方便的了。
2 正文
ISO的七层模型都非常熟悉了,当然,对于internet,用四层模型更为适合。在这两份模型里,
网络协议以层次的形式出现。而LINUX的内核代码中,严格分出清楚的层次却比较困难,因为
除了一些"内核线程(kernel thread外)",整个内核其实是个单一的进程。因此所谓"网络层"
,只是一组相关的函数,而各层之间大多通过一般的函数调用的方式完成交互。
而从逻辑上,网络部分的代码更应该这样分层更为合理:
.BSD socket层:这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结构体现。
这一部分的文件主要有:/net/socket.c /net/protocols.c etc
.INET socket层:BSD socket是个可以用于各种网络协议的接口,而当用于tcp/ip,即建立了AF_INET
形式的socket时,还需要保留些额外的参数,于是就有了struct sock结构。
文件主要有:/net/ipv4/protocol.c /net/ipv4/af_inet.c /net/core/sock.c etc
.TCP/UDP层:处理传输层的操作,传输层用struct inet_protocol和struct proto两个结构表示。
文件主要有:/net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp.c /net/ipv4/tcp_input.c
/net/ipv4//tcp_output.c /net/ipv4/tcp_minisocks.c /net/ipv4/tcp_output.c
/net/ipv4/tcp_timer.c etc
.IP层:处理网络层的操作,网络层用struct packet_type结构表示。
文件主要有:/net/ipv4/ip_forward.c ip_fragment.c ip_input.c ip_output.c etc.
.数据链路层和驱动程序:每个网络设备以struct net_device表示,通用的处理在dev.c中,
驱动程序都在/driver/net目录下。
网络部分还有很多其他文件,如防火墙,路由等,一般根据看到名字便能猜测出相应的处理,此处不再赘述。
现在我要给出一张表,全文的内容就是为了说明这张表(如果你觉得我在文章中的语言比较乏味,尽可
抛掉他们,结合这张表自己看代码)。在我最初看网络部分代码时,比较喜欢《linux kernel internals》
的第八章的一段,其中有一个进程A通过网络远程向另一进程B发包的例子,详细介绍了一个数据包如何
从网络堆栈中走过的过程。我觉得这样可以更迅速的帮助读者看清森林的全貌,因此本文参照这种结构来
叙述。
^
| sys_read fs/read_write.c
| sock_read net/socket.c
| sock_recvmsg net/socket.c
| inet_recvmsg net/ipv4/af_inet.c
| udp_recvmsg net/ipv4/udp.c
| skb_recv_datagram net/core/datagram.c
| -------------------------------------------
| sock_queue_rcv_skb include/net/sock.h
| udp_queue_rcv_skb net/ipv4/udp.c
| udp_rcv net/ipv4/udp.c
| ip_local_deliver_finish net/ipv4/ip_input.c
| ip_local_deliver net/ipv4/ip_input.c
| ip_recv net/ipv4/ip_input.c
| net_rx_action net/dev.c
| -------------------------------------------
| netif_rx net/dev.c
| el3_rx driver/net/3c309.c
| el3_interrupt driver/net/3c309.c
==========================
| sys_write fs/read_write.c
| sock_writev net/socket.c
| sock_sendmsg net/socket.c
| inet_sendmsg net/ipv4/af_inet.c
| udp_sendmsg net/ipv4/udp.c
| ip_build_xmit net/ipv4/ip_output.c
| output_maybe_reroute net/ipv4/ip_output.c
| ip_output net/ipv4/ip_output.c
| ip_finish_output net/ipv4/ip_output.c
| dev_queue_xmit net/dev.c
| --------------------------------------------
| el3_start_xmit driver/net/3c309.c
V
我们假设的环境如下:有两台主机通过互联网联在一起,其中一台机子运行这一个进程A,
另外一台运行进程B,进程A将向进程B发出一条信息,比如"Hello",而B接受此信息。
TCP处理本身非常复杂,为了便于叙述,在后面我们将用UDP作为例子。
2.1 建立套接字
在数据发送之前,要建立一个套接字(socket),在两边的程序中都会调用如下语句:
...
int sockfd;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
...
这是个系统调用,因此会通过0x80中断进入系统内核,调用内核中的相应函数.当寻找
系统调用在内核中的对应流程时,一般前面加入"sys_"再找就是了,如对fork来说,就是
调用sys_fork。但是socket相关调用有些特殊,所有的这类调用都是通过一个入口,即
sys_socketcall进入系统内核,然后再通过参数调用具体的sys_socket,socket_bind等函数。
sys_socket会调用sock_create产生一个struct socket结构(见include/linux/net.h),
每个套接字在内核中都有一个这样的结构对应,在初始化了此结构的一些通用成员后(如
分配inode,根据第二个参数为type项赋值等),会根据其一个参数作响应的调度,即这
一句:
...
net_families[family]->create(sock, protocol);
...
我们的程序的第一个参数是AF_INET,所以此函数指针会指向inet_create();(net_families
是个数组,保留了网络协议族(net families)的信息,而这些协议族用sock_register加载。)
在struct socket结构结构中最重要的信息保留在struct sock结构中,这个结构在网络代码中经常
使用,建议把它和其他常见结构(如struct sk_buff)打印出来放在手边。在inet_create会为此
结构分配内存,并根据套接字类型(其实就是socket函数的第二个参数),作各自不同的初始化:
...
if (sk->prot->init)
sk->prot->init(sk);
...
如果类型是SOCK_STREAM的话会调用tcp_v4_init_sock,而SOCK_DGRAM类型的socket没有额外的初始化了,
到此socket调用结束。
还有一个值得注意的地方是当inet_create()调用完后,会接着调用sock_map_fd函数,这个
函数中会为套接字分配一个文件描述符并分配一个file文件。在应用层便可象处理文件一样
处理套接字了。
开始的时候可能有些流程难以跟下去,主要便是这些函数指针的实际指向会根据类型变化。
2.2 发送数据
当进程A想发送数据时,程序中会调用如下语句(如果用send函数的话会走类似的流程,略):
...
write(sockfd,"Hello",strlen("Hello"));
...
write在内核中对应的函数就是sys_write,此函数首先根据文件描述符找到struct file结构,如果此文件
存在(file指针非空)且可写(file->f_mode & FMODE_WRITE为true),便调用此文件结构的写操作:
...
if (file->f_op && (write = file->f_op->write) != NULL)
ret = write(file, buf, count, &file->f_pos);
...
其中f_op是个struct file_operations结构指针,在sock_map_fd中将其指向socket_file_ops,其
定义如下(/net/socket.c):
static struct file_operations socket_file_ops = {
llseek: sock_lseek,
read: sock_read,
write: sock_write,
poll: sock_poll,
ioctl: sock_ioctl,
mmap: sock_mmap,
open: sock_no_open, /* special open code to disallow open via /proc */
release: sock_close,
fasync: sock_fasync,
readv: sock_readv,
writev: sock_writev
};
此时wirte函数指针显然指向了sock_write,我们跟下去看,此函数将一个字符串缓冲整理成struct msghdr,
最后调用了sock_sendmsg.
sock_sendmsg中的scm_send我不了解(scm是Socket level control messages的简写),好在它也不是很关键,
我们注意到这句:
...
sock->ops->sendmsg(sock, msg, size, &scm);
...
又是个函数指针,sock->ops在inet_create()函数中被初始化,由于我们我们是UDP的套接字,sock->ops
指向了inet_dgram_ops(即sock->ops = &inet_dgram_ops;),其定义在net/ipv4/Af_inet.c中:
struct proto_ops inet_dgram_ops = {
family: PF_INET,
release: inet_release,
bind: inet_bind,
connect: inet_dgram_connect,
socketpair: sock_no_socketpair,
accept: sock_no_accept,
getname: inet_getname,
poll: datagram_poll,
ioctl: inet_ioctl,
listen: sock_no_listen,
shutdown: inet_shutdown,
setsockopt: inet_setsockopt,
getsockopt: inet_getsockopt,
sendmsg: inet_sendmsg,
recvmsg: inet_recvmsg,
mmap: sock_no_mmap,
};
因此我们要看得便是inet_sendmsg()函数了,而马上,这个函数又通过函数指针调用了另一函数:
...
sk->prot->sendmsg(sk, msg, size);
...
我们不得不再次寻找其具体指向。看到这里,说点题外话,怎么才能找到其具体定义呢?我一般是这样:
对上例而言,sk是个struct sock结构,到其定义(linux/net/sock.h中)出看到prot是个struct proto
结构,此时我们便在源代码树中寻找所有此结构的实例(这些诸如跳到定义,寻找引用等工作在source
insight中实在太方便快速了^_^),很快便会发现诸如udp_prot,tcp_prot,raw_prot等,猜测是用了
udp_prot,便再找一下它在源代码中的引用情况,果然发现在inet_create中有这么一句:
...
prot=&udp_prot;
...
其实如果前面看inet_create函数时仔细一点会早点发现了,但我总没有这么细心:)。
我们顺着udp_sendmsg往下走:
在这个函数的主要作用是填充UDP头(源端口,目的端口等),接着调用了
ip_route_output,作用是查找出去的路由,而后:
...
ip_build_xmit(sk,
(sk->no_check == UDP_CSUM_NOXMIT ?
udp_getfrag_nosum :
udp_getfrag),
&ufh, ulen, &ipc, rt, msg->msg_flags);
...
ip_build_xmit函数的很大比例是生成sk_buff,并为数据包加入IP头。
后面有这么一句:
...
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,output_maybe_reroute);
...
简单的说,在没有防火墙代码干预的情况下,你可以将此处理解为直接调用output_maybe_reroute,
(具体可参看绿盟月刊14期中的《内核防火墙netfilter入门 》)
而output_maybe_reroute中只有一句:
return skb->dst->output(skb);
依旧照上面的方法(不过这个确实不太好找),发现其实这个指针是在ip_route_output中指定的,
(提示:ip_route_output_slow中:rth->u.dst.output=ip_output;),ip_route_output的作用
便是查找路由,并将结果记录到skb->dst中。
于是,我们开始看ip_output函数了,而它马上又走向了ip_finish_output~~。
每个网络设备,如网卡,在内核中由一个net_device表示,在ip_finish_output中找到其用到的设备
(也是在ip_route_output中初始化的),这个参数在会传给netfilter在NF_IP_POST_ROUTING点登记
的函数,结束后调用ip_finish_output2,而这个函数中又会调用:
...
hh->hh_output(skb);
...
闲话少叙,实际调用了dev_queue_xmit,到此我们完成了TCP/IP层的工作,开始数据链路层的处理。
在做了一些判断之后,实际的调用是这句:
...
dev->hard_start_xmit(skb, dev);
...
这个函数是在网卡的驱动程序中定义的,每个不同的网卡有不同的处理,我的网卡是比较通用的3c509
(其驱动程序是3c509.c),在网卡处理化的时候(el3_probe),有:
...
dev->hard_start_xmit = &el3_start_xmit;
...
再往下便是IO操作,将数据包真正的发到网络上去,至此发送过程结束。
中间我说的有些草率,完全没顾的上中间的如出错,阻塞,分片等特殊处理,只是将理想的过程描述出来。
这篇短文的目的也只是帮助大家建立个大致的印象,其实每个地方的都有非常复杂的处理(尤其是TCP部分)。
2.3 接受数据
当有数据到达网卡的时候,会产生一个硬件中断,然后调用网卡驱动程序中的函数来处理,对我的3c509网卡来说,
其处理函数为:el3_interrupt。(相应的IRQ号是在系统启动,网卡初始化时通过request_irq函数决定的。)
这个中断处理程序首先要做的当然就是进行一些IO操作将数据读入(读IO用inw函数),当数据帧成功接受后,
执行el3_rx(dev)进一步处理。
在el3_rx中,收到的数据报会被封装成struct sk_buff,并脱离驱动程序,转到通用的处理函数netif_rx
(dev.c)中。为了CPU的效率,上层的处理函数的将采用软中断的方式激活,netif_rx的一个重要工作就
是将传入的sk_buff放到等候队列中,并置软中断标志位,然后便可放心返回,等待下一次网络数据包的到来:
...
__skb_queue_tail(&queue->input_pkt_queue,skb);
__cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ);
...
这个地方在2.2内核中一直被称为"底半"处理--bottom half,其内部实现基本类似,目的是快速的从中断中返回。
过了一段时间后,一次CPU调度会由于某些原因会发生(如某进程的时间片用完)。在进程调度函数即schedule()
中,会检查有没有软中断发生,若有则运行相应的处理函数:
...
if (softirq_active(this_cpu) & softirq_mask(this_cpu))
goto handle_softirq;
handle_softirq_back:
...
...
handle_softirq:
do_softirq();
goto handle_softirq_back;
...
在系统初始化的时候,具体说是在net_dev_init中,此软中断的处理函数被定为net_rx_action:
...
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
...
当下一次进程调度被执行的时候,系统会检查是否发生NET_TX_SOFTIRQ软中断,若有则调用net_rx_action。
net_tx_action函数既是2.2版本中的net_bh函数,在内核中有两个全局变量用来登记网络层的,
一个是链表ptype_all,另外一个是数组ptype_base[16],他们记载了所有内核能够处理
的第三层(按照OSI7层模型)协议。每个网络层的接收处理由一个
struct packet_type表示,而这个结构将通dev_add_pack函数将他们登记到ptype_all或
ptype_base中。只有packet_type中的type项为ETH_P_ALL时,才会登记到ptype_all链表
中,否则如ip_packet_type,会在数组ptype_base[16]找到相应的位置。两者不同点是
如果是以ETH_P_ALL类型登记,那么处理函数会受到所有类型的包,否则只能处理自己登记
的类型的。
skb->protocol是在el3_rx中赋值的,其实就是以太帧头信息中提取出的上层协议名,对
于我们的例子来说,这个值是ETH_P_IP,所以在net_tx_action中,会选择IP层的接收处理
函数,而从ip_packet_type 不难看出,这个函数便是ip_recv()。
pt_prev->func(实际指向ip_recv)前面有一个atomic_inc(&skb->users)操作(在2.2
内核中这个地方是一句skb_clone,原理类似),目的是增加这个sk_buff的引用数。网络层
的接收函数在处理完或因为某些原因要丢弃此sk_buff时(如防火墙)会调用kfree_skb,
而kfree_skb中首先会检查是否还有其他地方需要此函数,如果没有地方再用,才真正释放
此内存(__kfree_skb),否则只是计数器减一。
现在我们便来看看ip_recv(net/ipv4/ip_input.c)。这个函数的操作是非常清晰的:
首先检查这个包的合法性(版本号,长度,校验和等是否正确),如果合法则进行接下来
的处理。在2.4内核中,为了灵活处理防火墙代码,将原来的一个ip_recv分成了两部分,
即将将原来的的ip_recv的后半段独立出一个ip_rcv_finish函数。在ip_rcv_finish中,
一部分是带有IP选项(如源路由等)的IP包,例外就是通过ip_route_input查找路由,
并将结果记录到skb->dst中。此时接收到的包有两种,发往本地进程(需要传往上层协议)
或转发(用作网关时),此时需要的处理函数也不相同,如果传往本地,则调用ip_local_deliver
(/net/ipv4/ip_input.c),否则调用ip_forward(/net/ipv4/ip_forward.c).skb->dst->input
这个函数指针会将数据报领上正确的道路。
对我们的例子而言,此时应该是调用ip_local_deliver的时候了。
发来的包很有可能是碎片包,这样的话则首先应该把它们组装好再传给上层协议,这当然也是
ip_local_deliver函数所做的第一份工作,如果组装成功(返回的sk_buff不为空),则继续处
理(详细的组装算法可参见绿盟月刊13期中的《IP分片重组的分析和常见碎片攻击》)。
但此时代码又被netfilter一分为二了,象前面一样,我们直接到后半段,即ip_local_deliver_finish
(/net/ipv4/ip_input.c)中去。
传输层(如TCP,UDP,RAW)的处理被登记到了inet_protos中(通过inet_add_protocol)。
ip_local_deliver_finish会根据IP头信息中的上层协议信息(即iph->protocol),调用相应的处理函数。为了简便,我们采用了udp,此时的ipprot->handler实际便是udp_rcv了。
前面已经提到,在应用程序中建立的每个socket在内核中有一个struct socket/struct sock对应。udp_rcv会通过udp_v4_lookup首先找到在内核中的sock,然后将其作参数调用
udp_queue_rcv_skb(/net/ipv4/udp.c)。马上,sock_queue_rcv_skb函数被调用,此函数将sk_buff放入等待队列,然后通知上层数据到达:
...
kb_set_owner_r(skb, sk);
skb_queue_tail(&sk->receive_queue, skb);
if (!sk->dead)
sk->data_ready(sk,skb->len);
return 0;
...
sk->data_ready的定义在sock结构初始化的时候(sock_init_data):
...
sk->data_ready=sock_def_readable;
...
现在我们便要从上往下看起了:
进程B要接收数据报,在程序里调用:
...
read(sockfd,buff,sizeof(buff));
...
此系统调用在内核中的函数是sys_read(fs/read_write.c)以下的处理类似write的操作,不再详述.udp_recvmsg函数会调用skb_recv_datagram,如果数据还没有到达,且socket设为阻塞模式时,进程会挂起(signal_pending(current)),直到data_ready通知进程资源得到满足后继续处理(wake_up_interruptible(sk->sleep);)。
2.4 skbuff
网络代码中有大量的处理涉及对sk_buff的操作,尽管此文中尽量将其回避了,但在仔细分析的时候则必须对此作分析,数据包在网络协议层是以sk_buff的形式传送处理的,可以说它是网络部分最重要的数据结构。具体分析建议参看alan cox的《Network Buffers And Memory Management》,这片发表
在1996年10月的linux journal上。
这里引用phrack 55-12期中的一幅图,尽管它只描绘了sk_buff的极小的一个侧面,但却非常有用,尤其是当你像我一样总忘记了skb_put是向前还是向后调指针的时候:)
--- -----------------hand
^ | |
| | | ^ skb_push
| | | |
| -----------------data--- ---
| | | ^ |
true | | | v skb_pull
size | | len
| | | | ^ skb_trim
| | | v |
| -----------------tail--- ---
| | | |
| | | v skb_put
v | |
--- -----------------end
linux网络层效率:在linux的网络层代码中指针被大量应用,其目的就是避免数据拷贝这类耗费系统资源的操作。
一个数据包的数据段部分在读入或发出时只经过两次拷贝,即从网卡中考到核心态内存,和从核心态内存考到
用户态内存。前些天看到,在一些提高sniffer抓包效率的尝试中,turbo packet(一个内核补丁)采用了核心态和
用户态共享一段内存的办法,又减少了一次数据拷贝,进一步提高了效率。
3 后记:
这次的投稿又是到了最后关头仓促写出来的,看着里面拙劣的文笔,实在觉得有点对不住观众~~如果有时间
我会把这部分好好重写的,其实这也是我一直的愿望:)
4 参考文献:
[1.] phrack 55-12期
[2.]
[3.] Network Buffers And Memory Management Alan Cox
[4.] 浙大源码分析报告《Linux网络设备分析》潘纲
[5.] Linux IP Networking--A Guide to the Implementation and Modification of theLinux Poptocol Stack
Glenn Herrin May 31,2000
第三部分:Alan Cox的妙文: Network Buffers: Introduction
An article on Linux by Alan Cox. Reproduced and updated from the .
IntroductionThe Linux operating system implements the industry standard Berkeley socket API, which has its origins in the BSD unix developments (4.2/4.3/4.4 BSD). In this article we will look at the way the memory management and buffering is implemented for network layers and network device drivers under the existing Linux kernel as well as explaining why some things have changed over time.
本文中讲述了linux内核有关网络层、网络设备的内存管理,缓存执行;同时也介绍了随着时间的推移而改变的一些内容。
Network Buffers: Implementation 网络缓存:执行 ImplementationThe primary goal of the sk_buff routines is to provide a consistent and efficient buffer handling method for all of the network layers, and by being consistent to make it possible to provide higher level sk_buff and socket handling facilties to all the protocols.
An sk_buff is a control structure with a block of memory attached. There are two primary sets of functions provided in the sk_buff library. Firstly routines to manipulate doubly linked lists of sk_buffs, secondly functions for controlling the attached memory. The buffers are held on linked lists optimised for the common network operations of append to end and remove from start. As so much of the networking functionality occurs during interrupts these routines are written to be atomic. The small extra overhead this causes is well worth the pain it saves in bug hunting.
一个sk_buff是附带一块内存的控制结构,sk_buff库主要提供两个系列的功能函数。首先是控制双倍链接的sk_buffs链表的规程,其次便是控制附属内存的函数。
We use the list operations to manage groups of packets as they arrive from the network, and as we send them to the physical interfaces. We use the memory manipulation routines for handling the contents of packets in a standardised and efficient manner.
我们使用链表操作来管理从网络而来或者向物理接口而去的分组群。我们使用内存管理规则来标准且高效地处理分组的内容
Network Buffers: Functions Provided网络缓存:提供的函数
Functions ProvidedThe following block of functions allow the user to manipulate the sk_buff lists. While it is possible to manipulate the lists directly by hand, this is strongly frowned upon unless neccessary. The list itself is a ring of buffers including the list header. Each has a prev and next pointer, which point back and forward to the previous and next member of the list.
用户可以使用下面一系列的函数来控制sk_buff链表。不提倡在可能的情况自己手动直接去修改链表,除非必须。链表本身是一个缓存的ring,包括链表头。每个都包含一个“前一个”或“下一个”的指针。
In order to optimise performance the header has its previous and next fields aligned to match those of the sk_buff header so that from both structures it is legal to look at the prev and next fields.
为了优化操作,头标有它自己的“前”或“后”域,用来与相应的sk_buff头标匹配。
struct sk_buff *skb_dequeue(struct skb_buff_head *list)
This takes the first buffer from a list. If the list is empty a NULL pointer is returned. This is used to pull buffers off queues. The buffers are added with the routines skb_queue_head and skb_queue_tail. If you know that any locks are already held you can use __skb_dequeue in the same way. This variant of the function avoids interrupt locking overhead.
将一个缓存从链表中移除。如果链表本空,则返回一个NULL指针。这个函数用来把缓存从队列中拉出去。按skb_queue_head 和 skb_queue_tail 的规程,可以加入新的缓存。如果你确信所有的锁定(lock?)都被保持,则你可以按同样的方法使用__skb_dequeue。 函数的变量避免了突发性的上层lock。
int skb_peek(struct sk_buff_head *list)
Returns a pointer to the first item on the list. Since the list is not locked by this routine anything using skb_peek must be careful to have the list locked before it does the peek and not to unlock it until it has finished with the data. In an ideal world skb_peek would not exist. For for some things it is however just too useful. Programmers should treat skb_peek as an invitation to bugs in their code and be very aware of race conditions.
返回指向链表第一个条目的指针。因为链表并不因这条规程而锁定,任何skb_peekde的使用都需要注意在peek之前都需要锁定链表,并且一直到相应数据使用结束前都不应该解锁。在一个理想世界中skb_peek并不应该存在,然而遇到有些事情时它又很有用。编程者可以使用skb_peek来发现代码中的bug,以及清楚地了解特征环境。
int skb_queue_empty(struct sk_buff_head *list)
Returns true if the list is empty.
如果链表为空,则返回true。
void skb_queue_head(struct sk_buff *skb)
This function places a buffer at the start of a list. As with all the list operations it is atomic. The faster __skb_queue_head form exists if you know the list operation is safe from pre-emption.
这个函数将一个缓存放到一个链表的起始。如果你知晓自上一次清空后链表操作是安全的,你可以使用更快的__skb_queue_head函数。
void skb_queue_head_init(struct sk_buff_head *list)
Initialise an sk_buff_head structure. This must be done before the list is ever used. It must not be called again, nor the list head freed until the list is empty.
初始化一个sk_buff_head结构。这个必须在链表被使用之前完成,并且不能再被调用;在链表为空之前,链表头不能被释放。
__u32 skb_queue_len(struct sk_buff_head *list)
Reports the number of sk_buffs sitting on the queue.
报告队列中sk_buff的数目。
void skb_queue_tail(struct sk_buff *skb)
Place a buffer at the end of a list, which is the most commonly used function. Almost all the queues are handled with one set of routines queueing data with this function and another set removing items from the same queues with skb_dequeue. The faster __skb_queue_tail form exists if you know the list operation is safe from pre-emption.
将一个缓存放置到链表的尾部。这是一个经常用到的函数。几乎所有的队列使用这个函数来处理一系列队列数据,同时使用skb_dequeue来将另一系列的条目从同一队列中移除。如果你知晓自上一次清空后链表操作是安全的,你可以使用更快的__skb_queue_tail函数。
void skb_unlink(struct sk_buff *skb)
This function removes a buffer from whatever list it was on. The buffer is not freed merely removed from the list. To make some operations easier you can always called skb_unlink() on a buffer not in a list, and you need not know what list it is on. This enables network code to pull a buffer out of use even when the network protocol has no idea who is currently using it. A seperate locking mechanism is provided so device drivers do not find someone removing a buffer they are using at that moment. If the list holding the sk_buff is known to be safe from pre-emption the faster void __skb_unlink(struct sk_buff *skb) form can be used.
这个函数无条件地将一个缓存从任意链表上移除。缓存并未被释放,而只是从链表上移除。为了使某些操作更简便,我们常常可以对一个缓存调用skb_unlink(),而不必知道这个缓存具体处在哪个链表上。这样可以使网络代码让一个缓存退出使用,尽管此时网络协议也许并不清楚谁正在使用这个缓存。我们提供了一个独立的锁定机制,这样设备驱动器就不会发现他们在那一时刻正在使用的缓存被某人移走。如果你知晓持有sk_buff的链表自上一次清空后是安全可靠的,你可以使用更快捷的__skb_unlink(struct sk_buff *skb)。
Some more complex protocols like TCP keep frames in order and re-order their input as data is received. Two functions void skb_append(struct sk_buff *entry, struct sk_buff *new_entry) and void skb_insert(struct sk_buff *entry, struct sk_buff *new_entry) exist to allow users to place sk_buff's before or after a specific buffer in a list.
一些更复杂的协议比如TCP保持帧的顺序,当数据接收后会重新排列它们的顺序。有两个函数可以帮助用户将sk_buff放在链表中某一特定缓存之前或之后,这两个函数是:
void skb_append(struct sk_buff *entry, struct sk_buff *new_entry)
void skb_insert(struct sk_buff *entry, struct sk_buff *new_entry)
Network Buffers: Higher Level Support Routines
网络缓存:高层支持程序
Higher Level Support RoutinesThe semantics of allocating and queueing buffers for sockets also involve flow control rules and for sending a whole list of interactions with signals and optional settings such as non blocking. Two routines are designed to make this easy for most protocols.
套结字分配、排列缓存的句法同时也包含流控规则,以及发送相互作用的包括信号、可选设置比如非模块化的链表的规则。设计了两套规程来使这些操作对绝大多数协议来说变得简便。
The sock_queue_rcv_skb function is used to handle incoming data flow control and is normally used in the form
sock_queue_rcv_skb函数用来处理对输入数据的流控,一般按下述形式使用:
sk=my_find_socket(whatever); if(sock_queue_rcv_skb(sk,skb)==-1) { myproto_stats.dropped++; kfree_skb(skb,FREE_READ); return; }This function uses the socket read queue counters to prevent vast amounts of data being queued to a socket. After a limit is hit data is discarded. It is up to the application to read fast enough or as in TCP for the protocol to do flow control over the network. TCP actually tells the sending machine to shut up when it can no longer queue data.
这个函数使用套结字来读取队列计数器,避免过多的数据被送入一个Socket。当达到限制数目时,数据将被丢弃。这样一个应用就有必要以足够快的速度读取数据,或者在TCP中协议要在网络中进行流控。当不能再列入数据时,TCP会告诉发送者停止发送数据,
On the sending side sock_alloc_send_skb handles all the semantics of blocking until there is space in the send queue so you cannot tie up all of memory with data queued for a slow interface, signal handling and the non blocking flags. The semantics and subtleties of this function are quite complex, and its strongly recommended protocol writers try very hard to use it.
Many protocol send routines have this function doing almost all the work.
在发送方,由sock_alloc_send_skb处理模块的所有语义,直到发送队列中有了可用空间,这样你便不能对队列中输出到一个较慢接口的带有非模块化标记、需要进行信号处理的数据,使用所有的内存。这个函数的语义及精妙之处非常复杂,以致极力推荐它的协议作者都要非常辛苦的去使用它,呵呵~~
很多协议发送规程几乎在所有的工作中都要使用到这个函数。
第四部分:Linux Kernel核心中文手册
Socket Buffers
使用分成许多层,每一层使用其它层的服务,这样的网络协议的一个问题是,每一个协议都需要在传送的时候在数据上增加协议头和尾,而在处理接收的数据的时候需要删除。这让协议之间传送数据缓冲区相当困难,因为每一层都需要找出它的特定的协议头和尾在哪里。一个解决方法是在每一层都拷贝缓冲区,但是这样会没有效率。替代的, Linux 使用 socket 缓冲区或者说 sock_buffs 在协议层和网络设备驱动程序之间传输数据。 Sk_buffs 包括指针和长度域,允许每一协议层使用标准的函数或方法操纵应用程序数据。
图 10.4 显示了 sk_buff 数据结构:每一个 sk_buff 都有它关联的一块数据。 Sk_buff 有四个数据指针,用于操纵和管理 socket 缓冲区的数据
参见 include/linux/skbuff.h
head 指向内存中的数据区域的起始。在 sk_buff 和它相关的数据块被分配的时候确定的。
Data 指向协议数据的当前起始为止。这个指针随着当前拥有这个 sk_buff 的协议层不同而变化。
Tail 指向协议数据的当前结尾。同样,这个指针也随拥有的协议层不同而变化。
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_buffs 和删除其中的 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 backlo 队列,确定接收到的报文需要传送到那个协议层。当 Linux 网络层初始化的时候,每一个协议都登记自己,在 ptype_all 列表或者 ptype_base hash table 中增加一个 packet_type 的数据结构。这个 packet_type 数据结构包括协议类型,一个网络驱动设备的指针,一个协议的数据接收处理例程的指针和一个指针,指向这个列表或者 hash table 下一个 packet_type 数据类型。 Ptype_all 链表用于探测( snoop )从任意网络设备上接收到的所有的数据报文,通常不使用。 Ptype_base hash table 使用协议标识符 hash ,用于确定哪一种协议应该接收进来的网络报文。网络的 bottom half 把进来的 sk_buff 的协议类型和任一表中的一个或多个 packet_type 条目进行匹配。协议可能会匹配一个或多个条目,例如当窥测所有的网络通信的时候,这时,这个 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 数据结构。包括使用的源 IP 地址,网络 device 数据结构的地址,有时候还会有一个预先建立的硬件头。这个硬件头和网络设备相关,包含源和目的物理地址和其它同介质相关的信息。如果网络设备是以太网设备,硬件头会在图 10.1 中显示,其中的源和目的地址会是物理的以太网地址。硬件头和路由缓存在一起,因为在这个路由传送的每一个 IP 报文都需要追加这个头,而建立这个头需要时间。硬件头可能包含必须使用 ARP 协议才能解析的物理地址。这时,发出的报文会暂停,直到地址解析成功。一旦硬件地址被解析,并建立了硬件头,这个硬件头就被缓存,这样以后使用这个接口的 IP 报文就不需要进行 ARP 。
参见 include/net/route.h
10.5.4 Data Fragmentation
每一个网络设备都有一个最大的报文尺寸,它无法传送或接收更大的数据报文。 IP 协议允许这种数据,会把数据分割成网络设备可以处理的报文大小的更小的单元。 IP 协议头包含一个分割字段,包含一个标记和分割的偏移量。
当要传输一个 IP 报文的时候, IP 查找用来发送 IP 报文的网络设备。通过 IP 路由表来查找这个设备。每一个设备都有一个字段描述它的最大传输单元(字节),这是 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 就被去除,并假设这个消息在传输过程中丢失了。然后由高层的协议负责重新传输这个消息。
参见 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 地址的机器会回应这个 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 表包含一个指针( arp_tables 向量表)的表,把 arp_table 的条目链接在一起。这些条目被缓存,以加速对它们的访问。每一个条目用它的 IP 地址的最后两个字节做表的索引进行查找,然后跟踪这个条目链,直到找到正确的条目。 Linux 也缓存从 arp_table 条目预先建立的硬件头,用 hh_cache 数据结构的形式进行缓存。
当请求一个 IP 地址转换的时候,没有对应的 arp_table 条目, ARP 必须发送一个 ARP 请求消息。它在表中创建一个新的 arp_table 条目,并把需要地址转换的包括了网络报文的 sk_buff 放到这个新的条目的 sk_buff 队列。它发出一个 ARP 请求并让 ARP 过时计时器运行。如果没有回应, ARP 会重试几次。如果仍旧没有回应, ARP 会删除这个 arp_table 条目。任何排队等待这个 IP 地址进行转换的 sk_buff 数据结构会被通知,由传输它们的上层协议负责处理这种失败。 UDP 不关心丢失的报文,但是 TCP 会在一个建立的 TCP 连接上试图重新发送。如果这个 IP 地址的属主用它的硬件地址应答,这个 arp_table 条目标记为完成,任何排队的 sk_buff 会被从对队列中删除,继续传送。硬件地址被写到每一个 sk_buff 的硬件头中。
ARP 协议层也必须回应指明它的 IP 地址的 ARP 请求。它登记它的协议类型( ETH_P_ARP ),产生一个 packet_type 数据结构。这意味着网络设备接收到的所有的 ARP 报文都会传给它。象 ARP 应答一样,这也包括 ARP 请求。它使用接收设备的 device 数据结构中的硬件地址产生 ARP 应答。
网络拓扑结构不断变化, 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 目标的路由。象所有缓存一样,它必须只包括最常访问的路由,它的内容是从转发信息数据库中得来的。
路由通过 BSD socket 接口的 IOCTL 请求增加和删除。这些请求被传递到具体的协议去处理。 INET 协议层只允许具有超级用户权限的进程增加和删除 IP 路由。这些路由可以是固定的,或者是动态的,不断变化的。多数系统使用固定路由,除非它们本身是路由器。路由器运行路由协议,不断地检查所有已知 IP 目标的可用的路由。不是路由器的系统叫做末端系统( end system )。路由协议用守护进程的形式来实现,例如 GATED ,它们也使用 BSD socket 接口的 IOCTL 来增加和删除路由。
10.7.1 The Route Cache
不论何时查找一个 IP 路由的时候,都首先在路由缓存中检查匹配的路由。如果在路由缓存中没有匹配的路由,才查找转发信息数据库。如果这里也找不到路由, IP 报文发送会失败,并通知应用程序。如果路由在转发信息数据库而不在路由缓存中,就为这个路由产生一个新的条目并增加到路由缓存中。路由缓存是一个表( ip_rt_hash_table ),包括指向 rtable 数据结构链的指针。路由表的索引是基于 IP 地址最小两字节的 hash 函数。这两个字节通常在目标中有很大不同,让 hash value 可以最好地分散。每一个 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_zones hash 表指向。 Hash 索引取自 IP 子网掩码。所有通向同一子网的路由都用排在每一个 fib_zone 数据结构的 fz_list 队列中得的成对的 fib_node 和 fib_info 数据结构来描述。如果这个子网的路由数目变得太大,就生成一个 hash table ,让 fib_node 数据结构的查找更容易。
对于同一个 IP 子网,可能存在多个路由,这些路由可能穿过多个网关之一。 IP 路由层不允许使用相同的一个网关对于一个子网有多于一个路由。换句话说,如果对于一个子网有多个路由,那么要保证每一个路由都是用不同的网关。和每一个路由关联的是它的量度( metric ),这是用来衡量这个路由的益处。一个路由的量度,基本上,是它在到达目标子网之前必须跳过的子网数目。这个量度越高,路由越差。