首先,Linux内核网络UDP协议层调用ip_send_skb并将skb传递给IP协议层。 本文通过分析内核IP协议层的关键函数共享内核数据包发送到IP协议层的处理,并共享了监测IP层的方法。
ip_send_skbip_send_skb函数在net/ipv4/ip_output.c中定义,非常短。
只要调用ip_local_out,如果调用失败,就会更新相应的错误数。
intIP_send_skb(structnet*net,struct sk_buff *skb ) { int err; ERR=IP_local_out(skb; if(err ) if ) err0 ) err=net_xmit_errno ) err; if(err ) IP_Inc_stats(net,IPSTATS_MIB_OUTDISCARDS ); } return err; }net_xmit_errno函数是错误的,可以通过将低层错误转换为IP和UDP协议层来理解。
如果发生错误,IP协议计数器OutDiscards将递增。
稍后,我们将检查可以获取此统计信息的文件。
接下来我们来看看ip_local_out。
IP _ local _ outand _ IP _ local _ outip _ local _ out和__ip_local_out都很简单。
ip_local_out只需调用_IP_local_out,如果返回值为1,则调用路由层dst_output发送包。
intip _ local _ out ( structsk _ buff * skb ) { int err; ERR=__IP_local_out(skb ); if(likely(err==1) ) err=dst_output ) skb; 返回错误; }接下来看看__ip_local_out的代码。
int _ IP _ local _ out ( structsk _ buff * skb ) ( structiphdr*iph=IP_HDR ) skb ); iph-tot_len=htons(SKB-len ); IP_send_check(IPH; returnnf_hook(nfproto_IPV4,NF_INET_LOCAL_OUT,skb,NULL,skb_dst(skb )-dev,dst_output ); }您可以看到,此函数做了两个重要的事情:首先设置IP包的长度。 调用ip_send_check计算写入IP标头的校验和。
ip_send_check函数还调用名为ip_fast_csum的函数来计算校验和。
在x86和x86_64体系结构中,此函数在程序集中实现。
然后,IP协议层调用nf_hook进入netfilter,其返回值传递给ip_local_out。
如果nf_hook返回1,则表示允许包通过,并且调用者必须自己发送包。
这是在ip_local_out检查返回值1时,自己调用dst_output发送包的情况。
netfilter and nf_hooknf_hook只是一个wrapper,它调用nf_hook_thresh,首先调用此协议族和hook类型(
如果存在非常多或非常复杂的netfilter或iptables规则,则这些规则将在触发sendmsg系统调用的用户进程的上下文中运行。
如果为此用户进程设置了CPU关联性,则相应的CPU将花费“系统时间”( system time )来处理出站iptables规则。
在进行性能回归测试时,根据系统负载的不同,应该考虑将相应的用户进程绑定到特定的CPU,或者降低netfilter/iptables规则的复杂性,以减少对性能测试的影响
出于讨论目的,假设nf_hook返回1,表示调用方(在本例中为IP协议层)应该自己发送包。
目的(路由)缓存dst代码在Linux内核中实现协议无关的目标缓存。
要继续发送UDP数据报的过程,必须了解dst条目的配置。 首先,我们来看看dst条目和路由是如何生成的。
目标缓存、路由、邻居子系统都可以单独详细介绍。
现在不要深究细节,快速看看它们是如何组合在一起的。
上面的代码调用了dst_output(skb )。
此函数只需搜索相关信息skb中的dst条目并调用output方法即可。
代码如下所示。
/* outputpackettonetworkfromtransport.*/staticinlineintdst _ output ( structsk _ buff * skb ) returnskb_dst ) skb } 首先,重要的是,目标缓存条目将以各种方式添加。
到目前为止,您在代码中看到的一种方法是从udp_sendmsg调用ip_route_output_flow。
ip_route_output_flow函数调用__ip_route_output_key,后者调用__mkroute_output。
__mkroute_output函数创建路由和目标缓存条目。
创建操作将确定哪个output方法适合此dst。
在大多数情况下,此函数是ip_output。
对于ip_outputUDPipv4,上面的output方法指向IP_output。
intIP_output(structsk_buff*skb ) structnet_device*dev=skb_dst ) skb )-dev; IP_upd_po_stats(dev_net ) dev )、IPSTATS_MIB_OUT、skb-len ); skb-dev=dev; SKB-protocol=htons(eth_p_IP ); return nf _ hook _ cond ( nf proto _ IP v4,NF_INET_POST_ROUTING,skb,NULL,dev,ip_finish_output, ( ipcb(skb )-flags IPSKB_REROUTED ); }首先,更新IPSTATS_MIB_OUT统计信息计数。
IP_UPD_PO_STATS宏更新字节数和包数统计信息。
接下来,设置发送此skb的设备和协议。
最后,调用NF_HOOK_COND将控制权传递给netfilter。
通过查看NF_HOOK_COND的函数原型,可以更清楚地说明它如何工作。
staticinlineintnf _ hook _ cond ( uint8_ TPF,unsigned int hook,struct sk_buff *skb,struct net_device *in,struct
这里的条件是! ( ipcb(skb )-flags IPSKB_REROUTED。
如果此条件为true,则skb将被发送到netfilter。
如果netfilter允许包通过,则调用okfn回调函数。
其中okfn是ip_finish_output。
IP _ finish _ outputstaticintip _ finish _ output ( structsk _ buff * skb ( # if defined ) config_netfilter ) defined=returndst_output(skb; } # endif if ( sk B- lenip _ skb _ dst _ MTU ) skb )! skb_is_GSO(skb ) ) return IP _ fragment ( skb,ip_finish_output2); else return IP _ finish _ output2( skb; }如果内核启用了netfilter和包转换( XFRM ),则更新skb标志并使用dst_output发送回。
更多Linux内核视频教程后台私信【内核】自己获取。
内核学习免费网站: Linux内核源代码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂更常见的两种情况是1、 如果没有向设备偏移片,则调用ip_fragment以在传输之前对分组进行片化2,否则直接将分组传输到IP _ finish _ output2pathmtudiscoverylinux
此功能允许内核自动确定最大路由传输单元( MTU )。
发送小于或等于此路由的MTU的包意味着可以避免IP分片。 这是推荐设定。 因为数据包分片会占用系统资源,所以避免分片看起来很简单。 只需要发送不需要瓷砖的小数据包。
通过在APP应用程序中调用setsockopt并指定SOL_IP和IP_MTU_DISCOVER选项,可以在packet级别调整路径MTU发现设置。 合适的有效值参考IP协议的最大页面。
例如,可以将IP_PMTUDISC_DO设置为指示“始终执行路径MTU检测”。
更高级的网络APP应用程序或诊断工具可能选择实现自己的RFC 4821,以便在APP应用程序启动时针对特定根执行PMTU。
在这种情况下,可以通过指示内核使用IP_PMTUDISC_PROBE选项设置" Do not Fragment "位来发送大于PMTU的数据。
APP应用程序可以通过调用带有SOL_IP和IP_MTU选项的getsockopt来查看当前PMTU。
可以使用它指示APP应用程序在发送之前创建UDP数据报的大小。
如果启用了PMTU检测,则发送大于PMTU的UDP数据时,APP应用程序将收到EMSGSIZE错误。
在这种情况下,APP应用程序只能减小包大小并重试。
强烈建议启用PTMU检测。 查看IP协议层统计信息时,将解释所有统计信息,包括与分片相关的统计信息。
其中很多都是用ip_fragment更新的。
无论是否平铺,代码最后都会移动到ip_finish_output2。
在ip_finish_output2IP片之后调用ip_finish_output2,并且直接调用ip_finish_output。
此函数在将包发送到相邻节点缓存之前处理各种统计计数器。
staticinlineintip _ finish _ output2( structsk _ buff * skb )/* variable declarations */if ( rt-rt _ type==RTN } else if ( rt-rt _ type==RTN _ broadcast ) IP_upd_po_stats(dev_net ) dev )、IPSTATS_MIB_OUTBCAST、/ratherthantooclever.*/if ( unlikely ) skb_Headroom(skb ) hh_len dev-header_ops ) ) structsk skb2=skb _ realed _ reale if(skb2==null ) kfree_skb ) skb; 返回- enom em; ( if ) skb-sk ) skb_set_owner_w ) skb2,skb-sk ); consume_skb(skb; skb=skb2; }如果与此包相关联的根为多播类型,请使用IP_UPD_PO_STATS宏增加OutMcastPkts和OutMcastOctets的计数。
对于广播路由,OutBcastPkts和OutBcastOctets的数量会增加。
然后,确保skb结构中有足够的空间容纳要添加的链路层头。
如果空间不足,请调用skb_realloc_headroom分配额外的空间,并在相关套接字上记录新skb的费用( charge )。
rcu_read_lock_bh (; nexthop=(__forceU32 ) rt_nexthop ) rt,IP_HDR ) skb(-daddr ); neigh=_ IP v4 _ neigh _ lookup _ noref ( dev,nexthop ); if (不完整(! neigh ( ) neigh=__neigh_create(ARP_TBL,nexthop,dev,false ); 然后,查询路由层以找到下一跳,并根据下一跳信息查找邻居缓存。
如果找不到,请调用__neigh_create创建邻居。
例如,第一次将数据发送到其他主机时会出现这种情况。
创建邻居缓存时具有arp_tbl参数。
其他系统(如IPv6和DECnet )维护自己的ARP表,并将不同的变量传递给__neigh_create。
创建相邻的缓存时,缓存表会增大。
邻居缓存导出一组统计信息,以便可以测量此增长。
if (! is_err(neigh ) ) intres=dst _ neigh _ output ( dst,neigh,skb ); rcu_read_unlock_bh (; 返回结果; } rcu_read_unlock_bh (; net _ dbg _ rate limited (“% s:noheadercacheandnoneighbour! n ',__func__ ); kfree_skb(skb; 返回最大值; }最后,如果成功创建邻居缓存,则继续调用dst_neigh_output并传递skb; 否则,释放skb并返回EINVAL,将其传递到上面,OutDiscards用ip_send_skb递增。
dst _ neigh _ output dst _ neigh _ output函数做了两件重要的事情。
首先,如果用户调用sendmsg并在次消息中指定MSG_CONFIRM参数,则会设置一个标志位,指示目标缓存条目仍然有效且不应该进行垃圾回收。
此检查在此函数中进行,旁边的confirm字段设置为当前jiffies计数。
staticinlineintdst _ neigh _ output ( struct dst _ entry * dst,struct neighbour *n,struct sk_buff *skb ) { conststststructststructststttttsttruct _d dst-pending_confirm=0;/* avoiddirtyingneighbour */if ( n-confirmed!=now(n-confirmed=now; }然后检查邻居的状态,并调用相应的output函数。
hh=n-hh; if(n-nud_statenud_connected ) hh-hh_len ) returnneigh_hh_output ) hh,skb ); ELSEreturnn-output(n,skb ); }邻居被视为NUD_CONNECTED。 1、NUD_PERMANENT :静态路由2、nud _ noarp :如果不需要ARP请求,例如,如果目标是多播或广播地址、或环回设备,则3、nud_noarp:ARP
“如果只要成功处理了ARP请求,目标就被标记为可以进一步访问,并且缓存了之前发送了数据并生成了缓存的‘hardware head’( hh ),则调用neigh_hh_output
否则,调用output函数。
在这两种情况下,最终都会到达dev_queue_xmit,将skb发送到Linux网络设备子系统,并在进入设备驱动程序层之前增加处理。
沿着neigh_hh_output和n-output代码向下移动,直到到达dev_queue_xmit。
当neigh_hh_output目标为NUD_CONNECTED并且缓存了硬件头部时,调用neigh_hh_output,并调用dev _ queue _ xmit skb
staticinlineintneigh _ hh _ output ( conststructhh _ cache * hh,struct sk_buff *skb ) { unsigned int seq; int hh_len; do { seq=read _ seq begin ( hh-hh _ lock; hh_len=hh-hh_len; likely ( hh _ len=hh _ data _ mod )/*thisisinlinedbygcc*/memcpy ) skB-data-hh_data_mod,hh-hh } memcpy(SKB-data-hh_alen,hh-hh_data,hh_Alen ); }while(read_seqretry ) hh-hh_lock,seq ); skb_push(skb,hh_len ); returndev_queue_xmit(skb; }这个函数的理解有点困难。 部分原因是seqlock,它用于将读/写锁定到缓存的硬盘上。
可以将上述do {} while ( )循环视为一种简单的重试机制,尝试在该循环中运行,直到成功。
在环路中对齐硬件头的长度。
这是必要的,因为某些硬件头(如IEEE 802.11 )大于HH_DATA_MOD(16字节)。
将标头数据复制到skb时,skb_push会更新指向skb中数据缓冲区的指针。
最后,调用dev_queue_xmit将skb传递给Linux网络设备子系统。
如果n-output目标不是NUD_CONNECTED或未缓存硬件标头,则代码将沿着n-output路径向下。
neigbour结构上的output指针指向哪个函数? 这要看情况了。
要知道这是如何设置的,需要了解更多的邻居缓存的工作方式。
struct neighbour包含几个重要字段。 上面看到的nud_state字段、output函数和ops结构。
请之前回忆起来。 如果在缓存中找不到现有条目,则会通过从ip_finish_output2调用__neigh_create来创建它。
调用__neigh_creaet时,会分配一个邻居,其output函数最初设置为neigh_blackhole。
当__neigh_create代码运行时,output值将根据邻居的状态进行更改,以指向正确的发送方法。
例如,如果确定代码是“已连接”的邻居,则neigh_connect将output设置为neigh-ops-connected_output。
或者,如果代码怀疑邻居可能已关闭,neigh_suspect会将output设置为neigh-ops-output。 超过/proc/sys/net/IP v4/neigh/default/delay _时等
也就是说,根据邻居的状态,neigh-output设置为neigh-ops_connected_output或neigh-ops-output。
neigh-ops是从哪里来的? 分配邻居后,通过调用ARP_constructor(net/ipv4/ARP.c )设置struct neighbor中的某些字段。
具体来说,此函数会检查具有cache方法的struct header_ops实例是否是从与其邻居相关联的设备导出的。
neigh-ops设置为net/ipv4/arp中定义的下一个实例。
staticconststructneigh _ ops ARP _ hh _ ops={.family=af _ inet, solicit=arp_solicit, error_report=ARP因此,无论neighbor是否“已连接”,或者邻居缓存代码是否怀疑连接“关闭”,neigh _ resolve _ resolve
运行到n-output时调用。
neigh_resolve_output此函数的目的是解析未连接的邻居或已连接但有未缓存硬件头的邻居。
/* slowandcareful.*/int neigh _ resolve _ output ( struct neighbour * neigh,struct sk_buff *skb ) struct dst _ entttttt if (! dst ) goto discard; if (! neigh_event_send(neigh,skb ) ) { int err; struct net_device *dev=neigh-dev; unsigned int seq; }代码首先进行基本检查,然后调用neigh_event_send。
neigh_event_send函数是__neigh_event_send的简单封装,后者使用大部分脏话。
您可以在net/core/neighbour.c上读取__neigh_event_send的源代码。 从大的方面来看,有三种情况。
NUD_NONE状态(默认状态)的邻居)假设/proc/sys/net/IP v4/neigh/default/app _ solicit和/proc/sys/net/IP v4/neigh
将邻居的状态更新为NUD_INCOMPLETENUD_STALE状态的邻居。 邻居将更新为NUD_DELAYED,并设置计时器以在以后进行检测。 (以后为当前的时间/proc/sys/net/IP v4/neigh/default/delay _ first _ probe _ time秒(包含上述的第一种情况)
如果超过该值,包将一直列出并被丢弃,直到它小于或等于proc的值。
统计信息有计数器。 因此,如果需要ARP探测器,ARP将立即发送。
__neigh_event_send返回0。 这表示邻居被视为“已连接”或“已延迟”,否则返回1。
返回值0允许eigh_resolve_output继续。
if(dev-header_ops-cache! neigh-hh.hh_len(neigh_hh_init ) neigh,dst ); 如果与邻居相关联的设备的协议实现(在本例中为以太网)支持缓存硬件头部,并且当前没有缓存,则neigh_hh_init将进行缓存。
do{__skb_pull(skb,skb_network_offset ) skb ); seq=read _ seq begin ( neigh-ha _ lock ); ERR=dev_hard_header(skb,dev,NTOHS ) skB-protocol )、neigh-ha、NULL、skb-len ); }while(read_seqretry ) neigh-ha_lock,seq ); 然后,seqlock锁控制对邻居硬件地址字段neigh-ha的访问。
当ev_hard_header为skb创建以太网标头时,将读取此字段。
然后进行错误检查:
if(err=0) RC=dev_queue_xmit ) skb; else goto out_kfree_skb; }如果以太网标头写入成功,则调用dev_queue_xmit将skb传递给Linux网络设备子系统并发送。
如果发生错误,goto会删除skb并返回错误代码。
out: return rc; discard:neigh_dbg(1,' %s: dst=%p neigh=%pn ',__func__,dst,neigh ); out_kfree_skb: rc=-EINVAL; kfree_skb(skb; goto out; } export _ symbol ( neigh _ resolve _ output; 接下来,让我们看一下用于监视和转换IP协议层的文件。
监视:名为IP层/proc/net/snmp的文件包扩展了多种协议的统计信息,在IP层的开头,各列说明了什么。
正如我们之前看到的,IP协议层有更新计数器的地方。
这些计数器的类型为c枚举类型,在include/uapi/linux/snmp.h中定义。
enum{ IPSTATS_MIB_NUM=0,/* frequentlywrittenfieldsinfastpath,keptinsamecacheline */ipstats _ MIB _ inpkts,//* in octets */ipstats _ MIB _ in delivers,/*indelivers*/ipstats__ )/* outforwdatagrams */ipstats _ MIB _ in .
out requests:incrementedeachtimeanippacketisattemptedtobesent.itappearsthatthisincrementedforeverysend, successfulornot.out discards:incrementedeachtimeanippacketisdiscarded.thiscanhappenifappendingdatatotheskb ( fails, orifthelayersbelowipreturnanerror.out no route:incrementedinseveralplaces,forexampleintheudprotocollayer ( UDP _ sendmsg ifnoroutecanbegeneratedforagivendestination.alsoincrementedwhenanapplicationcalls " connect " onaudpsocketbutnoroutecanbefoutecanbefourouted ketthatisfragmented.forexample, apacketsplitinto3fragmentswillcausethiscountertobeincrementedonce.frag creates:incrementedonceperfragmentthatiscreated.fore apacketsplitinto3fragmentswillcausethiscountertobeincrementedthrice.frag fails:incrementediffragmentationwasattttempted, butisnotpermitted ( because the“don’tfragment”bit isset ).alsoincrementedifoutputingthefragmentfails./proc/net/net
有有趣的统计:
outmcastpkts:incrementedeachtimeapacketdestinedforamulticastaddressissent.outbcastpkts:incrementedeachtimeapacketdestinests thenumberofpacketbytesoutput.outmcastoctets:thenumberofmulticastpacketbytesoutput.outbctets er of broadcast packet bytes outes 总结时,主要是ip_send_skb、ip_local_out、ip_output、ip_finish_output、ip_finish_output2、ST _ nip