Documents/LinuxIPNetworking
第七章
7.数据包转发
这一章从纯路由侧面(IP数据包转发)来描述消息传输。概述整个处理过程,研究经过每一层次的数据包,详细描述每一层的操作,总结内核代码的实现。
7.1.概述
图7.1是转发过程的一个抽象图表。(实质是一个接收和发送过程的组合。)
Figure 7.1: IP Forwarding.
一个转发数据包到达后便产生一个中断调用,同时系统告诉设备有消息来了,设备分配存储空间并告诉总线把消息存进来,然后把数据包传给链路层,在链路层把数据包存入后备队列,设置网络标志以让后面的bottom-half程序能够运行,最后把CPU控制权还给当前进程。
当进程调度器下次再运行时,它发现有网络任务则运行网络bottom-half程序,该程序把数据包从后备队列中取出,检查是否是IP数据包,然后把数据包传给接收函数。IP网络层检查数据包是否有误并为数据包选择路由,数据包再向上传给传输层(如TCP或UDP,如果是发给本地主机的情况下)或转给IP转发函数处理。在转发函数中,IP协议检查数据包,如果有错误则发送ICMP消息。然后复制数据包到新的缓存中,如果需要则进行数据包分组。
最后IP网络层把数据包传给链路层函数,该函数把数据包移入发送设备的xmit队列中并确认设备是否已经把数据包发送出去了。最后,设备(如网卡)告诉总线要发送数据包。
7.2. IP协议转发流程
7.2.1.接收一个数据包
· 唤醒接收设备(中断方式)
· 检查通信媒介(设备设备操作)
· 接收到连接头信息
· 为数据包分配存储空间
· 告诉总线把数据包存入缓存中
· 把数据包存入后备队列中
· 设备标志如果可能则网络bottom-half程序得以运行
· 把CPU控制权还给当前进程
7.2.2.运行网络“bottom-half”程序
· 运行网络bottom-half程序(调度器调度)
· 发送正在等待发送的所有数据包并阻止中断(net_bh)
· 遍历后备队列中的所有数据包并把数据包传给因特网接收协议--IP
· 再次清除发送队列
· 退出bottom-half程序
7.2.3.在IP网络层处理一个数据包
· 检查数据包是否有误:太短?太长?版本不对?校验和不对?
· 如果需要则组合数据包
· 取得数据中的路由信息(可能是发给本地主机或需要被转发)
· 把数据包传给后面的处理子程序(在这个例子中重新传给另一个主机)
7.2.4.在IP网络层转发一个数据包
· 检查TTL域值(并减1)
· 检查数据包的路由信息是否有误
· 如果存在任何问题则发回ICMP响应消息给发送者
· 把数据包复制到新建立的缓存中并释放原数据包占用的内存
· 设置所有IP网络参数
· 如果要发出去的数据包太大了则进行分组
· 把数据包发给能使之到达目标主机的设备输出函数
7.2.5.发送一个数据包
· 把数据包存入到设备输出队列中
· 唤醒设备
· 唤醒调度器运行设备驱动程序
· 检查通信媒介(设备操作)
· 发送连接头消息
· 告诉总线通过通信媒介传输数据包
7.3.内核函数
下面按字母次序列出对于与IP转发消息有关的最重要的Linux内核函数列表,列表中注明了源代码位置及实现方法,下面函数的调用开始于函数“DEVICE_rx()”。
dev_queue_xmit() - net/core/dev.c (579) 调用函数“start_bh_atomic()” 如果设备有一个队列 调用函数“enqueue()”添加数据包到队列中 调用函数“qdisc_wakeup() [= qdisc_restart()]”唤醒设备 否则调用函数“hard_start_xmit()” 调用函数“end_bh_atomic()” DEVICE->hard_start_xmit() -与设备有关, drivers/net/DEVICE.c 检查通信媒介是否打开 发送消息头 告诉总线要发送数据包 更新状态值 >>> DEVICE_rx() -与设备有关, drivers/net/DEVICE.c (通过中断取得控制权) 检查状态以确定是否要接收数据 调用函数“dev_alloc_skb()”为数据包分配存储空间 从系统总线取得数据包 调用函数“eth_type_trans()”确定协议类型 调用函数“netif_rx()” 更新网卡状态 (退出中断程序) ip_finish_output() - include/net/ip.h (140) 按已知的路由信息设置发送设备为输出设备 调用输出函数把数据包送到目的地[= dev_queue_xmit()] ip_forward() - net/ipv4/ip_forward.c (72) 检查路由器警告信息 如果数据包不是发给任何主机的则抛弃数据包 如果TTL变量值为0则抛弃数据包并返回ICMP消息 如果根据路由信息无法转发数据包则抛弃数据包并返回ICMP消息 如果需要则发送ICMP消息告诉发送者数据包需要更改路径发送 复制并释放处理过的数据包 变量减1 如果需要设置则调用函数“ip_forward_options()”进行设置 调用函数“ip_send()” ip_rcv() - net/ipv4/ip_input.c (395) 检查数据包是否有错误: 长度不对(太短或太长) 版本不对(不是4) 校验和错误 调用函数“__skb_trim()”去掉数据包中下层协议所加的包头信息 如果需要则进行数据包组合 调用函数“ip_route_input()”“ip_route_input()”在数据包中加入路由信息 检查并处理IP设置 返回函数“skb->dst->input() [= ip_forward()]”返回值 ip_route_input() - net/ipv4/route.c (1366) 调用函数“rt_hash_code()”取得路由表的索引值 循环遍历路由表(从哈希位置开始)查找数据包所需要的路由信息 如果找到: 更新路由状态(时间和使用范围) 设置数据包路由信息为匹配到的路由表中某项信息 返回成功信息 其它 检查是否是多路发送地址 返回函数“ip_route_input_slow()”(获取路由)调用结果 ip_route_output_slow() - net/ipv4/route.c (1421) 如果源地址是已知的则寻找输出设备 如果目标地址未知则设置为回环地址 函数查找路由 为新的路由表项信息分配内存 初始化路由表项中的源地址,目标地址,TOS,输出设备,标签等信息 调用函数“rt_set_nexthop()”查找一下个目标地址 返回函数“rt_intern_hash()”调用返回值,该函数用于安装路由信息到路由表中 ip_send() - include/net/ip.h (162) 如果数据包对于发送设备来说太大则调用函数“ip_fragment()” 调用函数“ip_finish_output()” net_bh() - net/core/dev.c (835) (由调度器调用) 如果存在数据包要等着发送,则调用函数“qdisc_run_queues()” (看发送一节) 当后备队列不为空时 让其它bottom-half程序运行 调用函数“skb_dequeue()”取下一个数据包 如果数据包是要发给其它主机的(FASTROUTED类型数据包)则把数据包加入到发送队列中 遍历协议列表以匹配协议类型 调用函数“pt_prev->func() [= ip_rcv()]”把数据包传给相应的协议处理函数 调用函数“qdisc_run_queues()”以清空输出队列(如果需要) netif_rx() - net/core/dev.c (757) 把时间赋值给“skb->stamp” 如果后备队列太满则抛弃数据包 否则 调用函数“skb_queue_tail()”把数据包加入到后备队列中 标记bottom-half函数为后面能够被执行 qdisc_restart() - net/sched/sch_generic.c (50) 把数据包移出队列 调用函数“dev->hard_start_xmit()” 更新状态 如果出现错误则把数据包再加入到队列中 rt_intern_hash() - net/ipv4/route.c (526) 把新路由信息加入到路由表中
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/28/1543754.aspx
第八章
8. IP路由基础
这章讲述IP路由的基础知识,概述路由选择过程,研究路由表的建立和更新方法,总结内核代码的实现方法。
8.1.概述
Linux维护三组的路由数据:一个是计算机直接连接到主机的路由信息(通过局域网,如下面的例子),另两个是计算机间接连接的路由信息(通过IP网络)。研究图8.1,可以看出一般情况下一个计算机是如何和外部连接的。
Figure 8.1: General Routing Table Example.
图中邻居表内容是终端和主机物理连接的地址信息(所以表名为“邻居”),包含了相邻设备之间连接的信息和进行数据交换的协议。Linux采用地址解析协议(ARP)来维护和更新这个表;表中每项内容是动态的,当需要时则添加,如果在一定时间内没用则被删除。(然而,管理员可以设置某些内容为永恒的且不被动态更新。)
Linux采用两组相关联的路由表来维护IP地址:一个是指向任何可能的地址的较全面的转发信息表(FIB),一个是内容较少(查找更快)的路由信息较经常被使用的路由缓存。当一个IP数据包需要传给远方的一个主机时,在IP网络层首先是从路由缓存中根据源地址、目标地址和协议类型进行查找,如果找到,IP则使用这个路由信息,如果没有找到,IP则从较完整(但较慢)的FIB中查找路由信息,并在路由缓存中建立一项新的路由信息,然后使用这个新的路由信息。FIB表中的内容是半永久性的(只有当路由器启动或关闭时才被更新),而路由缓存中的内容则保存到没有用时为止(长时间没有被使用)。
8.2. 路由表
注意:在这些表中存在诸如u32(主机字节顺序)和__u32(网络字节顺序)的不同数据类型。在Intel架构上它们都等同于无符号整型数据,实际上总要进行数据转换(使用函数“ntohl”);这个类型只表示存储的字节顺序而已。
8.2.1. 邻居表
邻居表(它的结构如图8.2)包含有关终端和主机物理连接的信息。(注意源代码中采用欧文拼写法,如“neighbour”。)表中每项内容(通常)不是永恒不变的,表中可能没有内容(如果计算机最近没有通过网络进行通信)或可能有和网络中每个主机物理连接的路由(如果该主机已经和网络中所有主机连接过)。该实际上是另一种结构,表中有地址、设备类型、协议和统计信息。
Figure 8.2: Neighbor Table Data Structure Relationships.
struct neigh_table *neigh_tables - 这个全局变量是一个指向邻居表数组的指针;每个表包含一组函数、数据和一个有关一组相邻主机的详细信息。这是一个非常详细、低层次的表,包含的详细信息有诸如传输消息的估计时间、队列长度、设备指针和设备函数指针。
Neighbor Table (struct neigh_table) Structure - 这个结构(一个基本元素表)包含相邻主机信息、相邻主机数据表和pneigh数据。通过同一个连接类型(如同一个以太网卡)的所有计算机都在同一个表中。
· struct neigh_table *next - 指向列表中下一个的表的指针。
· struct neigh_parms parms - 包含消息传输时间、队列长度和统计信息的结构体;实际上这是一个列表的头。
· struct neigh_parms *parms_list - 指向相邻主机信息结构体列表的指针。
· struct neighbour *hash_buckets[] -与这个表有关的相邻主机的哈希表,表中共有NEIGH_HASHMASK+1 (32)桶。
· struct pneigh_entry *phash_buckets[] -包含设备指针和键值的数据结构的哈希表;表中共有PNEIGH_HASHMASK+1 (16)桶。
· 其它域包括定时器信息、函数指针、锁标志和统计信息。
Neighbor Data (struct neighbour) Structure - t这些数据结构包含每个相邻主机的详细信息。
· struct device *dev -指向连接相邻主机的设备的指针。
· __u8 nud_state -状态标志;值可以是未完成、可获取、失效等;也可能包含永久的状态信息和用于ARP协议。
· struct hh_cache *hh - 指向用于向相邻主机通信的设备缓存头的指针。
· struct sk_buff_head arp_queue -指向与某个相邻主机有关的ARP数据包的指针。
· 其它域包括有列表指针、函数(表)指针和统计状态信息。
8.2.2.转发信息库
Figure 8.3: Forwarding Information Base (FIB) conceptual organization.
转发信息库在内核中是最重要的路由结构;它是一个复杂的结构,包含了通过网络掩码到达任何合法IP地址的路由信息。本质上它是一个存有一般上层地址信息和底层非常精细的信息的大表。IP层根据数据包中目标地址在这个表中进行比较,通过最适合的网络掩码在表中进行匹配,如果适配不到,则使用下一个最一般的网络掩码再次比较。当最终匹配到时,IP层把到达远程主机的路由复制到路由缓存中并根据这个路由发送数据。见图8.3和8.4,图中表示FIB的组织和数据结构。(注意图8.3展现了几个不同的FIB性能,如两组单纯网络区的网络信息,而不是一般性的例子)。
struct fib_table *local_table, *main_table -这些全局变量是访问FIB表的入口;它们指向表的结构,通过该结构又可以指向哈希表,通过哈希表指向网络区。main_table变量在/proc/net/route中定义。
FIB Table fib_table Structure - include/net/ip_fib.h - 这些结构包含了函数跳转表和表中每个指针指向包含网络区的哈希表。通常只使用表中的一两个指针。
· int (*tb_functions)() -表操作函数指针(查找,删除,插入等。),这些函数指针在初始过程中的函数fn_hash_function()被设置。
· unsigned char tb_data[0] - 指向与FIB中哈希表关联的指针(尽管它被定义成一个字符数组)。
· unsigned char tb_id -表标识符;255表示local_table,254表示main_table。
· unsigned tb_stamp
Netmask Table fn_hash Structure - net/ipv4/fib_hash.c -这些结构包含指向每个单独区域的指针,通过网络掩码进行数据组织。(每个网络区对应一个唯一的网络掩码。)每个FIB表都有一个这样的结构(除非两个表都指向同一个哈希表)。
· struct fn_zone *fn_zones[33] - 每个网络区的指针(网络掩码中每一位表示一个网络区;fn_zone[0]指向网络掩码为0x0000的网络区,fn_zone[1]指向网络掩码为0x8000的网络区,fn_zone[32]指向网络掩码为0xFFFF的网络区。)
· struct fn_zone *fn_zone_list -指向表中第一个非空网络区(最特殊)的指针;如果网络掩码为0xFFFF的地址则指向这个网络区,否则可能指向0xFFF0、0xFF00、0xF000等网络区。
这些结构包含一些哈希信息和指向哈希表中结点的指针。每个已知的网络掩码都在这里。
· struct fn_zone *fz_next -指向下一个哈希结构中非空的网络区(下一个最接近的网络掩码:如fn_hash- > fn_zone[28]- > fz_next可能指向fn_hash- > fn_zone[27])。
· `struct fib_node **fz_hash - 哈希表中指向某个网络区的指针。
· `int fz_nent - 这个网络区中结点个数。
· `int fx_divisor -在哈希表中和某个网络区有关的桶的个数;大部分网络区有16个桶(除了第一个网络区-0000-回送设备)。
· `u32 fz_hashmask - a mask for entering the hash table of nodes; 15 (0x0F) for most zones, 0 for zone 0). 进入哈希表中结点的方法标志;15(0x0F)表示所有网络区,0表示0网络区。
· int fz_order - 在fn_hash父结构(0到32)中的网络区序号。
· u32 fz_mask - 子网掩码定义,如~((1<<(32-fz_order))-1)形式;例如,对于第一个网络区则掩码是把1左移32减0次(结果为0x10000),再减1(结果为0xFFFF),取反(结果为0x0000);第二个网络区则是0x8000,下一个是0xC000,再下一个是0xE000,0xF000,0xF800,依此到最后一个(32d)是0xFFFF。
这些结构包含网络地址每组唯一的网络地址和指向常用接口信息的指针(如设备和协议);每个已知的网络都有一个这样的结构(unique source/destination/TOS combination)。
· struct fib_node *fn_next -指向下一个结点的指针。
· struct fib_info *fn_info - 指向有关这个结点更多信息的指针(这个指针被其它许多结点共用)。
· fn_key_t fn_key -目标地址中最小且最重要的8位数。(0为回送设备)
· 其它域包含了有关该结点的详细信息(如fn_tos和fn_state)。
Network Protocol Information (fib_info) Structure - include/net/ip_fib.h - 这些结构包含协议和硬件的接口详细信息,因此这些结构对于许多潜在的网络区是通用的;几个网络可能通过相同的接口来寻址(如这里到达外部因特网的方法)。每个接口都有这样的一个结构。
· fib_protocol -用于这个路由的网络协议(如IP协议)的序号。
· struct fib_nh fib_nh[0] -包含用于这个路由的发送和接收传输的设备指针。
· 其它域包含有指针列表、统计信息和参考数据(如fib_refcnt和fib_flags)。
Figure 8.4: Forwarding Information Base (FIB) data relationships.
FIB遍历例子
1. ip_route_output_slow()(路由不在路由缓存中则要调用这个函数)根据源地址172.16.0.7、目标地址172.16.0.34和值为2的TOS设置rt_key结构。
2. ip_route_output_slow()调用fib_lookup()函数并传入搜索关键字。
3. fib_lookup()调用local_table- > tb_lookup()(即fn_hash_lookup函数)在本地表中查找该关键字。
4. fn_hash_lookup()搜索本地表中的哈希表,在最特殊的网络区(24:掩码为以点分隔的10进制255.255.255.0)中开始搜索(fn_zone_list列表变量指向了每个网络区)。
5. fz_key()通过把目标地址与子网掩码进行与运算得到一个查找值,这里得到的查找值是172.16.0.0。
6. fz_chain()执行哈希函数(看fn_hash()),通过与网络区中的fz_hashmask值(15)进行与运算得到哈希表是的结点索引(6)。可惜这个结点是空的,在这个网络区中匹配不到。
7. fn_hash_lookup()转换到下一下非空网络区进行查找-16(子网掩码为255.255.0.0的10进制点分隔形式)(通过当前网络区中的fz_next变量指向下一个网络区)。
8. fz_key()通过把目标地址与子网掩码进行与运算得到一个新查找值,这里得到的查找值是172.16.0.0。
9. fz_chain()执行哈希函数(看fn_hash()),通过与网络区中的fz_hashmask值(15)进行与运算得到哈希表是的结点索引(10)。在索引位置有一个结点。
10.fn_hash_lookup()比较搜索关键值和结点键值。它们不匹配,但搜索关键值比结点键值要小,于是到下一个结点进行比较。
11.fn_hash_lookup()把搜索关键值和新结点关键值进行比较。值一样,于是做些错误检查,把该结点和相关信息变量进行精确匹配。
12.如果所有都匹配,fn_hash_lookup()函数把所有有关路由信息填入fib_result数据结构。(否则继续检查其它结点和其它网络区直到发现一个匹配点或完全失败为止。)
13.取得fib_result数据结构,然后继续按序执行后面的操作,根据取得的数据建立新的路由缓存。
8.2.3.路由缓存
Figure 8.5: Routing Cache Conceptual Organization.
路由缓存是linux操作系统中最快找到路由的方法;路由缓存中保存着当前被使用的路由或最近在哈希表中已被使用的路由。当IP层需要一个路由时,它进入适宜的哈希桶,搜索路由缓存中的链表直到找到匹配,然后根据找到的路径发送数据包。(8.2.2节介绍了在路由缓存中找不到路由的处理方法。)路由按序排成链表,最常用的放在第一个位置,每个路由信息包含了因不再被使用而被移出哈希表的间隔时间和次数。图8.5是一个路由缓存总体描述,图8.6和8.7是数据结构详细图表。
struct rtable *rt_hash_table[RT_HASH_DIVISOR] -这个全局变量包含256个路由缓存链表组成的桶;哈希函数把源地址、目标地址和TOS组合起来,以此在表中找到入口点(0到255之间)。该表内容在/proc/net/rt_cache目录下被列举。
Routing Table Entry (rtable) Structure - include/net/route.h - 这些结构包含到达目的地的路由缓存以及每个路由的标识信息。
· union < struct dst_entry dst; struct rtable* rt_next) > u - 这是表中一个单元;如果需要这个单元结构可以通过rtable中的指向下一个缓存单元的指针快速访问下一个表单元。
· __u32 rt_dst -到达目的地地址。
· __u32 rt_src - 源地址。
· rt_int iif - 输入接口。
· __u32 rt_gateway -通过某个相邻的主机可以到达目的地的主机地址(网关地址)。
· struct rt_key key - 包含缓存查找关键值的数据结构(有src, dst, iif, oif, tos, 和 scope等域)。
· 其它域包含标识、类型和其它各色信息。
Destination Cache (dst_entry) Structure - include/net/dst.h - 这些结构包含特定输入输出函数和路由数据。
· struct device *dev -针对这个路由的输入和输出设备。
· unsigned pmtu - 这个路由能够传输的最大数据包长度。
· struct neighbor *neighbor -指向这个路由邻居表(下一个连接)的指针。
· struct hh_cache *hh -指向硬件缓存的头指针;因为对于每个向个传送的数据包都是用同一个物理链路层,所以这个指针保持着可以被快速访问和重用。
· int (*input)(struct sk_buff*) - 这个路由的输入函数指针(如tcp_recv())。
· int (*output)(struct sk_buff*) -这个路由的输出函数指针(如dev_queue_xmit())。
· struct dst_ops *ops -指向包含这个路由的网络协议类型、网络协议、校验和、路由重定义和释放函数指针的结构体的指针。
· 其它域有统计信息、状态信息和连接到其它路由表单元的指针。
Neighbor Link (neighbor) Structure - include/net/neighbor.h -这些数据结构表示相连的主机信息,为每个主机设置这样的一个数据结构,每隔一个主机为一个跳数,这些结构包含了它们的读取函数的指针和其它信息。
· struct device *dev -指向物理连接到相邻主机的设备指针。
· struct hh_cache *hh - 指向硬件缓存头的指针,缓存头信息在传输中总是先被发给相邻主机。
· int (*output)(struct sk_buff*) - 输出到相邻主机的函数指针(如dev_queue_xmit())。
· struct sk_buff_head arp_queue - t ARP队列中的第一个单元,决定在通信中从这个相邻主机取数据还是向它发数据。
· struct neigh_ops *ops - 指向包含协议类型数据和跟这个连接相关的输出函数的结构指针。
· 其它域包含统计信息、状态信息和其它相邻主机的有关信息。
Figure 8.6: Routing Cache data structure relationships.
Figure 8.7: Destination Cache data structure relationships.
Routing Cache Traversal Example:
1. ip_route_output()(调用这个函数查找路由)调用函数rt_hash_code()并传入一个源地址172.16.1.1、一个目标地址172.16.1.6和值为2的TOS(服务类型)。
2. rt_hash_code()根据源、目标地址和TOS值执行哈希搜索,把搜索结果跟255做与运算得到哈希表位置(5)。
3. ip_route_output()进入哈希表第5个位置。那儿有内容,但目标地址不匹配。
4. ip_route_output()移到下一个单元(通过u.rt_next指针指到最后一个单元)。这次匹配每一情况如:目标地址、源地址、iif是否为0、匹配oif值和TOS是否一致。
5. ip_route_output() 在表单元中新建的dst_cache数据结构更新统计信息,设置与这个路由相应的调用函数指针,返回0值表示执行成功。
8.2.4.更新路由信息
Linux系统只有当需要时才更新路由信息,但路由表会在不情况下发生变化。路由缓存是最不稳定的,虽然FIB总是没发生变化。
当网络传输被改变时邻居表也会被改变。当一个主机要发数据到本地子网中某个地址但这个地址不在邻居表中时,便广播一个ARP请求,当得到回应后便在邻居表中加入一个新单元。邻居表中的表项周期性地被因长时间没用而删除;这个循环不确定性地继续着(除非一个路由被特定标志为永久性ARP)。系统内核自动处理这些大多数的变化。
在大多数主机甚至路由器中的FIB为静态不变的内容;在初始化过程中通过连接所有的路由器可达的每个可能的网络区的路由被写入到FIB中,除非路由器瘫痪不FIB表才会以改变。(见第9章中适于IP路由后台程序的详细介绍)。需要改变FIB表时通过调用函数ioctl()来添加或删除一个网络区。
根据消息传输路由缓存经常被改变。如果一个主机需要发送数据包到一个远程地址,它先在路由缓存中查找该地址(如果需要则在FIB表中查找),然后通过适当的路由把数据包发出去。对于在局域网中通过路由器连接到因特网的主机,每个路由可能连接到一个相邻主机或一个路由器,但大部分路由是指向路由器(对于到远程地址的路由)。这些路由信息当建立连接时就被建立,当网络传输停止时会有较短的超时限制。建立路由和系统内核因超时删除路由都是在IP层完成的。
8.3.函数
下面是按字母顺序排列的Linux内核函数列表,这些都是有关路由的重要函数,这些都有源代码,可以知道函数怎么执行。
arp_rcv() - net/ipv4/arp.c (542) 错误检查(非ARP设备、不是设备、不是发给该主机的数据包、设备类型不匹配等)。 检查操作类型-只检查是否是REPLY和REQUEST 从数据包中提取数据 检查请求是否有错-回送或多路广播地址 检查地址侦测数据包副本(如果需要发送回复信息) 如果是个请求消息并函数ip_route_input()返回值为真: 如果数据包是本地数据包 调用函数neigh_event_ns()查找并更新发来数据包的相邻主机信息。 检查是否是隐藏设备(如果是隐藏设备则不回复) 发送带设备地址的回复信息 否则: 调用函数neigh_event_ns()查找并更新发来数据包的相邻主机信息 调用函数neigh_release() 如果需要则根据地址调用arp_send()函数 否则调用函数pneigh_enqueue()并返回值0 如果消息是个回复消息: 调用函数__neigh_lookup() 检查多路ARP请求是否回复;只保留最快(最近)一个 调用neigh_update()函数更新APR表 调用函数neigh_release 释放skbuffer并返回值0 arp_send() - net/ipv4/arp.c (434) 检查设备是否支持ARP操作 为skbuffer分配内存空间 填写缓存头信息 填写ARP信息 建立数据包后调用函数dev_queue_xmit() arp_req_get() - net/ipv4/arp.c (848) 根据已知的地址调用函数__neigh_lookup()查找相邻主机表项 把邻居表项内容复制到ARP请求内容中 如果找到则返回值0,如果地址不在ARP表中则返回ENXIO fib_get_procinfo() - net/ipv4/fib_frontend.c (109) 打印头信息和函数main_table->fn_hash_get_into()FS处理结果 fib_lookup() - include/net/ip_fib.h (153) 调用local_table和main_table中的tb_lookup() [= fn_hash_lookup()]函数 如果两个表都为空则填入fib_result内容并返回值0 其它则返回不可达错误 fib_node_get_info() - net/ipv4/fib_semantics.c (971) 为FS处理打印fib_node和fib_info内容 fib_validate_source() - net/ipv4/fib_frontend.c (191) 检查传入数据包的设备和地址 如果出现错误则返回错误值 如果数据包是合法的则返回值为0 fn_hash() - net/ipv4/fib_hash.c (108) 根据目标地址进行哈希运算: u32 h = ntohl(daddr)>>(32 - fib_zone->fz_order); h ^= (h>>20); h ^= (h>>10); h ^= (h>>5); h &= FZ_HASHMASK(fz); // FZ_HASHMASK is 15 for almost all zones fn_hash_get_info() - net/ipv4/fib_hash.c (723) 在FIB表中查找所有网络区循环打印fib_node_get_info()函数的返回信息 fn_hash_lookup() - net/ipv4/fib_hash.c (261) 在给定的表中遍历所有网络区 遍历每个网络区中的链表(从哈希位置开始) 如果网络掩码(结点内容和查找内容)匹配 检查TOS值和结点状态 调用fib_semantic_match()函数检查数据包类型 把成功找到的数据赋值给fib_result并返回0 如果不匹配则返回1 fn_new_zone() - net/ipv4/fib_hash.c (220) 为新的网络区分配内存空间(内核中) 为网络区分配16结点的桶内存空间(除非第一个网络区-0.0.0.0[回送]-只有一个这样的网络区) 存储子网掩码(左移n位,n是网络区在表中的位置) 在母表中搜索更多的网络区 插入新的网络区到网络区表(大部分是加到链表头中) 安装新网络区到母表中 返回新网络区 fz_chain() - net/ipv4/fib_hash.c (133) 调用函数fn_hash()取得一个哈希值 返回在哈希位置中fib_zone的fib_node值 ip_dev_find() - net/ipv4/fib_frontend.c (147) 在本地表中查找并返回给定地址的设备 ip_route_connect() - include/net/route.h (140) 调用ip_route_output()函数取得远程地址 如果成功或发生错误则返回 否则清除路由指针并重试 ip_route_input() - net/ipv4/route.c (1366) 根据地址计算哈希值 从哈希位置遍历表查找匹配的表项(匹配源、目标地址、TOS值和TIF/OIF) 如果找到匹配则更新状态并返回找到的路由表项 否则调用函数ip_route_input_slow() ip_route_input_slow() - net/ipv4/route.c (1097) 建立路由表缓存键值 检查地址(是否是回送地址、广播地址或地址不合法) 调用fib_lookup()函数查找路由 为新路由表项分配内存 根据源地址、目标地址、TOS值、输出设备、标记初始化表项 调用函数fib_validate_source()检查数据包内容 如果数据包内容被破坏了则打印出消息并返回错误 调用rt_set_nexthop()函数查找一下远程地址(相邻主机) 调用函数rt_intern_hash()(在路由表中安装路由)并返回 ip_route_output() - net/ipv4/route.c (1664) 根据地址计算哈希值 从哈希位置遍历表查找匹配的表项(匹配源、目标地址、TOS值和TIF/OIF) 如果找到匹配则更新状态并返回找到的路由表项 否则调用函数ip_route_input_slow()ip_route_output_slow() - net/ipv4/route.c (1421) 建立路由表缓存键值 如果知道源地址,调用ip_dev_find函数取得输出设备 如果知道目标地址,则设立回送方法 调用函数fib_lookup()查找路由 为新的路由表项分配内存 根据源地址、目标地址、TOS值、输出设备、标记初始化表项 调用rt_set_nexthop()函数查找一下远程地址(相邻主机) 调用函数rt_intern_hash()(在路由表中安装路由)并返回 ip_rt_ioctl() - net/ipv4/fib_frontend.c (250) 在SIOCADDRT和SIOCDELRT之间切换(不是这两种状态则返回EINVAL错误) 确定是否允许执行并复制参数到内核空间中 转换参数数据为rtentry数据结构 如果要删除一个路由则调用函数fib_get_table()和table->delete() 否则调用函数fib_new_table和table_insert()函数 释放参数内存空间并返回值0表示成功 neigh_event_ns() - net/core/neighbour.c (760) 调用函数__neigh_lookup()在邻居表中查找地址 调用函数neigh_update() 返回指向找到的相邻主机的指针 neigh_update() - net/core/neighbour.c (668) 检查是否可以修改相邻主机表 如果不是新的路由缓存项检查邻居状态 比较给定的地址和路由缓存中的地址: 如果缓存中地址为空或设备没有地址则使用当前地址 如果不同则检查是可以替换标志 调用函数neigh_sync()检查相领主机是否在启动中 更新与相邻主机通讯的时间 如果旧的路由缓存项是合法的且新的没有改变地址,则返回值0 如果新的地址相对于旧的不同,则把旧的更新掉 如果新的和旧的状态一样,则返回值0 调用函数neith_connect()或neigh_suspect()建立或检查连接 如果原来的状态不对: 遍历ARP队列中的数据包,在每个正确的ARP队列数据包中调用邻居输出函数output() 返回值0 rt_cache_get_info() - net/ipv4/route.c (191) 打印头信息和和FS处理中的rt_hash_table中每个元素信息rt_hash_code() - net/ipv4/route.c (18) 根据源地址,目标地址,服务类型来计算哈希值: hash = ((daddr&0xF0F0F0F0)>>4)|((daddr&0x0F0F0F0F)<<4); hash = hash^saddr^tos; hash = hash^(hash>>16); hash = (hash^(hash>>8)) & 0xFF; rt_intern_hash() - net/ipv4/route.c (526) 在路由表中添加新路由
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/28/1544080.aspx
第九章
9. routed程序实现动态路由
本章讲述在路由器上采用的动态路由(与终端计算机相反)。这里有在Linux中routed程序怎么实现路由协议的概述,研究其怎么修改内核路由表,总结有关实现代码。
9.1.概述
一个普通计算机的路由请求数据包的功能非常简单;请求消息可能发给自己也可能发给另一个计算机,如果是发给另一个计算机则是一个非常简单的路由请求。通常主机只需要把数据包通过局域网发给作为“网关”的计算机(路由器),网关收到数据包并把数据包转发出去。Linux通常不会维护关于路由的任何路由长度信息(距离),即使在FIB中保存了各种不同的路由信息。对于简单的终端路由查找中,只有一个重要问题就是“我能不能从这儿到达那儿”,并不是“哪条路径最短?”。
然而,一个路由器必须在选择发送路径上做决定。可能有几个路由可以到达远程目的地,路由器必须在其中选择一个路径来发送(根据到达目标地址需要的跳数或其它表示路由性能的衡量方法)。路由信息协议(RIP)是一个简单的协议,允许作为路由的计算机跟踪到达不同远程地址的路由并在这些计算机之间共享路由信息。
使用RIP协议,每个网络结点维护一个表,这个表包含从本地到其它网络的路径,顺着这个路径发送数据包就可以到达远程目的地。路由器定期地互相更新路由信息;当有更短路径时,该网络结点则更新路由表。更新数据是一个简单的RIP消息,消息中带有目标地址和表中的路由性能信息。图9.1是有RIP表和RIP数据包的图表表示。
Figure 9.1: Routing Information Protocol packet and table.
9.2.路由程序routed使用方法
routed是一个在POSIX计算机上通过UDP消息实现RIP协议的被广泛应用的程序。它实质上是一个独立的程序,这个程序在主机上通过调用icoctl()函数取得路由信息并更新路由表。
9.2.1.数据结构
routed程序维护两个一样的数据表:一个用于本地主机,一个用于网络。每个表是一个哈希表,表中每个路由项由ROUTEHASHSIZE(32)个桶的链表组成。路由项包含RIP协议信息(但也可以以rtentry结构形式排列,这样可以通过调用ioctl()函数把数据传给内核)。表中存储标志、状态和计时信息,也包括目标地址、路由器和路由性能信息。
9.2.2.初始化
当routed程序开始运行时,先执行各种初始化操作,然后调用ioctl()函数从内核取得接口信息,接着发送一个RIP/UDP消息向所有相邻路由器中请求路由信息,最后进入一个死循环,循环等待通信事件或定时处理某些任务。
9.2.3.常用操作
当RIP消息到达时(通过UDP协议通信),routed程序解析数据包,或者修改routed程序中的路由表(如果消息是因请求得到的路由消息),或返回信息给请求路由器(如果消息是路由请求)。 发送路由信息是一个简单的方法:在本地路由表中查找目标地址,把找到的路由信息打包成RIP数据包,最后通过UDP协议把数据包发送出去。如果得到的路由信息没有变化或这种变化不会造成路由不同则不会更新路由表,否则则会改变路由方法。如果更新时发现是一个到达某个目的地的更短的路由,routed程序则更新本程序中的路由表,然后调用函数ioctl()更新内核中的路由表(FIB表)。
当更新操作超时时(定时时间是TIMER_RATE秒),routed程序遍历RIP表和FIB表中所有表项并更新它们的定时值。路由请求超时的表项被设置成无穷远的距离(HOPCNT_INFINITY),删除太久不能用的路由表项(只从RIP表中删除,不是内核中的FIB表)。最后routed程序发送一个更新消息到相邻路由器中,更新消息(响应请求)包含表中任何表项有从最后一次更新到当前被更新的信息。
routed程序并不实现路由方法,路由方法是在内核机制中实现的,所做的是根据其它路由器的信息更新内核中的FIB表和传递本身自己的路由信息。Routed程序的更新操作会改变内核路由方法,但routed程序本身实际并不做任何路由操作。
9.3. routed 函数
下面是按字母顺序排列的Linux内核函数列表,这些都是有关路由的最重要函数,这些都有源代码,可以知道函数怎么执行。SOURCES目录中有相应的网络文件,这些代码在SOURCES目录下可以找到。
routed程序是独立于内核程序的一个程序包(Red Hat Linux使用rpm命令来管理程序包)。下面的代码来自于netkit-routed-0.10源代码包(1997-03-08),可以从www.redhat.com/apps/download网页中获取,具体址是www.redhat.com/swt/src/netkit-routed-0.10.src.html。下载后,用下面的命令以超级用户来安装这个程序包(在程序包所在目录下安装):
rpm -i netkit-routed-0.10.src.rpmcd /usr/src/redhat/SOURCEStar xzf netkit-routed-0.10.tar.gz运行上面命令后会生成一个/usr/src/redhat/SOURCES/netkit-routed-0.10目录,routed程序源代码放在这个目录下面。这个处理方法和其它的Linux程序包安装方法是一样的(但毫无疑问是不会完全一样的)。
ifinit() - SOURCES/routed/startup.c (88) 打开UDP套接字 调用ioctl(SIOCFIGCONF)取得配置接口信息 循环遍历所有接口: 调用ioctr()函数取得标志信息、广播地址、路由度量方法和子网掩码 建立一个新接口数据结构 把取得的信息复制到上面建立的数据结构中 调用函数addrouteforif()为该接口添加路由项 如果需要则设置supplier变量值 关闭SOCKET process() - SOURCES/routed/main.c (298) 开始进入一个持续的循环过程: 收到一个数据包(等待接收数据) 确定数据包长度是否正确 调用rip_input()函数处理收到的数据包(RIP数据包) rip_input() - SOURCES/routed/input.c (60) 如果需要则跟踪输入 检查数据包以确定协议和地址是否被支持 检查RIP版本(不可以为0) 根据数据包内容进行处理: 如果数据包是一个请求数据包: 检查请求是否有效 如果是请求所有路由,则调用函数supply() 否则查找请求的地址,建立并发送响应数据包 如果是一个跟踪打开或关闭请求数据包: 检验请求是否是来自于有效的端口 如果所有的检查结果是正确的,则打开或关闭跟踪方法 如果数据包是一个响应数据包: 检验响应是否是来自于路由器 在接口信息中更新计时值 在接收到的数据包中遍历所有内容: 解析路由信息 检验地址结构中内容、主机名和路由距离信息 更新跳数(增加到达发送消息的路由器的跳数,最大值为HOPCNT_INFINITY) 调用函数rtlookup()在路由表中查找地址 如果可能是一个新的路由: 调用rtfind()函数查找是否存在一样的路由 如果真的是一个新的路由,则调用函数rtadd()函数并返回 如果需要(如新路由或跳数有变)则调用函数rtchange()修改路由 更新路由计时值 如果有发生变化: 如果需要则发送更新消息 更新总的计时更新信息 >>> routed main() - SOURCES/routed/main.c (78) 打开routed程序日志文件 调用getservbyname()函数取得以UDP协议传输的路由器的地址 建立传输RIP消息的UDP套接字 根据命令行中用于设置运行方法的参数运行命令 如果不是在调试,则在新的通信会话中载入并运行程序(中止上层程序的运行) 调用函数reinit()初初化数据表 调用函数ifinit()初始化接口信息 调用函数toall()向其它所有路由器请求信息 安装ALARM、HUP、TERM、INT、USR1、USR2句柄信号 开始一个持续的循环过程: 如果需要更新则建立计时变量 调用函数select()等待通信事件 如果select()函数返回一个错误(不是EINTR错误),记下该情况 如果select()函数超时(更新超时) 调用函数toall()做广播更新 重新设置计时变量值 如果SOCKET通信过程需要等待则调用函数process() rtadd() - SOURCES/routed/tables.c (138) 确定地址结构中内容是否是在正确的范围 调用协议函数af_rtflags()设置路由标记 计算哈希值寻找合适的表(根据主机或网络名称) 建立并设置新的rt_entry数据结构 调用函数insque()把路由表项添加到表中 调用rtioctl()函数把路由表项添加到系统内核中 如果调用失败: 如果需要确定路由,则调用协议函数af_format()添加目标地址和网关到内核表中 如果主机是不可达的,则删除并释放路由项 rtchange() - SOURCES/routed/tables.c (207) 确定路由变化是添加还是删除网关 调用函数rtioctl()添加、删除路由项 rtfind() - SOURCES/routed/tables.c (100) 计算哈希值以确定该主机路由表的位置 遍历路由表;如果地址相等则返回路由表项 计算哈希值以确定网络表的位置 重新遍历路由表,如果协议函数af_netmatch()返回值为真则返回路由表项 如果找不到匹配则返回为空 rtinit() - SOURCES/routed/tables.c (336) 遍历网络哈希表,设置向前和向后指针 遍历主机哈希表,设置向前和向后指针 rtioctl() - SOURCES/routed/tables.c (346) 根据参数值设置rtentry数据结构 如果需要则输出跟踪结果 调用ioctl(SIOCADDRT或SIOCDELRT)函数更新内核中FIB表 返回函数ioctl()调用结果(如果参数有错误则返回-1) rtlookup() - SOURCES/routed/tables.c (65) 计算哈希值以找到地址 遍历主机表查找相匹配的表项 如果第一次找不到,则到网络表中查找 返回指向表项指针或返回空(0) sndmsg() - SOURCES/routed/output.c (77) 调用相应的协议输出函数 如果需要则跟踪这个数据包 supply() - SOURCES/routed/ouput.c (91) 建立RIP响应消息 遍历路由主机表 遍历表中路由项 检查以确定该路由表是否是该主机的路由 如果符合则把路由信息加入到数据包中并发送数据包 如果不符合则再返回到路由网络表中再次查找 timer() - SOURCES/routed/timer.c (56) 更新计时变量 遍历主机表 为每个表项更新计时信息 如果路由表项太过时了则删除该表项 如果路由表项持续得不到更新则改变路由距离为无穷远 返回并在网络表中进行相同的处理 如果将要做更新操作则调用函数toall() toall() - SOURCES/routed/output.c (55) 遍历所有接口: 设置目标地址为广播地址或某特定地址 根据地址调用传递函数[sendmsg()或supply()]
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/28/1544148.aspx
第十章
10.编辑Linux源代码
10.1.源代码目录树
Linux源代码通常放在/usr/src目录中(如果安装了源代码包)。可能有许多版本存在于不同的目录树中(如linux-2.2.5或linux-2.2.14)。 但应当有一个目录是最近的代码版本(linux目录下)。
这是一个Linux源代码目录结构总述(但并不展示所有目录分支):
/usr/src/linux/
· arch -与CPU体系结构相关的代码,根据处理器名称来分类
o i386 -针对Intel处理器的代码(包括486和奔腾系列)
§ boot -内核被编译后生成的位置(系统引导汇编程序)
· drivers -所有硬件类型的驱动代码
o block -块设备驱动(例如硬盘)
o cdrom - CD ROM设备驱动
o net -网络设备驱动
o pci - PCI总线驱动
· fs -不同文件系统代码(EXT2,MS-DOS,etc.)
· include - 被所有代码使用的头文件
o asm ® asm-i386 - 依赖于处理器的头文件
o config -总的配置头文件
o linux -普通头文件
o net -网络头文件
· kernel -内核子程序代码(内核进程调试、信号处理、系统调用等子程序)
· lib - 内核库函数代码(错误处理、字符串操作和打印函数)
· mm -内存管理代码
· modules -当需要时让内核载入的对象文件和其它代码
· net - 网络代码
o core -与网络协议无关的代码
o ipv4 - IPv4通信代码
o packet -与网络协议无关的数据包打包和解包代码
o sched -网络操作调度代码
10.2.使用EMACS Tags编辑工具
Linux源代码显然是非常大并由许多文件组成。TAGS文件可以让你快速机动地找到要参考的指定文件。
10.2.1.用标签(Tags)来浏览源代码
在一个文件中移动光标到你要查找的关键字(例如“sock”),按下“ESC”键,EMACS将提示找到该关键字定义的地方,再按“ENTER”回车键就可以找到了。第一次使用时必须指定使用哪个TAGS文件(例如:/usr/src/TAGS)。按下回车键后EMACS将自动在新的缓存中打开相应的文件(例如/usr/src/linux/include/linux/sock.h)并把光标定在数据结构定义、宏定义或函数定义位置。如果此处定义不是你想要查找的,先后按“CTRL+U”、“ESC”、“.”则可以切换到下一个定义位置。
当你更改源文件,虽然改得越多则运行会越慢,但EMACS中的标签还能照常起作用。EMACS在一个文件(默认是TAGS文件)存储每个关键字定义、文件名和行号。如果标签中行号变了,EMACS则会在文件中查找新的位置。
10.2.2.建立TAGS文件
如果你需要从头开始做,则按下面的步骤来操作。
建立tags文件命令是:
· etags filename
添加新信息到一个tags文件的命令是:
· etags -a filename
这些命令把新标签添加到当前目录下的TAGS文件中,输入的文件名被存起来,这样绝对查找则总是在相同的文件中查找,相对查找则要根据TAGS文件中的位置来确定。(关于etags命令的更多信息可以看EMACS帮助文档中有关etags命令的内容)
例如,要为ipv4目录下文件建立一个标签文件,则输入:
· etags /usr/src/linux/net/ipv4/*.c
要添加头文件,则输入:
· etags -a /usr/src/include/net/*.h
这时TAGS文件将包含在那些目录下的所有C源代码和头文件信息,以便能够快速查找有关信息。
10.3.使用vi Tags编辑工具
vi编辑器也支持tags文件的使用(建立tags文件采用gctags命令,跟上面的etags命令使用方法几乎一样)。
10.4.重建Linux内核
(看Linux-kernel-HOWTO文档,可以看到更详细的命令说明)
这是一个从头开始、一步一步地重编译和安装内核的速成指导。
1. 进入源代码目录的最顶层目录中(/usr/src/linux)。如果以前没有对可用的config文件进行备份,则备份该文件,除非你有足够的把握你可以不再用这个配置文件了,不要重写这个文件,直到你已经确定你已有备份并可以还原回去为止。(从另一方面来说,一旦你有一个稳定的内核版本,就没有理由去保存旧的版本。甚至一个开发好的系统或许只要一个原来可用的版本、一个最后稳定的版本和当前版本。)
2. 运行make xconfig命令(make config和make menuconfig都可以用,但xconfig最好用)。这样就可以根据你的需要配置系统,且大部分参数都有帮助信息。配置文件默认为当前设置,所以你应当根据你想要的去做取舍。总的原则是,对于基本的或常用的功能则选择“Y”(如ext2文件系统),对于有时才用得到的则选择“M”(如声卡驱动),不需要提供的则选择“N”(如不成熟的无线通信支持系统)。如果无法确定,则去参考帮助说明或作为一个模块包含进来。
3. 运行make dep命令以确定所有的配置将完全被编译。这个要花几分钟的时间,计算机要检查所有支持系统。如果一切顺利,make程序会简单地退出来;如果存在问题,则会显示错误消息并中止编译。
4. 如果你要重新编译所有文件则运行make clean命令删除以前生成的对象文件。显然这样会导致编译的过程要更长时间。
5. 运行make bzImage命令建立新的内核。(make zImage和make boot也可以建立内核映象文件,但bzImage会编译成最简洁的文件,如果你为某种原因使用这两个命令,当你运行lilo时可能会产生内核太大的错误,这时就必须得用bzImage命令了。)这个要花一些时间,取决于可用的内存大小。
6. 运行make modules命令建立模块(没有被包含进内核映象的模块)
7. 如果需要重命名旧模块,如:
o mv /lib/modules/2.2.xx /lib/modules/2.2.xx-old
o (注意如果你正在全编译一个新的版本时不要这样做;当你建立2.2.yy版本时/lib/modules/2.2.xx版本中仍然会保存原来的。)
8. 运行make modules_install命令来安装新的模块。如果你是在建立一个独立的内核(不包含任何模块)也要这样做。(注意这个命令可能会生成一个Red Hat module-info文本文件,也可能链接到boot目录下,这个不是很重要,不用去更改。)
9. 拷贝新的内核到/boot目录下并修改内核链接(通常是vmlinuz):
o cp arch/i386/boot/bzImage /boot/vmlinuz-2.2.xx ln -sf /boot/vmlinuz-2.2.xx /boot/vmlinuz
10.拷贝System.map文件到/boot目录下并修改映象链接:
o cp System.map /boot/System.map-2.2.xx ln -sf /boot/System.map-2.2.xx /boot/System.map
11.如果计算机上有SCSI设备则建立一个新的initrd文件:
o /sbin/mkinitrd /boot/initrd-2.2.xx.img 2.2.xx
12.编辑/etc/lilo.conf文件以安装新内核;备份旧的内核(image=vmlinuz)并把原来的内核修改为备用。例如,把原来的映象文件重命名为vmlinuz-2.2.xx-old并更改标签为stable。这样做当你修改出现问题时可以以当前稳定的内核来重新启动。
13.运行/sbin/lilo命令把新内核安装到启动选择中。
14.用新的内核重新启动计算机。
15.如果新的内核出现问题,则用旧的内核来启动并在重试前重新配置系统。
10.5.升级内核代码
Linux是一个经常在改变的操作系统;每几个月就会被更新。有两种方法来安装新的内核版本:下载全部新代码或下载并打上补丁。
下载全部代码以保证不出问题,这样可能更好。为此,下载最新内核源代码并安装(untar)它。注意这样做下载的是(或许是)完整的版本,不是想要的一部分,并且会包含许多额外代码,其中许多代码可以被删除掉,但makefiles配置文件跟某些信息有关系。如果存储空间不足,则把arch/和include/asm-*目录中不是属于i386的所有.C和.h文件删除,但这样做要非常小心。
下载补丁可能做起来更快,但有些难。因为版本有很多种,所做的更改或其它修改会使补丁程序不可能彻底可以用,必须按版本顺序来使用补丁程序(如要从版本2.2.12 升级到2.2.14,则要先使用2.2.13版本,然后使用2.2.14版本)。不过,补丁也许会更好,因为补丁是用于已经存在的目录树中。
一旦下载一个补丁(可能需要解压),只需把补丁放到linux目录中(例如/usr/src/),然后运行patch程序来安装:
· patch -Np0 -verbose -r rejfile < patch-2.2.xx (当中xx是补丁版本号)
-N表示忽略已经安装的补丁,-p0表示安装针对Linux目录中一个源代码文件的补丁,-r rejfile表示把所有没用的补丁程序放到一个rejfile文件中(你可能不想这样做)。如果你没有下载整个内核源代码版本,你就得跳过许多修改(因为有些补丁是针对不同处理器架构的),在“patch which file”和“ignore patch”提示的地方按下回车键就可以跳过某项修改,如果你喜欢这样的处理过程,则可以不用-verbose 和 –r rejfile两个参数。
当你有一个新的内核版本时,重新编译完内核后就可用了,你可能不用更改任何配置,但你一定要运行make clean命令来删除任何以前的对象文件。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/28/1544168.aspx
第十一章
11.Linux模块(Modules)
本章描述Linux模块系统,对模块使用方法进行概述,描述怎样安装和删除模块,还举了一个例子程序。
11.1.概述
2.0版本以后的linux内核是个模块化的内核。内核中的一部分永久保存在内存中(对一些最常用的处理过程如调度器),但其它处理则是在需要时才载入到内存中。例如,对于要读一个MS-DOS文件系统的磁盘,当要装载这样的磁盘时则载入MS-DOS文件系统模块,当不再需要时则卸载掉,这样可以使得内核占用较少的内存并能做更多的事。仍然有可能可以把所有模块到集成到内核中,去掉模块化功能,但这通常只用于特殊用途(所有的处理都是优先级较高的)的机器上。
内核模块化的另一个优点是可以动态地载入和卸载模块(内核自动控制)。这意味着用户(超级用户)可以不用重新启动计算机反复载入一个模块,测试它,卸载它,调试它。这里假设用户是以超级用户来访问(必须用root用户名来安装和卸载模块)并且内核是以模块化来配置的。(对于一个集成内核,可以设置配置但不能做模块配置。)
11.2.编写,安装和卸载模块
11.2.1.编写模块
模块跟其它程序一样不在内核空间中运行。建立模块程序,必须定义MODULE宏定义名,并包含module.h头文件和其它有使用到的函数和变量定义的内核头文件。模块可以非常简单(如这里的例子了),但也可以非常复杂,如设备驱动程序和整个文件系统。
这里是一个一般的模块格式:
#define MODULE#include <linux/module.h>/* ... other required header files ... */ /* * ... module declarations and functions ... */ int init_module() { /* code kernel will call when installing module */} void cleanup_module() { /* code kernel will call when removing module */}使用内核代码的模块必须用gcc和带-I/usr/src/linux/include参数来编译;这是确保包含的文件是来自于正确的目录。
注意并非所有的内核变量都被导出被模块使用,即使代码用extern表示也一样。/proc/ksyms文件或ksyms程序显示导出的符号中许多对于网络是没有用的。最近的Linux内核会把符号和它的版本号(使用EXPORT_SYMBOL(x)宏)都导出来。用户在建立变量时,采用EXPORt_SYMBOL_NOVERS(x)宏可以使链接工具不把变量保存在内核符号表中。设计模块也可以使用EXPORT_NO_SYMBOLS宏,使其按默认方法把的有变量都导出来。
11.2.2.安装和卸载模块
· 安装和卸载模块跟程序调用一个已编译好的模块名一样的简单。(你必须是超级用户才能安装和卸载一个模块。)
insmod是一个安装模块的程序,它是先通过内核导出符号表处理有关符号,以此来链接模块,然后在内核空间中载入代码。
· /sbin/insmod module_name
rmmod程序是用来卸载已安装的模块并删除所有导出的信息。
· /sbin/rmmod module_name
lsmod程序是列出当前所有已安装的模块。
/sbin/lsmod Module Size Used by cdrom 13368 0 (autoclean) [ide-cd] 3c59x 19112 1 (autoclean)11.3.例子
这是一个针对非常简单模块的完整例子
simple_module.c
/* simple_module.c * * This program provides an example of how to install a trivial module * into the Linux kernel. All the module does is put a message into * the log file when it is installed and removed. * */ #define MODULE#include <linux/module.h>/* kernel.h contains the printk function */#include <linux/kernel.h> /*************************************************************** init_module * the kernel calls this function when it loads the module */int init_module() { printk("<1>The simple module installed itself properly./n"); return 0;} /* init_module */ /************************************************************ cleanup_module * the kernel calls this function when it removes the module */void cleanup_module() { printk("<1>The simple module is now uninstalled./n");} /* cleanup_module */下面是Makefile文件:
# Makefile for simple_module CC = gcc -I/usr/src/linux/include/config CFLAGS = -O2 -D__KERNEL__ -Wall simple_module.o: simple_module.c install: /sbin/insmod simple_module remove: /sbin/rmmod simple_module使用方法(必须是root用户):
root# makeroot# make installroot# make removeroot# tail /var/log/messages... kernel: The simple module installed itself properly.... kernel: The simple module is now uninstalled.
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/28/1544178.aspx
第十二章
12. proc文件系统
本章描述虚拟proc文件系统,概述该文件系统是如何工作的,展示现存的网络代码是如何使用该文件系统,详细讲解如何在proc文件系统上编写和运用程序。
12.1.概述
因为在大多数Linux机器中它总是被放在/proc目录下,所以把它称为proc文件系统。是否要包含proc文件系统可以通过配置来进行设置,但该文件系统是一个强大的工具,许多程序经常用到该文件系统。虽然它被设计成使用目录结构和结点来表示的文件系统,但实际上是一个由注册函数来组织起来的结构,这些注册函数可以提供关于重要变量的信息。
proc目录下有许多子目录,一个是应用于每个正在运行的进程,其它的是针对诸如文件系统、接口、退出和网络(/proc/net)的子系统。在/proc主目录下本身也有许多文件:interrupts, ioports, loadavg和版本说明文件。在针对每个进程的子目录中(目录名为进程序号),有关于保存进程命令行、当前工作目录、状态等的文件。
当内核捕捉到要访问proc文件时,则把执行“普通”文件操作切换到调用特定(独立注册的)的函数方法。当一个文件在/proc目录中“建立”后,便会注册一组函数告诉内核怎么去读和写这个文件。其它程序使用这些文件或深知这些文件的用户读取这些文件时,大多数文件只允许被读并只是简单地打印出系统状态。
使用proc文件唯一不好解决的是:在每次文件被读时内核会调用信息生成函数,在没有复制和缓存文件内容情况下并发读正在变化的proc文件时,信息生成函数可能会得到非常不一样的结果。最好的方法是使用proc文件时把它读到一个PAGE_SIZE字节长度的缓存中,这样就可以一次性读到所要的所有内容,而且缓存可以持续地被随时访问。
12.2. proc中网络文件系统
这里列出了/proc/net/目录中最重要的文件,列出了文件中包含的内容、函数说明及创建函数的文件。注意一个文件中可能包含许多proc文件处理的函数,如/proc/sys、/proc/ksyms、/proc/modules中的文件。
arp
· 显示邻居表信息(arp_tbl):IP地址和硬件地址,硬件类型,设备名称及其它符号(arp_get_info() : net/ipv4/arp.c 988)
dev
· 显示每一个注册接口的接收和传输统计信息
dev_stat
· 显示收到的数据包中被接收和抛弃的数目以及FASTROUTE统计信息(dev_proc_stats() : net/core/dev.c 1228)
netstat
· 显示同步cookie、pruning和ICMP统计信息 (netstat_get_info() : net/ipv4/proc.c 355)
raw
· 显示每一个打开的RAW套接字中地址、队列、超时信息(来自于proto结构类型的raw_prot变量) (get__netinfo() : net/ipv4/proc.c 165)
route
· 显示FIB表(main_table):接口,地址,网关,标志,应用范围等信息(fib_get_procinfo()) : net/ipv4/fib_frontend.c 109)
rt_cache
· 显示路由缓存(rt_hash_table):接口、地址、网关、应用范围、源地址和其它信息 (rt_cache_get_info() : net/ipv4/route.c 191)
sockstat
· 显示被用过的套接字数目和有关TCP、UDP、RAW数目的统计信息(afinet_get_info() : net/ipv4/proc.c 244)
tcp
· 显示每个打开的TCP套接字的地址、队列、超时信息(来自于proto结构类型的tcp_prot变量)(get__netinfo() : net/ipv4/proc.c 165)
udp
· 显示每个打开的UDP套接字的地址、队列、超时信息(来自于proto结构类型的udp_prot变量)(get__netinfo() : net/ipv4/proc.c 165)
12.3.注册proc文件
这一节描述注册一个只读的proc文件的最简单方法(这里的方法只适用于linux2.0以上版本)。通过定义file_operations和inode_operations数据结构可以建立一个具有更完整功能的proc文件,但是这个方法会比这里只建立一个功能的方法要复杂很多,可以看一下关于实现完整功能的详细代码。下面描述的方法是:定义一个函数,然后注册这个函数,取消注册这个函数,这个函数实现了最通用的功能,即测试和跟踪系统资源。只有内核才可以注册一个proc文件,用户(只有超级用户)可以通过建立和安装内核模块来实现这种功能。这些过程前提是Linux源代码已被安装,内核是被编译成可用模块的内核。
12.3.1.格式化输出函数
· static int read_proc_function(char *buf,char **start,off_t offset,int len,int unused)
只要新建立的proc文件一被读取Linux内核就会调用上面这个函数。唯一要注意的参数是buf:指向内核用来存储信息的缓存指针。其它参数正常情况下不会被改变。(read_proc_function当然是泛指这类新函数的名称了)
特别之处是这个函数打印出头信息,遍历链表或表并打印其中的内容(使用普通的sprintf函数来打印),最后返回输出字符串的长度。唯一的限制是缓存(buf)的长度最大是PAGE_SIZE字节(至少有4KB)。
这类函数的例子可以看net/ipv4/fib_frontend.c文件中第109行的函数fib_get_procinfo(),这个函数是显示FIB表的主要内容。
12.3.2.建立一个proc项
因为这是属于文件系统的一部分,所以proc项需要建立一个结点,这个很容易,就是建立一个proc_dir_entry结构体的数据:
#include <linux/proc_fs.h>struct proc_dir_entry new_proc_entry = { 0, // low_ino - inode number (0 for dynamic) 5, // namelen - length of entry name "entry", // name S_IFREG | S_IRUGO, // mode 1, // nlinks 0, // uid - owner 0, // gid - group 0, // size - not used NULL, // ops - inode operations (use default) &read_proc_function // read_proc - address of read function // leave rest blank!}上面内容中的名字长度、名字和读函数指针都可被改为其它的值。注意许多内核在定义这个数据结构前都会先定义结点数目(如PROC_NET_ROUTE宏定义,在include/linux/proc_fs.文件中被定义)。
这类例子可以看net/ipv4/fib_frontend.c文件中607行的函数__init_func()。这个函数调用proc_net_register()(后面会解析这个函数),并传入新建立的proc_dir_entry结构的参数。
12.3.3.注册一个proc项
当读函数和结点项建立好后,剩下的就是注册这个新“文件”为proc系统文件。
int proc_register(struct proc_dir_entry *dir, struct proc_dir_entry *entry)int proc_net_register(struct proc_dir_entry *entry)dir是这个proc项所在目录的指针:一般为&proc_root和proc_net(在include/proc_fs.h中定义)。Entry是指向上面建立的proc项的指针。上面这两个函数除了proc_net_register函数是自动使用/proc/net目录外,其它功能一样。当函数调用成功时都返回值为0,如果没有可用的结点则返回EAGAIN。
12.3.4.取消proc项的注册
当一个proc项不再需要时,则可以通过取消注册方法来删除掉。
int proc_unregister(struct proc_dir_entry *dir,int inode)int proc_net_unregister(int inode)dir是proc文件所在目录,inode是文件中结点数目。(如果不是一个恒量则为proc_dir_entry结构中low_ino域的值)一样地,上面这两个函数除了proc_net_unregister函数是自动使用/proc/net目录外,其它功能一样。当函数调用成功时都返回值为0,如果没有结点则返回EINVAL。
12.4.例子
这是一个安装一个简单proc项模块的完整例子。
simple_entry.c
/* simple_entry.c * * This program provides an example of how to install an entry into the * /proc File System. All this entry does is display some statistical * information about IP. */ #define MODULE#include <linux/module.h>/* proc_fs.h contains proc_dir_entry and register/unregister prototypes */#include <linux/proc_fs.h>/* ip.h contains the ip_statistics variable */#include <net/ip.h> /************************************************************ show_ip_stats * this function is what the /proc FS will call when anything tries to read * from the file /proc/simple_entry - it puts some of the kernel global * variable ip_statistics's contents into the return buffer */int show_ip_stats(char *buf,char **start,off_t offset,int len,int unused) { len = sprintf(buf,"Some IP Statistics:/nIP Forwarding is "); if (ip_statistics.IpForwarding) len += sprintf(buf+len,"on/n"); else len += sprintf(buf+len,"off/n"); len += sprintf(buf+len,"Default TTL: %lu/n",ip_statistics.IpDefaultTTL); len += sprintf(buf+len,"Frag Creates: %lu/n",ip_statistics.IpFragCreates); /* this could show more.... */ return len;} /* show_ip_stats */ /**************************************************************** test_entry * this structure is a sort of registration form for the /proc FS; it tells * the FS to allocate a dynamic inode, gives the "file" a name, and gives * the address of a function to call when the file is read */struct proc_dir_entry test_entry = { 0, /* low_ino - inode number (0 for dynamic) */ 12, /* namelen - length of entry name */ "simple_entry", /* name */ S_IFREG | S_IRUGO, /* mode */ 1, /* nlinks */ 0, /* uid - owner */ 0, /* gid - group */ 0, /* size - not used */ NULL, /* ops - inode operations (use default) */ &show_ip_stats /* read_proc - address of read function */ /* leave rest blank! */}; /*************************************************************** init_module * this function installs the module; it simply registers a directory entry * with the /proc FS */int init_module() { /* register the function with the proc FS */ int err = proc_register(&proc_root,&test_entry); /* put the registration results in the log */ if (!err) printk("<1> simple_entry: registered with inode %d./n", test_entry.low_ino); else printk("<1> simple_entry: registration error, code %d./n",err); return err;} /* init_module */ /************************************************************ cleanup_module * this function removes the module; it simply unregisters the directory * entry from the /proc FS */void cleanup_module() { /* unregister the function from the proc FS */ int err = proc_unregister(&proc_root,test_entry.low_ino); /* put the unregistration results in the log */ if (!err) printk("<1> simple_entry: unregistered inode %d./n", test_entry.low_ino); else printk("<1> simple_entry: unregistration error, code %d./n",err);} /* cleanup_module */这个是Makefile文件:
# Makefile for simple_entry CC = gcc -I/usr/src/linux/include CFLAGS = -O2 -D__KERNEL__ -Wall simple_entry.o: simple_entry.c install: /sbin/insmod simple_entry remove: /sbin/rmmod simple_entry应用(必须是超级用户root):
root# makeroot# make installroot# cat /proc/simple_entrySome IP Statistics:IP Forwarding is onDefault TTL: 64Frag Creates: 0root# make removeroot# tail /var/log/messages... kernel: simple_entry: registered with inode 4365.... kernel: simple_entry: unregistered inode 4365.
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/28/1544189.aspx
第十三章
13.丢包器例子
这个例子实现在内核中加入一个能选择性地把数据包丢给某个主机的子程序。这里将讨论如何实现代码,略述从实验中得到的数据,简要分析实验结果以及实现代码。
13.1.概述
这个程序采用模块的方法实现,安装该程序和安装模块方法一样,这个程序会把每个出去的数据包的目标地址和给定目标地址进行比较,如果地址一样,则按一定百分比把其中某些数据包丢掉,程序对所有IP传输中的数据包都这样做,不管数据包是从哪里来的,也不管用的是什么传输协议。实现这个目的需要修改内核(让设计的模块能够访问传输过程中的函数)和实现一个模块以便于修改。
13.2.需要考虑的事项
代码位置
· 直接在内核中编写代码或把程序设计成一个模块:
o 内核:从概念上来讲会更简单,单纯地把一些代码添加到内核中是一件非常容易的事。然而,这样做会产生半永久的修改并且调试更费时间,因为每次修改后整个内核要被重新编译、安装、重新启动。
o 模块:这个方法会更安全和容易,因为超级用户可以非常轻松地安装、卸载和调试模块。然而,这样需要访问内核,且内核并不总是可用,从模块访问则更会有这样的情况,内核不一定会导出模块可能需要访问的变量。(可以看第11章中关于ksyms程序的讨论。)
o 二者都用:这是最好的方法,最少地修改内核中的代码以导出需要的变量,如果需要导出这些变量信息则建立一个模块,用户只要重编译内核一次就可以了,以后就可以通过建立模块来检验和实验。这样做仍会有不好的地方,就是会在一个系统上打开一个潜在的安全漏洞,但因为这只有做这个实验的人才知道是怎么实现的,所以这是一个最小的风险。
协议层
· 这个代码可以在许多协议层上实现:
o 设备驱动:因为所有通信都是要经过该设备,所以这是可能做到的。然而,这个会破坏协议的层次关系并且需要打乱原本可能稳定的驱动程序。
o 通用的设备函数:这是最好的选择,因为这是在最底层中进行,所有的通信都要经过这里(如dev_queue_xmit()和netif_rx()函数)。这样做仍会破坏协议的分层关系,但所有的修改可以是一小段代码中实现。
o IP协议层:从概念上看这是一个插入函数的正确位置,可以在输入、路由查找和输出子程序中插入。然而,精确地看是不合适的,因为要在三个不同的子程序中实现:数据包可能要经过ip_forward()函数(对于转发数据包而言),也可能经过函数ip_queue_xmit()(对于TCP数据包而言),也可能经过函数ip_build_xmit()(对于UDP数据包而言)。在第5和第7章中的代码解解释中可以看出这些函数是怎么相互作用的。在这些函数中插入丢包方法是一个好的选择,但没有一个函数可以覆盖所有通信。
o 传输层协议:这层协议中的函数是针对特定通信类型(如只用于UDP通信类型),对于这个例子没有用。
13.3.试验系统环境及试验内容
如下图13.1,这个例子是在两台通过一个路由器相连接的计算机上实现,路由器上运行的是被修改的内核和丢包器模块。在这个例子中,通信发生在主机neon和eagle之间,dodge/viper路由器把与eagle有关的数据包丢弃掉。
Figure 13.1: Experimental System Setup.
交换机是一个Cisco Catalyst 2900设备,为每个子网搭起一个虚拟局域网(一个子网中有源计算机,另一个子网中有目标计算机,两个计算机之间有一个行使路由功能的计算机作为路由器),交换机的整个操作是在链路层上进行,本质上不行使路由功能。
这个路由计算机是一个Dell Optiplex GX1机器,CPU是Pentium II/350,内存有128M,有三个3Com 3c59x速度为10Mbps的以太网卡连接到交换机。
其中一个主机(neon)是一个AST Premmia GX机器,CPU是Pentium/100,32M内存,有一个速度为10Mbps的AMD Lance以太网卡连接到交换机。
另一个主机(eagle)是一个Dell Optiplex XL590机器,CPU是Pentium/90,32M内存,有一个速度为10Mbps的3Com 3c509以太网卡连接到交换机。
所有的计算机的操作系统都是Red Hat 6.1版本的Linux,源、目标计算机上运行的是重编译的2.2.14版本的标准内核,路由器上使用标准内核(2.2.14)或根据需要稍微修改后的内核。
第一个任务是做“乒乓(ping-pong)”测试,即建立一个TCP连接,然后来回反复发送数据包。返回总的传输时间(从开始到结束,不包括建立和关闭连接),把总的传输时间除以来回发送次数得到一个平均的数据包往返时间(Round Trip Time:RTT)。这次测试是反复发送5字节长度的数据包20,000次,反复发送500字节长度的数据包5,000次。
每二个任务是“暴力(blast)”测试,即建立一个TCP连接,从一个地址到另一个地址发送数据。返回总的传输时间(从开始到结束,不包括建立和关闭连接),把数据包个数乘以数据包长度现地除以总的传输时间,即可得到传输速率。这次测试是反复发送5字节长度的数据包50,000次, 500字节长度的数据包5,000次,1500字节长度的数据包1,000次。
这两个测试都两台机器上进都进行(如,发数据包从neon到eagle,发数据包从eagle到neon),但是这两种情况下都是把发给eagle的数据包给丢掉。在这些测试中暴力测试在性能测试前会先来个发送100个1个字节的数据包的测试,以确定路由缓存和所有协议表都处于正常状态。每组测试都要完整地被做10遍,以发现每种测试的不同(这里列出了每次测试的平均值)。所有的机器(包括路由器)除了运行登陆外壳程序和相关模块、客户端、服务端程序外没有运行其它的程序(连X Windows也没运行)。
13.4.实验结果及初步分析
13.4.1.标准内核
这些测试采用标准的内核,有些是在两台直接相连的计算机之间(没有经过路由器)进行测试,有些在有安装有未修改的2.2.14Linux内核的路由器的情况下进行。在两台直接相连的计算机中的错误率接近于0。
乒乓(ping-pong )
Mean Time (sec) Average RTT (millisec) Drop Rate 20K@5 5K@500 20K@5 5K@500Direct - neon to eagle: --- 17.24 28.98 0.86 5.80 eagle to neon: --- 17.20 28.99 0.86 5.80Routed - neon to eagle: (0.0%) 24.53 48.59 1.23 9.72 eagle to neon: (0.0%) 24.36 48.46 1.22 9.69暴力(blast)
Mean Time (sec) Throughput (Mbits/sec) Drop Rate 50K*5 10K*500 1K*1500 50K*5 10K*500 1K*1500Direct - neon to eagle: --- 0.56 3.19 1.89 3.55 6.26 6.36 eagle to neon: --- 0.78 3.03 1.77 2.58 6.61 6.76Routed - neon to eagle: (0.0%) 0.56 3.19 1.92 3.60 6.27 6.26 eagle to neon: (0.0%) 0.77 3.19 1.93 2.60 6.27 6.2313.4.2.修改内核中的丢包方法
下面是得到的实验结果,用精确度为0.0%的丢包率来衡量那些不会做丢包操作的函数(随机取某些函数来测试)的通信效率。
乒乓(ping-pong)
Mean Time (sec) Average RTT (millisec) Drop Rate 20K@5 5K@500 20K@5 5K@500neon to eagle: 0.0% 25.55 49.12 1.28 9.82 0.1% 29.87 51.11 1.49 10.22 0.5% 44.78 58.07 2.24 11.61 1.0% 65.37 68.77 3.27 13.75 5.0% 245.51 160.09 12.28 32.02 10.0% 506.03 290.77 25.30 58.15eagle to neon: 0.0% 25.53 49.21 1.28 9.84 0.1% 29.08 50.92 1.45 10.18 0.5% 45.87 59.21 2.29 11.84 1.0% 66.19 68.66 3.31 13.73 5.0% 235.68 156.94 11.78 31.39 10.0% 519.61 297.02 25.98 59.40暴力(blast)
Mean Time (sec) Throughput (Mbits/sec) Drop Rate 50K*5 10K*500 1K*1500 50K*5 10K*500 1K*1500neon to eagle: 0.0% 0.55 3.19 1.91 3.64 6.26 6.27 0.1% 0.55 3.07 1.93 3.62 6.51 6.21 0.5% 0.55 2.95 1.76 3.64 6.77 6.82 1.0% 0.55 2.87 1.75 3.65 6.96 6.87 2.5% 0.59 3.36 2.04 3.38 5.59 5.90 5.0% 0.63 4.63 2.71 3.19 4.31 4.43 10.0% 1.06 7.08 5.11 1.89 2.83 2.35 20.0% 3.43 30.35 18.55 0.58 0.66 0.65eagle to neon: 0.0% 0.79 3.21 1.93 2.53 6.23 6.23 0.1% 0.77 3.22 1.89 2.59 6.20 6.35 0.5% 0.80 3.24 1.88 2.51 6.17 6.39 1.0% 0.77 3.24 1.91 2.60 6.17 6.27 2.5% 0.79 3.17 1.90 2.53 6.31 6.33 5.0% 0.78 3.17 1.91 2.57 6.31 6.29 10.0% 0.81 3.85 2.51 2.48 5.20 4.78 20.0% 2.02 4.06 2.51 0.99 4.92 4.7813.4.3.初步分析
下面是对试验结果的一个初步研究,这里不做详细分析,这际上这个实验无法提供足够的硬数据来确定确切结果。然而,这里可以分析出实现网络通信中的几行代码的多种影响因素。如果要做更深层次的分析,这个可以做为读者的一个练习。
Figure 13.2: Ping-pong Benchmark Results.
Figure 13.3: blast Benchmark Results.
修改内核和插入模块对TCP连接会有一个小的但可测量的影响(会增加RTT时间),对于非常小的数据包,RTT时间会增加0.05兆秒,对于大数据包则是增加0.10兆秒。为什么会有差别呢?注意传输方向和数据包大小会造成传输效率有很大的差别。这可说明一点就是处理器速度和协议处理效率影响着RTT,对于1500字节的数据包,66字节的打包数据(20字节TCP信息,20字节IP信息,还有至少26字节的以太网传输信息)是微乎其微的,但对于5字节的数据包,这个开销是太大了。所以可以假定加入模块后的代价是发送大数据包时会慢0.10兆秒。
在TCP连接中做乒乓测试的结果是丢包率和RTT是个线性关系,看图13.2。这是预想得到的,不管是数据包丢失还是没有回应,发送都会被暂停然后再次发送。不管是用什么机器来发送,RTT值都是非常接近的(在实验允许的误差范围内),这是因为这个测试方法是在两台机器中同一时间内进行。
当数据包很小时,传输效率高低依赖于传输路径,这是因为其中一个机器中(eagle)比另一个机器要慢。在发送大量的小数据包中,使得传输变慢的原因不在于传输介质或接口,是在于处理器建立和发送数据包的速度。然而,对于大数据包,在两个机器上的传输效率是相近的(在试验低误差率下),这种情况下网络便是个制约因素,跟处理器速度关系不大。
最奇怪的是当丢包率在大约1%时传输效率明显达到最好(对于暴力测试情况,把从接收方发向发送方发的ACK确认数据包丢弃掉对传输效率影响很小),甚至比不丢包的情况还要好。这完全是一个直觉上的发现,为什么丢失数据包会提高传输效率呢?一个1%传输错误率可能刚好足以阻止因TCP传输指数倒退算法造成传输速率变慢。当接收方收到一个序列外的数据包时会立即发一个ACK确认数据包,数据包中可能会包含窗口长度信息让以送方中止发送数据。传输因发送数据包序列乱了而被中断,可能会导致发送数据更加频繁,也会导致清空缓存窗口,并再次使得发送方暂停发送数据。这种情况会有许多的原因,确定哪种原因是真实的需要更加深入地去研究,便这会很有意思。
13.5.代码
13.5.1.内核
下面代码是在内核中植入一个后门。建立一个函数用于被dev_queue_xmit()函数调用,并导出这个函数以便于模块能够使用这个函数。这几行代码是直接添加到内核代码中,添加后内核被重编译,编译后安装新内核并且新内核来启动计算机。注意当测试模块没被安装时内核功能还是跟以前一样(显然是额外相似的)。
net/core/dev.c (after line 579)
...int *test_function(struct sk_buff *)=0; /* new */ int dev_queue_xmit(struct sk_buff *skb)... ...struct Qdisc *q; if (test_function && (*test_function)(skb)) { /* new */ kfree_skb(skb); /* new */ return 0; /* new */ } /* new */ #ifdef CONFIG_NET_PROFILE...net/netsyms.c (after line 544)
...extern int (*test_function)(struct sk_buff *); /* new */EXPORT_SYMBOL_NOVERS(test_function); /* new */EXPORT_SYMBOL(register_gifconf);...13.5.2.模块
下面是关于丢包模块的代码。安装后运行时,会先计算削减百分比,并把地址赋值给上面定义的函数指针,这样,任何经过函数dev_queue_xmit()发出的数据包也会经过packet_dropper函数,packet_dropper函数会比较目标地址和代码中的写死的地址,如果地址一样根据削减百分比产生一个随机数来确定是否要丢弃数据包,不用丢弃则把包原封不动传递出去。当删除该模块时,则再把函数指针重置为0(null)。(注意:这里代码不是很完善,丢包策略是简单地根据两个字节的短整形随机数值。函数get_random_bytes()只是对内核或模块可用,当然,采用的随机数会跟密码一样不易被破解。
packet_dropper.c
/* packet_dropper.c * * This program provides an example of how to install a module into a * slightly modified kernel that will randomly drop packets for a specific * (hard-coded) host. * * See linux/drivers/char/random.c for details of get_random_bytes(). * * Usage (must be root to use): * /sbin/insmod packet_dropper * /sbin/rmmod packet_dropper */ #define MODULE#define MAX_UNSIGNED_SHORT 65535 #include <linux/module.h>#include <linux/skbuff.h> /* for struct sk_buff */#include <linux/ip.h> /* for struct iphdr */ extern int (*test_function)(struct sk_buff *); /* calling function */extern void get_random_bytes(void *buf, int nbytes); /* random function */unsigned short cutoff; /* drop cutoff */float rate = 0.050; /* drop percentage */__u32 target = 0x220010AC; /* 172.16.0.34 */ /************************************************************ packet_dropper * this is what dev_queue_xmit will call while this module is installed */int packet_dropper(struct sk_buff *skb) { unsigned short t; if (skb->nh.iph->daddr == target) { get_random_bytes(&t,2); if (t <= cutoff) return 1; /* drop this packet */ } return 0; /* continue with normal routine */} /* packet_dropper */ /*************************************************************** init_module * this function replaces the null pointer with a real one */int init_module() { EXPORT_NO_SYMBOLS; cutoff = rate * MAX_UNSIGNED_SHORT; test_function = packet_dropper; printk("<1> packet_dropper: now dropping packets/n"); return 0;} /* init_module */ /************************************************************ cleanup_module * this function resets the function pointer back to null */void cleanup_module() { test_function = 0; printk("<1> packet_dropper: uninstalled/n");} /* cleanup_module */
第十四章
14.其它资源
14.1.网站
Linux Documentation Project
· http://metalab.unc.edu/mdw/index.html
Linux Headquarters
Linux HOWTOs
· ftp://metalab.unc.edu/pub/Linux/docs/HOWTO
Linux Kernel Hackers' Guide
· http://metalab.unc.edu/mdw/LDP/khg/HyperNews/get/khg.html
Linux Router Project
New TTCP
· http://users.leo.org/~bartel
Red Hat Software
Requests for Comment
· http://www.rfc-editor.org/isi.html
14.2.书本
Computer Networks
· Tanenbaum, Andrew, Prentice-Hall Inc., Upper Saddle River, NJ, 1996.
High Speed Networks
· Stallings, William, Prentice-Hall Inc., Upper Saddle River, NJ, 1998.
Linux Core Kernel Commentary
· Maxwell, Scott, CoriolisOpen Press, Scottsdale, AZ, 1999.
Linux Device Drivers
· Rubini, Alessandro, O'Reilly & Associates, Inc., Sebastopol, CA, 1998.
Linux Kernel Internals
· Beck, Michael, et al., Addison-Wesley, Harlow, England, 1997.
Running Linux
· Welsh, Matt, Dalheimer, Matthias, and Kaufman, Lar, O'Reilly & Associates, Inc., Sebastopol, CA, 1999.
Unix Network Programming, Vol. 1 (2d Ed.)
· Stevens, W. Richard, Prentice-Hall Inc., Upper Saddle River, NJ, 1998.
第十五章
15.缩写词
ARP
· Address Resolution Protocol
ATM
· Asynchronous Transfer Mode (a protocol)
BSD
· Berkeley Software Distribution
DHCP
· Dynamic Hardware Configuration Protocol
DNS
· Domain Name Server
FIB
· Forwarding Information Base
GUI
· Graphical User Interface
ICMP
· Internet Control Message Protocol
INET
· Internet
IP
· Internet Protocol
ISP
· Internet Service Provider
LAN
· Local Area Network
LDP
· Linux Documentation Project
lo
· Loopback (device or interface)
MTU
· Maximum Transfer Unit
PPP
· Point-to-Point Protocol
RARP
· Reverse Address Resolution Protocol
RIP
· Routing Information Protocol
RTT
· Round Trip Time
TCP
· Transmission Control Protocol
UDP
· User Datagram Protocol
UNH
· University of New Hampshire
VLAN
· Virtual Local Area Network
WAN
· Wide Area Network
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/28/1544227.aspx