导致出现很多相同端口的UDP套接字的原因有两点:
1)在reload的时候,调用shutdown()而不是close()来关闭UDP套接字。shutdown()是用来关闭连接
的
,但是文件描述符还是会存在。UDP协议并没有提供shutdown接口(参见
udp_prot),所以调用shutdown()并没有真的关闭套
接字,而且reload的次数
太多
。
2)设置了SO_REUSEADDR选项。如果没有设置这个选项,在bind的时候就会报错,漏关或者使用shutdown()关闭的错误就很容易发现。UDP套接字没有TIME_WAIT状态,而且要使用的UDP套接字数量不多,所以没有必要设置该选项。
现在我们结合内核代码来分析一下为什么相同端口的UDP套接字数量过多导致软中断占用CPU利用率高。
UDP套接字在绑定端口后才会添加到udp_table哈希表中,解决冲突使用的是“拉链法”,放置的槽位是由绑定的端口决定的(参见不考虑CONFIG_NET_NS选项
)。所以绑定到相同端口的套接字会放置到同一个槽位的链表中,这种套接字数量越多,链表就越长。
在接收到UDP协议的SKB包后,会调用__udp4_lib_lookup()函数来查找对应的UDP套接字,相关代码如下所示:
static
struct sock
*__udp4_lib_lookup(
struct net
*net, __be32 saddr,
__be16 sport, __be32 daddr, __be16 dport,
int dif,
struct udp_table
*udptable)
{
struct sock
*sk,
*result;
struct hlist_nulls_node
*node;
unsigned
short hnum
= ntohs(dport);
unsigned
int hash
= udp_hashfn(net, hnum);
struct udp_hslot
*hslot
=
&udptable
-
>hash[hash];
int score, badness;
rcu_read_lock();
begin
:
result
= NULL;
badness
=
-
1;
sk_nulls_for_each_rcu(sk, node,
&hslot
-
>head) {
score
= compute_score(sk, net, saddr, hnum, sport,
daddr, dport, dif);
if (score
> badness) {
result
= sk;
badness
= score;
}
}
......
}
首先根据SKB包的目的端口号来查找要遍历的链表,然后通过
sk_nulls_for_each_rcu宏来遍历链表上的所有套接字。我们可以看到,不管这个链表上有多少个套接字,都要从头到尾遍历一遍。如果遍历的链表越长,耗费的时间就越多,这个处理是在软中断的上下文中,因此会导致软中断占用CPU利用率高。
下面是使用perf top命令看到的结果(
可以清楚地看到__udp4_lib_lookup会成为热点,在同等压力下是不会出现的
):