【Linux 内核网络协议栈源码剖析】数据包发送

时间:2021-08-20 11:01:53

由于在connect函数中涉及数据包的发送与接收问题,事实上,发送与接收函数不限于connect函数,所以这里单独剖析。

承前文继续剖析 connect 函数,数据包的发送和接收在 ip_queue_xmit 函数和 release_sock 函数中实现。本文着重分析 ip_queue_xmit 函数,下篇将补充分析 connect 函数剩下的部分。

值得注意的是:这些函数是数据包发送函数,在数据传输阶段,基本上都会调用该函数,因为connect涉及该函数,就放在这里介绍了,不意味着这个函数只属于connect下层函数。

5、网络层——ip_queue_xmit 函数

/*
 * Queues a packet to be sent, and starts the transmitter
 * if necessary.  if free = 1 then we free the block after
 * transmit, otherwise we don't. If free==2 we not only
 * free the block but also don't assign a new ip seq number.
 * This routine also needs to put in the total length,
 * and compute the checksum
 */
 //数据包发送函数
 //sk:被发送数据包对应的套接字;dev:发送数据包的网络设备
 //skb:被发送的数据包         ;flags:是否对数据包进行缓存以便于此后的超时重发
void ip_queue_xmit(struct sock *sk, struct device *dev,
	      struct sk_buff *skb, int free)
{
	struct iphdr *iph;
	unsigned char *ptr;

	/* Sanity check */
	//发送设备检查
	if (dev == NULL)
	{
		printk("IP: ip_queue_xmit dev = NULL\n");
		return;
	}

	IS_SKB(skb);//数据包合法性检查

	/*
	 *	Do some book-keeping in the packet for later
	 */


	skb->dev = dev;
	skb->when = jiffies;//重置数据包的发送时间,只有一个定时器,每次发数据包时,都要重新设置

	/*
	 *	Find the IP header and set the length. This is bad
	 *	but once we get the skb data handling code in the
	 *	hardware will push its header sensibly and we will
	 *	set skb->ip_hdr to avoid this mess and the fixed
	 *	header length problem
	 */
    //skb->data指向的地址空间的布局: MAC首部 | IP首部 | TCP首部 | 有效负载
	ptr = skb->data;//获取数据部分首地址
	ptr += dev->hard_header_len;//后移硬件(MAC)首部长度个字节,定位到ip首部
	iph = (struct iphdr *)ptr;//获取ip首部
	skb->ip_hdr = iph;//skb对应字段建立关联
	//ip数据报的总长度(ip首部+数据部分) = skb的总长度 - 硬件首部长度
	iph->tot_len = ntohs(skb->len-dev->hard_header_len);

#ifdef CONFIG_IP_FIREWALL
	//数据包过滤,用于防火墙安全性检查
	if(ip_fw_chk(iph, dev, ip_fw_blk_chain, ip_fw_blk_policy, 0) != 1)
		/* just don't send this packet */
		return;
#endif	

	/*
	 *	No reassigning numbers to fragments...
	 */
    //如果不是分片数据包,就需要递增id字段
//free==2,表示这是个分片数据包,所有分片数据包必须具有相同的id字段,方便以后分片数据包重组
	if(free!=2)
		iph->id      = htons(ip_id_count++);//ip数据报标识符
		//ip_id_count是全局变量,用于下一个数据包中ip首部id字段的赋值
	else
		free=1;

	/* All buffers without an owner socket get freed */
	if (sk == NULL)//没有对应sock结构,则无法对数据包缓存
		free = 1;

	skb->free = free;//用于标识数据包发送之后是缓存还是立即释放,=1表示无缓存

	/*
	 *	Do we need to fragment. Again this is inefficient.
	 *	We need to somehow lock the original buffer and use
	 *	bits of it.
	 */
	//数据包拆分
    //如果ip层数据包的数据部分(各层首部+有效负载)长度大于网络设备的最大传输单元,就需要拆分发送
    //实际是skb->len - dev->hard_header_len > dev->mtu
    //因为MTU最大报文长度表示的仅仅是IP首部及其数据负载的长度,所以要考虑MAC首部长度
	if(skb->len > dev->mtu + dev->hard_header_len)
	{
	//拆分成分片数据包传输
		ip_fragment(sk,skb,dev,0);
		IS_SKB(skb);//检查数据包skb相关字段
		kfree_skb(skb,FREE_WRITE);
		return;
	}

