- 浏览: 1407397 次
- 性别:
- 来自: 火星
文章分类
最新评论
-
aidd:
内核处理time_wait状态详解 -
ahtest:
赞一下~~
一个简单的ruby Metaprogram的例子 -
itiProCareer:
简直胡说八道,误人子弟啊。。。。谁告诉你 Ruby 1.9 ...
ruby中的类变量与类实例变量 -
dear531:
还得补充一句,惊群了之后,数据打印显示,只有一个子线程继续接受 ...
linux已经不存在惊群现象 -
dear531:
我用select试验了,用的ubuntu12.10,内核3.5 ...
linux已经不存在惊群现象
这次我们来看数据包如何从4层传递到3层。
先看下面的图,这张图表示了4层和3层之间(也就是4层传输给3层)的传输所需要调用的主要的函数:
我们注意到3层最终会把帧用dst_output函数进行输出,而这个函数,我们上一次已经讲过了,他会调用skb->dst->output这个虚函数(他会对包进行3层的处理),而最终会调用一个XX_finish_output的函数,从而将数据传递到neighboring子系统。
这张我们主要聚焦于ip_push_pending_frames,ip_append_data,ip_append_page,ip_queue_xmit这几个函数。
ip_queue_xmit:
4层协议(主要指tcp 和 sctp)将数据包按照pmtu切片(如果需要),然后3层的工作只需要给传递下来的切片加上ip头就可以了(也就是说调用这个函数的时候,其实4层已经切好片了)。因此这个函数的处理逻辑比较简单。
ip_push_pending_frames和后面的2个函数:
4层调用这几个函数不会考虑切片,4层调用ip_append_data时会存储请求,也就是会将数据包排队(其中每个都不大于pmtu)到一个输出队列.这样的话使3层的处理更加方便和高效。
当4层需要flush输出队列到3层时,他需要显式的调用ip_push_pending_frames.其实也就是发送包到dst_output.
ip_append_page只是ip_append_data的一个变体。
我们还看到rawip和igmp都是直接调用dst_output,也就是直接和3层交互。
在linux中,每一个bsd socket都被表示为一个socket的数据结构,而每一个protocol family都被表示为一个包含着sock的数据结构,这里我们来看PF_INET的结构:
可以看到每个inet_sock都包含一个sock也就是socket,它存储了每个协议簇的私有部分的数据。这样只要给定我们一个sock,我们都能通过inet_sk来得到inet_sock的指针。其实按照他们的内存分布,他们的地址是一样的。
而cork域则在ip_append_data和ip_append_page中扮演的重要的角色,它存储被这两个函数所需要的正确切片的一些上下文信息。
接下来来看ip_queue_xmit的实现,这个函数主要是被tcp和sctp所使用,第一个参数表示被传递的buffer的指针,第二个参数主要是被sctp来使用,就是是否切片被允许的标志:
接下来来看ip_append_data函数,先来看它的参数的含义:
sk: 这个传输包的socket
getfrag: 这个函数用来复制从4层接收到的负荷到数据帧(3层)。
from: 4层的data起始指针。
length: 将要传输的数据的大小,包括4层的头和4层的负荷。
transhdrlen: 四层头的大小
ipc: 需要正确forward数据报的一些信息。
rt: 路由信息
flags:这个变量样子是MSG_XXX,他们包括下面几个定义:
MSG_MORE: 这个是应用程序用来告诉4层这儿将会有更多的小数据包的传输,然后将这个标记再传递给3层,3层就会提前划分一个mtu大小的数据包,来组合这些数据帧。
MSG_DONTWAIT: 当这个flag被设置,调用ip_append_data将不会阻塞。
MSG_PROBE :当这个标记被设置,说明用户不想要真正的传输什么东西,而是知识探测路径。例如测试一个pmtu。
解释下ip+append_data的大体架构,在ip_queue_xmit中,也就是tcp协议使用的传输中,每次传递下来的数据包都要扔给dst_output来处理,而在ip_append_data中,它可以通过MSG_MORE来创建一个最接近mtu大小的数据块,然后将传递下来数据包(小于mtu)的,多个组成一个最接近mtu大小的数据包,然后传递给dst_output.而且他还有一个sk_write_queue队列,这个队列保存了数据传输的请求,也就是将要传递给dst_output的数据包(上面所说的最接近mtu大小的数据包)组成一个队列,从而当ip_push_pending_frams调用时,传递给dst_output.
下面这张图解释了,一个不需要切片,并且包含一个ipsec头的ip包通过ip_append_data后的结果:
这里要注意3层头的填充是通过ip_push_pending来进行填充的。而且一般的4层协议不会直接调用ip_push_pending_frams,而是调用它的包装函数,比如udp就会调用udp_push_pending_frames。
还有一个要注意的是,当没有msg_more时,如果有一个大于pmtu的包传递下来时,他会切包,其中第一个包为pmtu大小,第二个包是剩下的大小,然后把这两个包加入到sk_write_queue队列。而设置了msg_more,此时第二个包的大小就是pmtu,也就是说当再有小的数据包下来,就不需要再次分配空间,而可以直接加入到剩余的数据空间中。
有些硬件设备提供Scatter/Gather I/o这也就意味着能够交由硬件来组合这些小的数据包(3层可以什么都不用做,当数据包离开host的时候,硬件会将它组合好),这样就降低了分配内存和复制数据的开销。
由于一个sk buff只会有一个ip头,因此放到page buff的只会是L4 payload,而不包括头。这里就不需要复制,而是直接将数据放到page buff,接下来的图表示了有Scatter/Gather I/O的情况时,调用ip_append_data之前和之后的区别:
这里可以看到nr_frags域来表示有多少个S/G I/O buffer在这个包中被使用。其实整个S/G I/O buffer相当于一个数组,每个元素都是一个skb_frag_t结构,而这个数组的大小就是nr_frags,最大的size是MAX_SKB_FRAGS.
这里要注意,当一个新的帧的大小,大于当前页的剩余大小是,他会被分为两部分,一部分在当前页,一部分在新的页。
没有 s/g I/O:
它会复制数据到当前的data。
4层可以调用ip_append_data多次,在flush这个buff之前。
还有一个getfrag,我再说明下,ip_append_data的任务之一就是复制输入数据到它创建的帧,而不同的协议需要不同的复制操作。比如4层的check sum。有些4层协议就是不需要的。
因此就有了这样一个虚函数,不同的协议实现自己的复制函数,然后传入到ip_append_data.
这个函数其实也就是将用户空间的数据复制到内核空间。
下面这个图就是一些协议实现的复制函数:
接下来的这个图表示了ip_append_data的流程图:
下来我们来看它的具体实现:
在上面的代码中,我们可以看到同一个物理页,有可能被sk_sndmsg_page和skb_frag_t 所共享,可以看下下面的图:
接下来来看ip_append_page,这个函数比较简单,我们大概分析下就可以了。
我们知道内核提供给用户空间的一个零拷贝的接口sendfile.这个接口只能当设备提供Scatter/Gather I/O的时候,才能使用。而它的实现就是基于ip_append_page这个函数来实现的。如果设备不支持S/G I/O,ip_append_page会直接返回错误。
它的逻辑实现和ip_append_page最后面那段实现很相似,不过有些不同,当加一个新的帧到page时,ip_append_page它会merge新的和也在当前页的前一个帧。它会通过调用skb_can_coalesce来进行检测这个。然后当merge是可能的,它就会update前一个帧的长度。
当merge是不可能的时候,处理和ip_append_data相似。
下面就是ip_append_page的一些代码片段:
ip_append_page只被udp使用。tcp不使用ip_append_data和ip_push_pending_frams是因为它把一些逻辑放到tcp_sendmsg来实现了。因此相似的,0拷贝接口,tcp不使用ip_append_page是因为他在do_tcp_sendpage中实现了相同的逻辑。
最后我们来看ip_push_pending_frams函数。
这个函数相当于一个notify函数,当4层决定传输帧到ip层的时候,他就需要调用这个函数.通过前面我们知道此时所有的数据(如果不支持Scatter/Gather I/O),都在sk_write_queue中。
这个函数要做的其实很简单,就是从sk_write_queue中取出数据,加上ip头,然后通过dst_output发送给3层。
当数据从sk_write_queue从移除后,加入到frag_list链表中。
下面这张图表示了从sk_write_queue中移除buffer之前和之后的区别(没有考虑Scatter/Gather I/O).
接下来来看它的实现:
接下来我们会来简要的介绍4层使用上面的函数接口和3层如何把帧传递给2层的接口:
先来看udp_sendmsg的代码片段:
我们现在知道4层到3层之后,最终通过dst_output来把帧进行输出,这个函数在单播的情况下,是被实例化为ip_output.这里和前面的netfilter一样,还存在一个ip_output_finish方法,当通过netfilter hook后,如果这个包可以被netfilter放过,那么帧就会传递到ip_output_finish方法,然后再调用ip_output_finish2方法。而最终dev_queue_xmit(前面的blog有介绍,也就是2层的传输方法)会被调用(这里是通过hh->hh_output方法或者fst->neighbour->output 2个虚函数)来传输。
先看下面的图,这张图表示了4层和3层之间(也就是4层传输给3层)的传输所需要调用的主要的函数:
我们注意到3层最终会把帧用dst_output函数进行输出,而这个函数,我们上一次已经讲过了,他会调用skb->dst->output这个虚函数(他会对包进行3层的处理),而最终会调用一个XX_finish_output的函数,从而将数据传递到neighboring子系统。
这张我们主要聚焦于ip_push_pending_frames,ip_append_data,ip_append_page,ip_queue_xmit这几个函数。
ip_queue_xmit:
4层协议(主要指tcp 和 sctp)将数据包按照pmtu切片(如果需要),然后3层的工作只需要给传递下来的切片加上ip头就可以了(也就是说调用这个函数的时候,其实4层已经切好片了)。因此这个函数的处理逻辑比较简单。
ip_push_pending_frames和后面的2个函数:
4层调用这几个函数不会考虑切片,4层调用ip_append_data时会存储请求,也就是会将数据包排队(其中每个都不大于pmtu)到一个输出队列.这样的话使3层的处理更加方便和高效。
当4层需要flush输出队列到3层时,他需要显式的调用ip_push_pending_frames.其实也就是发送包到dst_output.
ip_append_page只是ip_append_data的一个变体。
我们还看到rawip和igmp都是直接调用dst_output,也就是直接和3层交互。
在linux中,每一个bsd socket都被表示为一个socket的数据结构,而每一个protocol family都被表示为一个包含着sock的数据结构,这里我们来看PF_INET的结构:
struct inet_sock { /* sk and pinet6 has to be the first two members of inet_sock */ struct sock sk; #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE) struct ipv6_pinfo *pinet6; #endif /* Socket demultiplex comparisons on incoming packets. */ ................................................... struct { .......................................... } cork; };
可以看到每个inet_sock都包含一个sock也就是socket,它存储了每个协议簇的私有部分的数据。这样只要给定我们一个sock,我们都能通过inet_sk来得到inet_sock的指针。其实按照他们的内存分布,他们的地址是一样的。
而cork域则在ip_append_data和ip_append_page中扮演的重要的角色,它存储被这两个函数所需要的正确切片的一些上下文信息。
接下来来看ip_queue_xmit的实现,这个函数主要是被tcp和sctp所使用,第一个参数表示被传递的buffer的指针,第二个参数主要是被sctp来使用,就是是否切片被允许的标志:
int ip_queue_xmit(struct sk_buff *skb, int ipfragok) { ///取出sock,inet_sock以及option struct sock *sk = skb->sk; struct inet_sock *inet = inet_sk(sk); struct ip_options *opt = inet->opt; struct rtable *rt; struct iphdr *iph; /* Skip all of this if the packet is already routed, * f.e. by something like SCTP. */ ///得到相关路由信息,如果buffer已经标记了相应的路由信息,则跳过下面的构造路由表。 rt = skb->rtable; if (rt != NULL) goto packet_routed; ///下面检测在这个sock中,路由是否已经cache,如果有,则检测这个路由是否还可以使用。 rt = (struct rtable *)__sk_dst_check(sk, 0); ///cache不存在,查找新路由。 if (rt == NULL) { __be32 daddr; /* Use correct destination address if we have options. */ daddr = inet->daddr; ///检测source route option if(opt && opt->srr) daddr = opt->faddr; { struct flowi fl = { .oif = sk->sk_bound_dev_if, .nl_u = { .ip4_u = { .daddr = daddr, .saddr = inet->saddr, .tos = RT_CONN_FLAGS(sk) } }, .proto = sk->sk_protocol, .uli_u = { .ports = { .sport = inet->sport, .dport = inet->dport } } }; /* If this fails, retransmit mechanism of transport layer will * keep trying until route appears or the connection times * itself out. */ security_sk_classify_flow(sk, &fl); ///如果是 strict source route option,则会在这个函数中进行下一跳的精确匹配。 if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0)) goto no_route; } ///主要是保存一些设备的features。 sk_setup_caps(sk, &rt->u.dst); } ///clone一个skb->dst,也就是引用计数+1了。 skb->dst = dst_clone(&rt->u.dst); packet_routed: ///当有strictroute option的时候,检测下一跳,如果不等,则丢掉这个包。这里丢掉包不需要发送icmp,因为我们本身就是源,因此只需要返回错误代码给高层就行了。 if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway) goto no_route; ///开始build ip头。 ///移动指针指向ip头。 skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0)); ///保存这个指针到network_head skb_reset_network_header(skb); ///取出ip头 iph = ip_hdr(skb); ///实例化ip头。 *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff)); if (ip_dont_fragment(sk, &rt->u.dst) && !ipfragok) iph->frag_off = htons(IP_DF); else iph->frag_off = 0; iph->ttl = ip_select_ttl(inet, &rt->u.dst); iph->protocol = sk->sk_protocol; iph->saddr = rt->rt_src; iph->daddr = rt->rt_dst; /* Transport layer set skb->h.foo itself. */ if (opt && opt->optlen) { iph->ihl += opt->optlen >> 2; ///设定ip头不进行切片。 ip_options_build(skb, opt, inet->daddr, rt, 0); } ///设置ip包的id。 ip_select_ident_more(iph, &rt->u.dst, sk, (skb_shinfo(skb)->gso_segs ?: 1) - 1); ///用来流量控制。 skb->priority = sk->sk_priority; skb->mark = sk->sk_mark; ///这个函数首先进行ip checksum,最终会通过netfilter的hook,从而由netfilter来决定包丢弃还是传递给dst_output. return ip_local_out(skb); no_route: IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); kfree_skb(skb); return -EHOSTUNREACH; }
接下来来看ip_append_data函数,先来看它的参数的含义:
sk: 这个传输包的socket
getfrag: 这个函数用来复制从4层接收到的负荷到数据帧(3层)。
from: 4层的data起始指针。
length: 将要传输的数据的大小,包括4层的头和4层的负荷。
transhdrlen: 四层头的大小
ipc: 需要正确forward数据报的一些信息。
rt: 路由信息
flags:这个变量样子是MSG_XXX,他们包括下面几个定义:
MSG_MORE: 这个是应用程序用来告诉4层这儿将会有更多的小数据包的传输,然后将这个标记再传递给3层,3层就会提前划分一个mtu大小的数据包,来组合这些数据帧。
MSG_DONTWAIT: 当这个flag被设置,调用ip_append_data将不会阻塞。
MSG_PROBE :当这个标记被设置,说明用户不想要真正的传输什么东西,而是知识探测路径。例如测试一个pmtu。
解释下ip+append_data的大体架构,在ip_queue_xmit中,也就是tcp协议使用的传输中,每次传递下来的数据包都要扔给dst_output来处理,而在ip_append_data中,它可以通过MSG_MORE来创建一个最接近mtu大小的数据块,然后将传递下来数据包(小于mtu)的,多个组成一个最接近mtu大小的数据包,然后传递给dst_output.而且他还有一个sk_write_queue队列,这个队列保存了数据传输的请求,也就是将要传递给dst_output的数据包(上面所说的最接近mtu大小的数据包)组成一个队列,从而当ip_push_pending_frams调用时,传递给dst_output.
下面这张图解释了,一个不需要切片,并且包含一个ipsec头的ip包通过ip_append_data后的结果:
这里要注意3层头的填充是通过ip_push_pending来进行填充的。而且一般的4层协议不会直接调用ip_push_pending_frams,而是调用它的包装函数,比如udp就会调用udp_push_pending_frames。
还有一个要注意的是,当没有msg_more时,如果有一个大于pmtu的包传递下来时,他会切包,其中第一个包为pmtu大小,第二个包是剩下的大小,然后把这两个包加入到sk_write_queue队列。而设置了msg_more,此时第二个包的大小就是pmtu,也就是说当再有小的数据包下来,就不需要再次分配空间,而可以直接加入到剩余的数据空间中。
有些硬件设备提供Scatter/Gather I/o这也就意味着能够交由硬件来组合这些小的数据包(3层可以什么都不用做,当数据包离开host的时候,硬件会将它组合好),这样就降低了分配内存和复制数据的开销。
由于一个sk buff只会有一个ip头,因此放到page buff的只会是L4 payload,而不包括头。这里就不需要复制,而是直接将数据放到page buff,接下来的图表示了有Scatter/Gather I/O的情况时,调用ip_append_data之前和之后的区别:
struct skb_frag_struct { struct page *page; __u32 page_offset; __u32 size; };
这里可以看到nr_frags域来表示有多少个S/G I/O buffer在这个包中被使用。其实整个S/G I/O buffer相当于一个数组,每个元素都是一个skb_frag_t结构,而这个数组的大小就是nr_frags,最大的size是MAX_SKB_FRAGS.
这里要注意,当一个新的帧的大小,大于当前页的剩余大小是,他会被分为两部分,一部分在当前页,一部分在新的页。
没有 s/g I/O:
它会复制数据到当前的data。
4层可以调用ip_append_data多次,在flush这个buff之前。
还有一个getfrag,我再说明下,ip_append_data的任务之一就是复制输入数据到它创建的帧,而不同的协议需要不同的复制操作。比如4层的check sum。有些4层协议就是不需要的。
因此就有了这样一个虚函数,不同的协议实现自己的复制函数,然后传入到ip_append_data.
这个函数其实也就是将用户空间的数据复制到内核空间。
下面这个图就是一些协议实现的复制函数:
接下来的这个图表示了ip_append_data的流程图:
下来我们来看它的具体实现:
int ip_append_data(struct sock *sk, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, struct ipcm_cookie *ipc, struct rtable *rt, unsigned int flags) { ///取出取出相关的变量。 struct inet_sock *inet = inet_sk(sk); struct sk_buff *skb; struct ip_options *opt = NULL; int hh_len; int exthdrlen; int mtu; int copy; int err; int offset = 0; unsigned int maxfraglen, fragheaderlen; int csummode = CHECKSUM_NONE; ///如果只是探测路径则直接返回。 if (flags&MSG_PROBE) return 0; ///当sk_write_queue 为空,意味着创建的是第一个ip帧。因此需要初始化一些相关域。 if (skb_queue_empty(&sk->sk_write_queue)) { /* * setup for corking. */ ///初始化cork的一些相关域。 opt = ipc->opt; if (opt) { if (inet->cork.opt == NULL) { inet->cork.opt = kmalloc(sizeof(struct ip_options) + 40, sk->sk_allocation); if (unlikely(inet->cork.opt == NULL)) return -ENOBUFS; } memcpy(inet->cork.opt, opt, sizeof(struct ip_options)+opt->optlen); inet->cork.flags |= IPCORK_OPT; inet->cork.addr = ipc->addr; } dst_hold(&rt->u.dst); inet->cork.fragsize = mtu = inet->pmtudisc == IP_PMTUDISC_PROBE ? rt->u.dst.dev->mtu : dst_mtu(rt->u.dst.path); inet->cork.dst = &rt->u.dst; inet->cork.length = 0; sk->sk_sndmsg_page = NULL; sk->sk_sndmsg_off = 0; if ((exthdrlen = rt->u.dst.header_len) != 0) { ///加上扩展头和传输层的头的大小。 length += exthdrlen; transhdrlen += exthdrlen; } } else { rt = (struct rtable *)inet->cork.dst; if (inet->cork.flags & IPCORK_OPT) opt = inet->cork.opt; ///不是第一个帧,则需要把ipsec头和4层的伪头的大小赋值为0.(因为同一个sk,共享相同的头。 transhdrlen = 0; exthdrlen = 0; mtu = inet->cork.fragsize; } ///得到2层头的大小(也就是预留2层头的大小). hh_len = LL_RESERVED_SPACE(rt->u.dst.dev); ///得到3层头的大小。 fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0); ///ip包的大小。基于路由pmtu。 maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; ///由于ip包的最大大小为64kb(oxFFFF),因此拒绝大于这个数据包。 if (inet->cork.length + length > 0xFFFF - fragheaderlen) { ip_local_error(sk, EMSGSIZE, rt->rt_dst, inet->dport, mtu-exthdrlen); return -EMSGSIZE; } /* * transhdrlen > 0 means that this is the first fragment and we wish * it won't be fragmented in the future. */ ///检测checksum是否需要硬件来做。 if (transhdrlen && length + fragheaderlen <= mtu && rt->u.dst.dev->features & NETIF_F_V4_CSUM && !exthdrlen) csummode = CHECKSUM_PARTIAL; inet->cork.length += length; ///检测长度是否大于mtu,以及是否是udp协议。然后进行udp分片。 if (((length> mtu) || !skb_queue_empty(&sk->sk_write_queue)) && (sk->sk_protocol == IPPROTO_UDP) && (rt->u.dst.dev->features & NETIF_F_UFO)) { ///进行udp分片。 err = ip_ufo_append_data(sk, getfrag, from, length, hh_len, fragheaderlen, transhdrlen, mtu, flags); if (err) goto error; return 0; } if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL) goto alloc_new_skb; ///开始将数据复制到创建的帧。 while (length > 0) { /* Check if the remaining data fits into current packet. */ copy = mtu - skb->len; ///空间不足时(也就是当前帧剩余的大小不够放入将要复制的数据). if (copy < length) copy = maxfraglen - skb->len; ///帧太大,需要切片。 if (copy <= 0) { char *data; unsigned int datalen; unsigned int fraglen; unsigned int fraggap; unsigned int alloclen; struct sk_buff *skb_prev; alloc_new_skb: skb_prev = skb; ///检测上一个skb是否存在 if (skb_prev) ///存在取得他的fraggap(小于8字节的).这里要解释下fraggap.除了最后一个ip帧,所有的ip帧都必须使他的ip帧的负荷的大小为8字节的倍数。因此当kernel分配一个新的buffer时,他可能需要移动一些数据从前一个buffer的尾部到新的buffer的头部。 fraggap = skb_prev->len - maxfraglen; else fraggap = 0; /* * If remaining data exceeds the mtu, * we know we need more fragment(s). */ ///得到数据长度 datalen = length + fraggap; if (datalen > mtu - fragheaderlen) datalen = maxfraglen - fragheaderlen; fraglen = datalen + fragheaderlen; ///如果flag为MSG_MORE并且设备设备不支持Scatter/Gather I/O.则需要分配一块等于mtu的内存。 if ((flags & MSG_MORE) && !(rt->u.dst.dev->features&NETIF_F_SG)) alloclen = mtu; else alloclen = datalen + fragheaderlen; /* The last fragment gets additional space at tail. * Note, with MSG_MORE we overallocate on fragments, * because we have no idea what fragment will be * the last. */ if (datalen == length + fraggap) alloclen += rt->u.dst.trailer_len; ///alloc相应的skb。 if (transhdrlen) { skb = sock_alloc_send_skb(sk, alloclen + hh_len + 15, (flags & MSG_DONTWAIT), &err); } else { skb = NULL; if (atomic_read(&sk->sk_wmem_alloc) <= 2 * sk->sk_sndbuf) skb = sock_wmalloc(sk, alloclen + hh_len + 15, 1, sk->sk_allocation); if (unlikely(skb == NULL)) err = -ENOBUFS; } ///检测是否成功 if (skb == NULL) goto error; ///设置校验位 skb->ip_summed = csummode; skb->csum = 0; skb_reserve(skb, hh_len); ///得到数据位置。 data = skb_put(skb, fraglen); skb_set_network_header(skb, exthdrlen); ///得到传输层的头部。 skb->transport_header = (skb->network_header + fragheaderlen); data += fragheaderlen; ///检测是否有fraggap. if (fraggap) { skb->csum = skb_copy_and_csum_bits( skb_prev, maxfraglen, data + transhdrlen, fraggap, 0); skb_prev->csum = csum_sub(skb_prev->csum, skb->csum); data += fraggap; pskb_trim_unique(skb_prev, maxfraglen); } ///得到所需要拷贝的数据的大小 copy = datalen - transhdrlen - fraggap; ///开始拷贝数据。 if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) { err = -EFAULT; kfree_skb(skb); goto error; } offset += copy; length -= datalen - fraggap; transhdrlen = 0; exthdrlen = 0; csummode = CHECKSUM_NONE; /* * Put the packet on the pending queue. */ ///加这个包到write_queue队列。 __skb_queue_tail(&sk->sk_write_queue, skb); continue; } if (copy > length) copy = length; ///如果不支持Scatter/Gather I/O.则直接拷贝数据 if (!(rt->u.dst.dev->features&NETIF_F_SG)) { unsigned int off; off = skb->len; if (getfrag(from, skb_put(skb, copy), offset, copy, off, skb) < 0) { __skb_trim(skb, off); err = -EFAULT; goto error; } } else { ///如果支持S/G I/O则开始进行相应操作 ///i为当前已存储的个数。 int i = skb_shinfo(skb)->nr_frags; //取出skb_frag_t指针。 skb_frag_t *frag = &skb_shinfo(skb)->frags[i-1]; ///得到当前的物理页。 struct page *page = sk->sk_sndmsg_page; ///得到当前的物理页的位移(也就是我们接下来要存储的位置的位移) int off = sk->sk_sndmsg_off; unsigned int left; ///如果有足够的空间则将数据放进相应的物理页的位置。 if (page && (left = PAGE_SIZE - off) > 0) { ///当剩余的空间不够放将要拷贝的数据时,则先将剩余的空间拷贝完毕。然后下次循环再进行拷贝剩下的。 if (copy >= left) copy = left; if (page != frag->page) { if (i == MAX_SKB_FRAGS) { err = -EMSGSIZE; goto error; } get_page(page); ///填充页 skb_fill_page_desc(skb, i, page, sk->sk_sndmsg_off, 0); frag = &skb_shinfo(skb)->frags[i]; } } ///检测是否存储空间已满。(此时说明page不存在或者,剩余大小威0,此时需要重新alloc一个物理页。 else if (i < MAX_SKB_FRAGS) { ///检测所需拷贝的数据的大小是否大于页的大小。 if (copy > PAGE_SIZE) copy = PAGE_SIZE; ///则新分配一个页。 page = alloc_pages(sk->sk_allocation, 0); if (page == NULL) { err = -ENOMEM; goto error; } sk->sk_sndmsg_page = page; sk->sk_sndmsg_off = 0; skb_fill_page_desc(skb, i, page, 0, 0); frag = &skb_shinfo(skb)->frags[i]; } else { err = -EMSGSIZE; goto error; } ///调用getfrag,填充相应的数据包(4层传递下来的数据) if (getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset, copy, skb->len, skb) < 0) { err = -EFAULT; goto error; } sk->sk_sndmsg_off += copy; frag->size += copy; skb->len += copy; skb->data_len += copy; skb->truesize += copy; atomic_add(copy, &sk->sk_wmem_alloc); } ///计算下次需要再拷贝的。。 offset += copy; length -= copy; } return 0; error: inet->cork.length -= length; IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS); return err; }
在上面的代码中,我们可以看到同一个物理页,有可能被sk_sndmsg_page和skb_frag_t 所共享,可以看下下面的图:
接下来来看ip_append_page,这个函数比较简单,我们大概分析下就可以了。
我们知道内核提供给用户空间的一个零拷贝的接口sendfile.这个接口只能当设备提供Scatter/Gather I/O的时候,才能使用。而它的实现就是基于ip_append_page这个函数来实现的。如果设备不支持S/G I/O,ip_append_page会直接返回错误。
它的逻辑实现和ip_append_page最后面那段实现很相似,不过有些不同,当加一个新的帧到page时,ip_append_page它会merge新的和也在当前页的前一个帧。它会通过调用skb_can_coalesce来进行检测这个。然后当merge是可能的,它就会update前一个帧的长度。
当merge是不可能的时候,处理和ip_append_data相似。
下面就是ip_append_page的一些代码片段:
if (skb_can_coalesce(skb, i, page, offset)) { skb_shinfo(skb)->frags[i-1].size += len; } else if (i < MAX_SKB_FRAGS) { get_page(page); skb_fill_page_desc(skb, i, page, offset, len); } else { err = -EMSGSIZE; goto error; }
ip_append_page只被udp使用。tcp不使用ip_append_data和ip_push_pending_frams是因为它把一些逻辑放到tcp_sendmsg来实现了。因此相似的,0拷贝接口,tcp不使用ip_append_page是因为他在do_tcp_sendpage中实现了相同的逻辑。
最后我们来看ip_push_pending_frams函数。
这个函数相当于一个notify函数,当4层决定传输帧到ip层的时候,他就需要调用这个函数.通过前面我们知道此时所有的数据(如果不支持Scatter/Gather I/O),都在sk_write_queue中。
这个函数要做的其实很简单,就是从sk_write_queue中取出数据,加上ip头,然后通过dst_output发送给3层。
当数据从sk_write_queue从移除后,加入到frag_list链表中。
下面这张图表示了从sk_write_queue中移除buffer之前和之后的区别(没有考虑Scatter/Gather I/O).
接下来来看它的实现:
int ip_push_pending_frames(struct sock *sk) { ///初始化一些数据 struct sk_buff *skb, *tmp_skb; struct sk_buff **tail_skb; struct inet_sock *inet = inet_sk(sk); struct net *net = sock_net(sk); struct ip_options *opt = NULL; struct rtable *rt = (struct rtable *)inet->cork.dst; struct iphdr *iph; __be16 df = 0; __u8 ttl; int err = 0; ///取得第一个buffer if ((skb = __skb_dequeue(&sk->sk_write_queue)) == NULL) goto out; ///得到他的frag_list. tail_skb = &(skb_shinfo(skb)->frag_list); /* move skb->data to ip header from ext header */ if (skb->data < skb_network_header(skb)) __skb_pull(skb, skb_network_offset(skb)); ///开始遍历并取出所有的buffer到frag_list. while ((tmp_skb = __skb_dequeue(&sk->sk_write_queue)) != NULL) { __skb_pull(tmp_skb, skb_network_header_len(skb)); *tail_skb = tmp_skb; tail_skb = &(tmp_skb->next); skb->len += tmp_skb->len; skb->data_len += tmp_skb->len; skb->truesize += tmp_skb->truesize; __sock_put(tmp_skb->sk); tmp_skb->destructor = NULL; tmp_skb->sk = NULL; } /* Unless user demanded real pmtu discovery (IP_PMTUDISC_DO), we allow * to fragment the frame generated here. No matter, what transforms * how transforms change size of the packet, it will come out. */ if (inet->pmtudisc < IP_PMTUDISC_DO) skb->local_df = 1; /* DF bit is set when we want to see DF on outgoing frames. * If local_df is set too, we still allow to fragment this frame * locally. */ if (inet->pmtudisc >= IP_PMTUDISC_DO || (skb->len <= dst_mtu(&rt->u.dst) && ip_dont_fragment(sk, &rt->u.dst))) ///标记ip头不要被切片。 df = htons(IP_DF); ///如果在头中包含ip option,则给option赋值,然后下面会处理这个option。 if (inet->cork.flags & IPCORK_OPT) opt = inet->cork.opt; ///如果是多播,则赋值多播的ttl if (rt->rt_type == RTN_MULTICAST) ttl = inet->mc_ttl; else ttl = ip_select_ttl(inet, &rt->u.dst); ///得到ip头的指针。 iph = (struct iphdr *)skb->data; ///开始初始化ip头。 iph->version = 4; iph->ihl = 5; if (opt) { iph->ihl += opt->optlen>>2; ip_options_build(skb, opt, inet->cork.addr, rt, 0); } iph->tos = inet->tos; iph->frag_off = df; ///得到ip包的id。 ip_select_ident(iph, &rt->u.dst, sk); iph->ttl = ttl; iph->protocol = sk->sk_protocol; iph->saddr = rt->rt_src; iph->daddr = rt->rt_dst; skb->priority = sk->sk_priority; skb->mark = sk->sk_mark; skb->dst = dst_clone(&rt->u.dst); ///如果协议是ICMP则进行相关处理。 if (iph->protocol == IPPROTO_ICMP) icmp_out_count(net, ((struct icmphdr *) skb_transport_header(skb))->type); /* Netfilter gets whole the not fragmented skb. */ ///输出到4层,这个函数上面有介绍过,会通过一个netfilter的hook. err = ip_local_out(skb); if (err) { if (err > 0) err = inet->recverr ? net_xmit_errno(err) : 0; if (err) goto error; } out: ip_cork_release(inet); return err; error: IP_INC_STATS(net, IPSTATS_MIB_OUTDISCARDS); goto out; }
接下来我们会来简要的介绍4层使用上面的函数接口和3层如何把帧传递给2层的接口:
先来看udp_sendmsg的代码片段:
up->len += ulen; getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag; ///将要传输的包交给ip_append_data来处理 err = ip_append_data(sk, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, rt, corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags); if (err) udp_flush_pending_frames(sk); else if (!corkreq) ///如果需要传递给3层,则调用udp_push_pending_frames,这个函数是对ip_push_pending_frames的简单封装。 err = udp_push_pending_frames(sk); else if (unlikely(skb_queue_empty(&sk->sk_write_queue))) up->pending = 0; release_sock(sk);
我们现在知道4层到3层之后,最终通过dst_output来把帧进行输出,这个函数在单播的情况下,是被实例化为ip_output.这里和前面的netfilter一样,还存在一个ip_output_finish方法,当通过netfilter hook后,如果这个包可以被netfilter放过,那么帧就会传递到ip_output_finish方法,然后再调用ip_output_finish2方法。而最终dev_queue_xmit(前面的blog有介绍,也就是2层的传输方法)会被调用(这里是通过hh->hh_output方法或者fst->neighbour->output 2个虚函数)来传输。
static inline int ip_finish_output2(struct sk_buff *skb) { ......................................... if (dst->hh) return neigh_hh_output(dst->hh, skb); else if (dst->neighbour) return dst->neighbour->output(skb); ................................... }
发表评论
-
Receive packet steering patch详解
2010-07-25 16:46 12207Receive packet steering简称rp ... -
内核中拥塞窗口初始值对http性能的影响分析
2010-07-11 00:20 9730这个是google的人提出的 ... -
linux 内核tcp拥塞处理(一)
2010-03-12 16:17 9625这次我们来分析tcp的拥塞控制,我们要知道协议栈都是很保守的, ... -
内核tcp协议栈SACK的处理
2010-01-24 21:13 12222上一篇处理ack的blog中我 ... -
内核tcp的ack的处理
2010-01-17 03:06 11217我们来看tcp输入对于ack,段的处理。 先是ack的处理, ... -
内核处理time_wait状态详解
2010-01-10 17:39 6859这次来详细看内核的time_wait状态的实现,在前面介绍定时 ... -
tcp协议栈处理各种事件的分析
2009-12-30 01:29 13664首先我们来看socket如何将一些状态的变化通知给对应的进程, ... -
linux内核sk_buff的结构分析
2009-12-25 00:42 47955我看的内核版本是2.6.32. 在内核中sk_buff表示一 ... -
tcp的输入段的处理
2009-12-18 00:56 8386tcp是全双工的协议,因此每一端都会有流控。一个tcp段有可能 ... -
内核协议栈tcp层的内存管理
2009-11-28 17:13 12133我们先来看tcp内存管理相关的几个内核参数,这些都能通过pro ... -
linux内核定时器的实现
2009-10-31 01:44 10219由于linux还不是一个实时的操作系统,因此如果需要更高精度, ... -
linux内核中tcp连接的断开处理
2009-10-25 21:47 10373我们这次主要来分析相关的两个断开函数close和shotdow ... -
linux内核tcp的定时器管理(二)
2009-10-05 20:52 5459这次我们来看后面的3个定时器; 首先是keep alive定 ... -
linux内核tcp的定时器管理(一)
2009-10-04 23:29 9867在内核中tcp协议栈有6种 ... -
linux 内核tcp接收数据的实现
2009-09-26 20:24 14570相比于发送数据,接收数据更复杂一些。接收数据这里和3层的接口是 ... -
linux 内核tcp数据发送的实现
2009-09-10 01:41 19845在分析之前先来看下SO_RCVTIMEO和SO_SNDTIME ... -
tcp connection setup的实现(三)
2009-09-03 00:34 5232先来看下accept的实现. 其实accept的作用很简单, ... -
tcp connection setup的实现(二)
2009-09-01 00:46 8452首先来看下内核如何处理3次握手的半连接队列和accept队列( ... -
tcp connection setup的实现(一)
2009-08-23 04:10 5856bind的实现: 先来介绍几个地址结构. struct ... -
linux内核中socket的实现
2009-08-15 04:38 21134首先来看整个与socket相关的操作提供了一个统一的接口sys ...
相关推荐
对于Linux系统而言,其内核中的TCP/IP协议栈是实现网络通信的基础,它负责解析、处理并转发网络数据。本文将深入探讨Linux协议栈实现的细节,特别是与TCP/IP协议流程相关的部分。 首先,TCP/IP协议栈可以分为四层,...
本文主要关注的是IP协议的实现,因为它是TCP/IP模型中最核心的部分之一。 #### 二、消息通讯简介 ##### 2.1 网络通讯路径 在Linux系统中,网络通讯路径涉及多个层次,从应用程序到物理层的数据传输过程中会经过多...
在构建Linux平台上的IP摄像头采集处理平台时,我们需要考虑多种技术方案来实现高效且可靠的视频流获取、处理和展示。以下是一些可行的解决方案及其详细说明: 1. USB摄像头采集处理方案: 利用Video4Linux接口是...
本教程将围绕Linux_v4l2接口来探讨如何在Linux环境下处理图像。 1. **Linux_v4l2接口**: Linux_v4l2是V4L(Video for Linux)的升级版,它提供了更强大的功能和更好的性能。V4L2接口为用户空间程序提供了与各种...
本文介绍了一种利用MPEG4IP流媒体处理平台在Linux环境下实现的实时授课系统,该系统能够有效地同步教师的授课视频、音频以及屏幕数据。 #### 关键技术与实现 本节详细介绍实时授课系统的三个关键方面:媒体数据的...
9. 通信工程:本文介绍了如何使用通信工程技术实现视频服务器信号传输,该系统使用TCP/IP协议将视频数据传输到客户端。 10. 软件工程:本文介绍了如何使用软件工程技术实现视频监控系统,该系统使用Linux内核和V4L2...
流媒体服务器则负责将这些数据通过TCP/IP协议在网络上传输,实现多用户访问。 V4L2是Linux下的一套视频设备驱动接口,提供了一种统一的方式来控制不同硬件的视频设备,简化了开发过程。使用V4L2接口,可以进行诸如...
在Linux环境下实现视频监控是一项技术含量较高的工作,它涉及到多个技术领域的融合,包括硬件设备的驱动、图像处理、网络通信以及前端展示等。本项目主要使用了Linux的Video for Linux Two(V4L2)框架来采集视频源...
7. **网络通信**:讲述如何配置网络接口,包括以太网控制器驱动和TCP/IP协议栈的配置,实现嵌入式设备的网络功能。 8. **应用程序开发**:介绍如何在Linux环境下进行C/C++编程,开发针对S3C2440的应用程序,以及...
标题 "v4l2onvif-master_ONVIF_ONVIFSERVER_onvifserver_v4l2onvif_" 暗示这是一个关于ONVIF协议实现的项目,特别是与Linux环境下的服务器和客户端相关。ONVIF是Open Network Video Interface Forum(开放网络视频...
总的来说,webcam_v4l2_x264项目是一个实用的示例,展示了如何在Linux环境下利用V4L2接口和x264编码器实现摄像头视频的捕捉、压缩和网络传输,以及在远程端进行解码和播放。这为开发自己的视频流应用或者研究相关...
总的来说,这个项目涉及到了Linux系统编程、QT图形界面开发、RTP协议的实现、V4L2接口的使用以及OpenCV的人脸识别技术,是一个综合性的实时视频传输解决方案。开发这样的系统有助于提升开发者在跨平台实时多媒体应用...
TCP三次握手是TCP/IP协议建立可靠连接的关键步骤,它确保了两端系统都能正确地同步通信。在Linux内核中,这个过程的实现涉及到多个函数和状态机的交互。以下是TCP三次握手在Linux内核2.4.0版本中的具体实现细节: ...
通过TCP/IP协议,它可以将视频流发送到网络,使用户能够通过监控终端(如Web浏览器)访问视频监控画面。 6. **硬件平台搭建**:系统硬件通常包括处理器(如S3C2440A)、存储器(如NAND/SDRAM/NOR Flash)以及视频...
7. 网络传输:如果摄像头用于网络视频传输,需要了解TCP/IP协议和相关网络编程知识。 总结,这个项目是关于在网络摄像头应用中整合v4l2框架,以提升其在Linux环境下的性能和兼容性。这涵盖了从底层硬件驱动到上层...
总的来说,EDNS的内核实现是一项复杂的工作,涉及到对Linux内核代码的深入了解,以及对DNS协议和网络安全的深刻理解。通过这种方式增强DNS功能,可以提高系统的效率和安全性,但同时也需要谨慎处理潜在的负面影响。
NAT-PT技术是IPv4与IPv6互通的重要手段,它通过改变IP地址和端口号来实现不同协议间的转换。DNS-ALG则是在DNS查询过程中帮助处理NAT-PT转换的组件,确保通信的正确性。 传统方案存在的问题是,当IPv6网络扩大、通信...
2. **服务器端的网络传输**:使用TCP/IP协议,通过QTcpServer监听客户端连接,当有连接建立时,将视频流和语音数据实时发送给客户端。 3. **客户端**:使用QTcpSocket建立与服务器的连接,接收视频流并解码显示,...
这里主要采用H.263编码器对视频数据进行压缩,再通过TCP/IP协议栈中的RTP(Real-time Transport Protocol)进行传输。 ##### 1. H.263视频编码 H.263是一种适用于低带宽环境下的视频编码标准,特别适合嵌入式系统中...
可以使用TCP/IP协议栈,结合socket编程,实现实时图像数据的封装和发送。 3. 性能优化与调试 3.1 性能优化:为了提高图像采集速度和处理效率,可以考虑硬件加速、多线程处理、内存管理优化等方法。 3.2 调试与...