深入剖析网络发送过程
深入剖析网络发送和接收过程本文在基于以下三个条件所写的:1) OSI七层网络通信模型。2) 所阐述的函数是基于 Linux2.6.1内核。3) 在面向连接的通信协议 TCP/IPV4的基础上。由于七层模型(应 用 层 , 表 示 层 , 会 话 层 , 传 输 层 , 网 络 层 , 数 据 链 路 层 , 物 理 层 )可以简化为以下五层结构: 应 用 层 (Application Layer), 传 输 层 (Transport Layer), 网 络 层 (Network Layer), 数 据 链 路 层 (Data Link Layer), 物 理 层 (Physical Layer).其 中 七 层 模 型 中 的 前 三 层 都 归 结 为 五 层 结 构 中 的 应 用 层 。 为 了 简 化 讨 论 , 本 文 主 要 从 这五 层 结 构 来 探 讨 。Layer 5:应用层(A pplication Layer)在 TCP协议上,当通过三方握手建立了连接之后,就进入数据包的实质发送阶段,在本文中以 send命令来阐述。当通过 send将数据包发送之后,glibc 函数库会启用另外一个其定义的别用名函数_libc_sendto(),该函数最后会间接执行到 sendto系统调用:inline_syscall#nr(name, args);/ #nr 说明是该系统调用带有 nr 个 args 参数 sendto 系统调用的参数值是 6,而 name 就是 sendto从上面的分析可以看出 glibc 将要执行的下面一条语句是inline_syscall6(name,arg1,arg2,arg3,arg4,arg5,arg6)在该函数中一段主要功能实现代码如下:_asm_ _volatile_ ("callsys # %0 %1 0) /* We have some space in skb head. Superb! */if (copy > skb_tailroom(skb)copy = skb_tailroom(skb);if (err = skb_add_data(skb, from, copy) != 0)goto do_fault;2.2)如果 skb没有了可用空间,内核会使用 TCP_PAGE宏来为发送的数据包分配一个高速缓存页空间,当该页被正确地分配后就调用 Copy_from_user(to(page地址),from(usr 空间),n)将用户空间数据包复制到 page所在的地址空间。但是我们都知道数据包在协议层之间的传输是通过 skb的,难道将数据包复制到这个新分配的 page中,内核就可以去睡大觉了吗?当然不是!接下来内核就要来处理这个问题了,那么怎样来处理呢?此时就需要使用到 skb中的另外一个数据区 struct skb_shared_info,但是该数据区在创建 skb时是没有为其分配空间的,也就是说它开始纯粹就是个指针,而没有具体的告诉它要指向什么地方。这时大家应该知道它可以指向什么地方了,对,就是 page!在内核中对这种情况的具体是通过 fill_page_desc(struct sk_buff *skb,int I,struct page *page,int off,int size)来实现的,代码如下:static inline void fill_page_desc(struct sk_buff *skb, int i,struct page *page, int off, int size)skb_frag_t *frag = frag->page = page;frag->page_offset = off;frag->size = size;skb_shinfo(skb)->nr_frags = i + 1;这里需要注意的是 struct skb_shared_info只能通过 skb_shinfo来获取,在该结构体中 skb_flag_t类型的 flagsi就是具体指向 page的数组。2.3)至此 skb数据包的装载工作算是结束了,接下来就需要做一些后续工作,包括是否要分片,以及后来的 TCP协议头的添加。先看在 tcp_sendmsg()中 的 最 后 一 个 重 要 函 数tcp_push,它 的 调 用 格 式 如 下 :static inline void tcp_push(struct sock *sk, struct tcp_opt *tp, int flags,int mss_now, int nonagle)细心的朋友会发现,在该函数中传输的竟然不是 skb,而是一个名为 sock的结构体,那这又是什么东东呢?个人理解是它在顶层协议层之间(例如:应用层和传输层之间)的传输起着非常重要的作用,相当于沟通两层之间的纽带。再深入查找下该结构体的构成,我们很容易发现这样一个结构体变量:struct sk_buff_head,有名称我们可以知道它是用来描述skb头部信息的一个结构体,它指向了 buffer的数据区。这下我们也明白了点,这个结构体其实还充当了一个队列作用,是用来存储 skb的数据区。协议层之间传输完之后,具体到该层处理时内核就会从 sk_buff_head逐个中取出 skb数据区来处理,例如添加协议头等。好了,t cp_sendmsg 到 此 结 束 了 它 的 使 命 了 , 下 面 将 要 需 要 的 一 个 函 数 就 是 在tcp_push()中 直 接 用 到 的 一 个 函 数 :_tcp_push_pending_frames(),该 函 数 又 直 接 调用 tcp_write_xmit()函 数 来 进 一 步 对 数 据 包 处 理 , 它 包 括 一 下 两 步 :1) 检 查 是 否 需 要 对 数 据 包 进 行 分 片 , 条 件 是 只 要 skb 中 全 部 数 据 长 度 大 于 当 前 路 由负 荷 量 就 需 要 分 片 。2) 采 用 skb_clone(skb,GFP_ATOMIC)为 TCP_HEAD 分 配 一 个 sk_buff 空 间 , 这 里 需 要注 意 的 是 skb_clone 分 配 空 间 的 特 点 , 它 首 先 是 依 照 参 数 skb 来 来 复 制 出 一 个 新的 sk_buff, 新 的 skb 和 旧 的 skb 共 享 数 据 变 量 缓 存 区 , 但 是 结 构 体 缓 冲 区 不 是共 享 的 , 这 似 乎 和 copy on write 机 制 有 些 相 似 。3) 在 分 配 了 一 个 新 的 skb 之 后 , 内 核 就 会 执 行 tcp_transmit_skb().其 实 内 核 中 是将 2, 3 步 合 在 一 起 的 , 如 下 :tcp_transmit_skb(sk, skb_clone(skb, GFP_ATOMIC)接下来就是 tcp_transmit_skb 函数的实现过程了。1) 通过 skb_push()在 skb 前面加入 tcp 协议头信息。这包括序列号,源地址,目的地址,校验和等。2) 通过 tcp_opt 结构体( 它是在该函数的开始部分从 sock 结构体中获得的) 来访问 tcp_func结构体中的.queue_xmit 虚拟功能函数,在 IPV4 中是调用了 ip_queue_xmit(),这样就进入了下一层网络层。Layer 3:网 络 层 (Network Layer)在 ip_queue_xmit()函数中需要做的事情有一下几件:1) 是否需要将数据包进行路由,如果需要的话就跳到包路由子程序段。判断是否需要路由是由如下语句执行的:rt = (struct rtable *) skb->dst;if (rt != NULL)goto packet_routed;在 skb的 dst变量中指明发送目标地址。它存放了路由路径中的下台主机地址。如果是需要对数据包进行路由,那么其执行分如下步骤:1.1) 使用 skb_push()在 skb前面插入一段 ip_headsize大小的空间。1.2) 填写 ip协议头,包括 ttl,protocol等1.3) 写入校验和,最后调用 NF_HOOK宏,关于 NF_HOOK后面介绍。调用的 NF_HOOK宏语句如下:NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,dst_output);2) 如果没有路由地址,内核会尝试从外部可选项中来获取该地址,此时传输层发现没有路由地址会不断地发出重发机制,直到路由地址获取到。当获取到路由地址之后,内核会通过以下语句重新将地址赋给 skb->dst.之后就会进入到 1)所述的路由子程序段执行。skb->dst = dst_clone(所以这样看来正常情况下内核都会进入 1.3)所阐述的 NF_HOOK宏的执行。关于 NF_HOOK宏,我也不怎么了解,但是查了下内核后可以大体的知道,当二维数组nf_hookspfhook(其下标分别是调用宏中的第一个和第二个参数)中定义了需要的钩子函数时,就会调用 nf_hook_slow函数来处理,如果没有定义钩子函数就直接调用 NF_HOOK中的最后一个参数所指向的函数,在这里是:dst_output(skb)。在网上搜了下,发现一篇讲解 NF_HOOK 的帖子,很详细,链接如下:http:/www.skynet.org.cn/redirect.php?goto=lastpost&tid=7上面已经谈到,当存在钩子函数时,内核转向 nf_hook_slow函数来处理。下面阐述下这个函数:1) 检查 hook函数是否真的已经设置,如果没有设置就将 hook对应位通过移位来设置;当确认已经设置后就取出该钩子函数,如下:elem = 2) 执行 nf_iterate()函数,该函数采用 list_for_each_continue_rcu()来搜索 HOOK 链表中的每个 nf_hook_ops 钩子结构体,通过其内部变量 priority来判断它的优先级是否大于系统所定义的 INT_MIN,如果小于就继续搜索,否则就执行该结构体单元中所指向的钩子函数。if (hook_thresh > elem->priority)continue;/* Optimization: we don't need to hold modulereference here, since function can't sleep. -RR */switch (elem->hook(hook, skb, indev, outdev, okfn) 。当钩子函数成功执行之后,它会返回一个 NF_ACCEPT标志,3) 判断 nf_iterate()函数的返回标志,如下:switch (verdict) case NF_ACCEPT:ret = okfn(skb);break;case NF_DROP:kfree_skb(skb);ret = -EPERM;break;由上面的代码可以看到,当标志是 NF_ACCEPT 时,内核会继续调用 okfn(skb)函数,也就是传递给 NF_HOOK 的最后一个参数 dst_output(skb)。该函数非常简单,就是间接启用和skb 相关的 output 函数,如下:for (;) err = skb->dst->output(skb); if (likely(err = 0)return err;if (unlikely(err != NET_XMIT_BYPASS)return