momo zone

调核人的blog

Monthly Archives: 五月 2013

headphone 参数

贴几个看耳机频响和其他参数的网站,最近想搞个ATH-W1000X 或 MDR-Z1000 要参考一下

http://www.innerfidelity.com/headphone-data-sheet-downloads

http://www.headphone.com/learning-center/build-a-graph.php

http://www.goldenears.net/

 

几个常见的参数含义:

Frequency response :周波数特性(頻率響應) 正負的範圍越小越三頻均衡
Noise level, dB (A):無音状態下的雜訊分貝數,越小越好(負的數值越大越好)
Dynamic range, dB (A):動態範圍,最大音量與最小音量的分貝比數,越大越好
THD:波形的扭曲程度,請直接讀作為解析度,越小越好
THD + Noise, dB :因為波形的扭曲而產生的不該有的附帶音+雜訊,越小越好(負的數值越大越好)
IMD + Noise, % :因為波形交疊而產生的不該有的附帶音的比率+雜訊、越小越好
Stereo crosstalk, dB :左右聲道間的干涉與影響,越小越好
IMD at 10 kHz, % :因為波形交疊而產生的扭曲比率,越小越好

Advertisements

why X86_64 share lib must be PIC

之前一个老掉牙的程序要从X86移植到X86_64上面, 编译的时候出错,提示很多要加-fpic去编译。