	/*
	 *	Add an IP checksum
	 */
    //ip首部校验和计算
	ip_send_check(iph);

	/*
	 *	Print the frame when debugging
	 */

	/*
	 *	More debugging. You cannot queue a packet already on a list
	 *	Spot this and moan loudly.
	 */
	if (skb->next != NULL)
	{
		printk("ip_queue_xmit: next != NULL\n");
		skb_unlink(skb);
	}

	/*
	 *	If a sender wishes the packet to remain unfreed
	 *	we add it to his send queue. This arguably belongs
	 *	in the TCP level since nobody else uses it. BUT
	 *	remember IPng might change all the rules.
	 */
    //free=0,表示对数据包进行缓存,一旦发生丢弃的情况,进行数据包重传(可靠性数据传输协议)
	if (!free)
	{
		unsigned long flags;
		/* The socket now has more outstanding blocks */

		sk->packets_out++;//本地发送出去但未得到应答的数据包数目

		/* Protect the list for a moment */
		save_flags(flags);
		cli();

		//数据包重发队列
		if (skb->link3 != NULL)
		{
			printk("ip.c: link3 != NULL\n");
			skb->link3 = NULL;
		}
		if (sk->send_head == NULL)
		{
		//数据包重传缓存队列则是由下列两个字段维护
			sk->send_tail = skb;
			sk->send_head = skb;
		}
		else
		{
			sk->send_tail->link3 = skb;
			sk->send_tail = skb;
		}
		/* skb->link3 is NULL */

		/* Interrupt restore */
		restore_flags(flags);
	}
	else
		/* Remember who owns the buffer */
		skb->sk = sk;

	/*
	 *	If the indicated interface is up and running, send the packet.
	 */
	 
	ip_statistics.IpOutRequests++;
#ifdef CONFIG_IP_ACCT
//下面函数内部调用ip_fw_chk,也是数据包过滤
	ip_acct_cnt(iph,dev, ip_acct_chain);
#endif	
	
#ifdef CONFIG_IP_MULTICAST	
    //对于多播和广播数据包,其必须复制一份回送给本机
	/*
	 *	Multicasts are looped back for other local users
	 */
	 /*对多播和广播数据包进行处理*/
	 //检查目的地址是否为一个多播地址
	if (MULTICAST(iph->daddr) && !(dev->flags&IFF_LOOPBACK))
	{
	//检查发送设备是否为一个回路设备
		if(sk==NULL || sk->ip_mc_loop)
		{
			if(iph->daddr==IGMP_ALL_HOSTS)//如果是224.0.0.1(默认多播地址)
				ip_loopback(dev,skb);//数据包回送给发送端
			else
			{  //检查多播地址列表,对数据包进行匹配
				struct ip_mc_list *imc=dev->ip_mc_list;
				while(imc!=NULL)
				{
					if(imc->multiaddr==iph->daddr)//如果存在匹配项,则回送数据包
					{
						ip_loopback(dev,skb);
						break;
					}
					imc=imc->next;
				}
			}
		}
		/* Multicasts with ttl 0 must not go beyond the host */
		//检查ip首部ttl字段,如果为0,则不可进行数据包发送(转发)
		if(skb->ip_hdr->ttl==0)
		{
			kfree_skb(skb, FREE_READ);
			return;
		}
	}
#endif
	//对广播数据包的判断
	if((dev->flags&IFF_BROADCAST) && iph->daddr==dev->pa_brdaddr && !(dev->flags&IFF_LOOPBACK))
		ip_loopback(dev,skb);

	//对发送设备当前状态的检查,如果处于非工作状态,则无法发送数据包,此时进入else执行
	if (dev->flags & IFF_UP)
	{
		/*
		 *	If we have an owner use its priority setting,
		 *	otherwise use NORMAL
		 */
        //调用下层接口函数dev_queue_xmit,将数据包交由链路层处理
		if (sk != NULL)
		{
			dev_queue_xmit(skb, dev, sk->priority);
		}
		else
		{
			dev_queue_xmit(skb, dev, SOPRI_NORMAL);
		}
	}
	else
	{
		ip_statistics.IpOutDiscards++;
		if (free)
			kfree_skb(skb, FREE_WRITE);//丢弃数据包,对tcp可靠传输而言,将造成数据包超时重传
	}
}
上面函数功能可以总结为:

1. 相关合法性检查;

2. 防火墙过滤;

3. 对数据包是否需要分片发送进行检查;

4. 进行可能的数据包缓存处理;

5. 对多播和广播数据报是否需要回送本机进行检查;

6. 调用下层接口函数 dev_queue_xmit 将数据包送往链路层进行处理。

上面函数内部涉及到一个函数,把数据包分片,当数据包大小大于最大传输单元时,需要将数据包分片传送,这里则是通过函数 ip_fragment 实现的。

ip_fragment函数:将大数据包(大于最大传输单元(最大传输单元指的是ip报,不包含mac头部))分片发送。这个函数条理很清楚

/*
 *	This IP datagram is too large to be sent in one piece.  Break it up into
 *	smaller pieces (each of size equal to the MAC header plus IP header plus
 *	a block of the data of the original IP data part) that will yet fit in a
 *	single device frame, and queue such a frame for sending by calling the
 *	ip_queue_xmit().  Note that this is recursion, and bad things will happen
 *	if this function causes a loop...
 *
 *	Yes this is inefficient, feel free to submit a quicker one.
 *
 *	**Protocol Violation**
 *	We copy all the options to each fragment. !FIXME!
 */
void ip_fragment(struct sock *sk, struct sk_buff *skb, struct device *dev, int is_frag)
{
	struct iphdr *iph;
	unsigned char *raw;
	unsigned char *ptr;
	struct sk_buff *skb2;
	int left, mtu, hlen, len;
	int offset;
	unsigned long flags;

	/*
	 *	Point into the IP datagram header.
	 */

	raw = skb->data;//得到数据部分,如果你还没清楚skb与data表示什么的话,请自行面壁
	iph = (struct iphdr *) (raw + dev->hard_header_len);//偏移mac首部就到了ip首部位置

	skb->ip_hdr = iph;//指定ip首部

	/*
	 *	Setup starting values.
	 */
    //ip数据报由ip首部和数据负载部分组成,其中数据负载部分又有tcp首部和tcp有效负载组成
	hlen = (iph->ihl * sizeof(unsigned long));//ip首部长度
	left = ntohs(iph->tot_len) - hlen;//ip数据负载长度	/* Space per frame */
	hlen += dev->hard_header_len;//加上mac首部长度,得到这两者的长度和		/* Total header size */
	mtu = (dev->mtu - hlen);//mtu初始化为ip数据负载长度		/* Size of data space */
	ptr = (raw + hlen);	//指向ip负载数据开始位置		/* Where to start from */

	/*
	 *	Check for any "DF" flag. [DF means do not fragment]
	 */
    //检查发送端是否允许进行分片
	if (ntohs(iph->frag_off) & IP_DF)//如果不允许
	{
		/*
		 *	Reply giving the MTU of the failed hop.
		 */
		 //返回一个ICMP错误
		ip_statistics.IpFragFails++;
		icmp_send(skb,ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, dev->mtu, dev);
		return;
	}

	/*
	 *	The protocol doesn't seem to say what to do in the case that the
	 *	frame + options doesn't fit the mtu. As it used to fall down dead
	 *	in this case we were fortunate it didn't happen
	 */
    //如果ip数据负载长度<8,则无法为其创建分片
    //规定,分片中数据长度必须是8的倍数
	if(mtu<8)
	{
		/* It's wrong but it's better than nothing */
		icmp_send(skb,ICMP_DEST_UNREACH,ICMP_FRAG_NEEDED,dev->mtu, dev);
		ip_statistics.IpFragFails++;
		return;
	}

	/*
	 *	Fragment the datagram.
	 */

	/*
	 *	The initial offset is 0 for a complete frame. When
	 *	fragmenting fragments it's wherever this one starts.
	 */

	if (is_frag & 2)//表示被分片数据包本身是一个分片
		offset = (ntohs(iph->frag_off) & 0x1fff) << 3;
	else
		offset = 0;


	/*
	 *	Keep copying data until we run out.
	 */
    //直到所有分片数据处理完
	while(left > 0)
	{
		len = left;
		/* IF: it doesn't fit, use 'mtu' - the data space left */
		if (len > mtu)//如果这个数据还是大于mtu,表示不是最后一个分片,还要继续分片处理
			len = mtu;
		/* IF: we are not sending upto and including the packet end
		   then align the next start on an eight byte boundary */
		if (len < left)
		{
//得到小于mtu的最大的一个为8的倍数的数值,这个运算很有意思,值得借鉴一下
//可以看出分片的原则是每个分片在条件下尽量携带最多数据,这个条件就是不能大于mtu值,且必须是8的倍数
			len/=8;
			len*=8;
		}
		/*
		 *	Allocate buffer.
		 */
//分配一个skb_buff,注意这里分配的大小,hlen是ip首部大小,得加上ip首部
//这里可以看出,每个分片数据包还得加上ip首部,典型的1+1>2,相比不分片下降低了效率,
//但是这是不可避免的,必须增加火车头,不然不知道往哪开
		if ((skb2 = alloc_skb(len + hlen,GFP_ATOMIC)) == NULL)
		{
			printk("IP: frag: no memory for new fragment!\n");
			ip_statistics.IpFragFails++;
			return;
		}

		/*
		 *	Set up data on packet
		 */

		skb2->arp = skb->arp;//表示mac首部是否创建成功,
		//参见ip_build_header函数,其内部调用了ip_send函数(eth_header)
		if(skb->free==0)
			printk("IP fragmenter: BUG free!=1 in fragmenter\n");
		skb2->free = 1;//数据包无须缓存
		skb2->len = len + hlen;//数据部分长度(分片大小+ip首部)
		skb2->h.raw=(char *) skb2->data;//让skb2对应层的raw指向分片数据包的数据部分
		/*
		 *	Charge the memory for the fragment to any owner
		 *	it might possess
		 */

		save_flags(flags);
		if (sk)
		{
			cli();
			sk->wmem_alloc += skb2->mem_len;//设置sk当前写缓冲大小为分片数据包的大小
			skb2->sk=sk;//关联
		}
		restore_flags(flags);
		skb2->raddr = skb->raddr;//数据包的下一站地址,所有分片数据包自然是原数据包是一个地址	/* For rebuild_header - must be here */

		/*
		 *	Copy the packet header into the new buffer.
		 */
//把skb数据部分拷贝到raw指向的位置,这里拷贝的是首部(mac首部+ip首部)
//实际上raw和skb2->data是指向同一个地址
		memcpy(skb2->h.raw, raw, hlen);
		//raw随着层次变化,链路层=eth,ip层=iph

		/*
		 *	Copy a block of the IP datagram.
		 */
		//这里则是拷贝ip数据负载部分
		memcpy(skb2->h.raw + hlen, ptr, len);
		left -= len;//剩下未传送的数据大小

		skb2->h.raw+=dev->hard_header_len;//raw位置定位到了ip首部
        //一定要清楚skb_buff->data到了某一层的数据布局
		/*
		 *	Fill in the new header fields.
		 */
		 //获取ip首部
		iph = (struct iphdr *)(skb2->h.raw/*+dev->hard_header_len*/);
		iph->frag_off = htons((offset >> 3));//片位移,offset在前面进行了设置
		/*
		 *	Added AC : If we are fragmenting a fragment thats not the
		 *		   last fragment then keep MF on each bit
		 */
		if (left > 0 || (is_frag & 1))//left>0,表示这不是最后一个分片,还有剩下数据包未发送
			iph->frag_off |= htons(IP_MF);

	//ip数据负载已经发送了len各大小的分片数据包,那么就要更新下一个分片数据包的位置,以便发送
		ptr += len;//ip数据负载位置更新
		offset += len;//偏移量更新,

		/*
		 *	Put this fragment into the sending queue.
		 */

		ip_statistics.IpFragCreates++;

		ip_queue_xmit(sk, dev, skb2, 2);//发送数据包
		//可以看出,发送一个数据包的过程就是,检查其大小是否小于mtu,否则需要进行分片,
		//然后对分片进行发送,分片数据包自然是小于mtu,直到原来的大于mtu的数据包全部分片发送完
	}
	ip_statistics.IpFragOKs++;
}
ip_fragment 函数条理很清晰,就是将大的拆分为小的,其拆分过程为,新建指定大小(小于MTU的是8的倍数的最大值)的分片数据包,然后将原大数据包中的数据负载截取前分片大小,再加上ip首部,每个分片数据包都要加上ip首部,这样降低效率的措施不得不采用。然后就是发送这个分片数据包,直到大数据包分片发送完成。
ip_queue_xmit 最后通过调用 dev_queue_xmit 函数将数据包发往链路层进行处理。

