linux-4.4.6内核,接收UDP大包,导致网络阻塞、卡死
目录
简介
记录分片包占用内存大小的变量——struct percpu_counter;
简介
对struct percpu_counter进行 加 / 减 和 读 的接口函数
网络协议栈操作struct percpu_counter的函数 和 阈值
查看当前系统下分片包占用内存的数量
命令
/proc/net/sockstat相关代码
出问题时的现象:查看分片包占用内存的数量是0
网络协议栈接收分片包的大致流程
数理分析
解决方法
简介
linux内核网络协议栈在发送大于MTU的UDP数据包时,会将大包拆成小包发出去,对应的,在接收端,网络协议栈会将小包组合、还原成原来的大包。
本文将“分段后的 IP 封包“称为分片包。
在接收端,收到分片包后,会先分配内存空间用于存放收到的分片包。等到一个大包的所有分片包都接收完整后,执行组包操作,将其组合、还原成原来的大包,并将分片包占用的内存释放。
需要注意的是,内核协议栈会限制接收到的分片包占用内存的数量。限制的条件如下:
ipfrag_high_thresh - INTEGERMaximum memory used to reassemble IP fragments. When ipfrag_high_thresh bytes of memory is allocated for this purpose,the fragment handler will toss packets until ipfrag_low_threshis reached. This also serves as a maximum limit to namespacesdifferent from the initial one.ipfrag_low_thresh - INTEGERMaximum memory used to reassemble IP fragments before the kernelbegins to remove incomplete fragment queues to free up resources.The kernel still accepts new fragments for defragmentation.
在kernel-4.4.6下,内核协议栈使用了struct percpu_counter结构体来记录分片包占用内存的数量。网络阻塞、卡死问题就出在对struct percpu_counter的使用上。本文将分析具体机理。
请注意:新版内核已经用原子变量 替换 struct percpu_counter来记录分片包占用内存的数量。
记录分片包占用内存大小的变量——struct percpu_counter;
简介
struct netns_frags {/* The percpu_counter "mem" need to be cacheline aligned.* mem.count must not share cacheline with other writers*/struct percpu_counter mem ____cacheline_aligned_in_smp; //记录分片包占用内存大小的变量/* sysctls */int timeout;int high_thresh;int low_thresh;
};
struct percpu_counter的定义如下:
struct percpu_counter {raw_spinlock_t lock;s64 count;
#ifdef CONFIG_HOTPLUG_CPUstruct list_head list; /* All percpu_counters are on a list */
#endifs32 __percpu *counters;
};
每处理器计数器 (struct percpu_counter) 的设计思想是:计数器有一个总的计数值 count ,每个处理器有一个临时计数值 counters ,每个处理器先把计数累加到自己的临时计数值,当临时计数值达到或超过阈值 ( 当前是 130000) 的时候,把临时计数值累加到总的计数值。
成员 count 是总的计数值,成员 lock 用来保护总的计数值,成员 counters 指向每处理器变量,每个处理器对应一个临时计数值。
《Linux 内核深度解析》P486
对struct percpu_counter进行 加 / 减 和 读 的接口函数
void __percpu_counter_add(struct percpu_counter *fbc, s64 amount, s32 batch) //加 和 减 使用同一个函数
{s64 count;preempt_disable();count = __this_cpu_read(*fbc->counters) + amount;if (count >= batch || count <= -batch) {unsigned long flags;raw_spin_lock_irqsave(&fbc->lock, flags);fbc->count += count;__this_cpu_sub(*fbc->counters, count - amount);raw_spin_unlock_irqrestore(&fbc->lock, flags);} else {this_cpu_add(*fbc->counters, amount);} preempt_enable();
}
EXPORT_SYMBOL(__percpu_counter_add);static inline s64 percpu_counter_read(struct percpu_counter *fbc)
{return fbc->count;
}
网络协议栈操作struct percpu_counter的函数 和 阈值
/* Memory Tracking Functions. *//* The default percpu_counter batch size is not big enough to scale to* fragmentation mem acct sizes.* The mem size of a 64K fragment is approx:* (44 fragments * 2944 truesize) + frag_queue struct(200) = 129736 bytes*/
static unsigned int frag_percpu_counter_batch = 130000;static inline int frag_mem_limit(struct netns_frags *nf)
{return percpu_counter_read(&nf->mem);
}static inline void sub_frag_mem_limit(struct netns_frags *nf, int i)
{__percpu_counter_add(&nf->mem, -i, frag_percpu_counter_batch);
}static inline void add_frag_mem_limit(struct netns_frags *nf, int i)
{__percpu_counter_add(&nf->mem, i, frag_percpu_counter_batch);
}
查看当前系统下分片包占用内存的数量
命令
# cat /proc/net/sockstat
sockets: used 1093
TCP: inuse 11 orphan 0 tw 0 alloc 15 mem 2
UDP: inuse 12 mem 3
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0 //分片包占用内存的数量
/proc/net/sockstat相关代码
/** Report socket allocation statistics [mea@utu.fi]*/
static int sockstat_seq_show(struct seq_file *seq, void *v)
{struct net *net = seq->private;unsigned int frag_mem;int orphans, sockets;local_bh_disable();orphans = percpu_counter_sum_positive(&tcp_orphan_count);sockets = proto_sockets_allocated_sum_positive(&tcp_prot);local_bh_enable();socket_seq_show(seq);seq_printf(seq, "TCP: inuse %d orphan %d tw %d alloc %d mem %ld\n",sock_prot_inuse_get(net, &tcp_prot), orphans,atomic_read(&tcp_death_row.tw_count), sockets,proto_memory_allocated(&tcp_prot));seq_printf(seq, "UDP: inuse %d mem %ld\n",sock_prot_inuse_get(net, &udp_prot),proto_memory_allocated(&udp_prot));seq_printf(seq, "UDPLITE: inuse %d\n",sock_prot_inuse_get(net, &udplite_prot));seq_printf(seq, "RAW: inuse %d\n",sock_prot_inuse_get(net, &raw_prot));frag_mem = ip_frag_mem(net); // *******************获取分片包占用的内存数量seq_printf(seq, "FRAG: inuse %u memory %u\n", !!frag_mem, frag_mem);return 0;
}
ip_frag_mem()函数相关代码如下:
//net/ipv4/ip_fragment.c
int ip_frag_mem(struct net *net)
{return sum_frag_mem_limit(&net->ipv4.frags);
}
//include/net/inet_frag.h
static inline unsigned int sum_frag_mem_limit(struct netns_frags *nf)
{unsigned int res;local_bh_disable();res = percpu_counter_sum_positive(&nf->mem);local_bh_enable();return res;
}
出问题时的现象:查看分片包占用内存的数量是0
在接收UDP大包导致阻塞、卡死时,通过/proc/net/sockstat文件看到分片包占用的内存数量是0。
网络协议栈接收分片包的大致流程

