Linux驱动 | 浅谈 Per-CPU 变量在统计网络收发报文的应用

为何要引入 Per-CPU 变量

(1)spin_lock 空转 CPU
在内核开发中,为了实现临界资源的互斥,我们常常直接使用 spin_lock,来实现临界区的保护,但是 spin_lock 实际上是一种忙等待锁。当锁不可用时,CPU会空转来等待资源可用。

(2)当锁的数量达到上千个时,并发容易死锁
当内核要维护的锁的数量急剧增加,由于锁争用的存在,并发情况下,spin_lock 进入死锁的概率会变高。

Per-CPU 变量的原理

Per-CPU变量,顾名思义,就是将某个变量,将其分 n 份存储在对应的 CPU 上,n 对应硬件的 CPU 核数。这样,每个 CPU 都有一份该变量的数据副本,每个 CPU 访问该变量时,访问的是自己的数据区域,从而避免了 SMP 多处理器系统同时访问临界区的问题,实现了真正的无锁技术。
这里需要注意的是,Per-CPU 处理的情况是多个 CPU 访问同一个临界区的并发问题,并没有解决单个 CPU 对该临界区域的并发。在访问此临界区时,我们需要避免内核进入抢占或者调度(包括休眠),所以访问临界区之前需要调用 preempt_disable() 函数禁用内核调度。在访问临界区之后,调用 preempt_enable() 恢复内核。
在这里插入图片描述
原图来自:http://www.wowotech.net/linux_kenrel/per-cpu.html

内核接口

(1)初始化和定义

分为两种,静态和动态

//静态定义
DEFINE_PER_CPU(变量类型, 变量名);

//动态定义
alloc_percpu(变量类型)
...
free_percpu();

(2)使用方法

//定义了 Per-CPU 变量
DEFINE_PER_CPU(int, ip_recevier);
int val;

//访问 Per-CPU变量开始,禁止抢占
val = get_cpu_var(ip_recevier);

/* 临界区 */

//访问 Per-CPU变量结束,恢复抢占
put_cpu_var(ip_recevier);

get_cpu_var 和 put_cpu_var 保证我们可以安全地访问临界区,记得在这个临界区内不能调用任何会发生进程调度的函数,比如 copy_from_user、vmalloc。

#define get_cpu_var(var)                        \                                                                                                                                                                                       
(*({   
	// 禁用抢占                                 \                                                                                                                                                                                               
    preempt_disable();                      \                                                                                                                                                                                           
    // 获取本CPU的变量副本
    this_cpu_ptr(&var);                     \                                                                                                                                                                                           
}))

#define put_cpu_var(var)                        \                                                                                                                                                                                       
do {                                    \                                                                                                                                                                                               
    (void)&(var);                           \                                                                                                                                                                                           
    // 恢复抢占
    preempt_enable();                       \                                                                                                                                                                                           
} while (0)

甚至,我们还可以遍历其他 CPU 的Per-CPU变量

// net/ipv4/af_inet.c
for_each_possible_cpu(i) {                                                                                                                                                                                                          
    struct ipstats_mib *af_inet_stats;                                                                                                                                                                                              
    af_inet_stats = per_cpu_ptr(net->mib.ip_statistics, i);                                                                                                                                                                         
    u64_stats_init(&af_inet_stats->syncp);                                                                                                                                                                                          
}

在统计网络收发报文的应用

我们通过 netstat 命令可以看到内核统计网络收发报文的详细信息:

$ netstat -s                                                                                                                                                                                                 
Ip:
    Forwarding: 2
    349128 total packets received
    2 with invalid addresses
    0 forwarded
    0 incoming packets discarded
    348808 incoming packets delivered
    246218 requests sent out
    10 outgoing packets dropped
    91 dropped because of missing route
    55 fragments dropped after timeout
    154 reassemblies required
    47 packets reassembled ok
    55 packet reassemblies failed
    30 fragments received ok
    90 fragments created
Icmp:
    814 ICMP messages received
    779 input ICMP message failed
    ICMP input histogram:
        destination unreachable: 814
    835 ICMP messages sent
    0 ICMP messages failed
    ICMP output histogram:
        destination unreachable: 833
        echo requests: 2
IcmpMsg:
        InType3: 814
        OutType3: 833
        OutType8: 2
...

实际 netstat 工具是通过解析 /proc/net/snmp 来实现读取内核这些统计信息的

