udp发送消息到网络层前路由查找和缓存数据的过程

本篇中分析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,&current->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_BINDTOIFINDEXflowi4_oif为0
  • flowi4_iif: LOOPBACK_IFINDEX (loopback 回环设备)
  • flowi4_mark: 取自SO_MARK这个socket option,如果没有配置SO_MARKflowi4_mark为0
  • flowi4_tos: 取自IP_TOS这个socket option,如果没有配置IP_TOSflowi4_tos为0
  • flowi4_scope: RT_SCOPE_UNIVERSE
  • flowi4_proto: IPPROTO_UDP
  • saddrfl4_sport: 通过对socket调用bind函数来指定,若没有调用过bind函数,则saddrfl4_sport为0
  • daddrfl4_dport: 对socket调用sendto函数时指定或者对socket调用connect函数来指定,没指定则daddrfl4_dport为0

虽然udp是无连接的协议,但上面那些参数中daddrfl4_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_flowflowi4: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时会引用rtablesk_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

12 这两篇笔记中有对策略路由的查找以及路由表的查找更为详细的介绍,这里不再进一步介绍。
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_selnhc其实是没有意义的,这种情况下还需要在多个路径中选择一个。

多路径选择

多路径选择方法大致是这样的:
假设有这样一条路由,它有两个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_selfib_result::nhc更新为正确的值。

构造 rtable

路由查找后,sk_buff在穿过下层的协议栈时,像邻居子系统、网络设备层它们关注的是下一跳地址以及出口设备是什么,fib_lookup函数返回的fib_result中携带了很多的信息,sk_buff不需要把这些信息都带上,所以__mkroute_output函数会从fib_result中提取底层协议栈需要的信息(输入路径上路由查找时也有对应的函数来完成这个工作),用来构造rtablesk_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::inputdst_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_skbip_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的结构可由下图简述:
sk_buff基本结构.png

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结构大致如下:
skb_buff with paged buffer.png

__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 bufferpaged 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局部变量含义.png

__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字节:
fraggap的产生.png

__ip_append_data函数的调用结果

__ip_append_data函数调用后,数据被装进了sk_buff,而sk_buff则挂在socket输出队列上,下面将用两张图来描述ip_append_data函数的调用后socket输出队列的样子(没考虑IPsec)。

当出口设备不支持聚集分散I/O时,ip_append_data函数的调用结果可以用下图来描述:
没有聚集分散IO时ip_append_data结果.png

当出口设备支持聚集分散I/O时,ip_append_data函数的调用结果可以用下图来描述:
支持聚集分散IO时ip_append_data结果.png

发送数据到下层协议栈

接下来分析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被取下并重新以链表进行组织,然后向下层传递,链表的形式如下:
由fraglist组织的ip分段.png

如上图所示,应用层传递的数据最终分割到了多个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_skbsk_buff传递到ip层,之后的流程如3中所述,不再进一步分析。

参考文档

  • https://www.rfc-editor.org/rfc/rfc791
  • 《深入理解linux网络技术内幕》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值