数理分析
由于struct percpu_counter的特性,网络协议栈在接收大、小包混合的UDP数据时,会偶发阻塞、卡死。举例如下:
假设high_thresh 的值是 40960,并且当前没有接收到任何分片包数据(struct percpu_counter的count=0, counters=0)。
马上,0号核收到了 105000 字节大小的一批分片包,1号核收到了总长 100000 字节的一批分片包,即:
count= 0
counters(0号核)= 105000
counters(1号核)= 100000
紧接着,0号核接收到的 30000 字节的一批分片包,30000 + 105000 = 135000,135000大于阈值 130000,所以要把0号核的counters的值累加到 count,并且0号核的conters清零,即:
count= 135000 (count > high_thresh,停止接收新的封包的分片包)
counters(0号核)= 0
counters(1号核)= 100000
而在0号核刚刚接收到的30000字节的分片包 与 之前接收到的分片包正好可以组成一个完整的封包,组成一个完整封包的分片包大小是120000字节,在组完包后,0号核的counters变成-120000,即:
count= 135000 (count > high_thresh,停止接收新的封包的分片包)
counters(0号核)= -120000
counters(1号核)= 100000
然后,1号核接收到2000字节的一批分片包,即:
count= 135000 (count > high_thresh,停止接收新的封包的分片包)
counters(0号核)= -120000
counters(1号核)= 102000
而在1号核刚刚接收到的2000字节的分片包中,正好可以和之前的已经接收到的,剩下的所有分片包组成一个封包。现存的所有分片包大小是117000字节,在组完包后,1号核的counters要减去117000,即:
count= 135000 (count > high_thresh,停止接收新的封包的分片包)
counters(0号核)= -120000
counters(1号核)= 102000 - 117000 = -15000
此时,count已经大与high_thresh,导致停止接收新的封包的分片包,进而导致counters的值不会增加。另外,因为所有分片包都已经组包完成,已经没有分片包,也就是不会再组包,进而导致counters的值不会降低。
counters的值不会变化,进一步导致count的值也不变,最终导致UDP大包接收完全阻塞、卡死。
此时,count= 135000,counters(0号核)= -120000,counters(1号核)=-15000 ,135000 -120000 -15000 = 0,所以执行命令:cat /proc/net/sockstat | grep FRAG,看到分片包占用内存的数量也是0。
解决方法
增大高水线,修改文件/proc/sys/net/ipv4/ipfrag_high_thresh
echo 高水线的数值 > /proc/sys/net/ipv4/ipfrag_high_thresh
高水线的数值为:struct percpu_counter的阈值(当前是130000) x CPU核的数量。
请注意:
此问题只影响UDP大包接收,并且再新版本的内核中,已经修复了此问题。
参考:
《深入理解 LINUX 网络技术内幕》P424
《Linux 内核深度解析》P486
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