$ cat /proc/net/snmp                                                                                                                                                                                         
Ip: Forwarding DefaultTTL InReceives InHdrErrors InAddrErrors ForwDatagrams InUnknownProtos InDiscards InDelivers OutRequests OutDiscards OutNoRoutes ReasmTimeout ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates
Ip: 2 64 349565 0 2 0 0 0 349245 246679 10 91 55 154 47 55 30 0 90
Icmp: InMsgs InErrors InCsumErrors InDestUnreachs InTimeExcds InParmProbs InSrcQuenchs InRedirects InEchos InEchoReps InTimestamps InTimestampReps InAddrMasks InAddrMaskReps OutMsgs OutErrors OutDestUnreachs OutTimeExcds OutParmProbs OutSrcQuenchs OutRedirects OutEchos OutEchoReps OutTimestamps OutTimestampReps OutAddrMasks OutAddrMaskReps
Icmp: 823 788 0 823 0 0 0 0 0 0 0 0 0 0 844 0 842 0 0 0 0 2 0 0 0 0 0
IcmpMsg: InType3 OutType3 OutType8
IcmpMsg: 823 842 2
Tcp: RtoAlgorithm RtoMin RtoMax MaxConn ActiveOpens PassiveOpens AttemptFails EstabResets CurrEstab InSegs OutSegs RetransSegs InErrs OutRsts InCsumErrors
Tcp: 1 200 120000 -1 2708 66 280 555 9 341336 239049 3744 17 1589 0
Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors InCsumErrors IgnoredMulti
Udp: 10405 52 0 6428 0 0 0 162
UdpLite: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors InCsumErrors IgnoredMulti
UdpLite: 0 0 0 0 0 0 0 0

可以看到 snmp 上有大量的统计信息,CPU 在收到一个数据包的时候,会将对应的数据包统计信息+1,由于这个统计信息是所有 CPU 公用,就会存在临界区问题。Linux 内核很巧妙地为这些统计信息,都设置为 Per-CPU 变量,当需要进行数据包统计时,不是通过加锁来访问临界区变量,而是每个 CPU 自己去统计相应的信息,即每个 CPU 都存储了相同变量的不同副本,而且这些副本的数据由于 CPU 的不同,肯定数值也不同。

当我们通过 cat /proc/net/snmp 查看统计信息的时候,系统为了返回给我们某个 Per-CPU 变量在整个系统中的统计信息,会统计该变量在不同 CPU 上的副本的数值,将其相加,之后才返回给我们。

无聊的代码环节:
这个函数是当我们输入 cat /proc/net/snmp 会调用到的函数

                                                                                                                                                                                                                                        
/*                                                                                                                                                                                                                                      
 *  Called from the PROCfs module. This outputs /proc/net/snmp.                                                                                                                                                                         
 */                                                                                                                                                                                                                                     
static int snmp_seq_show_ipstats(struct seq_file *seq, void *v)                                                                                                                                                                         
{                                                                                                                                                                                                                                       
    struct net *net = seq->private;                                                                                                                                                                                                     
    u64 buff64[IPSTATS_MIB_MAX];                                                                                                                                                                                                        
    int i;                                                                                                                                                                                                                              
                                                                                                                                                                                                                                        
    memset(buff64, 0, IPSTATS_MIB_MAX * sizeof(u64));                                                                                                                                                                                   
                                                                                                                                                                                                                                        
    seq_puts(seq, "Ip: Forwarding DefaultTTL");                                                                                                                                                                                         
    for (i = 0; snmp4_ipstats_list[i].name; i++)                                                                                                                                                                                        
        seq_printf(seq, " %s", snmp4_ipstats_list[i].name);                                                                                                                                                                             
                                                                                                                                                                                                                                        
    seq_printf(seq, "\nIp: %d %d",                                                                                                                                                                                                      
           IPV4_DEVCONF_ALL(net, FORWARDING) ? 1 : 2,                                                                                                                                                                                   
           net->ipv4.sysctl_ip_default_ttl);                                                                                                                                                                                            
                                                                                                                                                                                                                                        
    BUILD_BUG_ON(offsetof(struct ipstats_mib, mibs) != 0);    
    
    // 统计所有数据                                                                                                                                                                          
    snmp_get_cpu_field64_batch(buff64, snmp4_ipstats_list,                                                                                                                                                                              
                   net->mib.ip_statistics,                                                                                                                                                                                              
                   offsetof(struct ipstats_mib, syncp));      
                                                                                                                                                                                             
    for (i = 0; snmp4_ipstats_list[i].name; i++)                                                                                                                                                                                        
        seq_printf(seq, " %llu", buff64[i]);                                                                                                                                                                                            
                                                                                                                                                                                                                                        
    return 0;                                                                                                                                                                                                                           
}

遍历每个 CPU,将对应的 Per-CPU 变量进行叠加。

#define snmp_get_cpu_field64_batch(buff64, stats_list, mib_statistic, offset) \                                                                                                                                                         
{ \                                                                                                                                                                                                                                     
    int i, c; \                                                                                                                                                                                                                         
    for_each_possible_cpu(c) { \                                                                                                                                                                                                        
        for (i = 0; stats_list[i].name; i++) \                                                                                                                                                                                          
            buff64[i] += snmp_get_cpu_field64( \                                                                                                                                                                                        
                    mib_statistic, \                                                                                                                                                                                                    
                    c, stats_list[i].entry, \                                                                                                                                                                                           
                    offset); \                                                                                                                                                                                                          
    } \                                                                                                                                                                                                                                 
} 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值