linux网络协议栈源码分析 - 网络层IP网际协议

Posted arm7star

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux网络协议栈源码分析 - 网络层IP网际协议相关的知识,希望对你有一定的参考价值。

1、IP报文

1.1、IP报文格式 

        更详细的介绍参考《TCP/IP详解卷 1:协议》第3章 IP:网际协议。

1.2、IP首部结构体

struct iphdr 
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u8	ihl:4,
		version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
	__u8	version:4,
  		ihl:4;
#else
#error	"Please fix <asm/byteorder.h>"
#endif
	__u8	tos;
	__be16	tot_len;
	__be16	id;
	__be16	frag_off;
	__u8	ttl;
	__u8	protocol;
	__sum16	check;
	__be32	saddr;
	__be32	daddr;
	/*The options start here. */
;

        3位标志位:

/* IP flags. */
#define IP_CE		0x8000		/* Flag: "Congestion"		*/
#define IP_DF		0x4000		/* Flag: "Don't Fragment"	*/
#define IP_MF		0x2000		/* Flag: "More Fragments"	*/
#define IP_OFFSET	0x1FFF		/* "Fragment Offset" part	*/

2、IP路由缓存查找

        (参考资料《Linux Kernel Networking -  Implementation and Theory》、机械工业出版社《Linux内核源码剖析:TCP/IP实现(上册)》)

2.1、输出路由缓存查询(ip_route_output_key)

        __ip_route_output_key_hash返回路由rtable,rtable里面包含路由缓存项dst_entry,对于输出路由,主要用到了路由及路由缓存项的输出函数指针output、网卡设备net_device、网关rt_gateway,__ip_route_output_key_hash调用栈:

          dst_output调用路由缓存的输出函数指针output,然后调用ip_output、ip_finish_output、ip_finish_output2,ip_finish_output2找到路由缓的下一跳(网关或者局域网内的其他主机),然后调用邻居子系统的dst_neigh_output发送报文,ip_finish_output2邻居查找及报文发送代码实现如下:

        ip_finish_output2函数调用栈如下:

2.2、输入路由缓存查询(ip_route_input/ip_route_input_noref)

      ip_route_input_slow调用skb_dst_set_noref设置skb的路由缓存,路由缓存dst包含一个输入函数指针,最终调用该输入函数处理输入报文:

        ip_route_input_slow调用栈:

         查找到路由缓存之后,调用dst_input函数,dst_input函数调用路由缓存的input函数处理报文,dst_input调用栈: 

3、IP报文输出

3.1、IP报文发送(ip_queue_xmit)

        ip_queue_xmit主要检查skb是否已经设置路由,如果没有就查找路由,如果找不到路由就丢弃报文;有路由就设置IP首部(IP首部的16位总长度、16位首部检验和在下一级函数设置),调用ip_local_out、rt->dst.output输出IP报文。

        ip_queue_xmit代码实现如下:

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)

	struct inet_sock *inet = inet_sk(sk);
	struct net *net = sock_net(sk);
	struct ip_options_rcu *inet_opt;
	struct flowi4 *fl4;
	struct rtable *rt;
	struct iphdr *iph;
	int res;

	/* Skip all of this if the packet is already routed,
	 * f.e. by something like SCTP.
	 */
	rcu_read_lock();
	inet_opt = rcu_dereference(inet->inet_opt);
	fl4 = &fl->u.ip4;
	rt = skb_rtable(skb); // skb路由项(输出网卡设备、下一跳地址等)
	if (rt)
		goto packet_routed; // 如果已经有路由,那么跳转到packet_routed,使用已经设置好的路由

	/* Make sure we can route this packet. */
	rt = (struct rtable *)__sk_dst_check(sk, 0); // 获取路由项缓存sk_dst_cache
	if (!rt) 
		__be32 daddr;

		/* Use correct destination address if we have options. */
		daddr = inet->inet_daddr;
		if (inet_opt && inet_opt->opt.srr)
			daddr = inet_opt->opt.faddr;

		/* If this fails, retransmit mechanism of transport layer will
		 * keep trying until route appears or the connection times
		 * itself out.
		 */
		rt = ip_route_output_ports(net, fl4, sk,
					   daddr, inet->inet_saddr,
					   inet->inet_dport,
					   inet->inet_sport,
					   sk->sk_protocol,
					   RT_CONN_FLAGS(sk),
					   sk->sk_bound_dev_if); // 调用ip_route_output_ports、ip_route_output_flow查询输出路由缓存;参考机械工业出版社《Linux内核源码剖析:TCP IP实现(下册)》"第20章 路由缓存"
		if (IS_ERR(rt))
			goto no_route; // 没有路由则跳转到no_route,丢弃报文
		sk_setup_caps(sk, &rt->dst);
	
	skb_dst_set_noref(skb, &rt->dst); // 设置skb->_skb_refdst

