本系列文章总结Linux网路栈,包括:
(1)Linux网路合同栈总结
(2)非虚拟化Linux环境中的网路分段卸载技术GSO/TSO/UFO/LRO/GRO
(3)QEMU/KVM+VxLAN环境下的SegmentationOffloading技术(发送端)
(4)QEMU/KVM+VxLAN环境下的SegmentationOffloading技术(接收端)
1.Linux网路路径
1.1发送端1.1.1应用层
(1)Socket
应用层的各类网路应用程序基本上都是通过LinuxSocket编程插口来和内核空间的网路合同栈通讯的。LinuxSocket是从BSDSocket发展而至的,它是Linux操作系统的重要组成部份之一,它是网路应用程序的基础。从层次上来说,它坐落应用层,是操作系统为应用程序员提供的API,通过它,应用程序可以访问传输层合同。
UDPsocket处理过程(来源)TCPSocket处理过程(来源)
(2)应用层处理流程
网路应用调用SocketAPIsocket(intfamily,inttype,intprotocol)创建一个socket,该调用最终会调用Linuxsystemcallsocket(),并最终调用LinuxKernel的sock_create()技巧。该方式返回被创建好了的那种socket的filedescriptor。对于每一个userspace网路应用创建的socket,在内核中都有一个对应的structsocket和structsock。其中,structsock有三个队列(queue),分别是rx,tx和err,在sock结构被初始化的时侯,这种缓冲队列也被初始化完成;在支票收发过程中,每位queue中保存要发送或则接受的每位packet对应的Linux网路栈sk_buffer数据结构的实例skb。对于TCPsocket来说,应用调用connect()API,促使顾客端和服务器端通过该socket构建一个虚拟联接。在此过程中,TCP合同栈通过三次握手会构建TCP联接。默认地,该API会等到TCP握手完成联接完善后才返回。在构建联接的过程中的一个重要步骤是,确定双方使用的MaxiumSegemetSize(MSS)。由于UDP是面向无联接的合同,因而它是不须要该步骤的。应用调用LinuxSocket的send或则writeAPI来发出一个message给接收端sock_sendmsg被调用,它使用socketdescriptor获取sockstruct,创建messageheader和socketcontrolmessage_sock_sendmsg被调用,按照socket的合同类型,调用相应合同的发送函数。对于TCP,调用tcp_sendmsg函数。对于UDP来说,userspace应用可以调用send()/sendto()/sendmsg()三个systemcall中的任意一个来发送UDPmessage,它们最终就会调用内核中的udp_sendmsg()函数。
1.1.2传输层
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括(1)构造TCPsegment(2)估算checksum(3)发送回复(ACK)包(4)滑动窗口(slidingwindown)等保证可靠性的操作。TCP合同栈的大致处理过程如右图所示:
TCP栈简略过程:
tcp_sendmsg函数会首先检测早已构建的TCPconnection的状态,之后获取该联接的MSS,开始segement发送流程。构造TCP段的playload:它在内核空间中创建该packet的sk_buffer数据结构的实例skb,从userspacebuffer中拷贝packet的数据到skb的buffer。构造TCPheader。估算TCP校准和(checksum)和顺序号(sequencenumber)。TCP校准和是一个端到端的校准和,由发送端估算,之后由接收端验证。其目的是为了发觉TCP首部和数据在发送端到接收端之间发生的任何改动。假如接收方检查到校准和有差错,则TCP段会被直接扔掉。TCP校准和覆盖TCP首部和TCP数据。TCP的校准和是必需的发到IP层处理:调用IPhandler句柄ip_queue_xmit,将skb传入IP处理流程。
UDP栈简略过程:
UDP将message封装成UDP数据报调用ip_append_data()方式将packet送到IP层进行处理。1.1.3IP网路层-添加header和checksum,路由处理linux协议栈分析,IPfragmentation
网路层的任务就是选择合适的网间路由和交换结点,确保数据及时传送。网路层将数据链路层提供的帧组成数据包,包中封装有网路层榆林,其中富含逻辑地址信息--源站点和目的站点地址的网路地址。其主要任务包括(1)路由处理,即选择下一跳(2)添加IPheader(3)估算IPheaderchecksum,用于监测IP报文脸部在传播过程中是否出错(4)可能的话,进行IP分片(5)处理完毕,获取下一跳的MAC地址,设置链路层报文头linux协议栈分析,之后转到链路层处理。
IP头:
IP栈基本处理过程如右图所示:
首先,ip_queue_xmit(skb)会检测skb->dst路由信息。若果没有,例如套接字的第一个包,就使用ip_route_output()选择一个路由。接着,填充IP包的各个数组,例如版本、包头厚度、TOS等。中间的一些分片等,可参阅相关文档。基本思想是,当报文的厚度小于mtu,gso的厚度不为0都会调用ip_fragment进行分片,否则都会调用ip_finish_output2把数据发送出去。ip_fragment函数中,会检测IP_DF标志位,假如待分片IP数据包严禁分片,则调用icmp_send()向发送方发送一个诱因为须要分片而设置了不分片标志的目的不可达ICMP报文,并遗弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。接出来就用ip_finish_ouput2设置链路层报文头了。假如,链路层报头缓存有(即hh不为空),那就拷贝到skb里。假如没,这么就调用neigh_resolve_output,使用ARP获取。
1.1.4数据链路层
功能上,在化学层提供比特流服务的基础上,构建相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的化学介质上提供可靠的传输。该层的作用包括:数学地址轮询、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层合同的代表包括:SDLC、HDLC、PPP、STP、帧中继等。
实现上,Linux提供了一个Networkdevice的具象层,虽然现今linux/net/core/dev.c。具体的数学网路设备在设备驱动中(driver.c)须要实现其中的虚函数。NetworkDevice具象层调用具体网路设备的函数。
、
1.1.5化学层-化学层封装和发送
化学层在收到发送恳求以后,通过DMA将该寻址中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网合同的相关header,IFG、前导符和CRC。对于以太网网路,化学层发送采用CSMA/CD,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将形成中断通知CPU,之后驱动层中的中断处理程序就可以删掉保存的skb了。1.1.6简单总结
(来源)
1.2接收端1.2.1化学层和数据链路层
简略过程:
一个package抵达机器的化学网路适配器,当它接收到数据帧时,才会触发一个中断,并将通过DMA传送到坐落linuxkernel显存中的rx_ring。网卡发出中断,通知CPU有个package须要它处理。中断处理程序主要进行以下一些操作,包括分配skb_buff数据结构,并将接收到的数据帧从网路适配器I/O端口拷贝到skb_buff缓冲区中;从数据帧中提取出一些信息,并设置skb_buff相应的参数,这种参数将被下层的网路合同使用,比如skb->protocol;终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。内核2.5中引入一组新的API来处理接收的数据帧,即NAPI。所以,驱动有两种形式通知内核:(1)通过先前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用Networkdevice的netif_rx_schedule函数,步入软中断处理流程,再调用net_rx_action函数。该函数关掉中断,获取每位Networkdevice的rx_ring中的所有package,最终pacakage从rx_ring中被删掉,步入netif_receive_skb处理流程。netif_receive_skb是链路层接收数据报的最后一站。它按照注册在全局字段ptype_all和ptype_base里的网路层数据报类型,把数据报提交给不同的网路层合同的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层合同的接收函数处理该skb包,步入第三层网路层处理。1.2.2网路层
IP层的入口函数在ip_rcv函数。该函数首先会做包括packagechecksum在内的各类检测,假如须要的话会做IPdefragment(将多个分片合并),之后packet调用早已注册的Pre-routingnetfilterhook,完成后最终抵达ip_rcv_finish函数。ip_rcv_finish函数会调用ip_router_input函数,步入路由处理环节。它首先会调用ip_route_input来更新路由,之后查找route,决定该package将会被发到本机还是会被转发还是遗弃:假如是发到本机的话,调用ip_local_deliver函数,可能会做de-fragment(合并多个IPpacket),之后调用ip_local_deliver函数。该函数依据package的下一个处理层的protocalnumber,调用下一层插口,包括tcp_v4_rcv(TCP),udp_rcv(UDP),icmp_rcv(ICMP),igmp_rcv(IGMP)。对于TCP来说,函数tcp_v4_rcv函数会被调用,因而处理流程步入TCP栈。假如须要转发(forward),则步入转发流程。该流程须要处理TTL,再调用dst_input函数。该函数会(1)处理NetfilterHook(2)执行IPfragmentation(3)调用dev_queue_xmit,步入链路层处理流程。
1.2.3传输层(TCP/UDP)传输层TCP处理入口在tcp_v4_rcv函数(坐落linux/net/ipv4/tcpipv4.c文件中),它会做TCPheader检测等处理。调用_tcp_v4_lookup,查找该package的opensocket。假如找不到,该package会被遗弃。接出来检测socket和connection的状态。假如socket和connection一切正常,调用tcp_prequeue使package从内核步入userspace,放进socket的receivequeue。之后socket会被唤起,调用systemcall,并最终调用tcp_recvmsg函数去从socketrecievequeue中获取segment。1.2.4接收端-应用层每每用户应用调用read或则recvfrom时,该调用会被映射为/net/socket.c中的sys_recv系统调用,并被转化为sys_recvfrom调用,之后调用sock_recgmsg函数。对于INET类型的socket,/net/ipv4/afinet.c中的inet_recvmsg方式会被调用,它会调用相关合同的数据接收方式。对TCP来说,调用tcp_recvmsg。该函数从socketbuffer中拷贝数据到userbuffer。对UDP来说,从userspace中可以调用三个systemcallrecv()/recvfrom()/recvmsg()中的任意一个来接收UDPpackage,这种系统调用最终就会调用内核中的udp_recvmsg方式。1.2.5报文接收过程简单总结
2.Linuxsk_buffstruct数据结构和队列(Queue)2.1sk_buff
(本章节摘选自)
2.1.1sk_buff是哪些
当网路包被内核处理时,底层合同的数据被传送更高层,当数据传送时过程反过来。由不同合同形成的数据(包括头和负载)不断往下层传递直至它们最终被发送。由于这种操作的速率对于网路层的表现至关重要,内核使用一个特定的结构叫sk_buff,其定义文件在skbuffer.h。Socketbuffer被拿来在网路实现层交换数据而不用拷贝来或去数据包–这明显获得速率利润。
它的主要结构成员:
struct sk_buff { /* These two members must be first. */ # packet 可以存在于 list 或者 queue 中,这两个成员用于链表处理 struct sk_buff *next; struct sk_buff *prev; struct sk_buff_head *list; #该 packet 所在的 list struct sock *sk; #跟该 skb 相关联的 socket struct timeval stamp; # packet 发送或者接收的时间,主要用于 packet sniffers struct net_device *dev; #这三个成员跟踪该 packet 相关的 devices,比如接收它的设备等 struct net_device *input_dev; struct net_device *real_dev; union { #指向各协议层 header 结构 struct tcphdr *th; struct udphdr *uh; struct icmphdr *icmph; struct igmphdr *igmph; struct iphdr *ipiph; struct ipv6hdr *ipv6h; unsigned char *raw; } h; union { struct iphdr *iph; struct ipv6hdr *ipv6h; struct arphdr *arph; unsigned char *raw; } nh; union { unsigned char *raw; } mac; struct dst_entry *dst; #指向该 packet 的路由目的结构,告诉我们它会被如何路由到目的地 char cb[40]; # SKB control block,用于各协议层保存私有信息,比如 TCP 的顺序号和帧的重发状态 unsigned int len, #packet 的长度 data_len, mac_len, # MAC header 长度 csum; # packet 的 checksum,用于计算保存在 protocol header 中的校验和。发送时,当 checksum offloading 时,不设置;接收时,可以由device计算 unsigned char local_df, #用于 IPV4 在已经做了分片的情况下的再分片,比如 IPSEC 情况下。 cloned:1, #在 skb 被 cloned 时设置,此时,skb 各成员是自己的,但是数据是shared的 nohdr:1, #用于支持 TSO pkt_type, #packet 类型 ip_summed; # 网卡能支持的校验和计算的类型,NONE 表示不支持,HW 表示支持, __u32 priority; #用于 QoS unsigned short protocol, # 接收 packet 的协议 security;
2.1.2skb的主要操作
(1)分配skb=alloc_skb(len,GFP_KERNEL)
(2)添加payload(skb_put(skb,user_data_len))
(3)使用skb->push添加protocolheader,或则skb->pull删掉header
2.2Linux网路栈使用的驱动队列(driverqueue)
(本章节摘选自QueueingintheLinuxNetworkStackbyDanSiemon)
2.2.1队列
在IP栈和NIC驱动之间,存在一个driverqueue(驱动队列)。典型地,它被实现为FIFOringbuffer,简单地可以觉得它是固定大小的。这个队列不包含packetdata,相反,它只是保存socketkernelbuffer(skb)的表针,而skb的使用如上节所述是贯串内核网路栈处理过程的一直的。
该队列的输入时IP栈处理完毕的packets。这种packets要么是本机的应用形成的,要么是步入本机又要被路由出去的。被IP栈加入队列的packets会被网路设备驱动(hardwaredriver)取出而且通过一个数据通道(databus)发到NIC硬件设备并传输出去。
在不使用TSO/GSO的情况下,IP栈发到该队列的packets的厚度必须大于MTU。
2.2.2skb大小-默认最大大小为NICMTU
绝大多数的网卡都有一个固定的最大传输单元(maximumtransmissionunit,MTU)属性红帽子linux,它是该网路设备才能传输的最大帧(frame)的大小。对以太网来说,默认值为1500bytes,然而有些以太网路可以支持巨帧(jumboframe),最大能到9000bytes。在IP网路栈内,MTU表示能发给NIC的最大packet的大小。例如,假若一个应用向一个TCPsocket写入了2000bytes数据,这么IP栈须要创建两个IPpackets来保持每位packet的大小等于或则大于1500bytes。可见linux ftp,对于大数据传输,相对较小的MTU会造成形成大量的小网路包(smallpackets)并被传入driverqueue。这成为IP分片(IPfragmentation)。
右图表示payload为1500bytes的IP包,在MTU为1000和600时侯的分片情况:
备注:
参考链接:
Linux网路合同栈(一)——Socket入门
Linux网路合同栈(四)——链路层(1)
WhatisSKBinLinuxkernel?WhatareSKBoperations?MemoryRepresentationofSKB?Howtosendpacketoutusingskboperations?
QueueingintheLinuxNetworkStack
TransmissionControlProtocol
TCP/IP合同栈中的数据收发
linux内核学习笔记------ip报文的分片