6、链路层——dev_queue_xmit 函数

/*
 *	Send (or queue for sending) a packet. 
 *
 *	IMPORTANT: When this is called to resend frames. The caller MUST
 *	already have locked the sk_buff. Apart from that we do the
 *	rest of the magic.
 */
 //该函数本身负责将数据包传递给驱动程序,由驱动程序最终将数据发送到物理介质上。
 /*
 skb:被发送的数据包;dev:数据包发送网络接口设备;pri:网络接口设备忙时,缓存该数据包时使用的优先级
 */
void dev_queue_xmit(struct sk_buff *skb, struct device *dev, int pri)
{
	unsigned long flags;
	int nitcount;
	struct packet_type *ptype;//用于网络层协议
	int where = 0;		/* used to say if the packet should go	*/
				/* at the front or the back of the	*/
				/* queue - front is a retransmit try	*/

	if (dev == NULL) 
	{
		printk("dev.c: dev_queue_xmit: dev = NULL\n");
		return;
	}

	//加锁
	if(pri>=0 && !skb_device_locked(skb))
		skb_device_lock(skb);	/* Shove a lock on the frame */
#ifdef CONFIG_SLAVE_BALANCING
	save_flags(flags);//保存状态
	cli();
	//检查是否使用了主从设备的连接方式
	//如果采用了这种方式,则发送数据包时,可在两个设备之间平均负载
	if(dev->slave!=NULL && dev->slave->pkt_queue < dev->pkt_queue &&
				(dev->slave->flags & IFF_UP))
		dev=dev->slave;
	restore_flags(flags);
#endif		
#ifdef CONFIG_SKB_CHECK 
	IS_SKB(skb);//检查数据包的合法性
#endif    
	skb->dev = dev;//指向数据包发送设备对应结构

	/*
	 *	This just eliminates some race conditions, but not all... 
	 */
    //检查以免造成竞争条件,事实上skb->next == NULL的
	if (skb->next != NULL) 
	{
		/*
		 *	Make sure we haven't missed an interrupt. 
		 */
		printk("dev_queue_xmit: worked around a missed interrupt\n");
		start_bh_atomic();//原子操作,宏定义
		dev->hard_start_xmit(NULL, dev);
		end_bh_atomic();
		return;
  	}

	/*
	 *	Negative priority is used to flag a frame that is being pulled from the
	 *	queue front as a retransmit attempt. It therefore goes back on the queue
	 *	start on a failure.
	 */
//优先级为负数,表示当前处理的数据包是从硬件队列中取下的,而非上层传递的新数据包
  	if (pri < 0) 
  	{
		pri = -pri-1;
		where = 1;
  	}

	if (pri >= DEV_NUMBUFFS) 
	{
		printk("bad priority in dev_queue_xmit.\n");
		pri = 1;
	}

	/*
	 *	If the address has not been resolved. Call the device header rebuilder.
	 *	This can cover all protocols and technically not just ARP either.
	 */
//arp标识是否完成链路层的硬件地址解析,如果没完成,则需要调用rebuild_header(eth_rebuild_header函数)
//完成链路层首部的创建工作
	if (!skb->arp && dev->rebuild_header(skb->data, dev, skb->raddr, skb)) {
		return;//这将启动arp地址解析过程,则数据包的发送则由arp协议模块负责,所以这里直接返回
	}

	save_flags(flags);
	cli();	
	if (!where) {//where=1,表示这是从上层接受的新数据包
#ifdef CONFIG_SLAVE_BALANCING	
		skb->in_dev_queue=1;//标识该数据包缓存在设备队列中
#endif		
		skb_queue_tail(dev->buffs + pri,skb);//插入到设备缓存队列的尾部
		skb_device_unlock(skb);		/* Buffer is on the device queue and can be freed safely */
		skb = skb_dequeue(dev->buffs + pri);//从设备缓存队列的首部读取数据包,这样取得的数据包可能不是我们之前插入的数据包
		skb_device_lock(skb);		/* New buffer needs locking down */
#ifdef CONFIG_SLAVE_BALANCING		
		skb->in_dev_queue=0;//该数据包当前不在缓存队列中
#endif		
	}
	restore_flags(flags);//恢复状态

	/* copy outgoing packets to any sniffer packet handlers */
	//内核对混杂模式的支持。不明白...
	if(!where)
	{
		for (nitcount= dev_nit, ptype = ptype_base; nitcount > 0 && ptype != NULL; ptype = ptype->next) 
		{
			/* Never send packets back to the socket
			 * they originated from - MvS (miquels@drinkel.ow.org)
			 */
			if (ptype->type == htons(ETH_P_ALL) &&
			   (ptype->dev == dev || !ptype->dev) &&
			   ((struct sock *)ptype->data != skb->sk))
			{
				struct sk_buff *skb2;
				if ((skb2 = skb_clone(skb, GFP_ATOMIC)) == NULL)//复制一份数据包
					break;
				/*
				 *	The protocol knows this has (for other paths) been taken off
				 *	and adds it back.
				 */
				skb2->len-=skb->dev->hard_header_len;//长度
				ptype->func(skb2, skb->dev, ptype);//协议处理函数
				nitcount--;
			}
		}
	}
	start_bh_atomic();
//下面调用hard_start_xmit函数,前面skb->next不为NULL时,也调用这个函数,不过参数数据包skb是NULL
//驱动层发送数据包,关联到了具体的网络设备处理函数,将进入真实的网卡驱动(物理层)
//高版本的内核协议栈,还有虚拟设备,这个版本就是直接进入真实设备
	if (dev->hard_start_xmit(skb, dev) == 0) {
		end_bh_atomic();
		/*
		 *	Packet is now solely the responsibility of the driver
		 */
		return;
	}
	end_bh_atomic();

	/*
	 *	Transmission failed, put skb back into a list. Once on the list it's safe and
	 *	no longer device locked (it can be freed safely from the device queue)
	 */
	cli();
#ifdef CONFIG_SLAVE_BALANCING
	skb->in_dev_queue=1;//如果使用主从设备,就缓存在队列中
	dev->pkt_queue++;//该设备缓存的待发送数据包个数加1
#endif		
	skb_device_unlock(skb);
	skb_queue_head(dev->buffs + pri,skb);//把数据包插入到数据包队列头中
	restore_flags(flags);
}
7、物理层

物理层则牵扯到具体的网络接口硬件设备了,实则是一个网络驱动程序。不同的网卡其驱动程序有所不同,这跟硬件的时序,延迟等有关。

【Linux 内核网络协议栈源码剖析】数据包发送

关于驱动,这里我们就不介绍了。
这部分介绍的就是数据包的发送过程,从网络层到最底层的网卡驱动。下篇将介绍数据包的接收过程。

connect 函数可真是渗透到网络栈的各个层啊,connect 函数是客户端向服务器端发出连接请求数据包,该数据包需要最终到达服务器端处理,自然要从客户端从上至下经过应用层、传输层、网络层、链路层、硬件接口到达对端(对端接收则是反过来从下往上)。所以通信双方进行数据传输的函数都要经过这些网络协议栈。

从侧面可看出,内核网络协议栈的设计体现了高内聚,低耦合的原则,各层之间只提供接口函数,协议栈某一层的改动,不需要取改动其余层,保持接口的一致性就可以了,面对日益复杂的网络栈,这种设计风格无疑很有利于维护和升级。

另外中间协议各层还有一些牵扯到的操作函数,会在后面一一介绍。