packet_routed:
	if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
		goto no_route;

	/* OK, we know where to send it, allocate and build IP header. */
	skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0)); // 获取IP首部在skb里面的地址(如果有选项,需要计算选项的长度,否则为0;从传输层数据之前预留的就是IP首部地址空间)
	skb_reset_network_header(skb);
	iph = ip_hdr(skb); // 获取IP首部地址
	*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff)); // IP首部前16位为: 4位版本号、4位首部长度、8位服务类型(TOS);(4 << 12)为4位版本号,IPv4,(5 << 8)为4位首部长度,5*4共20字节(此次没加上选项长度), (inet->tos & 0xff)为8位服务类型(TOS)
	if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
		iph->frag_off = htons(IP_DF); // IP不分片(Don’t Fragment)
	else
		iph->frag_off = 0;
	iph->ttl      = ip_select_ttl(inet, &rt->dst); // 8位生存时间(TTL)
	iph->protocol = sk->sk_protocol; // 8位协议(IPPROTO_TCP/IPPROTO_UDP)
	ip_copy_addrs(iph, fl4); // 32位源IP地址

	/* Transport layer set skb->h.foo itself. */

	if (inet_opt && inet_opt->opt.optlen)  // 选项(如果有)
		iph->ihl += inet_opt->opt.optlen >> 2; // 修正4位首部长度
		ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0); // 拷贝选项
	

	ip_select_ident_segs(net, skb, sk,
			     skb_shinfo(skb)->gso_segs ?: 1); // 16位标识(每个IP报文有唯一的标识,通过16位标识区分是否是一个IP报文的分片)

	/* TODO : should we use skb->sk here instead of sk ? */
	skb->priority = sk->sk_priority;
	skb->mark = sk->sk_mark;

	res = ip_local_out(net, sk, skb); // 调用ip_local_out发送IP报文(IP首部的16位总长度在函数__ip_local_out里面设置,16位首部检验和在函数ip_send_check设置)
	rcu_read_unlock();
	return res;

no_route:
	rcu_read_unlock();
	IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
	kfree_skb(skb);
	return -EHOSTUNREACH;

 3.2、ip_queue_xmit调用栈

        ip_queue_xmit调用栈如下:

4、IP报文输入

4.1、IP报文输入流程

        ip_packet_type定义ETH_P_IP报文输入函数:

static struct packet_type ip_packet_type __read_mostly = 
	.type = cpu_to_be16(ETH_P_IP),
	.func = ip_rcv,
;

        以太网首部:

struct ethhdr 
	unsigned char	h_dest[ETH_ALEN];	/* destination eth addr	*/
	unsigned char	h_source[ETH_ALEN];	/* source ether addr	*/
	__be16		h_proto;		/* packet type ID field	*/
 __attribute__((packed));

        smsc911x_poll调用eth_type_trans读取以太网首部类型字段并设置接收报文类型skb->protocol:

         __netif_receive_skb_core找到ip_packet_type,并调用输入函数ip_rcv处理输入报文:

         ip_rcv函数调用栈:

 4.2、IP报文校验(ip_rcv)

        ip_rcv校验IP首部各字段、校验和、总长度等,校验通过之后调用ip_rcv_finish继续处理IP报文。

        ip_rcv函数代码实现:

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)

	const struct iphdr *iph;
	struct net *net;
	u32 len;

	/* When the interface is in promisc. mode, drop all the crap
	 * that it receives, do not try to analyse it.
	 */
	if (skb->pkt_type == PACKET_OTHERHOST) // eth_type_trans比较以太网首部的目的地址是否是输入网卡的地址,如果不是,设置skb->pkt_type为PACKET_OTHERHOST
		goto drop; // 丢弃发往其他主机的IP报文


	net = dev_net(dev);
	IP_UPD_PO_STATS_BH(net, IPSTATS_MIB_IN, skb->len);

	skb = skb_share_check(skb, GFP_ATOMIC);
	if (!skb) 
		IP_INC_STATS_BH(net, IPSTATS_MIB_INDISCARDS);
		goto out;
	

	if (!pskb_may_pull(skb, sizeof(struct iphdr))) // 报文长度检查(IP首部的长度)
		goto inhdr_error; // 报文的长度小于IP首部的长度(不包含选项),也就是不完整的IP报文,或者有其他错误,跳转到inhdr_error

	iph = ip_hdr(skb); // 获取IP首部地址

	/*
	 *	RFC1122: 3.2.1.2 MUST silently discard any IP frame that fails the checksum.
	 *
	 *	Is the datagram acceptable?
	 *
	 *	1.	Length at least the size of an ip header
	 *	2.	Version of 4
	 *	3.	Checksums correctly. [Speed optimisation for later, skip loopback checksums]
	 *	4.	Doesn't have a bogus length
	 */

	if (iph->ihl < 5 || iph->version != 4) // 如果IP首部4位首部长度小于5(IP首部不包含选项至少有5*4个字节数据),那么4位首部长度错误,跳转到inhdr_error;如果IP首部4位版本不是4(不是IPv4),那么跳转到inhdr_error
		goto inhdr_error;

	BUILD_BUG_ON(IPSTATS_MIB_ECT1PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_1);
	BUILD_BUG_ON(IPSTATS_MIB_ECT0PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_0);
	BUILD_BUG_ON(IPSTATS_MIB_CEPKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_CE);
	IP_ADD_STATS_BH(net,
			IPSTATS_MIB_NOECTPKTS + (iph->tos & INET_ECN_MASK),
			max_t(unsigned short, 1, skb_shinfo(skb)->gso_segs));

	if (!pskb_may_pull(skb, iph->ihl*4)) // 报文长度检查(IP首部长度(包含选项))
		goto inhdr_error; // 报文长度不够IP首部长度(包含选项)或者其他错误,跳转到inhdr_error

	iph = ip_hdr(skb); // 获取IP首部地址

	if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl))) // 16位首部检验和校验(包含选项)
		goto csum_error; // 16位首部检验和校验失败,跳转到csum_error

	len = ntohs(iph->tot_len); // 16位总长度(字节数)
	if (skb->len < len)  // 报文总长度小于16位总长度
		IP_INC_STATS_BH(net, IPSTATS_MIB_INTRUNCATEDPKTS);
		goto drop; // 跳转到drop,丢弃报文
	 else if (len < (iph->ihl*4)) // 报文长度检查(IP首部长度(包含选项))
		goto inhdr_error; // 报文长度不够IP首部长度(包含选项)或者其他错误,跳转到inhdr_error

	/* Our transport medium may have padded the buffer out. Now we know it
	 * is IP we can trim to the true length of the frame.
	 * Note this now means skb->len holds ntohs(iph->tot_len).
	 */
	if (pskb_trim_rcsum(skb, len))  // 删除SKB尾部的数据(len之后的数据)
		IP_INC_STATS_BH(net, IPSTATS_MIB_INDISCARDS);
		goto drop;
	

	skb->transport_header = skb->network_header + iph->ihl*4; // 获取传输层首部

	/* Remove any debris in the socket control block */
	memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));

	/* Must drop socket now because of tproxy. */
	skb_orphan(skb);

	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish); // 调用ip_rcv_finish

csum_error:
	IP_INC_STATS_BH(net, IPSTATS_MIB_CSUMERRORS);
inhdr_error:
	IP_INC_STATS_BH(net, IPSTATS_MIB_INHDRERRORS);
drop:
	kfree_skb(skb);
out:
	return NET_RX_DROP;

