本篇中分析udp协议的消息发送函数,以此来观察udp查找路由的过程,以及udp将应用层传递的数据封装到sk_buff
并传递到网络层的过程。
应用层对udp socket调用send/sendmsg/sedto
函数后,内核中udp_sendmsg
函数会被调用,udp_sendmsg
主要完成三个工作,如下:
- 调用路由模块提供的函数来查找路由
- 按照MTU值的大小将应用层传递的数据进行分割,将它们封装到
sk_buff
中去,并将sk_buff
缓存于socket的发送队列上 - 当应用层没有更多数据要发送时,将socket发送队列上的
sk_buff
取下,构造udp头部,然后发往网络层
udp_sendmsg
调用的一些关键函数如下:
// linux_*/net/ipv4/udp.c
int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len){
struct inet_sock *inet = inet_sk(sk);
struct udp_sock *up = udp_sk(sk); // udp socket
......
// corkreq非表示应用层还有更多数据要发送(设置MSG_MORE标志或者socket option配置了UDP_CORK)
int corkreq = READ_ONCE(up->corkflag) || msg->msg_flags&MSG_MORE;
......
// 1 up->pending非0,表示该udp socket发送队列上已经缓存了数据,这种情况下直接跳转到do_append_data处继续缓存数据
if (up->pending) {
goto do_append_data;
}
......
// 2 查找路由
if (!rt) {
......
// 查找路由
rt = ip_route_output_flow(net, fl4, sk);
......
}
......
// 3 如果应用层没有更多数据要发送,则调用ip_make_skb来将应用层传递的数据分割成合适大小,放入sk_buff,送到下层协议栈
if (!corkreq) {
struct inet_cork cork;
skb = ip_make_skb(sk, fl4, getfrag, msg, ulen,sizeof(struct udphdr), &ipc, &rt,&cork, msg->msg_flags);
// 分割数据前的准备工作
ip_setup_cork(sk, cork, ipc, rtp);
// 将应用层传递的数据分割成合适大小放入sk_buff,把sk_buff放入socket发送队列
__ip_append_data(sk, fl4, &queue, cork,¤t->task_frag, getfrag,from, length, transhdrlen, flags);
// 从发送队列上取出sk_buff,构造ip头部,
__ip_make_skb(sk, fl4, &queue, cork);
// 构造udp头部,发往ip层
udp_send_skb(skb, fl4, &cork);
......
goto out;
}
......
// 4 socket发送队列上已经有数据了(已经处于缓存数据的过程中)
do_append_data:
up->len += ulen;
// 将应用层传递的数据分割成合适大小放入sk_buff,把sk_buff放入socket发送队列
err = ip_append_data(sk, fl4, getfrag, msg, ulen,sizeof(struct udphdr), &ipc, &rt,corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
ip_setup_cork(sk, &inet->cork.base, ipc, rtp); // 分割数据前的一些准备工作
__ip_append_data(sk, fl4, &sk->sk_write_queue, &inet->cork.base,sk_page_frag(sk), getfrag, from, length, transhdrlen, flags);
......
else if (!corkreq)
// 应用层没有更多数据要发送了(应用层不再传递MSG_MORE或者将UDP_CORK配置为0了)
err = udp_push_pending_frames(sk);
// 从发送队列上取出sk_buff,构造ip头部
ip_finish_skb(sk, fl4);
__ip_make_skb(sk, fl4, &sk->sk_write_queue, &inet_sk(sk)->cork.base);
// 构造udp头部,发往ip层
udp_send_skb(skb, fl4, &inet->cork.base);
......
out:
ip_rt_put(rt);
out_free:
......
return err;
......
}
接下来依次从路由查找、分割和缓存数据、发送数据到下层协议栈这三个部分来分析。
查找路由
路由查找的入参
对于本地输出流程,路由模块提供了一个ip_route_output_flow
函数给udp/raw/tcp这些协议调用,ip_route_output_flow
的入参是一个struct flowi4
类型的结构体变量,flowi4
用于传递路由查找的匹配条件(主要就是源\目的ip、端口、mark、输入输出接口那些参数,它们会用于匹配路由策略或者用于查找路由),struct flowi4
的结构体成员如下:
// linux_*/include/flow.h
struct flowi4 {
struct flowi_common __fl_common;
#define flowi4_oif __fl_common.flowic_oif // 在执行策略路由的过程中会用路由策略的oif与flowi4_oif进行比对,比对上了才会执行路由策略的动作。在路由查表的过程中,会使用nexthop的dev与flowi4_oif进行比对,比对上了才会使用该nexthop
#define flowi4_iif __fl_common.flowic_iif // 在执行策略路由的过程中会用路由策略的iif与flowi4_iif进行比对,比对上了才会执行路由策略的动作
#define flowi4_mark __fl_common.flowic_mark // 在执行策略路由的过程中会用路由策略的fwmark与flowi4_mark进行比对,比对上了才会执行路由策略的动作
#define flowi4_tos __fl_common.flowic_tos // 在执行策略路由的过程中会用路由策略的tos与flowi4_tos进行比对,比对上了才会执行路由策略的动作。在路由查表的过程中,会使用路由的tos与flowi4_tos进行比对,比对上了才会命中该路由
#define flowi4_scope __fl_common.flowic_scope // 在路由查表的过程中,会使用路由的scope与flowi4_scope进行比对,路由的scope大于等于flowi4_scope才会命中该路由(路由的距离比flowi4_scope指定的距离更小)
#define flowi4_proto __fl_common.flowic_proto // 在执行策略路由的过程中会用路由策略的ipproto与flowi4_proto进行比对,比对上了才会执行路由策略的动作
#define flowi4_flags __fl_common.flowic_flags
#define flowi4_secid __fl_common.flowic_secid
#define flowi4_tun_key __fl_common.flowic_tun_key
#define flowi4_uid __fl_common.flowic_uid
#define flowi4_multipath_hash __fl_common.flowic_multipath_hash
/* (saddr,daddr) must be grouped, same order as in IP header */
__be32 saddr; // 在执行策略路由的过程中会用路由策略的from与saddr进行比对,比对上了才会执行路由策略的动作
__be32 daddr; // 在执行策略路由的过程中会用路由策略的to与saddr进行比对,比对上了才会执行路由策略的动作。再路由查表的过程中,或使用daddr去前缀树上找路由对应的节点。
union flowi_uli uli;
#define fl4_sport uli.ports.sport // 在执行策略路由的过程中会用路由策略的sport与fl4_sport进行比对,比对上了才会执行路由策略的动作
#define fl4_dport uli.ports.dport // 在执行策略路由的过程中会用路由策略的dport与fl4_dport进行比对,比对上了才会执行路由策略的动作
#define fl4_icmp_type uli.icmpt.type
#define fl4_icmp_code uli.icmpt.code
#define fl4_ipsec_spi uli.spi
#define fl4_mh_type uli.mht.type
#define fl4_gre_key uli.gre_key
} __attribute__((__aligned__(BITS_PER_LONG/8)));
本地输出的路径上,flowi4
的各个匹配条件有的来自于socket option,有的来自于应用层发送数据包传递给内核的参数,udp在构造flowi4
时部分参数的取值如下:
flowi4_oif
: 取自SO_BINDTOIFINDEX
这个socket option,如果没有配置SO_BINDTOIFINDEX
那flowi4_oif
为0flowi4_iif
:LOOPBACK_IFINDEX
(loopback 回环设备)flowi4_mark
: 取自SO_MARK
这个socket option,如果没有配置SO_MARK
那flowi4_mark
为0flowi4_tos
: 取自IP_TOS
这个socket option,如果没有配置IP_TOS
那flowi4_tos
为0flowi4_scope
:RT_SCOPE_UNIVERSE
flowi4_proto
:IPPROTO_UDP
saddr
和fl4_sport
: 通过对socket调用bind
函数来指定,若没有调用过bind
函数,则saddr
和fl4_sport
为0daddr
和fl4_dport
: 对socket调用sendto
函数时指定或者对socket调用connect
函数来指定,没指定则daddr
和fl4_dport
为0
虽然udp是无连接的协议,但上面那些参数中daddr
和 fl4_dport
是可以通过connect
函数来指定的,对udp socket调用connect
函数时,内核会直接使用connect
指定的地址来查找路由,查找成功后会将路由查找的结果缓存起来(缓存于 struct sock::sk_dst_cache
),在udp socket发送数据时就可以不再执行路由查找。在udp_sendmsg
函数中有这样一小段代码,如下:
int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
......
// 1 选择目的地址,应用层传递了地址就用应用层传递的地址,否则用connect函数指定的地址
// 当应用层发送数据时,指定的目的地址会通过msg->msg_name传递到udp_sendmsg函数 ,如果没有指定目的地址则msg->msg_name为null
DECLARE_SOCKADDR(struct sockaddr_in *, usin, msg->msg_name);
......
if (usin) {
// 1.1 usin不为null,表示应用层指定了目的地址(比如通过sendto函数发送数据时传递了目的地址)
......
daddr = usin->sin_addr.s_addr;
dport = usin->sin_port;
......
} else {
// 1.2 对socket调用connect函数后,路由查找成功后会把socket的状态会配置为TCP_ESTABLISHED,并且会将目的地址/端口记录于inet->inet_daddr/inet->inet_dport
if (sk->sk_state != TCP_ESTABLISHED)
return -EDESTADDRREQ;
daddr = inet->inet_daddr;
dport = inet->inet_dport;
connected = 1;
}
......
// 2.1 如果使用的是connect函数指定的地址,则尝试使用 connect 函数查找到的路由
if (connected)
rt = (struct rtable *)sk_dst_check(sk, 0);
// 2.2 如果没有调用过connect函数或者connect 函数查找到的路由因某些原因不能用了(出口网卡设备down了,或者收到了其它主机发送的ICMP Destination Unreachable),则重新查找路由
if (!rt) {
......
// 构造 flowi4
flowi4_init_output(fl4, ipc.oif, ipc.sockc.mark, tos,
RT_SCOPE_UNIVERSE, sk->sk_protocol,
flow_flags,
faddr, saddr, dport, inet->inet_sport,
sk->sk_uid);
// 查找路由
rt = ip_route_output_flow(net, fl4, sk);
......
}
}
可以发现,对udp socket调用connect
其实是一个不错的选择,调用sendto
函数发送数据时不用传递目的地址,也可以节省路由查找的时间。
若是既没有对udp socket调用过connect
,发送数据时也没有传递目的地址,那么传递给ip_route_output_flow
的flowi4:daddr
就是0,这时候ip_route_output_flow
函数会认为我们的目的地址是127.0.0.0
,会把出口设备设置为回环设备。
路由查找的结果
在构造好flowi4
后,udp_sendmsg
会调用ip_route_output_flow
来查找路由,ip_route_output_flow
函数会间接的调用fib_lookup
来查找策略路由和路由表,查找完成后会构造一个struct rtable
类型的变量(rtable
会包含出口设备等关键信息),udp_sendmsg
在构造sk_buff
时会引用rtable
,sk_buff
往下层协议栈传递时,下层的协议栈就可以通过sk_buff
携带的rtable
来获取下一跳地址以及出口设备,ip_route_output_flow
函数中一些关键的函数调用大致如下:
struct rtable *ip_route_output_flow(struct net *net, struct flowi4 *flp4, const struct sock *sk)
__ip_route_output_key(net, flp4)
ip_route_output_key_hash(net, flp, NULL);
// 查找路由表后,最终的结果会被放在一个struct fib_result类型的结构里面,fib_result里面包含了路由查找命中的路由表、fib_info、前缀、前缀长度、scope、路由的类型、nexthop等信息
struct fib_result res = {.type=RTN_UNSPEC, .fi=NULL, .table=NULL, .tclassid=0,};
struct rtable *rth;
rth = ip_route_output_key_hash_rcu(net, fl4, &res, skb);
// 1 查找策略路由,查找路由表,把查找结果放在fib_result中
fib_lookup(net, fl4, res, 0);
// 2 若被命中的路由配置了多个nexthop,则从多个nexthop中选择一个
fib_select_path(net, res, fl4, skb);
// 获取出口设备
dev_out = FIB_RES_DEV(*res);
// 3 根据fib_lookup返回的 fib_result,构造一个struct rtable类型的变量
rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
return rth;
fib_lookup 函数
ip_route_output_flow
函数会调用fib_lookup
函数,fib_lookup
函数分两个步骤:
- 首先会按照
flowi4::flowi_iif
,flowi4::flowi_oif
,flowi4::flowi_mark
,flowi4::flowi4_tos
,flowi4::saddr
,flowi4::daddr
,flowi4::flowi4_proto``flowi4::fl4_sport
,flowi4::fl4_dport
等参数去查找策略路由。 - 找到匹配的策略路由后,再去策略路由指定的路由表中查找路由,在查找路由表时就先按照
flowi4::daddr
查找路由表的前缀树,命中某个前缀树节点后再按照flowi4::flowi4_tos
,flowi4::flowi4_scope
,flowi4::flowi4_oif
去查找fib_alias
,fib_info
,fib_nh_common
(nexthop)等信息,查找完成后将命中的路由对应的路由表、fib_info
、前缀、前缀长度、scope、路由的类型、nexthop等信息全部通过fib_result
返回给ip_route_output_flow
。
在 1 和 2 这两篇笔记中有对策略路由的查找以及路由表的查找更为详细的介绍,这里不再进一步介绍。
fib_result
的结构体成员如下:
// linux_*/include/net/ip_fib.h
struct fib_result {
__be32 prefix; // 路由的前缀值
unsigned char prefixlen; // 前缀长度
unsigned char nh_sel; // 一条路由的所有nexthop会存放在一个数组中,nh_sel代表着被选中的nexthop的数组下标
unsigned char type; // 路由的类型(RTN_UNICAST/RTN_LOCAL/RTN_BROADCAST/RTN_MULTICAST/RTN_UNREACHABLE......),在输入路径的路由决策过程中,就会根据这个值来判断数据包该本地输入还是转发,在输出路径上,如果路由类型为RTN_LOCAL,则出口设备会被选定为回环设备
unsigned char scope; // 路由的scope
u32 tclassid;
struct fib_nh_common *nhc; // 指向被选中的nexthop
struct fib_info *fi; // 路由对应的fib_info
struct fib_table *table; // 路由表
struct hlist_head *fa_head;
};
因为fib_lookup
函数在选择nexthop的时候是根据flowi4::flowi4_oif
来匹配的,如果传递给fib_lookup
函数的flowi4::flowi4_oif
为0(没有给socket绑定出口设备),那么fib_lookup
返回的fib_result
中的nh_sel
和nhc
其实是没有意义的,这种情况下还需要在多个路径中选择一个。
多路径选择
多路径选择方法大致是这样的:
假设有这样一条路由,它有两个nexthop,weight分别为1和2。
# ip route add 192.168.56.0/24 table 66 nexthop dev eth1 weight 1 nexthop dev eth2 weight 2
# ip route list table 66
192.168.56.0/24
nexthop dev eth1 weight 1
nexthop dev eth2 weight 2
内核选择nexthop时会先使用flowi4
计算出一个hash值(报文输入路径上会使用sk_buff
携带的报文来计算hash值),hash值是int类型的,hash值在[0, INT_MAX/3]
这个区间内就会选择eth1作为出口设备,hash值在[INT_MAX/3, INT_MAX]
这个区间内就会选择eth2作为出口设备。
fib_select_path
函数选中一个合适的nexthop后,会将fib_result::nh_sel
和fib_result::nhc
更新为正确的值。
构造 rtable
路由查找后,sk_buff
在穿过下层的协议栈时,像邻居子系统、网络设备层它们关注的是下一跳地址以及出口设备是什么,fib_lookup
函数返回的fib_result
中携带了很多的信息,sk_buff
不需要把这些信息都带上,所以__mkroute_output
函数会从fib_result
中提取底层协议栈需要的信息(输入路径上路由查找时也有对应的函数来完成这个工作),用来构造rtable
,sk_buff
只需要引用rtable
就行了。
struct rtable
的结构体成员如下:
// linux_*/include/net/route.h
struct rtable {
struct dst_entry dst;
int rt_genid; // 分配rtable时,rt_genid取值于net->ipv4.rt_genid,rt_genid会用于校验rtable是否有效,如果net->ipv4.rt_genid与rtable::rt_genid不相等,那么该rtable无效(目前暂且没发现什么情况下会改动net->ipv4.rt_genid)
unsigned int rt_flags;
__u16 rt_type; // 路由类型
__u8 rt_is_input; // rtable是来自于在输入路径上还是在输出路径上的路由查找
__u8 rt_uses_gateway; // 下一跳是否是网关 (下一跳是否是网关是由路由决定的,ip route指令添加路由时携带了via参数就相当于告诉内核需要通过网关转发)
int rt_iif; // 报文的输入接口,输出路径上 rt_iif与flowi4::flowi4_oif相同
u8 rt_gw_family; // 网关的协议类型,AF_INET(ipv4)或者AF_INET6(ipv6),如果不通过网关的转发就能到达目的地则rt_gw_family值为0
/* Info on neighbour */
union {
__be32 rt_gw4; // 下一跳地址,如果需要通过网关的转发,那rt_gw4就是网关地址,如果不需要,那rt_gw4就是目的地址
struct in6_addr rt_gw6;
};
/* Miscellaneous cached information */
u32 rt_mtu_locked:1,
rt_pmtu:31; // path mtu,这个值会来自于 fib_nh_exception (内核收到ICMP Destination Unreachable (need frag)后更新的mtu)
// rtable可以被缓存起来,其它查找流程中命中了相同路由就可以直接使用缓存的rtable,如果不需要缓存就链入percpu的rt_uncached_list链表
struct list_head rt_uncached;
struct uncached_list *rt_uncached_list;
};
rt_uses_gateway
,rt_gw_family
,rt_gw4
,rt_gw6
等参数会被邻居子系统使用,邻居子系统会用下一跳地址去查找邻居表,确定报文的二层地址。
rtable
内嵌了一个dst_entry
,它的部分结构体成员如下:
// linux_*/include/net/dst.h
struct dst_entry {
struct net_device *dev; // 出口设备
struct dst_ops *ops; // 一些回调函数,用于检查dst_entry、获取mtu、销毁dst_entry,查找邻居
unsigned long _metrics;
unsigned long expires; //
......
int (*input)(struct sk_buff *); // 输入回调,输入路径上查找路由后就会调用该回调,本地输入和转发这两种情况下给input赋值不同,另外单播和组播赋的值也是不一样的
int (*output)(struct net *net, struct sock *sk, struct sk_buff *skb); // 输出回调,转发报文或者本地输出的报文在经过netfilter prerouting这个hook点后就会调用该回调
......
short obsolete; // sk_buff或者其它模块引用该 dst_entry时会校验dst_entry是否可用,obsolete值为DST_OBSOLETE_FORCE_CHK时会去校验rtable::rt_genid,obsolete为其他值时校验函数会直接返回校验失败,通常dst_entry刚分配时obsolete取值为DST_OBSOLETE_FORCE_CHK,出口设备down掉的时候会把obsolete设置为DST_OBSOLETE_DEAD
#define DST_OBSOLETE_NONE 0
#define DST_OBSOLETE_DEAD 2
#define DST_OBSOLETE_FORCE_CHK -1
#define DST_OBSOLETE_KILL -2
unsigned short header_len; // 分配sk_buff时需要预留的头部空间
unsigned short trailer_len;// 分配sk_buff时需要预留的尾部空间
......
#ifdef CONFIG_64BIT
atomic_t __refcnt; // 引用计数,引用计数为0时释放该dst_entry(整个rtable一起释放),sk_buff引用rtable、路由缓存rtable、socket缓存rtable都会增加引用计数
#endif
......
__u32 tclassid; // 假如路由查找命中的是这条路由:'ip route add 192.168.2.0/24 dev eth0 realm 2',那么tclassid就为2,sk_buff经过tc模块时可以被'tc filter add ... route from 2 classid ...'这样的规则匹配
};
无论是输入路径上路由查找还是输出路径上的路由查找,内核都会根据路由的类型来给dst_entry::input
和dst_entry::output
赋值。
输入路径上,如果报文需要本地输入则dst_entry::input
会被赋值为ip_local_deliver
,如果需要转发则dst_entry::input
会被赋值为ip_forward
,需要转发时dst_entry::output
会被赋值为ip_output
。输出路径上,dst_entry::output
会被赋值为ip_output
。
在输出路径上,__mkroute_output
函数负责构造rtable
,有的情况下构造出的rtable
会被缓存起来,之后输出路径上的其它路由查找再度命中相同的路由以及相同的nexthop就可以直接使用缓存的rtable
,不用再重新分配。接下来简单看看__mkroute_output
构造和缓存rtable
的过程。
static struct rtable *__mkroute_output(const struct fib_result *res,
const struct flowi4 *fl4, int orig_oif,
struct net_device *dev_out,
unsigned int flags)
{
struct fib_info *fi = res->fi;
struct fib_nh_exception *fnhe;
struct in_device *in_dev;
u16 type = res->type;
struct rtable *rth;
bool do_cache; // do_cache为true表明是否需要缓存rtable
......
// 1 确定转发类型,type原值取自于路由类型,这里根据目的地址类型调整转发类型
if (ipv4_is_lbcast(fl4->daddr))
type = RTN_BROADCAST;
else if (ipv4_is_multicast(fl4->daddr))
type = RTN_MULTICAST;
else if (ipv4_is_zeronet(fl4->daddr))
return ERR_PTR(-EINVAL);
if (dev_out->flags & IFF_LOOPBACK)
flags |= RTCF_LOCAL;
// 2 确定是否需要缓存rtable
// do_cache初始值会设置为true,接下来的判断中会修改do_cache和fi的值,fi被设置为NULL时也不会缓存rtable
do_cache = true;
if (type == RTN_BROADCAST) {
// 2.1广播不缓存
flags |= RTCF_BROADCAST | RTCF_LOCAL;
fi = NULL;
} else if (type == RTN_MULTICAST) {
flags |= RTCF_MULTICAST | RTCF_LOCAL;
// 2.2组播路由不存在也不缓存
if (!ip_check_mc_rcu(in_dev, fl4->daddr, fl4->saddr,
fl4->flowi4_proto))
flags &= ~RTCF_LOCAL;
else
do_cache = false;
if (fi && res->prefixlen < 4)
fi = NULL;
} else if ((type == RTN_LOCAL) && (orig_oif != 0) &&
(orig_oif != dev_out->ifindex)) {
// 2.3本地转发时,若orig_oif与dev_out->ifindex不同(绑定的出口设备不是loopback)不缓存
do_cache = false;
}
fnhe = NULL;
do_cache &= fi != NULL;
// 3 查看是否有之前缓存的rtable可用
if (fi) {
struct fib_nh_common *nhc = FIB_RES_NHC(*res);
struct rtable __rcu **prth;
// 3.1 查找fib_nh_exception,当内核收到ICMP Destination Unreachable (need frag)或者ICMP Redirect消息时,会尝试修改路由中记录的mtu或者网关地址,这个时候就会创建fib_nh_exception,用fib_nh_exception来存储修改过后的mtu和网关
fnhe = find_exception(nhc, fl4->daddr);
if (!do_cache)
goto add;
if (fnhe) {
// 3.2 找到了fib_nh_exception,就直接使用fib_nh_exception中缓存的rtable
prth = &fnhe->fnhe_rth_output;
} else {
......
// 3.3 没找到fib_nh_exception,就使用fib_nh_common (描述nexthop的数据结构)中缓存的rtable
prth = raw_cpu_ptr(nhc->nhc_pcpu_rth_output);
}
// 3.4 校验rtable是否有效,如果有效,增加dst_entry的引用计数,然后直接返回
rth = rcu_dereference(*prth);
if (rt_cache_valid(rth) && dst_hold_safe(&rth->dst))
return rth;
}
add:
// 4 分配dst_entry(rtable),给dst_entry::inpu,dst_entry::output,dst_entry::dev赋值,将dst_entry的引用计数设置为1
rth = rt_dst_alloc(dev_out, flags, type,
IN_DEV_CONF_GET(in_dev, NOPOLICY),
IN_DEV_CONF_GET(in_dev, NOXFRM),
do_cache);
......
// 5 如果是组播或者广播,dst_entry::output会被赋为ip_mc_output
if (flags & (RTCF_BROADCAST | RTCF_MULTICAST)) {
if (flags & RTCF_LOCAL &&
!(dev_out->flags & IFF_LOOPBACK)) {
rth->dst.output = ip_mc_output;
RT_CACHE_STAT_INC(out_slow_mc);
}
#ifdef CONFIG_IP_MROUTE
if (type == RTN_MULTICAST) {
if (IN_DEV_MFORWARD(in_dev) &&
!ipv4_is_local_multicast(fl4->daddr)) {
rth->dst.input = ip_mr_input;
rth->dst.output = ip_mc_output;
}
}
#endif
}
// 6 根据fib_nh_common中的nexthop信息给rtable::rt_uses_gateway,rtable::rt_gw_family,rtable::rt_gw4这些参数赋值,根据路由配置realm给dst::tclassid赋值
// 6.1 如果上面的步骤3中找到了fib_nh_exception,那么使用fib_nh_exception来更新rtable::rt_gw4,rtable::rt_gw_family,rtable::rt_pmtu这些值。如果do_cache为true,还会把rtable缓存到fib_nh_exception::fnhe_rth_output(输入路径上是赋值于fib_nh_exception::fnhe_rth_input)
// 6.2 如果上面的步骤3中没找到fib_nh_exception,如果do_cache为true,那么就把rtable缓存到fib_nh_common::nhc_pcpu_rth_output(输入路径上是赋值于fib_nh_common::nhc_rth_input)
rt_set_nexthop(rth, fl4->daddr, res, fnhe, fi, type, 0, do_cache);
......
return rth;
}
对于udp socket而言,如果对udp socket调用过connect
函数,则路由查找在调用connect
函数时就已经完成了,路由查找产生的rtable
也会被缓存在udp socket上(sock::sk_dst_cache
)。当然,如果connect
函数查找到的路由失效了,那udp_sendmsg
在被调用时还是会再查找一次路由,然后再把它缓存到udp socket上。
分割和缓存数据
从前面udp_sendmsg
的调用过程可以看到,当应用层没有传递MSG_MORE
时,udp_sendmsg
会调用ip_make_skb
函数来将数据拷贝到sk_buff
,然后马上调用udp_send_skb
来把sk_buff
发送到ip层。当应用层传递MSG_MORE
了时,udp_sendmsg
会调用ip_append_data
函数来将数据拷贝到sk_buff
,这时udp_sendmsg
不会立即将sk_buff
发送到下层函数,在应用层不再传递MSG_MORE
之前,ip_append_data
函数可能会被调用多次,这期间产生的sk_buff
全都缓存于socket的发送对列上,当应用层不再传递MSG_MORE
之时,udp_sendmsg
才会调用udp_push_pending_frames
函数来将缓存的sk_buff
发送到ip层。无论应用层传不传递MSG_MORE
标志,ip_make_skb
和ip_append_data
都是通过调用ip_setup_cork
函数和__ip_append_data
函数来拷贝数据到sk_buff
的。对于应用层传递了MSG_MORE
的情形,ip_append_data
只在缓存流程开始时调用ip_setup_cork
函数一次,让它初始化一些控用于制缓存流程的变量,调用一次后直到缓存流程结束(不再传递MSG_MORE
)都不再调用。
接下来看一看应用层传递过来的数据是如何被分割和缓存的。
sk_buff的基础结构
__ip_append_data
缓存数据的流程中涉及了许多对sk_buff
的操作,先来简单了解下sk_buff
的结构。
内核分配sk_buff
的时候,涉及到两部分内存的分配,一部分用于存储struct sk_buff
结构体变量自身,另一部分是用于存储数据。sk_buff
的分配函数alloc_skb
如下:
alloc_skb
__alloc_skb
// 调用alloc_skb时只需要传入缓冲区尺寸, alloc_skb会在缓冲区尺寸的基础上多分配一些内存用来存储关键信息
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, int flags, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;
......
// 分配sk_buff结构体自身
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
......
// 分配数据缓冲区,需要注意size的值在原先的基础上增加了 sizeof(struct skb_shared_info)
// 也就是说在缓冲区的尾部还有一个struct skb_shared_info结构体变量
size = SKB_DATA_ALIGN(size);
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
......
// 这里将size重新减去了sizeof(struct skb_shared_info)
size = SKB_WITH_OVERHEAD(ksize(data));
......
// 接下来的几行语句初始化skb->head、skb->data、skb->tail、skb->end等指针
memset(skb, 0, offsetof(struct sk_buff, tail));
// skb->truesize代表着该sk_buff总占用的内存(size + sizeof(struct sk_buff) + sizeof(struct skb_shared_info))
skb->truesize = SKB_TRUESIZE(size);
......
// skb->head指向了数据缓冲区起始位置
skb->head = data;
// skb->data、skb->tail被初始为数据缓冲区起始位置,但后续在各层协议处理skb的过程中,它们指向的位置会发生变化
skb->data = data;
skb_reset_tail_pointer(skb);
// skb->end指向了skb->head + size这个位置,其实就是数据缓冲区的结束位置,也是struct skb_shared_info的起始位置
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;
// 初始化struct skb_shared_info,skb_shinfo(skb)通过 skb->end获取skb_shared_info的起始位置
shinfo = skb_shinfo(skb);
memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
atomic_set(&shinfo->dataref, 1);
......
}
sk_buff
的结构可由下图简述:
alloc_skb
函数除了分配struct sk_buff
结构体变量和数据缓冲区,还在数据缓冲区的尾部额外分配了一些空间用于存储struct skb_shared_info
,那skb_shared_info
又是用来做什么的呢?
其实sk_buff
存储数据时可以不将数据全都存储在一片连续的内存中,它支持将数据存储在一些离散的内存片段中,而skb_shared_info
则是用来记录这些离散内存片段地址的。
做个约定,由alloc_skb
分配的那片缓冲区我们称之为linear buffer
, 而离散的那些缓冲区我们称之为paged buffer
,在稍后的__ip_append_data
函数分析中将会看到如何使用paged buffer
。
在写者看来,使用paged buffer
的好处是可以减轻大片连续内存的分配压力,但sk_buff
若想使用paged buffer
来存储数据需要满足一定的条件,那便是输出报文的接口设备需要支持分散聚集I/O。支持分散聚集I/O的网络设备在向内核注册时会设置 NETIF_F_SG
标志,__ip_append_data
函数在缓存数据时便会根据接口设备是否配置了NETIF_F_SG
标志来决定是否使用paged buffer
来缓存数据。
使用paged buffer
缓存数据时,sk_buff
结构大致如下:
__ip_append_data
函数调用alloc_skb
来分配sk_buff
时会为二层头部预留空间,上图中linear buffer
头部和尾部有一部分空间就是预留给下层协议使用的。
sk_buff
结构比较复杂,这里记录一些接下来将会频繁用到的结构体成员以及它们所代表的含义:
- head: 指向
linear buffer
起始位置 - end: 指向
linear buffer
结束位置 - data: 指向
linear buffer
区数据的起始位置 - tail: 指向
linear buffer
中数据的结束位置,data和tail两个指针会随着sk_buff
在协议层间传递而发生变化 - data_len: 缓存于
paged buffer
数据的长度 - len:
sk_buff
数据总长度,它代表缓存于linear buffer
和paged buff
区的数据的总和 - truesize:
sk_buff
结构体变量自身和数据缓冲区总占用的内存大小
关于IP分段
链路层协议对帧的数据部分的最大长度有一个限制值,也就是我们常说的MTU,ip报文作为链路层的数据荷载封装成帧时也需要遵循数据部分不能超过MTU的准则,为了使输出到下层的ip报文满足MTU,__ip_append_data
函数在缓存数据时则会按照一定原则对数据进行分割,__ip_append_data
函数遵循的原则如下:
- ip报文总长度不应该超过64KB
- 除非是最后一个ip分段,其它ip分段的数据部分长度应该是8字节的倍数
- ip分段的长度应尽可能达到MTU,但不能超过MTU
__ip_append_data
做的这些分割ip报文的工作我们可以称之为预分段,在ip层向下层输出数据包前会进行真正的ip分段工作,__ip_append_data
所做的预分段工作会使得稍后的分段工作更易于处理。
ip_setup_cork函数
ip_setup_cork
函数会分配并初始化一个struct inet_cork
类型的结构体变量,如下:
static int ip_setup_cork(struct sock *sk, struct inet_cork *cork,
struct ipcm_cookie *ipc, struct rtable **rtp)
{
struct ip_options_rcu *opt;
struct rtable *rt;
rt = *rtp;
......
// 在udp_push_pending_frames调用时会构造ip头部,这里拷贝ip option以便后续使用
opt = ipc->opt;
if (opt) {
if (!cork->opt) {
cork->opt = kmalloc(sizeof(struct ip_options) + 40, sk->sk_allocation);
......
}
memcpy(cork->opt, &opt->opt, sizeof(struct ip_options) + opt->opt.optlen);
cork->flags |= IPCORK_OPT;
cork->addr = ipc->addr;
}
// cork->fragsize记录ip分段的最大长度,网卡配置的mtu或者path mtu
cork->fragsize = ip_sk_use_pmtu(sk) ? dst_mtu(&rt->dst) : READ_ONCE(rt->dst.dev->mtu);
......
// 构造sk_buff时需要路由信息,cork->dst 记录路由信息
cork->dst = &rt->dst;
// ip_append_data函数的调用者传递进来的是指向路由信息指针的指针,这里将调用者保存路由信息的指针置为NULL,以避免调用者释放该路由信息(路由信息在整个缓存数据流程中都要使用,不能让调用者调用一次ip_append_data后就把它释放)
*rtp = NULL;
// cork->length记录着已经缓存于发送队列上的数据的总长度
cork->length = 0;
// 在udp_push_pending_frames调用时,ttl/tos 会放入ip头部,mark/priority/transmit_time会放入skb对应字段并影响后续的传输流程,这里也把它们记录到inet_cork中
cork->ttl = ipc->ttl;
cork->tos = ipc->tos;
cork->mark = ipc->sockc.mark;
cork->priority = ipc->priority;
cork->transmit_time = ipc->sockc.transmit_time;
cork->tx_flags = 0;
sock_tx_timestamp(sk, ipc->sockc.tsflags, &cork->tx_flags);
......
}
__ip_append_data函数
__ip_append_data
函数在分割数据分配sk_buff
时除了需要考虑IP分段的原则,还要为链路层头部空间以及扩展头部预留空间。 (IPsec需要预留扩展头部空间,这部分信息是从《深入理解linux网络技术内幕了》了解到的,写者不清楚是否只有IPsec需要扩展头部,也不太清楚IPsec的原理).
__ip_append_data
函数为了计算出一个合理的sk_buff
分配尺寸以及拷贝合适长度的数据,使用了很多的局部变量,使得逻辑变得有点复杂,这里先给出一张图,简单说明一下一些会用到的局部变量含义
包含hh_len、exthdrlen、fragheaderlen、transhdrlen、fraggap、trailer_len、pagedlen、alloc_extra 、fraglen 、alloclen 、maxfraglen等变量
__ip_append_data
函数考虑的条件有点多,稍微会有一点复杂,它的代码大致如下:
// sk: socket
// getfrag: 从from拷贝数据的函数,__ip_append_data会调用getfrag来将数据拷贝到sk_buff,因为__ip_append_data会被raw/udp/icmp这些协议栈调用,而不同的L4协议传递给__ip_append_data的数据来源都各有特色(raw/udp数据来自用户空间,icmp的数据来自于内核空间),所以L4协议有必要提供一个能够从from处拷贝数据的函数。
// from: 数据来源
// length: L4传递的数据长度,它将L4头部的长度也包含在内
// transhdrlen: L4头部长度, __ip_append_data需要根据该值来预留L4协议头部空间,只有第一个sk_buff需要预留L4头部空间
// cork: 在ip_setup_cork函数构造的控制结构,该结构中包含了构建sk_buff以及报文头部所需要的各种信息,比如ttl/tos/ip option/路由
// rtp: 路由查找得到的rtable
// flags: 应用层传递下来的flag, 有四个标志对__ip_append_data而言有意义: MSG_MORE: 借此告知__ip_append_data后续还会有数据需要缓存;MSG_PROBE:应用程序可能只是在探测路径,并不想传递数据,__ip_append_data只需返回即可;MSG_DONTWAIT:__ip_append_data分配sk_buff时不会阻塞;MSG_ZEROCOPY:__ip_append_data会尝试不为sk_buff分配数据缓冲区,直接将L4传递过来的数据赋予sk_buff
static int __ip_append_data(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork,
struct page_frag *pfrag,
int getfrag(void *from, char *to, int offset,
int len, int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
unsigned int flags)
{
struct inet_sock *inet = inet_sk(sk);
struct ubuf_info *uarg = NULL;
// skb将指向当前操作的sk_buff
struct sk_buff *skb;
struct ip_options *opt = cork->opt;
// hh_len代表每个ip分段都需要预留的二层头部空间长度
int hh_len;
// exthdrlen代表需预留的IPsec头部空间长度, 一个缓存流程中仅第一个sk_buff需要预留这部分空间
int exthdrlen;
// MTU值:可能是路径MTU,也可能是出口设备MTU
int mtu;
// __ip_append_data有一个拷贝数据的大循环,copy值代表着每轮循环需要拷贝多少数据到当前sk_buff
int copy;
int err;
// offset代表着从哪处偏移开始拷贝数据,getfrag函数用它来确定从哪开始拷贝数据
int offset = 0;
// maxfraglen 代表着非最后一个ip分段的最大长度(根据ip协议约定非最后一个ip分段的长度需要满足MTU且是8字节的倍数,而最后一个ip分段的最大长度满足MTU即可)
// fragheaderlen 代表着每个ip分段都需要预留的ip头部空间长度
// maxnonfragsize代表着不分段时的最大ip封包长度,通常情况下是64K
unsigned int maxfraglen, fragheaderlen, maxnonfragsize;
// csummode将用于给skb->ip_summed附值,这个值用以告知硬件是否需要计算L4校验值, CHECKSUM_NONE代表不需要,CHECKSUM_PARTIAL代表需要
// 需要注意的是, getfrag函数在拷贝数据到sk_buff时,会根据skb->ip_summed来决定是否需要用软件计算L4校验值,若计算了,则会将校验值存储在skb->csum
int csummode = CHECKSUM_NONE;
// rt指针指向rtable
struct rtable *rt = (struct rtable *)cork->dst;
unsigned int wmem_alloc_delta = 0;
// TODO:paged? extra_uref? tskey?
bool paged, extra_uref = false;
u32 tskey = 0;
// 1 获取socket发送队列尾部的sk_buff, 若skb为空则代表着刚刚进入一轮新的数据缓存流程,我们接下来要分配和操作的是第一个ip分段,若skb不为空则我们需要判度该skb是否还能塞进去一些数据
skb = skb_peek_tail(queue);
// 2 若skb为空,则马上要为第一个ip分段分配sk_buff,仅第一个sk_buff需要为IPsec预留空间
exthdrlen = !skb ? rt->dst.header_len : 0;
// 3 获取mtu值
mtu = cork->gso_size ? IP_MAX_MTU : cork->fragsize;
paged = !!cork->gso_size;
......
// 4 获取L2头部空间长度
hh_len = LL_RESERVED_SPACE(rt->dst.dev);
// 5 计算ip头部空间长度(包含固定ip header和ip option)
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
// 6 计算非最后一个ip分段的最大分段长度,需要遵循ip分段数据部分长度为8字节倍数
maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;
// 7 获取ip报文最大长度,64KB
maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu;
// 8 检查当前发送队列上缓存的数据是否已经超过了ip报文的最大长度
// cork->length代表着已经缓存的ip数据长度,length代表此次调用__ip_append_data传入的数据长度,它两的和加上ip头部长度不能大于ip报文最大长度64KB
if (cork->length + length > maxnonfragsize - fragheaderlen) {
ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport,
mtu - (opt ? opt->optlen : 0));
return -EMSGSIZE;
}
......
// 9 如果调用者传递了MSG_ZEROCOPY标志,则将数据拷贝到sk_buff时不会真正的拷贝,而是直接将调用者传递的数据地址记录到skb_shared_info。
// 使用zero copy有个前提条件便是出口设备需要支持聚集/分散IO,这里的判段根据MSG_ZEROCOPY标志和NETIF_F_SG标志,并给uarg->zerocopy赋值。
// 后续拷贝数据前会判段uarg->zerocopy的值,uarg->zerocopy非0时就会使用zero copy
if (flags & MSG_ZEROCOPY && length && sock_flag(sk, SOCK_ZEROCOPY)) {
uarg = sock_zerocopy_realloc(sk, length, skb_zcopy(skb));
if (!uarg)
return -ENOBUFS;
extra_uref = !skb_zcopy(skb); /* only ref on new uarg */
// ZERO COPY需要出口设备支持聚集/分散IO(NETIF_F_SG),这里判断出口设备是否配置了NETIF_F_SG标志
if (rt->dst.dev->features & NETIF_F_SG && csummode == CHECKSUM_PARTIAL) {
paged = true;
} else {
uarg->zerocopy = 0; // 不支持聚集/分散IO,则将uarg->zerocopy置0
skb_zcopy_set(skb, uarg, &extra_uref);
}
}
// 10 更新cork->length
cork->length += length;
// 11 skb为空代表着我们需要为第一个ip分段分配sk_buff,而不是往先前的sk_buff里塞数据,所以直接跳转到alloc_new_skb
if (!skb)
goto alloc_new_skb;
// 12 分配sk_buff、拷贝数据的大循环, length代表着剩余的需要拷贝的数据长度
while (length > 0) {
/* Check if the remaining data fits into current packet. */
// 13 循环开始,会先判度当前sk_buff中是否有剩余空间可以塞一些数据进去
// 13.1 skb->len是当前sk_buff中的数据长度,这里用mtu - skb->len来判段当前skb还能塞下的多少数据
copy = mtu - skb->len;
// 13.2若copy小于length,则代表该sk_buff塞不下剩余的所有数据,则可以确定当前sk_buff不会是最后一个ip分段,所以重新使用maxfraglen来计算该sk_buff还可以塞下多少数据
if (copy < length)
copy = maxfraglen - skb->len;
// 13.3 若copy小于等于0,则代表当前sk_buff塞不下任何数据了,需要分配新的sk_buff
if (copy <= 0) {
char *data;
// datalen代表着总的需要拷贝多少数据到新分配的sk_buff的
unsigned int datalen;
// fraglen代表着此ip分段的长度,datalen(ip荷载长度)+ip头部长度
unsigned int fraglen;
// fraggap代表着需要从老sk_buff拷贝多少数据到新分配的sk_buff
unsigned int fraggap;
// alloclen 代表着新sk_buff需要分配的linear buffer区间是多少长度,alloc_extra 代表着为二层头部、IPsec 头部尾部预留的空间,分配sk_buff时需要把alloc_extra考虑在内
unsigned int alloclen, alloc_extra;
// pagedlen代表有多少数据需要放入paged buffer区
unsigned int pagedlen;
// skb_prev指针将指向老sk_buff(或者说当前sk_buff的前一个sk_buff)
struct sk_buff *skb_prev;
alloc_new_skb:
// 因为接下来马上要分配新的sk_buff了,先把skb_prev指向当前sk_buff
skb_prev = skb;
// 13.3.1 计算需要从老sk_buff拷贝多少数据到新分配的sk_buff, fraggap的产生见后面解释
if (skb_prev)
fraggap = skb_prev->len - maxfraglen;
else
fraggap = 0;
// 13.3.2 计算总的需要拷贝多少数据到新分配的sk_buff, length是剩余待拷贝的数据长度,若length + fraggap > mtu - fragheaderlen,
// 则代表新分配的sk_buff也装不下剩余所有数据,所以这个新sk_buff的最大尺寸应该用maxfraglen来计算,datalen取值为maxfraglen - fragheaderlen
datalen = length + fraggap;
if (datalen > mtu - fragheaderlen)
datalen = maxfraglen - fragheaderlen;
// 13.3.3 计算ip分段长度,此时ip荷载长度(datalen)和ip头部长度(fragheaderlen)都已经确定, ip分段长度(fraglen)也就可以计算出来
fraglen = datalen + fragheaderlen;
pagedlen = 0;
// 13.3.4 接下来几行代码计算需要为二层头部、IPsec 头部尾部预留的空间,hh_len是每个ip分段都要预留的,exthdrlen只在第一个ip分段时不为0
alloc_extra = hh_len + 15;
alloc_extra += exthdrlen;
// 13.3.5 因为__ip_append_data无法确定自己是否还会被调用,所以它无法确定新的sk_buff是不是最后一个分段,datalen == length + fraggap 代表着新sk_buff有可能是最后一个ip分段,所以预留了IPsec尾部空间
if (datalen == length + fraggap)
alloc_extra += rt->dst.trailer_len;
// 13.3.6 接下来的几行将确定新sk_buff的分配尺寸,注意这里提到的分配尺寸还没有将alloc_extra考虑在内
if ((flags & MSG_MORE) &&
!(rt->dst.dev->features&NETIF_F_SG))
// 出口设备不支持分散聚集I/O,则所有的数据都应该放在linear buffer区间,不能放在paged buffer区间,而调用者传递了MSG_MORE则代表着后续还有数据需要缓存,所以alloclen被赋值为MTU,尽可能的为后续的数据预留空间
alloclen = mtu;
else if (!paged &&
(fraglen + alloc_extra < SKB_MAX_ALLOC ||
!(rt->dst.dev->features & NETIF_F_SG)))
// 设备不支持分散聚集I/O,且无法确定后续是否还有数据需要缓存,则不需考虑为后续数据预留空间,sk_buff的分配尺寸按照此次ip分段需要使用的空间长度来确定
alloclen = fraglen;
else {
// 设备支持分散聚集I/O,附加数据变得更加方便,不管后续还有没有数据,alloclen取fraglen和MAX_HEADER的最小值,linear buffer放不下的数据就放在paged buffer区间
alloclen = min_t(int, fraglen, MAX_HEADER);
// pagedlen即代表着需要放在paged buffer区间的数据长度
pagedlen = fraglen - alloclen;
}
// 13.3.7 计算最终的分配长度:ip分段的长度(头部+荷载)+为二层头部以及ipsec预留的长度
alloclen += alloc_extra;
// 13.3.8 分配新的sk_buff
if (transhdrlen) { // transhdrlen非0代表这是第一个Ip分段
skb = sock_alloc_send_skb(sk, alloclen,
(flags & MSG_DONTWAIT), &err);
} else {
// 若非第一个ip分段,则分配前需要考虑socket已经占用的内存是否已经超过了配额,这里的sk->sk_sndbuf可以通过SO_SNDBUF这个socket option来配置
skb = NULL;
if (refcount_read(&sk->sk_wmem_alloc) + wmem_alloc_delta <= 2 * sk->sk_sndbuf)
skb = alloc_skb(alloclen,sk->sk_allocation);
if (unlikely(!skb))
err = -ENOBUFS;
}
......
// 13.3.9 skb_reserve会将skb->data和skb->tail指针值加上hh_len,预留二层头部空间
skb_reserve(skb, hh_len);
// 13.3.10 skb_put会将skb->tail和skb->len指针值加上fraglen + exthdrlen - pagedlen,此时只是先将sk_buff的数据指针和长度先配置好,其实还没有拷贝数据进去
// data指针此时指向了IPsec头部的位置,若没有IPsec,则data指针就指向了ip头部的位置
data = skb_put(skb, fraglen + exthdrlen - pagedlen);
// 13.3.11 设置网络层(skb->network_header)和传输层头部(skb->transport_header)所处偏移位置
skb_set_network_header(skb, exthdrlen);
skb->transport_header = (skb->network_header + fragheaderlen);
// 13.3.12 data指针此时指向了L4头部的位置,若没有L4头部,则data就指向了L4荷载的位置
data += fragheaderlen + exthdrlen;
// 13.3.13 若fraggap存在,则从老sk_buff拷贝fraggap长度的数据到新sk_buff
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);
}
// 13.3.14 判断sk_buff的linear buffer区是否还能放进去一些数据
copy = datalen - transhdrlen - fraggap - pagedlen;
// copy大于0代表仍能塞入一些数据, 调用getfrag来往linear buffer拷贝数据
// 传递给getfrag的第二个参传递data + transhdrlen是为了预留L4头部空间
if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
err = -EFAULT;
kfree_skb(skb);
goto error;
}
// 拷贝数据后,更新一下偏移
offset += copy;
// 13.3.15 传递给ip_append_data函数的length参数其实将L4头部长度计算在内了,所以此处length会自减(copy + transhdrlen)
length -= copy + transhdrlen;
// 13.3.16 只有第一个ip分段有L4头部,所以这里将transhdrlen配置为0,后续的再为新的sk_buff计算分配长度时就不会将L4头部计算在内了
transhdrlen = 0;
// 13.3.17 只有第一个ip分段需要预留IPsec头部空间,所以这里将exthdrlen配置为0,后续的再为新的sk_buff计算分配长度时就不会将IPsec头部计算在内了
exthdrlen = 0;
......
// 13.3.18 将skb放入socket发送队列尾部
__skb_queue_tail(queue, skb);
// 需要注意的是刚创建完sk_buff的这轮循环中只往linear塞了些数据,没有往paged buffer塞数据,下一轮循环时才会往paged buffer塞数据
continue;
}
// 13.4 copy值大于0,表示当前sk_buff仍可以塞入一些数据
if (copy > length)
copy = length;
// 13.4.1 如果出口设备不支持分散聚集I/O,则直接往linear buffer区间拷贝,不支持分散聚集I/O情况下,分配sk_buff时linear buffer区会分配足够的空间
if (!(rt->dst.dev->features&NETIF_F_SG) &&
skb_tailroom(skb) >= copy) {
unsigned int off;
off = skb->len;
// 拷贝后skb_put会更新skb->tail和skb->len值
if (getfrag(from, skb_put(skb, copy),
offset, copy, off, skb) < 0) {
__skb_trim(skb, off);
err = -EFAULT;
goto error;
}
} else if (!uarg || !uarg->zerocopy) {
// 13.4.2 若出口设备支持分散聚集I/O,且不使用zero copy, 则分配paged buffer,并拷贝.
int i = skb_shinfo(skb)->nr_frags;
err = -ENOMEM;
// 13.4.2.1 分配paged buffer, sk_page_frag_refill函数会去找一段空闲内存(由pfrag指针描述)
if (!sk_page_frag_refill(sk, pfrag))
goto error;
// 13.4.2.2 skb_can_coalesce会判断skb_shinfo(skb)->frag[i-1]所记录的paged buffer是否和pfrag指向的paged buffer属于同一个page且两段buffer地址相连,
// 若是则可以把它们合并起来,直接用skb_shinfo(skb)->frag[i-1]来描述两段buffer
if (!skb_can_coalesce(skb, i, pfrag->page,
pfrag->offset)) {
err = -EMSGSIZE;
if (i == MAX_SKB_FRAGS)
goto error;
// 若不能合并,则需要使用skb_shinfo(skb)->frag[i]来描述新的paged buffer的page信息(page以及offset)
__skb_fill_page_desc(skb, i, pfrag->page,
pfrag->offset, 0);
skb_shinfo(skb)->nr_frags = ++i;
get_page(pfrag->page);
}
// 13.4.2.3 拷贝数据到新分配的paged buffer
copy = min_t(int, copy, pfrag->size - pfrag->offset);
if (getfrag(from,
page_address(pfrag->page) + pfrag->offset,
offset, copy, skb->len, skb) < 0)
goto error_efault;
pfrag->offset += copy;
// 13.4.2.4 更新skb_shinfo(skb)->frags[i - 1]所描述的paged buffer的尺寸信息
skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
// 13.4.2.5 更新skb->len、skb->data_len、skb->truesize、wmem_alloc_delta
skb->len += copy;
skb->data_len += copy;
skb->truesize += copy;
wmem_alloc_delta += copy;
} else {
// 13.4.3 支持分散聚集I/O,且使用zero copy, 在该分支中不会去分配新的paged buffer, 也不进行数据拷贝,skb_zerocopy_iter_dgram将直接记录数据的page信息到skb_shinfo(skb)->frags[]
err = skb_zerocopy_iter_dgram(skb, from, copy);
if (err < 0)
goto error;
}
offset += copy;
length -= copy;
}
if (wmem_alloc_delta)
refcount_add(wmem_alloc_delta, &sk->sk_wmem_alloc);
......
}
在__ip_append_data
函数中注释13.3.1处有提到为新sk_buff
计算分配尺寸时需要考虑是否有fraggap 存在,fraggap是需要从前一个sk_buff
拷贝的数据长度,为什么还需要从前一个sk_buff
拷贝数据呢?我们来看一个例子,如下图所示,图中假设MTU为104字节:
__ip_append_data函数的调用结果
__ip_append_data
函数调用后,数据被装进了sk_buff
,而sk_buff
则挂在socket输出队列上,下面将用两张图来描述ip_append_data
函数的调用后socket输出队列的样子(没考虑IPsec)。
当出口设备不支持聚集分散I/O时,ip_append_data
函数的调用结果可以用下图来描述:
当出口设备支持聚集分散I/O时,ip_append_data
函数的调用结果可以用下图来描述:
发送数据到下层协议栈
接下来分析udp_push_pending_frames
函数,看看它做了哪些事情,再回顾一下udp_push_pending_frames
函数调用的关键函数:
udp_push_pending_frames
ip_finish_skb // 将sk_buff从socket发送队列上取下,构造ip头部
__ip_make_skb
udp_send_skb // 构造udp头部,将skb发送到ip层
ip_send_skb
ip_local_out
__ip_make_skb函数
__ip_make_skb
会将socket发送队列上所有sk_buff
全部取下,重新组织成链表结构。
struct sk_buff *__ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork)
{
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 *)cork->dst;
struct iphdr *iph;
__be16 df = 0;
__u8 ttl;
// 1 从队列头部取下第一个sk_buff,skb指针在该函数中将一直指向第一个ip片段对应的sk_buff
skb = __skb_dequeue(queue);
if (!skb)
goto out;
// 稍后会看到,除了第一个ip片段对应的sk_buff外,剩余的sk_buff会链接于skb_shinfo(skb)->frag_list这个链表上
tail_skb = &(skb_shinfo(skb)->frag_list);
// 2 当__ip_append_data函数中为IPsec预留了空间时,会将skb->data指向IPsec头部位置,就会导致 skb->data < skb_network_header(skb),这里的__skb_pull函数会修改skb->data指针将其指向ip头部
if (skb->data < skb_network_header(skb))
__skb_pull(skb, skb_network_offset(skb));
// 3 将socket的发送队列上的sk_buff逐个取下,地址赋值给tmp_skp指针
while ((tmp_skb = __skb_dequeue(queue)) != NULL) {
// 3.1 这里的__skb_pull函数会将 tmp_skb->data指针指向ip荷载(注意是ip荷载不是ip头部)的起始位置,还会将tmp_skb->len自减skb_network_header_len(skb),此时的tmp_skb->len就代表着tm_skb所携带的ip数据长度
__skb_pull(tmp_skb, skb_network_header_len(skb));
// 3.2 从第二个ip片段起,它们对应的sk_buff都需要链接到首个ip片段sk_buff的skb_shinfo(skb)->frag_list链表上
*tail_skb = tmp_skb;
tail_skb = &(tmp_skb->next);
// 3.3 接下来的三行代码,将所有sk_buff的长度都累加记录到第一个sk_buff中
skb->len += tmp_skb->len;
skb->data_len += tmp_skb->len;
skb->truesize += tmp_skb->truesize;
tmp_skb->destructor = NULL;
tmp_skb->sk = NULL;
}
// 4 经过上面的循环,skb->len累加了所有sk_buff携带的ip数据长度,它就代表着总的ip报文的实际长度
// skb->data_len则代表着:第一个sk_buff paged buffer区的ip数据长度+其它sk_buff中ip数据的长度
// skb->truesize代表着所有sk_buff的缓冲区和数据结构的总尺寸
// 5 接下来的部分就是构造ip头部,ip头部信息来自 cork 和fl4
skb->ignore_df = ip_sk_ignore_df(sk);
if (inet->pmtudisc == IP_PMTUDISC_DO ||
inet->pmtudisc == IP_PMTUDISC_PROBE ||
(skb->len <= dst_mtu(&rt->dst) &&
ip_dont_fragment(sk, &rt->dst)))
df = htons(IP_DF);
if (cork->flags & IPCORK_OPT)
opt = cork->opt;
if (cork->ttl != 0)
ttl = cork->ttl;
else if (rt->rt_type == RTN_MULTICAST)
ttl = inet->mc_ttl;
else
ttl = ip_select_ttl(inet, &rt->dst);
// 构造ip头部的各个字段
iph = ip_hdr(skb);
iph->version = 4;
iph->ihl = 5;
iph->tos = (cork->tos != -1) ? cork->tos : inet->tos;
iph->frag_off = df;
iph->ttl = ttl;
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
ip_select_ident(net, skb, sk);
// 构造ip选项
if (opt) {
iph->ihl += opt->optlen>>2;
ip_options_build(skb, opt, cork->addr, rt, 0);
}
// 将priority,mark记录到sk_buff
skb->priority = (cork->tos != -1) ? cork->priority: sk->sk_priority;
skb->mark = cork->mark;
skb->tstamp = cork->transmit_time;
cork->dst = NULL;
// 让sk_buff引用rtable
skb_dst_set(skb, &rt->dst);
......
out:
return skb;
}
经__ip_make_skb
函数后socket输出队列上的sk_buff被取下并重新以链表进行组织,然后向下层传递,链表的形式如下:
如上图所示,应用层传递的数据最终分割到了多个sk_buff
里,整个链表其实只代表着一个ip报文,而第一个sk_buff
会作为代表,其len/datalen/truesize
等字段将更新为整个链表所有sk_buff
对应字段的和。另外, __ip_make_skb
函数只为首个sk_buff
构造了IP报头,没有为剩余sk_buff
构造IP报头,它们的IP报头将在IP分段时构造。
__ip_make_skb
调用完成后,udp_send_skb
函数会对udp头部进行一些补充,比如端口号、udp数据长度、校验值等,当信息补充完成后则调用ip_send_skb
将sk_buff
传递到ip层,之后的流程如3中所述,不再进一步分析。
参考文档
- https://www.rfc-editor.org/rfc/rfc791
- 《深入理解linux网络技术内幕》