【Linux 内核网络协议栈源码剖析】数据包发送
http://blog.csdn.net/wenqian1991/article/details/46725105
由于在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_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);
- }
物理层则牵扯到具体的网络接口硬件设备了,实则是一个网络驱动程序。不同的网卡其驱动程序有所不同,这跟硬件的时序,延迟等有关。
关于驱动,这里我们就不介绍了。
这部分介绍的就是数据包的发送过程,从网络层到最底层的网卡驱动。下篇将介绍数据包的接收过程。
connect 函数可真是渗透到网络栈的各个层啊,connect 函数是客户端向服务器端发出连接请求数据包,该数据包需要最终到达服务器端处理,自然要从客户端从上至下经过应用层、传输层、网络层、链路层、硬件接口到达对端(对端接收则是反过来从下往上)。所以通信双方进行数据传输的函数都要经过这些网络协议栈。
从侧面可看出,内核网络协议栈的设计体现了高内聚,低耦合的原则,各层之间只提供接口函数,协议栈某一层的改动,不需要取改动其余层,保持接口的一致性就可以了,面对日益复杂的网络栈,这种设计风格无疑很有利于维护和升级。
另外中间协议各层还有一些牵扯到的操作函数,会在后面一一介绍。