2006年12月12日 星期二 上午 07:44
构建完成的IP选项(通过setsockopt系统调用,使用IP_OPTIONS命令),存放在套接字结构体struct inet_sock的成员opt中,在随后的数据报发送过程中,会根据opt的指示把相关的IP选项加到IP首部中,下面以一个原始套接字上的数据报发送为例,详细分析整个过程。
raw_sendmsg定义了一个局部变量struct ipcm_cookie ipc,其定义如下:
struct ipcm_cookie
{
u32 |
2006年12月11日 星期一 下午 10:27
IP首部中有4bit用于表示首部长度,其单位是4个字节,所以,IP首部的最大长度是15*4=60字节,而IP固定首部的长度是20字节,所以一个IP首部最大允许有40字节长度的选项。
IP选项的一般格式是1个字节的代码(code),一个字节的长度(len),一个字节的指针(ptr),指针的值从1开始计数,指向IP选项的内容,一般其值为4(跳过前面3字节的code, len, ptr),长度包括前面3字节在内的整个IP选项,最大值为40。
代码表示不同类型的IP选项,其可能的取值有:
|
2006年12月07日 星期四 下午 06:38
发生ICMP不可达差错的另一种情况是,当路由器收到一份需要分片的数据报(数据的长度大于路由器的MTU),但是该数据报的IP首部上设置了IP_DF标志(即IP首部的frag_off字段的高三位第二位,表示不可分片)。
由于实验环境的缺乏,为了出现ICMP_DEST_UNREACH类型的中的代码为ICMP_FRAG_NEEDED(需要分片)的ICMP报文,需要对协议栈的代码作少许改动,首先在inet_create函数创建socket时,强制给socket的成员pmtudisc赋值IP_PMTUDISC_DO,即总是要求不分片,包括在本机协议栈中发送数据报的时候。socke |
2006年12月06日 星期三 下午 09:25
回显请求与回显应答是两种icmp报文类型,类型号分别是8和0,这两种类型下都只有一种代码0。这两种icmp报文属查询报文,主要用于测试网络中另一台主机是否可达,向欲测试主机发送一份ICMP回显请求,并等待返回ICMP回显应答,如果能收到,表明该主机可达。这也是网络工具ping程序的实现原理,下面通过ping程序的实现来分析这两种icmp报文的实现原理。
建立ping程序,首先要创建一个INET域的类型为RAW_SOCK的原始套接字,绑定协议为icmp。前面讲过,icmp首部除前4个字节分别用于表示类型,代码 |
2006年12月05日 星期二 下午 10:27
当.2收到来自.1的icmp出错报文时,在协议栈的ip_local_deliver_finish函数中,会找到inet_protos[protocol&(MAX_INET_PROTOS - 1)]指向的icmp报文接收处理函数。因为在协议栈初始化的时候,通过函数调用inet_add_protocol( &icmp_protocol, IPPROTO_ICMP ),把结构体struct net_protocol icmp_protocol的地址直接赋给了数组inet_protos[IPPROTO_ICMP]指针。而icmp_protocol的初始化如下:
static struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
};
所以,收到的数据报交给了icmp_rcv。icmp_rcv首先检查skb->ip_summed,如果需要软件执行校验和(值为CHECKSUM_NONE),则先进行校验和验证。然后从icmp首部中取出类型,对类型的正确性进行验证。如果收到的icmp数据报是一个广播报文或组播报文,则需要对类型进行进一步的检查,如果类型是ICMP_ECHO(请求回显)或者ICMP_TIMESTAMP(时间戳请求),并且sysctl_icmp_echo_ignore_broadcasts不为0(即要求忽略这两类的广播报文),则不处理,直接出错返回,如 |
2006年12月05日 星期二 下午 06:37
tcp/ip module的开发到目前为止,都是围绕着dummy协议进行的,从最初的纯dummy版仅仅通过一个环回接口演示了数据在协议栈中走过的整个流程,到目前,已在链路层,网络层全面替换原来的dummy协议,实现了真正的IP协议。所以,现在的dummy协议仅仅是一个传输层协议,它是一个相当简陋的协议,其首部只是一个字符串“DUMMY”。在没有实现真正的传输层协议(TCP/UDP)之前,仅仅使用dummy协议向网络中的另一台主机发送数据报,其结果是肯定的:失败。
试验之前,先修改试验主机的网卡的驱动程序,把netif_rx改为mynetif_rx,使tcp/ip module(以下简称myinet模块)能与网卡配合工作,并屏蔽掉了内核的TCP/IP协议栈,网卡的IP地址设为172.16.48.2,对端主机的IP地址为172.16.48.1,从172.16.48.2向172.16.48.1发送一个dummy协议的数据,下面是在172.16.48.1上抓到的数据报(忽略ARP数据报):
172.16.48.2->172.16.48.1:
0000 00 50 56 c0 00 08 00 0c 29 80 d8 70 08 00 45 00 .PV.....)..p..E.
0010 00 26 01 |
2006年12月04日 星期一 上午 08:55
INET域中有两个地方需要查询输入路由,一个是当收到一个IP数据报,ip_rcv将其交给ip_rcv_finish后,ip_rcv_finish判断skb->dst是否为NULL(因为对于环回接口上收到的数据报,其dst是存在的,不需要查询输入路由),如果为NULL,则需要查询得到输入路由。另一个地方是当收到一个ARP数据报,arp_rcv将其交给arp_process处理时,arp_process也需要查询得到该skb的输入路由。
输入路由查询的调用接口是ip_route_input,该函数首先从路由缓存表rt_hash_table中查找,如果缓存表中不存在,则调用函数ip_route_input_slow生成一个新的输入路由项,ip_route_input_slow对输入路由分多种情况处理,如果skb的目的地址是广播地址,则调用dst_alloc创建本地输入的路由项,struct rtable的rt_flags成员的值为RTCF_DIRECTSRC|RTCF_BROADCAST|RTCF_LOCAL;如果skb的目的地址是本地接收地址,则创建本地输入的路由项,struct rtable的rt_flags成员的值为RTCF_DIRECTSRC|RTCF_LOCAL。
如果收到的skb不是本地接收的,是需要进行转发的,并且该网络设备接口也确实允许转发,则调用ip_mkroute_input创建路由项,最后真正 |
2006年12月01日 星期五 下午 01:04
网络设备在接收到来自网络中其它主机的数据报,或本地环回接口的数据报之后,交给协议栈的netif_rx函数,该函数首先要为收到的这个skb打上当前的时间戳(skb->tstamp成员),这个时间戳表示该数据到达的时间,它不是必选的,可以通过套接字选项SO_TIMESTAMP将其打开,该选项打开时间戳时,会将链路层的全局变量netstamp_needed加1,netif_rx在检查到这个变量不为零时,为skb打上时间戳。
softnet_data是类型为struct softnet_data结构体的全局变量,每个CPU定义一个,它是链路层的数据接收队列,该结构体的定义如下:
struct softnet_data
{
struct net_device *output_queue;
struct sk_buff_head input_pkt_queue;
struct list_head poll_list;
struct sk_buff *completion_queue;
|
2006年11月30日 星期四 下午 09:51
前面讲述IP数据报的分片与重组时,已完整描述了数据报在网络层是如何被传递的,网络层发送数据报的最后一站是ip_finish_output2,它根据skb->dst->hh是否已被创建来决定如何调用链路层的输出函数,hh实际是neighbour的hh成员,它在ARP解析完成,邻居节点被更新时进行创建,对于不需要ARP解析的设备接口(loopback等),它在第一次发送数据报时被创建。所以,不管网络层如何调用链路层的输出函数,链路层的第一个输出函数始终是dev_queue_xmit。
该函数首先检查skb_shinfo(skb)->frag_list是否有值,如果有,但是网络设备接口不支持skb的碎片列表(NETIF_F_FRAGLIST),则需要把这些碎片重组到一个完整的skb中(通过函数__skb_linearize)。第二步检查skb_shinfo(skb)->nr_frags,如果不为0,表示这个skb使用了分散/聚焦IO,如果网络设备接口不支持(NETIF_F_SG),同样需要重新线性化(通过函数__skb_linearize)。
第三步检查是关于校验和的,需要注意的是这个校验和不是IP首部的首部校验和,IP首部校验和在每个IP数据报中是必需的,由软件来完成,对IP首部以16bit为段进行反码求和得到,只覆盖到IP |
2006年11月29日 星期三 下午 10:02
前面有讲到过在函数ip_append_data中实现了对IP数据报的分片,这个讲法是错误的,需要纠正一下,ip_append_data的主要任务只是创建发送网络数据的套接字缓冲区(skb),它根据输出路由查询得到的输出网络设备接口的MTU,把超过MTU长度的应用数据分割开,并创建了多个skb,放入套接字的发送缓冲队列(sk_write_queue),但它并没有为任何一个skb数据加上网络层首部,并且,随后在ip_push_pending_frames函数中,又把发送缓冲队列中的所有的skb,以一个链表的形式追加到第一个skb的end成员后面的struct skb_shared_info结构体中的frag_list上,并只为第一个skb加上了网络层首部,所以,实际上,整个应用数据还只是在一个skb中,ip_append_data这样做只是为接下来的真正的IP数据的分片作好准备。
ip_push_pending_frames在完成了skb的组装后,把它交给了函数ip_output,ip_output又调用了函数ip_finish_output,该函数对skb的长度再次进行判断,如果长度超过输出设备的mtu的值,并且符合其它分片条件,则调用ip_fragment进行数据报的分片,否则直接调用ip_finish_output2输出到数据链路层。
IP数据的分片涉及 |
2006年11月28日 星期二 下午 06:48
Linux内核用结构体struct net_device表示一个网络设备接口,该结构体的成员hard_start_xmit是一个函数指针,用于完成数据报在网络上的发送工作,其原型是:
int (*hard_start_xmit)( struct sk_buff *skb, struct net_device *dev );
skb是待发送的数据缓冲区,dev是该网络设备接口本身的一个指针。环回设备接口由于是把数据报发给本机,所以其发送数据报函数比较特殊,它把skb稍加处理后,又转回给协议栈的数据报接收函数netif_rx。其发送函数的函数名是loopback_xmit。
首先,loopback_xmit调用skb_orphan把skb孤立,使它跟发送socket和协议栈不再有任何联系,也即对本机来说,这个skb的数据内容已经发送出去了,而skb相当于已经被释放掉了。skb_orphan所做的实际事情是,首先从skb->sk(发送这个skb的那个socket)的sk_wmem_alloc减去skb->truesize,也即从socket的已提交发送队列的字节数中减去这个skb,表示这个skb已经发送出去了,同时,如果有进程在这个socket上写等待,则唤醒这些进程继续发送数据报,然后把socket的引用计数减1,最后,令sk->destructor和s |
2006年11月26日 星期日 下午 05:02
前面讲到在INET域支持三种类型的套接字:流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。流套接字支持传输层的TCP协议,数据报套接字支持传输层的UDP协议,原始套接字则支持网络层的附属协议ICMP,IGMP等。在前面,已经为TCP/IP module加上了对原始套接字(SOCK_RAW)的支持,现在再为它加上对ICMP协议的支持。
结构体struct net_protocol表示一个协议(包括传输层协议和网络层附属协议)的接收处理函数集,一般包括一个正常接收函数,和一个出错接收函数。icmp协议的该结构体内容如下:
struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = NULL;
.no_policy = 0;
};
因为icmp本身就是一个控制报文协议,为其它协议提供差错报文传递及其它需要注意的信息的传递服务,所以,它就没有出错接收处理函数,只有一个正常接收函数。
|
2006年11月25日 星期六 上午 08:16
前面讲到,在数组inetsw_array中有INET域支持的全部套接字类型和协议类型,在一个结构体struct inet_protosw中,记录了一个套接字类型,该类型下的一种协议类型,以及该类型套接字的操作集和该类型协议的操作集,对于原始套接字,其协议类型为通配类型,通配所有网络层附属协议,其协议操作集为所有网络层附属协议的通过协议操作集,即struct proto raw_prot。在INET域进行初始化的时候,其要做的第一件事情就是通过函数proto_register注册协议操作集。
struct proto的成员slab是一个后备高速缓冲区,所有该类型下的网络层套接字struct sock都是从该后备高速缓冲区分配内存,struct proto的成员obj_size记录的是单个网络层套接字的空间大小,供分配slab时使用,其值是sizeof(struct raw_sock),raw_sock是对inet_sock的一个扩充(inet_sock是对sock的扩充)。在inet_create创建套接字的时候,调用sk_alloc创建struct sock,该函数直接从slab中分配内存。proto_register要做的第一件事情就是创建slab,struct proto还有成员rsk_prot和twsk_prot需要分配内存,也要在proto_register函数中完成,它是TCP协议相关的,暂时不关注。 |
2006年11月24日 星期五 下午 07:04
变量inetsw_array是inet域的一个全局数组,其类型是struct inet_protosw,该结构体的定义如下:
struct inet_protosw {
struct list_head list;
unsigned short type;
int protocol;
struct proto *prot;
const struct proto_ops *ops;
int capability;
char no_check;
|
2006年11月23日 星期四 下午 11:29
还是在测试环境中,环回设备mylo的mtu被设置为16*1024+20+20+12,20+20+12是为传输层及网络层首部及它们可能出现的选项预留的,16K是真正的应用数据,所以,一般的环回接口的mtu被设置为16K+52字节。为了测试应用数据在网络层被依据设备的mtu进行分片,下面把mylo的mtu设置为6+20+20+12,总共为58字节。因为环回接口的mtu不受以太网mtu的68至1500的范围的限制,同时,可以确保DUMMY协议不会出现传输层首部和网络层首部的选项,我们可以这么设。
具体的,IP数据报的分片是在做完输出路由的查询后,在ip_append_data函数中完成,因为在完成输出路由的查询前,不知道本次输出的输出设备接口是哪个,自然也不知道mtu是多大。现在,测试程序dummytest发送676字节长度的应用数据,由于DUMMY协议的传输层首部长度为5字节(只在第一个分片里面出现),网络层首部长度为20字节,同时,58字节的mtu被对齐为52字节( (58-20) & ~7 + 20 ),所以,676字节的数据要被分成 (676+5)/(52-20) = 22个IP数据报分片。
22个数据报分片中,第一个分片是比较特殊的,因为它是唯一一个包含有传输层首部的分片,该分片的skb用 |
|
|