4.3、输入路由查找(ip_rcv_finish)

        物理层只管物理地址不管IP地址,IP报文校验完整之后还得调用ip_route_input_noref查找输入路由,看看是否有输入路由(发往本地或者转发),如果没有输入路由则丢弃报文,否则调用dst_input根据路由处理报文(发往本地或者转发)。

        ip_rcv_finish函数实现代码如下:

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)

	const struct iphdr *iph = ip_hdr(skb);
	struct rtable *rt;

	if (sysctl_ip_early_demux &&
	    !skb_dst(skb) &&
	    !skb->sk &&
	    !ip_is_fragment(iph)) 
		const struct net_protocol *ipprot;
		int protocol = iph->protocol;

		ipprot = rcu_dereference(inet_protos[protocol]);
		if (ipprot && ipprot->early_demux) 
			ipprot->early_demux(skb);
			/* must reload iph, skb->head might have changed */
			iph = ip_hdr(skb);
		
	

	/*
	 *	Initialise the virtual path cache for the packet. It describes
	 *	how the packet travels inside Linux networking.
	 */
	if (!skb_valid_dst(skb))  // 没有输入路由缓存
		int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
					       iph->tos, skb->dev); // 查找输入路由
		if (unlikely(err))  // 没有输入路由
			if (err == -EXDEV)
				NET_INC_STATS_BH(net, LINUX_MIB_IPRPFILTER);
			goto drop; // 跳转到drop,丢弃报文
		
	

#ifdef CONFIG_IP_ROUTE_CLASSID
	if (unlikely(skb_dst(skb)->tclassid)) 
		struct ip_rt_acct *st = this_cpu_ptr(ip_rt_acct);
		u32 idx = skb_dst(skb)->tclassid;
		st[idx&0xFF].o_packets++;
		st[idx&0xFF].o_bytes += skb->len;
		st[(idx>>16)&0xFF].i_packets++;
		st[(idx>>16)&0xFF].i_bytes += skb->len;
	
#endif

	if (iph->ihl > 5 && ip_rcv_options(skb)) // 4位首部长度大于5(包含选项),处理选项
		goto drop; // 选项处理失败,跳转到drop,丢弃报文

	rt = skb_rtable(skb);
	if (rt->rt_type == RTN_MULTICAST) 
		IP_UPD_PO_STATS_BH(net, IPSTATS_MIB_INMCAST, skb->len);
	 else if (rt->rt_type == RTN_BROADCAST)
		IP_UPD_PO_STATS_BH(net, IPSTATS_MIB_INBCAST, skb->len);

	return dst_input(skb); // 调用dst_input处理输入报文

drop:
	kfree_skb(skb);
	return NET_RX_DROP;

        ip_local_deliver调用栈:

4.4、报文发送到传输层(ip_local_deliver_finish) 

        对于发送本地的报文,调用ip_local_deliver、ip_local_deliver_finish,ip_local_deliver_finish根据IP首部的8位协议找到传输层对应协议的输入处理函数,调用传输层的处理函数处理传输层报文,对于IPv4的TCP协议就是tcp_v4_rcv。

        ip_local_deliver_finish函数代码实现如下:

static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)

	__skb_pull(skb, skb_network_header_len(skb));

	rcu_read_lock();
	
		int protocol = ip_hdr(skb)->protocol; // 8位协议(IPPROTO_TCP/IPPROTO_UDP/IPPROTO_RAW...)
		const struct net_protocol *ipprot;
		int raw;

	resubmit:
		raw = raw_local_deliver(skb, protocol);

		ipprot = rcu_dereference(inet_protos[protocol]); // 获取传输层的net_protocol(tcp_protocol/udp_protocol...)
		if (ipprot) 
			int ret;

			if (!ipprot->no_policy) 
				if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) 
					kfree_skb(skb);
					goto out;
				
				nf_reset(skb);
			
			ret = ipprot->handler(skb); // 调用传输层的处理函数(tcp_v4_rcv/udp_rcv...)
			if (ret < 0) 
				protocol = -ret;
				goto resubmit;
			
			IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
		 else 
			if (!raw) 
				if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) 
					IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);
					icmp_send(skb, ICMP_DEST_UNREACH,
						  ICMP_PROT_UNREACH, 0);
				
				kfree_skb(skb);
			 else 
				IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
				consume_skb(skb);
			
		
	
 out:
	rcu_read_unlock();

	return 0;

        ip_local_deliver_finish调用栈:

 

以上是关于linux网络协议栈源码分析 - 网络层IP网际协议的主要内容,如果未能解决你的问题,请参考以下文章

linux网络协议栈源码分析 - 网络层IP网际协议

TCP/IP 协议详解

tcpip协议分为哪几层

linux网络协议栈源码分析 - 链路层ARP地址解析协议

linux网络协议栈源码分析 - 链路层ARP地址解析协议

linux网络协议栈源码分析 - 传输层(TCP连接的建立)