ld: /tmp/18cd9ee.o: relocation R_X86_64_32 against `XXXX' can not be used when making a shared object; recompile with -fPIC

呃,看了一下Makefile竟然原程序的作者都没有把动态链接库编译成位置无关,难道这个老程序用的时候那个平台的pic导致性能下降太多? 折腾一下总算都跑了起来。不过为啥X86-64 必须要求share lib要用pic编译呢?

首先回顾一下PIC

PIC和非PIC的最核心不同在于前者在编译链接时做了重定位操作,而后者是在运行时做重定位操作。

先看一下32位非PIC代码的情况

比如下列代码:

extern int _g1;
static int _g2;
int func()
{
	int i = _g2++;
	return _g1+i;
}

编译成gcc汇编码:
gcc -m32 –share -S pic.c -o pic-nopic.s

	.file	"pic.c"
	.local	_g2
	.comm	_g2,4,4
	.text
	.globl	func
	.type	func, @function
func:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	movl	_g2, %eax
	movl	%eax, -4(%ebp)
	addl	$1, %eax
	movl	%eax, _g2
	movl	_g1, %edx
	movl	-4(%ebp), %eax
	addl	%edx, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc

编译成so:
gcc -m32 –share pic.c -o pic-nopic.so
然后反汇编:

0000055c :
 55c:   55                      push   %ebp
 55d:   89 e5                   mov    %esp,%ebp
 55f:   83 ec 10                sub    $0x10,%esp
 562:   a1 10 20 00 00          mov    0x2010,%eax
 567:   89 45 fc                mov    %eax,-0x4(%ebp)
 56a:   83 c0 01                add    $0x1,%eax
 56d:   a3 10 20 00 00          mov    %eax,0x2010
 572:   8b 15 00 00 00 00       mov    0x0,%edx
 578:   8b 45 fc                mov    -0x4(%ebp),%eax
 57b:   01 d0                   add    %edx,%eax
 57d:   c9                      leave  
 57e:   c3                      ret    
 57f:   90                      nop

可以发现_g2的地址是0x2010, 而_g1的地址是0x0,这样的结果是因为_g1是一个外部的全局变量,在编译时根本无法确定其确切地址,所以这里就填0(估计填其他也行)。在运行加载时,ld会对_g1的地址进行修正,届时就不是0了。同样对于_g2也会修正,因为代码加载到内存后的地址肯定不是从0开始的,实际_g2的地址应该是某个偏移+0x2010。加载时ld是怎么知道_g1需要地址修正,而不是像_g2那样加个偏移就ok呢?答案在readelf -d :

readelf --dyn-syms pic-nopic.so
Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND 
1: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (2)
3: 00000000 0 NOTYPE GLOBAL DEFAULT UND _g1
4: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
6: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
7: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS _edata
8: 00002014 0 NOTYPE GLOBAL DEFAULT ABS _end
9: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
10: 000003dc 0 FUNC GLOBAL DEFAULT 10 _init
11: 00000580 0 FUNC GLOBAL DEFAULT 13 _fini
12: 0000055c 35 FUNC GLOBAL DEFAULT 12 func

32位PIC代码的情况

	.file	"pic.c"
	.local	_g2
	.comm	_g2,4,4
	.text
	.globl	func
	.type	func, @function
func:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	call	__x86.get_pc_thunk.cx
	addl	$_GLOBAL_OFFSET_TABLE_, %ecx
	movl	_g2@GOTOFF(%ecx), %eax
	movl	%eax, -4(%ebp)
	addl	$1, %eax
	movl	%eax, _g2@GOTOFF(%ecx)
	movl	_g1@GOT(%ecx), %eax
	movl	(%eax), %edx
	movl	-4(%ebp), %eax
	addl	%edx, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	func, .-func
	.section	.text.__x86.get_pc_thunk.cx,"axG",@progbits,__x86.get_pc_thunk.cx,comdat
	.globl	__x86.get_pc_thunk.cx
	.hidden	__x86.get_pc_thunk.cx
	.type	__x86.get_pc_thunk.cx, @function
__x86.get_pc_thunk.cx:
.LFB1:
	.cfi_startproc
	movl	(%esp), %ecx
	ret
	.cfi_endproc

看到不同了吗,首先多了一个__x86.get_pc_thunk.cx函数,在调整好esp后它首先被调用,addl $_GLOBAL_OFFSET_TABLE_, %ecx 这条指令的地址入栈。该函数实际是把addl $_GLOBAL_OFFSET_TABLE_, %ecx这条指令的地址传到ecx寄存器中,该寄存器从此被挪作他用。该函数返回后ecx又加上了一个$_GLOBAL_OFFSET_TABLE的变量值。该值指的是addl这个指令到GOT表的偏移。由ld填写,所以这里还只是个标号。加上了以后ecx实际指向了一个叫GOT的动态符号偏移表。movl _g2@GOTOFF(%ecx), %eax 意思是在这个偏移表内根据_g2在表内的偏移最终算出_g2变量的绝对地址。一语道破天机其实这种PIC机制就是基于位置无关代码的位置与数据位置偏移是一个定值。这样在编译时只要数据操作数用偏移去表示,运行时用代码位置+GOT+符号偏移即可间接找到数据位置,而无须进行数据位置的重定位。

64位非PIC代码的情况

gcc –share -S pic.c -o pic-nopic.s

	.file	"pic.c"
	.local	_g2
	.comm	_g2,4,4
	.text
	.globl	func
	.type	func, @function
func:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	_g2(%rip), %eax
	movl	%eax, -4(%rbp)
	addl	$1, %eax
	movl	%eax, _g2(%rip)
	movl	_g1(%rip), %eax
	addl	-4(%rbp), %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

对比32位的版本发现好像即使不用-fpic编译实际上_g2和_g1的地址都是相对rip的偏移。这也算是位置无关代码吗?

64位PIC代码的情况

gcc –share -fpic -S pic.c -o pic-pic.s

	.file	"pic.c"
	.local	_g2
	.comm	_g2,4,4
	.text
	.globl	func
	.type	func, @function
func:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	_g2(%rip), %eax
	movl	%eax, -4(%rbp)
	addl	$1, %eax
	movl	%eax, _g2(%rip)
	movq	_g1@GOTPCREL(%rip), %rax
	movl	(%rax), %eax
	addl	-4(%rbp), %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

发现_g2的情况没有变化,但_g1改用GOT查表来寻址了。_g1和_g2不同的是,前者是一个外部全局变量,而后者是一个全局静态变量,也就是说这个文件编译成so后,_g2位于.bss段它和调用他的代码相对偏移是已知的。所以gcc这里干脆就用位置无关的方式来寻址得了。但_g1就不行了,道理前文讲过了。编译成so再反汇编看看:

00000000000005dc :
 5dc:   55                      push   %rbp
 5dd:   48 89 e5                mov    %rsp,%rbp
 5e0:   8b 05 3a 0a 20 00       mov    0x200a3a(%rip),%eax        # 201020 <_g2>
 5e6:   89 45 fc                mov    %eax,-0x4(%rbp)
 5e9:   83 c0 01                add    $0x1,%eax
 5ec:   89 05 2e 0a 20 00       mov    %eax,0x200a2e(%rip)        # 201020 <_g2>
 5f2:   48 8b 05 e7 09 20 00    mov    0x2009e7(%rip),%rax        # 200fe0 <_DYNAMIC+0x1a8>
 5f9:   8b 00                   mov    (%rax),%eax
 5fb:   03 45 fc                add    -0x4(%rbp),%eax
 5fe:   5d                      pop    %rbp
 5ff:   c3                      retq

说了半天似乎还是没有搞清楚为什么64位下为什么必须pic。其实答案在于mov指令。mov在32和64位下操作数只能是32位。利用它在32位环境下使用绝对地址寻址当然没有问题。但在64位环境下就不行了,32位的mov无法表示这么大的虚拟地址空间。而只能用它在有限的32位范围内寻址到GOT表,再通过查表去得到变量的64位绝对地址。

有没有其他什么奇技淫巧,只要能用类似mov的,但支持更长操作数的指令,或者压缩一下64位下虚拟地址空间就可以搞定了。

当然有!用如下方法去编译:

gcc pic.c -mcmodel=large –share -S -o pic-nopic.s

	.file	"pic.c"
	.local	_g2
	.comm	_g2,4,4
	.text
	.globl	func
	.type	func, @function
func:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movabsq	$_g2, %rax
	movl	(%rax), %eax
	movl	%eax, -4(%rbp)
	leal	1(%rax), %edx
	movabsq	$_g2, %rax
	movl	%edx, (%rax)
	movabsq	$_g1, %rax
	movl	(%rax), %eax
	addl	-4(%rbp), %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

movabsq 允许操作一个64位的立即数

000000000000060c :
 60c:   55                      push   %rbp
 60d:   48 89 e5                mov    %rsp,%rbp
 610:   48 b8 20 10 20 00 00    movabs $0x201020,%rax
 617:   00 00 00 
 61a:   8b 00                   mov    (%rax),%eax
 61c:   89 45 fc                mov    %eax,-0x4(%rbp)
 61f:   8d 50 01                lea    0x1(%rax),%edx
 622:   48 b8 20 10 20 00 00    movabs $0x201020,%rax
 629:   00 00 00 
 62c:   89 10                   mov    %edx,(%rax)
 62e:   48 b8 00 00 00 00 00    movabs $0x0,%rax
 635:   00 00 00 
 638:   8b 00                   mov    (%rax),%eax
 63a:   03 45 fc                add    -0x4(%rbp),%eax
 63d:   5d                      pop    %rbp
 63e:   c3                      retq   
 63f:   90                      nop

实际编译出来的代码觉得movabs 40位的立即数就足够了,不需要64位。

e1000e网卡驱动中的copybreak 与 skb

E1000 和E1000E的网卡代码里有个很奇怪的特性,就是copybreak参数,长度小于该参数的skb将另外做一份copy,不知道目的为何:
e1000e_copybreak

 

google一下,发现有人已经解答了:

 

List:       e1000-devel
Subject:    Re: [E1000-devel] What is the copybreak doing in e1000 driver?
From:       "Kok, Auke" <auke-jan.h.kok () intel ! com>
Date:       2007-03-26 15:37:32
Message-ID: 4607E8BC.1090908 () intel ! com

bekars wrote:
> There is a piece code in e1000 driver about copybreak improve small packet
> performance.
> 
>   /* code added for copybreak, this should improve
>    * performance for small packets with large amounts
>    * of reassembly being done in the stack */
>   if (length < copybreak) {
>    struct sk_buff *new_skb =
>        netdev_alloc_skb(netdev, length + NET_IP_ALIGN);
>    if (new_skb) {
>     skb_reserve(new_skb, NET_IP_ALIGN);
>     memcpy(new_skb->data - NET_IP_ALIGN,
>            skb->data - NET_IP_ALIGN,
>            length + NET_IP_ALIGN);
>     /* save the skb in buffer_info as good */
>     buffer_info->skb = skb;
>     skb = new_skb;
>    }
>    /* else just continue with the old one */
>   }
> 
> I test the 64 byte packet throughput. Firstly, using the copybreak, the
> result is 35%. Secondly, remove the code by echo 20 >
> /sys/module/e1000/parameters/copybreak, the result is 38%.
> 
> Questions:
> 1) Why removing the code make higher result?

the copybreak code does a memcpy which can be expensive and take a cache hit, 
while the stack can do much better in the case of forwarding (I think). In your 
case, turning copybreak off is certainly a good choice, as your numbers prove.

> 2) Why add the copybreak code to the receive skb process? Do it just what it
> should to do?

most of our adapters see a benefit from it. Please realize that ""general"" 
network traffic is not limited to 64-byte packets and usually 200-300 bytes is 
much more common, not to mention that we also want to make sure that we don't 
hurt large packet performance too much. You might be able to demonstrate that 
copybreak helps with 200-byte packets for example.

Auke

intel网卡状态统计疑问

最近在搞intel 82580的网卡驱动,发现一个之前未决的问题:

对于igb驱动,ifconfig中看到的rx dropped统计其实和ethtool -S 中的一些统计值无关,而与内核协议栈处理统计有关。也就是说如果网卡驱动丢包了,ifconfig的drop是看不到的。那么rx_no_buffer_count和rx_missed_errors到底是什么?

看下源代码:

drivers/net/igb/igb_ethtool.c

#define IGB_STAT(_name, _stat) { \
	.stat_string = _name, \
	.sizeof_stat = FIELD_SIZEOF(struct igb_adapter, _stat), \
	.stat_offset = offsetof(struct igb_adapter, _stat) \
}
static const struct igb_stats igb_gstrings_stats[] = {
	.......
	IGB_STAT("rx_no_buffer_count", stats.rnbc),
	IGB_STAT("rx_missed_errors", stats.mpc),
        .......
}

其实就是igb_adapter.stats.rnbc和igb_adapter.stats.mpc这两个值

struct igb_adapter {
	......
	/* structs defined in e1000_hw.h */
	struct e1000_hw hw;
	struct e1000_hw_stats stats;
	struct e1000_phy_info phy_info;
	struct e1000_phy_stats phy_stats;
        ......
}

struct e1000_hw_stats {
	u64 crcerrs;
	u64 algnerrc;
	u64 symerrs;
	u64 rxerrc;
	u64 mpc;
	u64 scc;
	u64 ecol;
	u64 mcc;
	u64 latecol;
	u64 colc;
	u64 dc;
	u64 tncrs;
	u64 sec;
	u64 cexterr;
	u64 rlec;
	u64 xonrxc;
	u64 xontxc;
	u64 xoffrxc;
	u64 xofftxc;
	u64 fcruc;
	u64 prc64;
	u64 prc127;
	u64 prc255;
	u64 prc511;
	u64 prc1023;
	u64 prc1522;
	u64 gprc;
	u64 bprc;
	u64 mprc;
	u64 gptc;
	u64 gorc;
	u64 gotc;
	u64 rnbc;
	u64 ruc;
	u64 rfc;
	u64 roc;
	u64 rjc;
	u64 mgprc;
	u64 mgpdc;
	u64 mgptc;
	u64 tor;
	u64 tot;
	u64 tpr;
	u64 tpt;
	u64 ptc64;
	u64 ptc127;
	u64 ptc255;
	u64 ptc511;
	u64 ptc1023;
	u64 ptc1522;
	u64 mptc;
	u64 bptc;
	u64 tsctc;
	u64 tsctfc;
	u64 iac;
	u64 icrxptc;
	u64 icrxatc;
	u64 ictxptc;
	u64 ictxatc;
	u64 ictxqec;
	u64 ictxqmtc;
	u64 icrxdmtc;
	u64 icrxoc;
	u64 cbtmpc;
	u64 htdpmc;
	u64 cbrdpc;
	u64 cbrmpc;
	u64 rpthc;
	u64 hgptc;
	u64 htcbdpc;
	u64 hgorc;
	u64 hgotc;
	u64 lenerrs;
	u64 scvpc;
	u64 hrmpc;
	u64 doosync;
	u64 o2bgptc;
	u64 o2bspc;
	u64 b2ospc;
	u64 b2ogprc;
};

void igb_update_stats(struct igb_adapter *adapter,
		      struct rtnl_link_stats64 *net_stats)
{
	struct e1000_hw *hw = &adapter->hw;
	struct pci_dev *pdev = adapter->pdev;
	u32 reg, mpc;
	u16 phy_tmp;
	int i;
	u64 bytes, packets;
	unsigned int start;
	u64 _bytes, _packets;

#define PHY_IDLE_ERROR_COUNT_MASK 0x00FF

	/*
	 * Prevent stats update while adapter is being reset, or if the pci
	 * connection is down.
	 */
	if (adapter->link_speed == 0)
		return;
	if (pci_channel_offline(pdev))
		return;

	bytes = 0;
	packets = 0;
	for (i = 0; i < adapter->num_rx_queues; i++) {
		u32 rqdpc_tmp = rd32(E1000_RQDPC(i)) & 0x0FFF;
		struct igb_ring *ring = adapter->rx_ring[i];

		ring->rx_stats.drops += rqdpc_tmp;
		net_stats->rx_fifo_errors += rqdpc_tmp;

		do {
			start = u64_stats_fetch_begin_bh(&ring->rx_syncp);
			_bytes = ring->rx_stats.bytes;
			_packets = ring->rx_stats.packets;
		} while (u64_stats_fetch_retry_bh(&ring->rx_syncp, start));
		bytes += _bytes;
		packets += _packets;
	}

	net_stats->rx_bytes = bytes;
	net_stats->rx_packets = packets;

	bytes = 0;
	packets = 0;
	for (i = 0; i < adapter->num_tx_queues; i++) {
		struct igb_ring *ring = adapter->tx_ring[i];
		do {
			start = u64_stats_fetch_begin_bh(&ring->tx_syncp);
			_bytes = ring->tx_stats.bytes;
			_packets = ring->tx_stats.packets;
		} while (u64_stats_fetch_retry_bh(&ring->tx_syncp, start));
		bytes += _bytes;
		packets += _packets;
	}
	net_stats->tx_bytes = bytes;
	net_stats->tx_packets = packets;

	/* read stats registers */
	adapter->stats.crcerrs += rd32(E1000_CRCERRS);
	adapter->stats.gprc += rd32(E1000_GPRC);
	adapter->stats.gorc += rd32(E1000_GORCL);
	rd32(E1000_GORCH); /* clear GORCL */
	adapter->stats.bprc += rd32(E1000_BPRC);
	adapter->stats.mprc += rd32(E1000_MPRC);
	adapter->stats.roc += rd32(E1000_ROC);

	adapter->stats.prc64 += rd32(E1000_PRC64);
	adapter->stats.prc127 += rd32(E1000_PRC127);
	adapter->stats.prc255 += rd32(E1000_PRC255);
	adapter->stats.prc511 += rd32(E1000_PRC511);
	adapter->stats.prc1023 += rd32(E1000_PRC1023);
	adapter->stats.prc1522 += rd32(E1000_PRC1522);
	adapter->stats.symerrs += rd32(E1000_SYMERRS);
	adapter->stats.sec += rd32(E1000_SEC);

	mpc = rd32(E1000_MPC);
	adapter->stats.mpc += mpc;
	net_stats->rx_fifo_errors += mpc;
	adapter->stats.scc += rd32(E1000_SCC);
	adapter->stats.ecol += rd32(E1000_ECOL);
	adapter->stats.mcc += rd32(E1000_MCC);
	adapter->stats.latecol += rd32(E1000_LATECOL);
	adapter->stats.dc += rd32(E1000_DC);
	adapter->stats.rlec += rd32(E1000_RLEC);
	adapter->stats.xonrxc += rd32(E1000_XONRXC);
	adapter->stats.xontxc += rd32(E1000_XONTXC);
	adapter->stats.xoffrxc += rd32(E1000_XOFFRXC);
	adapter->stats.xofftxc += rd32(E1000_XOFFTXC);
	adapter->stats.fcruc += rd32(E1000_FCRUC);
	adapter->stats.gptc += rd32(E1000_GPTC);
	adapter->stats.gotc += rd32(E1000_GOTCL);
	rd32(E1000_GOTCH); /* clear GOTCL */
	adapter->stats.rnbc += rd32(E1000_RNBC);
	adapter->stats.ruc += rd32(E1000_RUC);
	adapter->stats.rfc += rd32(E1000_RFC);
	adapter->stats.rjc += rd32(E1000_RJC);
	adapter->stats.tor += rd32(E1000_TORH);
	adapter->stats.tot += rd32(E1000_TOTH);
	adapter->stats.tpr += rd32(E1000_TPR);

	adapter->stats.ptc64 += rd32(E1000_PTC64);
	adapter->stats.ptc127 += rd32(E1000_PTC127);
	adapter->stats.ptc255 += rd32(E1000_PTC255);
	adapter->stats.ptc511 += rd32(E1000_PTC511);
	adapter->stats.ptc1023 += rd32(E1000_PTC1023);
	adapter->stats.ptc1522 += rd32(E1000_PTC1522);

	adapter->stats.mptc += rd32(E1000_MPTC);
	adapter->stats.bptc += rd32(E1000_BPTC);

	adapter->stats.tpt += rd32(E1000_TPT);
	adapter->stats.colc += rd32(E1000_COLC);

	adapter->stats.algnerrc += rd32(E1000_ALGNERRC);
	/* read internal phy specific stats */
	reg = rd32(E1000_CTRL_EXT);
	if (!(reg & E1000_CTRL_EXT_LINK_MODE_MASK)) {
		adapter->stats.rxerrc += rd32(E1000_RXERRC);
		adapter->stats.tncrs += rd32(E1000_TNCRS);
	}

	adapter->stats.tsctc += rd32(E1000_TSCTC);
	adapter->stats.tsctfc += rd32(E1000_TSCTFC);

	adapter->stats.iac += rd32(E1000_IAC);
	adapter->stats.icrxoc += rd32(E1000_ICRXOC);
	adapter->stats.icrxptc += rd32(E1000_ICRXPTC);
	adapter->stats.icrxatc += rd32(E1000_ICRXATC);
	adapter->stats.ictxptc += rd32(E1000_ICTXPTC);
	adapter->stats.ictxatc += rd32(E1000_ICTXATC);
	adapter->stats.ictxqec += rd32(E1000_ICTXQEC);
	adapter->stats.ictxqmtc += rd32(E1000_ICTXQMTC);
	adapter->stats.icrxdmtc += rd32(E1000_ICRXDMTC);

	/* Fill out the OS statistics structure */
	net_stats->multicast = adapter->stats.mprc;
	net_stats->collisions = adapter->stats.colc;

	/* Rx Errors */

	/* RLEC on some newer hardware can be incorrect so build
	 * our own version based on RUC and ROC */
	net_stats->rx_errors = adapter->stats.rxerrc +
		adapter->stats.crcerrs + adapter->stats.algnerrc +
		adapter->stats.ruc + adapter->stats.roc +
		adapter->stats.cexterr;
	net_stats->rx_length_errors = adapter->stats.ruc +
				      adapter->stats.roc;
	net_stats->rx_crc_errors = adapter->stats.crcerrs;
	net_stats->rx_frame_errors = adapter->stats.algnerrc;
	net_stats->rx_missed_errors = adapter->stats.mpc;

	/* Tx Errors */
	net_stats->tx_errors = adapter->stats.ecol +
			       adapter->stats.latecol;
	net_stats->tx_aborted_errors = adapter->stats.ecol;
	net_stats->tx_window_errors = adapter->stats.latecol;
	net_stats->tx_carrier_errors = adapter->stats.tncrs;

	/* Tx Dropped needs to be maintained elsewhere */

	/* Phy Stats */
	if (hw->phy.media_type == e1000_media_type_copper) {
		if ((adapter->link_speed == SPEED_1000) &&
		   (!igb_read_phy_reg(hw, PHY_1000T_STATUS, &phy_tmp))) {
			phy_tmp &= PHY_IDLE_ERROR_COUNT_MASK;
			adapter->phy_stats.idle_errors += phy_tmp;
		}
	}

	/* Management Stats */
	adapter->stats.mgptc += rd32(E1000_MGTPTC);
	adapter->stats.mgprc += rd32(E1000_MGTPRC);
	adapter->stats.mgpdc += rd32(E1000_MGTPDC);

	/* OS2BMC Stats */
	reg = rd32(E1000_MANC);
	if (reg & E1000_MANC_EN_BMC2OS) {
		adapter->stats.o2bgptc += rd32(E1000_O2BGPTC);
		adapter->stats.o2bspc += rd32(E1000_O2BSPC);
		adapter->stats.b2ospc += rd32(E1000_B2OSPC);
		adapter->stats.b2ogprc += rd32(E1000_B2OGPRC);
	}
}

可见最终rx_no_buffer_count = E1000_RNBC
rx_missed_error = E1000_MPC
两个都是寄存器里的值。

查了一下intel的82580datasheet,说明如下:
7.19.5 Missed Packets Count – MPC (0x4010; RC)
Counts the number of missed packets. Packets are missed when the receive FIFO has insufficient space
to store the incoming packet. This can be caused because of too few buffers allocated, or because there
is insufficient bandwidth on the PCI bus. Events setting this counter causes ICR.Rx Miss, the Receiver
Overrun Interrupt, to be set. This register does not increment if receives are not enabled.
These packets are also counted in the Total Packets Received register as well as in Total Octets
Received.

7.19.34 Receive No Buffers Count – RNBC (0x40A0; RC)
This register counts the number of times that frames were received when there were no available
buffers in host memory to store those frames (receive descriptor head and tail pointers were equal).
The packet is still received if there is space in the FIFO. This register only increments if receives are enabled (RCTL.RXEN is set).
This register does not increment when flow control packets are received.

看上面说的其实也不太清楚什么情况下会导致RNBC计数加1,什么情况下会导致MPC计数加1。而且这些都是硬件对寄存器的操作,所以无法进一步通过代码来摸清原因。只有继续用pktgen发包做一些测试来尝试一下。

先用ethtool -G eth1 rx 48, ethtool -G eth1 tx 48 把DMA的skb个数减少到48。然后设置发包数为40W QPS。用

watch -n1 ‘ethtool -S eth1 |grep -E “rx_no_buffer|rx_missed”‘观察到rx_no_buffer_count数持续大幅上涨。而rx_missed_error却一直为0。然后用ethtool -G eth1 rx 4096, ethtool -G eth1 tx 4096 将DMA skb个数设置到最大。发现rx_no_buffer_count上涨速度放慢很多。这样可以推测,rx_no_buffer_count和内核中设置的DMA skb个数有关。具体代码如下:

/**
 * igb_setup_rx_resources - allocate Rx resources (Descriptors)
 * @rx_ring:    rx descriptor ring (for a specific queue) to setup
 *
 * Returns 0 on success, negative on failure
 **/
int igb_setup_rx_resources(struct igb_ring *rx_ring)
{
	struct device *dev = rx_ring->dev;
	int orig_node = dev_to_node(dev);
	int size, desc_len;

	size = sizeof(struct igb_rx_buffer) * rx_ring->count;
	rx_ring->rx_buffer_info = vzalloc_node(size, rx_ring->numa_node);
	if (!rx_ring->rx_buffer_info)
		rx_ring->rx_buffer_info = vzalloc(size);
	if (!rx_ring->rx_buffer_info)
		goto err;

	desc_len = sizeof(union e1000_adv_rx_desc);

	/* Round up to nearest 4K */
	rx_ring->size = rx_ring->count * desc_len;
	rx_ring->size = ALIGN(rx_ring->size, 4096);

	set_dev_node(dev, rx_ring->numa_node);
	rx_ring->desc = dma_alloc_coherent(dev,
					   rx_ring->size,
					   &rx_ring->dma,
					   GFP_KERNEL);
	set_dev_node(dev, orig_node);
	if (!rx_ring->desc)
		rx_ring->desc = dma_alloc_coherent(dev,
						   rx_ring->size,
						   &rx_ring->dma,
						   GFP_KERNEL);

	if (!rx_ring->desc)
		goto err;

	rx_ring->next_to_clean = 0;
	rx_ring->next_to_use = 0;

	return 0;

err:
	vfree(rx_ring->rx_buffer_info);
	rx_ring->rx_buffer_info = NULL;
	dev_err(dev, "Unable to allocate memory for the receive descriptor"
		" ring\n");
	return -ENOMEM;
}

可以看到这里的rx_ring count 就是DMA 映射skb的总长度,映射方式是固定映射。然后在NAPI的poll函数中进行单个DMA skb的传输:

/**
 * igb_poll - NAPI Rx polling callback
 * @napi: napi polling structure
 * @budget: count of how many packets we should handle
 **/
static int igb_poll(struct napi_struct *napi, int budget)
{
	struct igb_q_vector *q_vector = container_of(napi,
	                                             struct igb_q_vector,
	                                             napi);
	bool clean_complete = true;

#ifdef CONFIG_IGB_DCA
	if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
		igb_update_dca(q_vector);
#endif
	if (q_vector->tx.ring)
		clean_complete = igb_clean_tx_irq(q_vector);

	if (q_vector->rx.ring)
		clean_complete &= igb_clean_rx_irq(q_vector, budget);

	/* If all work not completed, return budget and keep polling */
	if (!clean_complete)
		return budget;

	/* If not enough Rx work done, exit the polling mode */
	napi_complete(napi);
	igb_ring_irq_enable(q_vector);

	return 0;
}

static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, int budget)
{
	struct igb_ring *rx_ring = q_vector->rx.ring;
	union e1000_adv_rx_desc *rx_desc;
	const int current_node = numa_node_id();
	unsigned int total_bytes = 0, total_packets = 0;
	u16 cleaned_count = igb_desc_unused(rx_ring);
	u16 i = rx_ring->next_to_clean;

	rx_desc = IGB_RX_DESC(rx_ring, i);

	while (igb_test_staterr(rx_desc, E1000_RXD_STAT_DD)) {
		struct igb_rx_buffer *buffer_info = &rx_ring->rx_buffer_info[i];
		struct sk_buff *skb = buffer_info->skb;
		union e1000_adv_rx_desc *next_rxd;

		buffer_info->skb = NULL;
		prefetch(skb->data);

		i++;
		if (i == rx_ring->count)
			i = 0;

		next_rxd = IGB_RX_DESC(rx_ring, i);
		prefetch(next_rxd);

		/*
		 * This memory barrier is needed to keep us from reading
		 * any other fields out of the rx_desc until we know the
		 * RXD_STAT_DD bit is set
		 */
		rmb();

		if (!skb_is_nonlinear(skb)) {
			__skb_put(skb, igb_get_hlen(rx_desc));
			dma_unmap_single(rx_ring->dev, buffer_info->dma,
					 IGB_RX_HDR_LEN,
					 DMA_FROM_DEVICE);
			buffer_info->dma = 0;
		}

		if (rx_desc->wb.upper.length) {
			u16 length = le16_to_cpu(rx_desc->wb.upper.length);

			skb_fill_page_desc(skb, skb_shinfo(skb)->nr_frags,
						buffer_info->page,
						buffer_info->page_offset,
						length);

			skb->len += length;
			skb->data_len += length;
			skb->truesize += PAGE_SIZE / 2;

			if ((page_count(buffer_info->page) != 1) ||
			    (page_to_nid(buffer_info->page) != current_node))
				buffer_info->page = NULL;
			else
				get_page(buffer_info->page);

			dma_unmap_page(rx_ring->dev, buffer_info->page_dma,
				       PAGE_SIZE / 2, DMA_FROM_DEVICE);
			buffer_info->page_dma = 0;
		}

		if (!igb_test_staterr(rx_desc, E1000_RXD_STAT_EOP)) {
			struct igb_rx_buffer *next_buffer;
			next_buffer = &rx_ring->rx_buffer_info[i];
			buffer_info->skb = next_buffer->skb;
			buffer_info->dma = next_buffer->dma;
			next_buffer->skb = skb;
			next_buffer->dma = 0;
			goto next_desc;
		}

		if (unlikely((igb_test_staterr(rx_desc,
					       E1000_RXDEXT_ERR_FRAME_ERR_MASK))
			     && !(rx_ring->netdev->features & NETIF_F_RXALL))) {
			dev_kfree_skb_any(skb);
			goto next_desc;
		}

		igb_rx_hwtstamp(q_vector, rx_desc, skb);
		igb_rx_hash(rx_ring, rx_desc, skb);
		igb_rx_checksum(rx_ring, rx_desc, skb);
		igb_rx_vlan(rx_ring, rx_desc, skb);

		total_bytes += skb->len;
		total_packets++;

		skb->protocol = eth_type_trans(skb, rx_ring->netdev);

		napi_gro_receive(&q_vector->napi, skb); //此函数后面就是链路层代码的入口

		budget--;
next_desc:
		if (!budget)
			break;

		cleaned_count++;
		/* return some buffers to hardware, one at a time is too slow */
		if (cleaned_count >= IGB_RX_BUFFER_WRITE) {
			igb_alloc_rx_buffers(rx_ring, cleaned_count);
			cleaned_count = 0;
		}

		/* use prefetched values */
		rx_desc = next_rxd;
	}

	rx_ring->next_to_clean = i;
	u64_stats_update_begin(&rx_ring->rx_syncp);
	rx_ring->rx_stats.packets += total_packets;
	rx_ring->rx_stats.bytes += total_bytes;
	u64_stats_update_end(&rx_ring->rx_syncp);
	q_vector->rx.total_packets += total_packets;
	q_vector->rx.total_bytes += total_bytes;

	if (cleaned_count)
		igb_alloc_rx_buffers(rx_ring, cleaned_count);

	return !!budget;
}

由上述代码可知,rx_ring->count 越大,网卡和主存的通道也就越宽。再看ethtool -G 对应的代码,设置的默认确实是256。

/* TX/RX descriptor defines */
#define IGB_DEFAULT_TXD 256
static int igb_setup_desc_rings(struct igb_adapter *adapter)
{
	struct igb_ring *tx_ring = &adapter->test_tx_ring;
	struct igb_ring *rx_ring = &adapter->test_rx_ring;
	struct e1000_hw *hw = &adapter->hw;
	int ret_val;

	/* Setup Tx descriptor ring and Tx buffers */
	tx_ring->count = IGB_DEFAULT_TXD;
	tx_ring->dev = &adapter->pdev->dev;
	tx_ring->netdev = adapter->netdev;
	tx_ring->reg_idx = adapter->vfs_allocated_count;

	if (igb_setup_tx_resources(tx_ring)) {
		ret_val = 1;
		goto err_nomem;
	}

	igb_setup_tctl(adapter);
	igb_configure_tx_ring(adapter, tx_ring);

	/* Setup Rx descriptor ring and Rx buffers */
	rx_ring->count = IGB_DEFAULT_RXD;
	rx_ring->dev = &adapter->pdev->dev;
	rx_ring->netdev = adapter->netdev;
	rx_ring->reg_idx = adapter->vfs_allocated_count;

	if (igb_setup_rx_resources(rx_ring)) {
		ret_val = 3;
		goto err_nomem;
	}

	/* set the default queue to queue 0 of PF */
	wr32(E1000_MRQC, adapter->vfs_allocated_count << 3);

	/* enable receive ring */
	igb_setup_rctl(adapter);
	igb_configure_rx_ring(adapter, rx_ring);

	igb_alloc_rx_buffers(rx_ring, igb_desc_unused(rx_ring));

	return 0;

err_nomem:
	igb_free_desc_rings(adapter);
	return ret_val;
}

这样看来rx_no_buffer_count 指的是在网卡通过DMA将设备FIFO中的skb->data传送到rx_buffer_info时,发现对应的rx_buffer_info还没有unmap,也就无法送到主存。说到底,这其实和软中断处理的速度有关。

猜测rx_missed_errors可能与硬中断有关。也就是在DMA传送完,发送硬中断之前,网卡的FIFO缓冲已经满了,导致接收的数据要立即丢掉。这个也可以验证:

首先卸载igb模块,然后insmod igb InterruptThrottleRate=10, 只让网卡每秒产生10个中断。关闭流控ethtool -A eth1 autoneg off , ethtool -A eth1 rx off , ethtool -A eth1 tx off ,再把DMA缓冲区调至最大ethtool -G eth1 rx 4096 , ethtool -G eth1 tx 4096。可以发现原来不增长或几乎不增长的rx_missed_errors 开始大幅增长,果然这个就是和网卡硬件的FIFO缓冲区有关。

幻灯片1

附数据来源:

rx_missed_errors =E1000_MPC
rx_fifo_errors=E1000_MPC+E1000_RQDPC
rx_no_buffer_count=E1000_RNBC

drop = rx_dropped(内核丢弃) + rx_missed_errors
overrun = rx_fifo_error

kfree_skb 中的疑惑

代码如下

void kfree_skb(struct sk_buff *skb)
{
	if (unlikely(!skb))
		return;
	if (likely(atomic_read(&skb->users) == 1))
		smp_rmb();
	else if (likely(!atomic_dec_and_test(&skb->users)))
		return;
	trace_kfree_skb(skb, __builtin_return_address(0));
	__kfree_skb(skb);
}

这其实是一个包裹函数,先检查skb的引用计数,符合调节的才会真正执行__kfree_skb去释放。
那么引用计数是几才会真正释放?
首先如果user=1是会被释放的,然后后面可以解释为
if(atomic_dec_and_test()) 
__kfree_skb();

说明user=1的时候也会被释放。user为其他值的时候则只是递减然后返回。那么前面那个atomic_read还要他干嘛?我觉得第一个判断atomic_read()是为了优化,比直接用atomic_dec_and_test节省几条指令,而且少了dec的写操作可以避免cacheline更新同步操作,在网络负载重的情况下能够提升速度。

但这里还有个值得注意的地方就是atomic_read后面有个smp_rmb()。这是一条运行时内存屏障(实际在X86上同时也是编译时内存屏障,详见后面介绍)。内存屏障的作用有两个:

1. 禁止指令重排

2. 强制刷新一次缓存,也就是多核间的缓存同步

第一个作用是否是针对x86的我不太确认,第二个增加了一次cacheline同步我觉得就和之前的atomic_read优化相矛盾。总之这里有一些疑问,我试图去找最早提交该补丁的commit log,发现在这里:

http://oss.sgi.com/archives/netdev/2005-02/msg00070.html

发现是来自SGI的,可以肯定这个不是针对X86的,然后时间大概是在05年初,可以确定是在Linux-2.6.12-rc2之前,看了下代码也确实是这样。mail loop讨论的问题是arp_queue进行__skb_unlink出队操作后紧接着进性kfree_skb会发生问题,代码如下:

			if (skb_queue_len(&neigh->arp_queue) >=
			    neigh->parms->queue_len) {
				struct sk_buff *buff;
				buff = neigh->arp_queue.next;
				__skb_unlink(buff, &neigh->arp_queue);
				kfree_skb(buff);
                        }

__skb_unlink操作中有skk->list=NULL,然后__kfree_skb有这句:

	if (skb->list) {
	 	printk(KERN_WARNING "Warning: kfree_skb passed an skb still "
		       "on a list (from %p).\n", NET_CALLER(skb));
		BUG();
	}

mail 提到一种情况(没有上smp_rmb时):

        cpu 0                   cpu 1
        skb_get(skb)
        unlock(neigh)
                                lock(neigh)
                                __skb_unlink(skb)
                                kfree_skb(sb)
        kfree_skb(skb)

cpu1 进行__skb_unlink后kfree_skb中走的是atomic_dec_and_test分支(因为前面user+=1了,且有自旋锁操作),因此cpu1此时看到了所有的cpu0对skb的操作。cpu1执行完kfree_skb后该轮到cpu0的操作(或者与此同时cpu0也去操作),因为atomic_dec_and_test也会同步缓存,所以cpu0 看到的user 也是更新的值,cpu0再去kfree_skb也不会产生问题。
接下来换一个场景(没有上smp_rmb时):

       cpu 0                   cpu 1
                               __skb_unlink()
                               kfree_skb()
       kfree_skb()

和之前的一样cpu1先出队,然后进行kfree_skb走的是atomic_read分支,那么这个skb就真正被释放掉了。假如在此刻同时cpu0也执行kfree_skb,由于之前的atomic_read没有缓存同步操作,导致cpu0看到的skb数据可能都是旧的。在__kfree_skb时skb->list是非空,导致panic。换句话说,cpu1没有kfree_skb也会有问题。也就是如果在一个cpu上进行__skb_unlink,却在另一个cpu上执行kfree_skb就是很容易出问题的。

这是我对这个补丁的理解,同步缓存似乎是主要理由。

现在的内核skb结构已经大改了,__skb_kfree也大变。但这个smp_rmb还在我觉得就是用来防止多个cpu同时kfree_skb一个skb(user==1)时发生问题。

另外一个话题,我原来以为X86不会对写操作进行指令重排,现在透过这个补丁得知我原来的理解是错的。举例看一下:

int ready;
int critical;

void br(int *i)
{
	critical = 4444*(*i);
	ready = 100;
}

gcc -O3 -S mbr.c -o mbr.s
可以看到

	.file	"mbr.c"
	.text
	.p2align 4,,15
	.globl	br
	.type	br, @function
br:
.LFB18:
	.cfi_startproc
	movl	(%rdi), %eax
	movl	$100, ready(%rip)
	imull	$4444, %eax, %eax
	movl	%eax, critical(%rip)
	ret
	.cfi_endproc
.LFE18:
	.size	br, .-br
	.comm	t,8,4
	.comm	critical,4,16
	.comm	ready,4,16
	.ident	"GCC: (SUSE Linux) 4.7.1 20120723 [gcc-4_7-branch revision 189773]"
	.section	.comment.SUSE.OPTs,"MS",@progbits,1
	.string	"Ospwg"
	.section	.note.GNU-stack,"",@progbits

发现ready变量赋值被提前了。因为critial赋值来自一个指针,该操作需要load操作,微架构的指令调度单元认为ready放前面合适。如果禁止编译时指令重排:barrier()肯定可以,mb,rmb,wmb如何呢,测试了一样也可以(又改变了我之前的认识)。atomic_read呢?试了一下竟然也可以。当然这一切都是GCC做的。

Gource

发现一个很有趣的工具,Gource可以把git 和 svn的提交用图形的方式显示出来,而且效果也比较动感。使用了GPU加速,不占用cpu速度很快。

https://code.google.com/p/gource/

在opensuse 下只要安装glew-devel,glew,libGLEW1_7,glm-devel 这几个包就能编译通过。使用方法就是gource <repo-folder> 很简单

gource

 

 

BTW: The OpenGL Extension Wrangler Library (GLEW) is a cross-platform C/C++ library that helps in querying and loading OpenGL extensions. GLEW provides efficient run-time mechanisms for determining which OpenGL extensions are supported on the target platform. All OpenGL extensions are exposed in a single header file, which is machine-generated from the official extension list.

CPU缓存刷新的误解

即使是资深的技术人员,我经常听到他们谈论某些操作是如何导致一个CPU缓存的刷新。看来这是关于CPU缓存如何工作和缓存子系统如何与执行核心交互的一个常见误区。本文将致力于解释CPU缓存的功能以及执行程序指令的CPU核心如何与缓存交互。我将以最新的Intel x86 CPU为例进行说明,其他CPU也使用相似技术以达到相同目的。

绝大部分常见的现代系统都被设计成在多处理器上共享内存。共享内存的系统都有一个单独的内存资源,它会被两个或者更多的独立CPU核心同时访问。核心到主存的延迟变化范围很大,大约在10-100纳秒。在100ns内,一个3GH的CPU可以处理多达1200条指令。每一个Sandy Bridge的CPU核心,在每个CPU时钟内可以并行处理4条指令。CPU使用缓存子系统避免了处理核心直接访问主存的延迟,这样能使CPU更高效的处理指令。一些缓存很小、非常快速并且集成在每个核心之内;而另一些则慢一些、更大、在各个核心间共享。这些缓存与寄存器和主内存一起构成了非持久性的内存体系。

当你在设计一个重要算法时要记住,缓存不命中所导致的延迟,可能会使你失去执行500条指令时间!这还仅是在单插槽(single-socket)系统上,如果是多插槽(multi-socket)系统,由于内存访问需要跨槽交互,可能会导致双倍的性能损失。

内存体系

MemoryHeirarchy

图1.对于2012 Sandy Bridge核心来说,内存模型可以大致按照如下进行分解:

1.寄存器:在每个核心上,有160个用于整数和144个用于浮点的寄存器单元。访问这些寄存器只需要一个时钟周期,这构成了对执行核心来说最快的内存。编译器会将本地变量和函数参数分配到这些寄存器上。当使用超线程技术( hyperthreading )时,这些寄存器可以在超线程协同下共享。

2.内存排序缓冲(Memory Ordering Buffers (MOB) ):MOB由一个64长度的load缓冲和36长度的store缓冲组成。这些缓冲用于记录等待缓存子系统时正在执行的操作。store缓冲是一个完全的相关性队列,可以用于搜索已经存在store操作,这些store操作在等待L1缓存的时候被队列化。在数据与缓存子系统传输时, 缓冲可以让处理器异步运转。当处理器异步读或者异步写的时候,结果可以乱序返回。为了使之与已发布的内存模型( memory model )一致,MOB用于消除load和store的顺序。

3.Level 1 缓存:L1是一个本地核心内的缓存,被分成独立的32K数据缓存和32K指令缓存。访问需要3个时钟周期,并且当指令被核心流水化时, 如果数据已经在L1缓存中的话,访问时间可以忽略。

4.L2缓存:L2缓存是一个本地核心内的缓存,被设计为L1缓存与共享的L3缓存之间的缓冲。L2缓存大小为256K,主要作用是作为L1和L3之间的高效内存访问队列。L2缓存同时包含数据和指令。L2缓存的延迟为12个时钟周期。

5.L3缓存: 在同插槽的所有核心都共享L3缓存。L3缓存被分为数个2MB的段,每一个段都连接到槽上的环形网络。每一个核心也连接到这个环形网络上。地址通过hash的方式映射到段上以达到更大的吞吐量。根据缓存大小,延迟有可能高达38个时钟周期。在环上每增加一个节点将消耗一个额外的时钟周期。缓存大小根据段的数量最大可以达到20MB。L3缓存包括了在同一个槽上的所有L1和L2缓存中的数据。这种设计消耗了空间,但是使L3缓存可以拦截对L1和L2缓存的请求,减轻了各核心私有的L1和L2缓存的负担。

6.主内存:在缓存完全没命中的情况下,DRAM通道到每个槽的延迟平均为65ns。具体延迟多少取决于很多因素,比如,下一次对同一缓存 行中数据的访问将极大降低延迟,而当队列化效果和内存刷新周期冲突时将显著增加延迟。每个槽使用4个内存通道聚合起来增加吞吐量,并通过在独立内存通道上流水线化( pipelining )将隐藏这种延迟。

7. NUMA:在一个多插槽的服务器上,会使用非一致性内存访问( non-uniform memory access )。所谓的非一致性是指,需要访问的内存可能在另一个插槽上,并且通过 QPI 总线访问需要额外花费40ns。 Sandy Bridge对于以往的兼容系统来说,在2插槽系统上是一个巨大的进步。在 Sandy Bridge上,QPI总线的能力从6.4GT/s提升到8.0GT/s,并且可以使用两条线路,消除了以前系统的瓶颈。对于 Nehalem and Westmere 来说,QPI只能使用内存控制器为一个单独插槽分配的带宽中的40%,这使访问远程内存成为一个瓶颈。另外,现在QPI链接可以使用预读取请求,而前一代系统不行。

关联度(Associativity Levels)

缓存是一个依赖于hash表的高效硬件。使用hash函数常常只是将地址中低位bit 进行映射 ,以实现缓存索引。hash表需要有解决对于同一位置冲突的机制。 关联度就是hash表中槽(slot)的数量,也被称为组(ways)和集合(sets),可以用来存储一个内存地址的hash版本。关联度的多少需要在存储数据的容量,耗电量和查询时间之间寻找平衡。(校对注:关联度越高,槽的数量越多,hash冲突越小,查询速度越快)

对于Sandy Bridge,L1和L2是8路组相连 ,L3是12路组相连 。(For Sandy Bridge the L1D and L2 are 8-way associative, the L3 is 12-way associative.)

缓存一致性

由于一些缓存在内核本地,我们需要一些方法保证一致性,使所有核心的内存视图一致。对于主流系统来说,内存子系统需要考虑“真实的来源(source of truth)”。如果数据只从缓存中来,那么它永远不会过期;当数据同时在缓存和主内存中存在时,缓存中存的是主拷贝(master copy)。这种内存管理被称为写-回(write-back),在此方式下,当新的缓存行占用旧行,导致旧行被驱逐时,缓存数据只会被写回主内存中。x86架构的每个缓存块的大小为64 bytes,称为缓存行( cache-line)。其它种类的处理器的缓存行大小可能不同。更大的缓存行容量降低延迟,但是需要更大的带宽校对注:数据总线带宽)。

为了保证缓存的一致性,缓存控制器跟踪每一个缓存行的状态,这些状态的数量是有限的。Intel使用MESIF协议,AMD使用 MOESI。在MESIF协议下,缓存行处于以下5个状态中的1个。

被修改(Modified):表明缓存行已经过期,在接下来的场景中要写回主内存。当写回主内存后状态将转变为排它( Exclusive )。

独享(Exclusive)表明缓存行被当前核心单独持有,并且与主内存中一致。当被写入时,状态将转变为修改(Modified)。要进入这个状态,需要发送一个 Request-For-Ownership (RFO)消息,这包含一个读操作再加上广播通知其他拷贝失效。

共享(Shared):表明缓存行是一个与主内存一致的拷贝。

失效(Invalid):表明是一个无效的缓存行。

向前( Forward ):一个特殊的共享状态。用来表示在NUMA体系中响应其他缓存的特定缓存。

为了从一个状态转变为另一个状态,在缓存之间,需要发送一系列的消息使状态改变生效。对于上一代(或之前)的 Nehalem核心的Intel CPU和 Opteron核心的AMD CPU,插槽之间确保缓存一致性的流量需要通过内存总线共享,这极大地限制了可扩展性。如今,内存控制器的流量使用一个单独的总线来传输。例如,Intel的QPI和AMD的HyperTransport就用于插槽间的缓存一致性通讯。

缓存控制器作为L3缓存段的一个模块连接到插槽上的环行总线网络。每一个核心,L3缓存段,QPI控制器,内存控制器和集成图形子系统都连接到这个环行总线上。环由四个独立的通道构成,用于:在每个时钟内完成请求、嗅探、确认和传输32-bytes的数据(The ring is made up of 4 independent lanes for: requestsnoopacknowledge, and 32-bytesdata per cycle)。L3缓存包含所有L1和L2缓存中的缓存行,这有助于帮助核心在嗅探变化时快速确认改变的行。用于L3缓存段的缓存控制器记录了哪个核心可能改变自己的缓存行。

如果一个核心想要读取一些数据,并且这些数据在缓存中并不处于共享、独占或者被修改状态;那么它就需要在环形总线上做一个读操作。它要么从主内存中读取(缓存没命中),要么从L3缓存读取(如果没过期或者被其他核心嗅探到改变)。在任何情况下,一致性协议都能保证,读操作永远不会从缓存子系统返回一份过期拷贝。

并发编程

如果我们的缓存总是保证一致性,那么为什么我们在写并发程序时要担心可见性?这是因为核心为了得到更好的性能,对于其它线程来说,可能会出现数据修改的乱序。这么做主要有两个理由。

首先,我们的编译器在生成程序代码时,为了性能,可能让变量在寄存器中存在很长的时间,例如,变量在一个循环中重复使用。如果我们需要这些变量在核心之间可见,那么变量就不能在寄存器分配。在C语言中,可以添加“volatile”关键字达到这个目标。要记住,c/c++中volatile并不能保证让编译器不重排我们的指令。因此,需要使用内存屏障。

排序的第二个主要问题是,一个线程写了一个变量,然后很快读取,有可能从读缓冲中获得比缓存子系统中最新值要旧的值。这对于遵循单写入者原则(Single Writer Principle)的程序来说没有任何问题,但是对于 DekkerPeterson锁算法就是个很大问题。为了克服这一点,并且确保最新值可见,线程不能从本地读缓冲中读取值。可以使用屏障指令,防止下一个读操作在另一线程的写操作之前发生。在Java中对一个volatile变量进行写操作,除了永远不会在寄存器中分配之外,还会伴随一个完全的屏障指令。在x86架构上,屏障指令在读缓冲排空之前,会显著影响放置屏障的线程的运行。在其它处理器上,屏障有更有效率的实现,例如 Azul Vega在读缓冲上放置一个标志用于边界搜索。

当遵循单写入者原则时,要确保Java线程之间的内存次序,避免store屏障,那么就使用j.u.c.Atomic(Int|Long|Reference).lazySet()方法,而非放置一个volatile变量。

误区

回到作为并发算法中的一部分的“刷新缓存”误区上,我想,可以说我们永远不会在用户空间的程序上“刷新”CPU缓存。我相信这个误区的来源是由于在某些并发算法需要刷新、标记或者清空store缓冲以使下一个读操作可以看到最新值。为了达到这点,我们需要内存屏障而非刷新缓存。

这个误解的另一个可能来源是,L1缓存,或者 TLB,在上下文切换的时候可能需要根据地址索引策略进行刷新。ARM,在ARMv6之前,没有在TLB条目上使用地址空间标签,因此在上下文切换的时候需要刷新整个L1缓存。许多处理器因为类似的理由需要L1指令缓存刷新,在许多场景下,仅仅是因为指令缓存没有必要保持一致。上下文切换消耗很大,除了污染L2缓存之外,上下文切换还会导致TLB和/或者L1缓存刷新。Intel x86处理器在上下文切换时仅仅需要TLB刷新。

(校对注:TLB是Translation lookaside buffer,即页表缓冲;里面存放的是一些页表文件,又称为快表技术,由于“页表”存储在主存储器中,查询页表所付出的代价很大,由此产生了TLB。)