在DPDK中,使用gcc的內聯匯編實現高效率的函數,比如自旋鎖,cas操作等。今天簡單介紹一下gcc內聯匯編語法和DPDK利用內聯匯編實現的函數。
gcc內聯匯編
這里簡單介紹一下內聯匯編的語法,更詳細的可以參考官方文檔。
內聯匯編格式如下,小括號中的參數使用分號分隔。AssemblerTemplate 中的匯編語句會從 InputOperands 讀取變量值,執行結束后會將結果寫到 OutputOperands 指定的變量中。
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
asm
是 GCC 里的關鍵字,或者使用 "asm",表示內聯匯編。
asm-qualifiers
asm修飾符,有三個值: volatile(指示GCC不要做優化),inline和goto(如果使用goto,小括號中必須有參數 GotoLabels)。
AssemblerTemplate
字符串,包含一條或多條匯編語句,也可以為空。GCC不會解析具體的匯編指令,因為GCC也不知道匯編語句的作用,甚至不知道匯編語法是否正確。
在匯編語句中可以引用 output,input和goto label中的變量,可以通過 %[name] 引用,也可以通過數字 %0, %1 等引用。
OutputOperands
指定零個或多個操作數,匯編語句最終會修改這些操作數。格式如下:
[ [asmSymbolicName] ] constraint (cvariablename)
asmSymbolicName: 指定 cvariablename 的一個別名,可以在匯編語句中訪問 %[name]。
如果不指定name,則可以使用基于數字的位置訪問。
比如有三個output操作數,可以使用 %0 引用第一個,使用 %1 引用第二個,使用 %2 引用第三個。
constraint: 字符串常量,指定了約束條件。輸出約束必須以=(只寫)或者+(可讀寫)開頭。
cvariablename: c的變量名,最終會修改此變量。
InputOperands
指定零個或多個變量或者表達式,匯編語句會從此讀取變量值。格式和OutputOperands一樣。
[ [asmSymbolicName] ] constraint (cexpression)
asmSymbolicName: 指定 cvariablename 的一個別名,可以在匯編語句中引用 %[name]。
constraint: 字符串常量,指定了約束條件。輸入約束不能以=(只寫)或者+(可讀寫)開頭。
cvariablename: c的變量名或者表達式。
Clobbers
指定一個列表,告訴GCC列表中的寄存器是有其他用處的,不能被GCC使用。
除了指定寄存器還有兩個特殊的Clobber: cc和memory。
cc會告訴GCC,匯編語句會修改 flags 寄存器。
memory告訴GCC,要將寄存器中的值刷新到內存,保證內存中包含正確的值,另外GCC也不要假定在執行匯編語句之前從內存讀的值和執行匯編之后的值相同,有可能會被匯編語句修改,所以執行完匯編語句 后要重新讀取。
DPDK利用內聯匯編實現的函數
下面看幾個DPDK利用內聯匯編實現的函數。
1. 讀取處理器時間戳計數
讀取處理器時間戳計數用到了一個匯編指令 rdtsc。下面介紹一下這個指令。
Reads the current value of the processor’s time-stamp counter (a 64-bit MSR) into the EDX:EAX registers. The EDX
register is loaded with the high-order 32 bits of the MSR and the EAX register is loaded with the low-order 32 bits.
(On processors that support the Intel 64 architecture, the high-order 32 bits of each of RAX and RDX are cleared.)
翻譯過來就是rdtsc 指令用來讀取處理器的時間戳計數(64位),并保存到寄存器 EDX:EAX 中,EDX 保存高32位,EAX保存低32位。如果為64位處理器,則寄存器 RAX 和 RDX 的高32位都會被清空,低32分別保存計數的高32和低32位。
DPDK中的實現代碼如下
static inline uint64_t
rte_rdtsc(void)
{
union {
uint64_t tsc_64;
RTE_STD_C11
struct {
uint32_t lo_32;
uint32_t hi_32;
};
} tsc;
asm volatile("rdtsc" :
//output
"=a" (tsc.lo_32), //a表示寄存器,GCC根據tsc.lo_32的類型決定使用32位還是64位,很顯然這里是32位的,則使用寄存器 EAX。
"=d" (tsc.hi_32)); //同上,d表示寄存器 EDX。
return tsc.tsc_64;
}
最終會將EAX代表的低32位值保存到 tsc.lo_32,EDX代表的高32位值保存到 tsc.hi_32。
2. 原子操作
原子操作(以加1為例)用到了兩個匯編指令: lock 和 inc。下面分別介紹這兩個指令。
a. lock 指令
Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the
instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal ensures that the
processor has exclusive use of any shared memory while the signal is asserted.
lock指令可以保證只有一個cpu訪問內存。
b. inc 指令
inc 用來給目的操作數加1。
This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically.
在inc指令前,必須先使用lock指令,保證原子操作。
下面看一下DPDK中如何實現原子操作。
定義了宏MPLOCKED,只在多cpu時才會使用lock指令,只有一個cpu,宏MPLOCKED為空。
#if RTE_MAX_LCORE == 1
#define MPLOCKED /**< No need to insert MP lock prefix. */
#else
#define MPLOCKED "lock ; " /**< Insert MP lock prefix. */
#endif
16位原子操作加1
typedef struct {
volatile int16_t cnt; /**< An internal counter value. */
} rte_atomic16_t;
static inline void
rte_atomic16_inc(rte_atomic16_t *v)
{
asm volatile(
MPLOCKED /* 首先使用lock指令鎖住總線 */
"incw %[cnt]" /* 使用incw指令給cnt加1,incw中的w應該是word,表示兩個字節*/
: [cnt] "=m" (v->cnt) /* output */ v->cnt即作為輸入參數,又作為輸出參數
: "m" (v->cnt) /* input */
);
}
32原子操作加1,和16位的區別是,換成了指令incl,參數v->cnt 變成了32位
typedef struct {
volatile int32_t cnt; /**< An internal counter value. */
} rte_atomic32_t;
static inline void
rte_atomic32_inc(rte_atomic32_t *v)
{
asm volatile(
MPLOCKED
"incl %[cnt]"
: [cnt] "=m" (v->cnt) /* output */
: "m" (v->cnt) /* input */
);
}
64原子操作加1,和前面的區別是,換成了指令incq(q為quadrupl,表示8個字節),參數v->cnt 變成了64位。
typedef struct {
volatile int64_t cnt; /**< Internal counter value. */
} rte_atomic64_t;
static inline void
rte_atomic64_inc(rte_atomic64_t *v)
{
asm volatile(
MPLOCKED
"incq %[cnt]"
: [cnt] "=m" (v->cnt) /* output */
: "m" (v->cnt) /* input */
);
}
3. 比較并交換操作
比較并交換操作用到了三個匯編指令: lock, cmpxchg 和 sete。下面分別介紹這三個指令。
a. lock
參考前面原子操作時的介紹。主要用來鎖住總線,保證只有一個cpu訪問內存。
b. cmpxchg 指令
Compares the value in the AL, AX, EAX, or RAX register with the first operand (destination operand). If the two
values are equal, the second operand (source operand) is loaded into the destination operand. Otherwise, the
destination operand is loaded into the AL, AX, EAX or RAX register. RAX register is available only in 64-bit mode.
This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically. To simplify the
interface to the processor’s bus, the destination operand receives a write cycle without regard to the result of the
comparison. The destination operand is written back if the comparison fails; otherwise, the source operand is
written into the destination. (The processor never produces a locked read without also producing a locked write
cmpxchg 指令將第一個操作數(目的操作數)和 A 寄存器比較,如果相等,則將第二個操作數(源操作數)賦給第一個操作數(目的操作數),并設置 ZF 為 1,否則將第一個操作數(目的操作數)賦給 A 寄存器,并設置 ZF 為0。使用此指令前也要先使用lock指令保證原子操作。
cmpxchg 實現的偽碼如下:
(* Accumulator = AL, AX, EAX, or RAX depending on whether a byte, word, doubleword, or quadword comparison is being performed *)
TEMP := DEST
IF accumulator = TEMP
THEN
ZF := 1;
DEST := SRC;
ELSE
ZF := 0;
accumulator := TEMP;
DEST := TEMP;
FI;
c. sete 指令
如果 ZF 為 1,則設置操作數為 1,否則設置為 0。
DPDK中的實現代碼如下
dst指向一塊內存,exp為此內存之前的值,現在去dst內存中最新值和exp作比較,如果相等,則將src的值賦給dst,并返回1,如果不相等,則返回0。
static inline int
rte_atomic32_cmpset(volatile uint32_t *dst, uint32_t exp, uint32_t src)
{
uint8_t res;
asm volatile(
MPLOCKED
"cmpxchgl %[src], %[dst];"
"sete %[res];"
/* output */
: [res] "=a" (res), /* 將結果0或者1寫到變量res中 */
[dst] "=m" (*dst) /* 將src賦給dst指向的內存 */
/* input */
: [src] "r" (src), /* 將src放在寄存器中,gcc會任選一個通用寄存器 */
"a" (exp), /* 將exp的值放到寄存器 a */
"m" (*dst) /* 讀取dst內存值,可以在上面的匯編語句中通過%[dst]訪問 */
: "memory"); /* no-clobber list */ memory通知gcc執行匯編語句前要刷新寄存器,從內存讀取數據
return res;
}
4. 自旋鎖的實現
自旋鎖操作用到了多個匯編指令,下面分別介紹一下。
a. xchg
Exchanges the contents of the destination (first) and source (second) operands. The operands can be two generalpurpose
registers or a register and a memory location. If a memory operand is referenced, the processor’s locking
protocol is automatically implemented for the duration of the exchange operation, regardless of the presence or
absence of the LOCK prefix or of the value of the IOPL. (See the LOCK prefix description in this chapter for more
information on the locking protocol.)
指令 xchg 用來交換兩個操作數的內容。操作數可以是兩個通用寄存器,或者是 a 寄存器,或者是內存。
如果操作數從內存取,處理器的locking協議會自動實現原子操作,不用使用lock指令來保證。
b. test
Computes the bit-wise logical AND of first operand (source 1 operand) and the second operand (source 2 operand)
and sets the SF, ZF, and PF status flags according to the result. The result is then discarded.
test指令將兩個操作數相與,如果結果為0,則設置 ZF 為1,否則設置 ZF 為0。
test指令的偽碼如下
TEMP := SRC1 AND SRC2;
SF := MSB(TEMP);
IF TEMP = 0
THEN ZF := 1;
ELSE ZF := 0;
FI:
c. jz和jnz
jz: 如果 ZF 為 1,則跳轉
jnz: 如果 ZF 為 0,則跳轉
d. cmp
Compares the first source operand with the second source operand and sets the status flags in the EFLAGS register
according to the results. The comparison is performed by subtracting the second operand from the first operand
and then setting the status flags in the same manner as the SUB instruction. When an immediate value is used as
an operand, it is sign-extended to the length of the first operand.
cmp 指令用來比較兩個操作數的大小,如果相等,則設置 ZF 為1。
cmp指令的偽碼如下
temp := SRC1 ? SignExtend(SRC2);
ModifyStatusFlags; (* Modify status flags in the same manner as the SUB instruction*)
DPDK中的實現代碼如下
使用一個 volatile 修飾的變量 locked,如果加鎖了,locked值為1,沒加鎖值為0。
typedef struct {
volatile int locked; /**< lock status 0 = unlocked, 1 = locked */
} rte_spinlock_t;
變量locked初始值為0
static inline void
rte_spinlock_init(rte_spinlock_t *sl)
{
sl->locked = 0;
}
加鎖操作。此段匯編中有三個label: 1,2和3。
在label1處,讀取變量 locked,使用指令xchg和局部變量 lv 的值交換,然后使用指令test判斷 lv 是否為0,即判斷變量 locked 是否為0,如果為0,表示加鎖成功,變量 locked 值也變成1了,則跳轉到label3,如果不為0,說明變量 locked 已經被其他線程加1,即被其他線程加鎖,則執行label2。
在label2處,先pause一下,然后再讀取變量 locked,使用指令cmp判斷是否為0,如果為0,說明其他線程已經解鎖,跳轉到label1處,如果不為0,則繼續在label2出循環判斷。
在label3處,能到label3,說明加鎖成功,退出即可。
static inline void
rte_spinlock_lock(rte_spinlock_t *sl)
{
int lock_val = 1;
asm volatile (
"1:\n"
"xchg %[locked], %[lv]\n" //locked和lv交換值
"test %[lv], %[lv]\n" //lv和lv相與,判斷結果
"jz 3f\n" //如果為0,則加鎖成功,跳轉到label3
"2:\n" //如果不為0,說明被其他線程加鎖了,則執行label2
"pause\n" //暫停一下
"cmpl $0, %[locked]\n" //locked和0比較
"jnz 2b\n" //locked不為0,說明其他線程還沒有釋放鎖
"jmp 1b\n" //locked為0,說明其他線程已經解鎖,跳轉到label1,和lv交換值,將locked變成1,即加鎖成功
"3:\n"
: [locked] "=m" (sl->locked), [lv] "=q" (lock_val)
: "[lv]" (lock_val)
: "memory");
}
解鎖操作,將sl->locked值變成0
static inline void
rte_spinlock_unlock (rte_spinlock_t *sl)
{
int unlock_val = 0;
asm volatile (
"xchg %[locked], %[ulv]\n" //locked和lv交換值,locked變成0,解鎖
: [locked] "=m" (sl->locked), [ulv] "=q" (unlock_val)
: "[ulv]" (unlock_val)
: "memory");
}
參考
https://cloud.tencent.com/developer/article/1520799
https://cloud.tencent.com/developer/article/1520798?from=article.detail.1520799
https://www.cnblogs.com/taek/archive/2012/02/05/2338838.html