摘要:源碼目錄下等文件針對(duì)不同操作系統(tǒng)實(shí)現(xiàn)了若干原子性操作函數(shù)。函數(shù)最后返回標(biāo)志寄存器位。總結(jié)本文簡要介紹了中鎖的實(shí)現(xiàn)原理,多核高速緩存沖突問題,內(nèi)聯(lián)匯編簡單語法,以及原子比較交換操作和原子累加操作的實(shí)現(xiàn)。
李樂
問題引入多線程或者多進(jìn)程程序訪問同一個(gè)變量時(shí),需要加鎖才能實(shí)現(xiàn)變量的互斥訪問,否則結(jié)果可能是無法預(yù)期的,即存在并發(fā)問題。解決并發(fā)問題通常有兩種方案:
1)加鎖:訪問變量之前加鎖,只有加鎖成功才能訪問變量,訪問變量之后需要釋放鎖;這種通常稱為悲觀鎖,即認(rèn)為每次變量訪問都會(huì)導(dǎo)致并發(fā)問題,因此每次訪問變量之前都加鎖。
2)原子操作:只要訪問變量的操作是原子的,就不會(huì)導(dǎo)致并發(fā)問題。那表達(dá)式么i++是不是原子操作呢?
nginx通常會(huì)有多個(gè)worker處理請(qǐng)求,多個(gè)worker之間需要通過搶鎖的方式來實(shí)現(xiàn)監(jiān)聽事件的互斥處理,由函數(shù)ngx_shmtx_trylock實(shí)現(xiàn)搶鎖邏輯,代碼如下:
ngx_uint_t ngx_shmtx_trylock(ngx_shmtx_t *mtx) { return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)); }
變量mtx->lock指向的是一塊共享內(nèi)存地址(所有worker都可以訪問);worker進(jìn)程會(huì)嘗試設(shè)置變量mtx->lock的值為當(dāng)前進(jìn)程號(hào),如果設(shè)置成功,則說明搶鎖成功,否則認(rèn)為搶鎖失敗。
注意ngx_atomic_cmp_set設(shè)置變量mtx->lock的值為當(dāng)前進(jìn)程號(hào)并不是無任何條件的,而是只有當(dāng)變量mtx->lock值為0時(shí)才設(shè)置,否則不予設(shè)置。ngx_atomic_cmp_set是典型的比較-交換操作,且必須加鎖或者是原子操作才行,函數(shù)實(shí)現(xiàn)方式下節(jié)分析。
nginx有一些全局統(tǒng)計(jì)變量,比如說變量ngx_connection_counter,此類變量由所有worker進(jìn)程共享,并發(fā)執(zhí)行累加操作,由函數(shù)ngx_atomic_fetch_add實(shí)現(xiàn);而該累加操作需要加鎖或者時(shí)原子操作才行,函數(shù)實(shí)現(xiàn)方式下節(jié)分析。
上面說的mtx->lock和ngx_connection_counter都是共享變量,所有worker進(jìn)程都可以訪問,這些變量在ngx_event_core_module模塊的ngx_event_module_init函數(shù)創(chuàng)建,且該函數(shù)在fork worker進(jìn)程之前執(zhí)行。
/* cl should be equal to or greater than cache line size */ cl = 128; size = cl /* ngx_accept_mutex */ + cl /*ngx_connection_counter */ + cl; /* ngx_temp_number */ if (ngx_shm_alloc(&shm) != NGX_OK) { return NGX_ERROR; } shared = shm.addr; if (ngx_shmtx_create(&ngx_accept_mutex, (ngx_shmtx_sh_t *) shared,cycle->lock_file.data)!= NGX_OK) { return NGX_ERROR; } ngx_connection_counter = (ngx_atomic_t *) (shared + 1 * cl);
這里需要重點(diǎn)思考這么幾個(gè)問題:
1)cache_line_size是什么?我們都知道CPU與主存之間還存在著高速緩存,高速緩存的訪問速率高于主存訪問速率,因此主存中部分?jǐn)?shù)據(jù)會(huì)被緩存在高速緩存中,CPU訪問數(shù)據(jù)時(shí)會(huì)先從高速緩存中查找,如果沒有命中才會(huì)訪問主從。需要注意的是,主存中的數(shù)據(jù)并不是一字節(jié)一字節(jié)加載到高速緩存中的,而是每次加載一個(gè)數(shù)據(jù)塊,該數(shù)據(jù)塊的大小就稱為cache_line_size,高速緩存中的這塊存儲(chǔ)空間稱為一個(gè)緩存行。cache_line_size32字節(jié),64字節(jié)不等,通常為64字節(jié)。
2)此處cl取值128字節(jié),可是cl為什么一定要大于等于cache_line_size?待下一節(jié)分析了原子操作函數(shù)實(shí)現(xiàn)方式后自然會(huì)明白的。
3)函數(shù)ngx_shm_alloc是通過系統(tǒng)調(diào)用mmap分配的內(nèi)存空間,首地址為shared;
4)這里創(chuàng)建了三個(gè)共享變量ngx_accept_mutex、ngx_connection_counter和ngx_temp_number;函數(shù)ngx_shmtx_create使得ngx_accept_mutex->lock變量指向shared;ngx_connection_counter指向shared+128字節(jié)位置處,ngx_temp_number指向shared+256字節(jié)位置處。
據(jù)說gcc某版本以后內(nèi)置了一些原子性操作函數(shù)(沒有驗(yàn)證),如:
//原子加 type __sync_fetch_and_add (type *ptr, type value); //原子減 type __sync_fetch_and_sub (type *ptr, type value); //原子比較-交換,返回true bool __sync_bool_compare_and_swap(type* ptr, type oldValue, type newValue, ....); //原子比較交換,返回之前的值 type __sync_val_compare_and_swap(type* ptr, type oldValue, type newValue, ....);
通過這些函數(shù)很容易解決上面說的多個(gè)worker搶鎖,統(tǒng)計(jì)變量并發(fā)累計(jì)問題。nginx會(huì)檢測系統(tǒng)是否支持上述方法,如果不支持會(huì)自己實(shí)現(xiàn)類似的原子性操作函數(shù)。
源碼目錄下src/os/unix/ngx_gcc_atomic_amd64.h、src/os/unix/ngx_gcc_atomic_x86.h等文件針對(duì)不同操作系統(tǒng)實(shí)現(xiàn)了若干原子性操作函數(shù)。
可通過內(nèi)聯(lián)匯編向C代碼中嵌入?yún)R編語言。原子操作函數(shù)內(nèi)部都使用到了內(nèi)聯(lián)匯編,因此這里需要做簡要介紹;
內(nèi)聯(lián)匯編格式如下,需要了解以下6個(gè)概念:
asm ( 匯編指令 : 輸出操作數(shù)(可選) : 輸入操作數(shù)(可選) : 寄存器列表(表明哪些寄存器被修改,可選) );
1)寄存器通常有一些簡稱;
r:表示使用一個(gè)通用寄存器,由GCC在%eax/%ax/%al, %ebx/%bx/%bl, %ecx/%cx/%cl, %edx/%dx/%dl中選取一個(gè)GCC認(rèn)為合適的。
a:表示使用%eax / %ax / %al
b:表示使用%ebx / %bx / %bl
c:表示使用%ecx / %cx / %cl
d:表示使用%edx / %dx / %dl
m: 表示內(nèi)存地址
等
2)匯編指令;
" popl %0 " " movl %1, %%esi " " movl %2, %%edi "
3)輸入操作數(shù),通常格式為——"寄存器簡稱/內(nèi)存簡稱"(值);這種稱為寄存器約束或者內(nèi)存約束,表明輸入或者輸出需要借助寄存器或者內(nèi)存實(shí)現(xiàn)。
: "m" (*lock), "a" (old), "r" (set)
4)輸出操作數(shù);
//+號(hào)表示既是輸入?yún)?shù)又是輸出參數(shù) :"+r" (add) //將寄存器%eax / %ax / %al存儲(chǔ)到變量res中 :"=a" (res)
5)寄存器列表,如
: "cc", "memory"
cc表示會(huì)修改標(biāo)志寄存器中的條件標(biāo)志,memory表示會(huì)修改內(nèi)存。
6)占位符與volatile關(guān)鍵字
__asm__ volatile ( " xaddl %0, %1; " : "+r" (add) : "m" (*value) : "cc", "memory");
volatile表明禁止編譯器優(yōu)化;%0和%1順序?qū)?yīng)后面的輸出或輸入操作數(shù),如%0對(duì)應(yīng)"+r" (add),%1對(duì)應(yīng)"m" (*value)。
比較-交換原子實(shí)現(xiàn)現(xiàn)代處理器都提供了比較-交換匯編指令cmpxchgl r, [m],且是原子操作。其含義如下為,如果eax寄存器的內(nèi)容與[m]內(nèi)存地址內(nèi)容相等,則設(shè)置[m]內(nèi)存地址內(nèi)容為r寄存器的值。偽代碼如下(標(biāo)志寄存器zf位):
if (eax == [m]) { zf = 1; [m] = r; } else { zf = 0; eax = [m]; }
因此利用指令cmpxchgl可以很容易實(shí)現(xiàn)原子性的比較-交換功能。
但是想想這樣有什么問題呢?對(duì)于單核CPU來說沒任何問題,多核CPU則無法保證。(參考深入理解計(jì)算機(jī)系統(tǒng)第六章)以Intel Core i7處理器為例,其有四個(gè)核,且每個(gè)核都有自己的L1和L2高速緩存。
前面提到,主存中部分?jǐn)?shù)據(jù)會(huì)被緩存在高速緩存中,CPU訪問數(shù)據(jù)時(shí)會(huì)先從高速緩存中查找;那假如同一塊內(nèi)存地址同時(shí)被緩存在核0與核1的L2級(jí)高速緩存呢?此時(shí)如果核0與核1同時(shí)修改該地址內(nèi)容,則會(huì)造成沖突。
目前處理器都提供有l(wèi)ock指令;其可以鎖住總線,其他CPU對(duì)內(nèi)存的讀寫請(qǐng)求都會(huì)被阻塞,直到鎖釋放;不過目前處理器都采用鎖緩存替代鎖總線(鎖總線的開銷比較大),即lock指令會(huì)鎖定一個(gè)緩存行。當(dāng)某個(gè)CPU發(fā)出lock信號(hào)鎖定某個(gè)緩存行時(shí),其他CPU會(huì)使它們的高速緩存該緩存行失效,同時(shí)檢測是對(duì)該緩存行中數(shù)據(jù)進(jìn)行了修改,如果是則會(huì)寫所有已修改的數(shù)據(jù);當(dāng)某個(gè)高速緩存行被鎖定時(shí),其他CPU都無法讀寫該緩存行;lock后的寫操作會(huì)及時(shí)會(huì)寫到內(nèi)存中。
以文件src/os/unix/ngx_gcc_atomic_x86.h為例。
查看ngx_atomic_cmp_set函數(shù)實(shí)現(xiàn)如下:
#define NGX_SMP_LOCK "lock;" static ngx_inline ngx_atomic_uint_t ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old, ngx_atomic_uint_t set) { u_char res; __asm__ volatile ( NGX_SMP_LOCK " cmpxchgl %3, %1; " " sete %0; " : "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory"); return res; }
cmpxchgl即為上面說的原子比較-交換指令;sete取標(biāo)志寄存器中ZF位的值,并存儲(chǔ)在%0對(duì)應(yīng)的操作數(shù)。函數(shù)最后返回標(biāo)志寄存器zf位。
累加指令格式為xaddl r [m],含義如下:
temp = [m]; [m] += r; r = temp;
查看ngx_atomic_fetch_add函數(shù)實(shí)現(xiàn):
static ngx_inline ngx_atomic_int_t ngx_atomic_fetch_add(ngx_atomic_t *value, ngx_atomic_int_t add) { __asm__ volatile ( NGX_SMP_LOCK " xaddl %0, %1; " : "+r" (add) : "m" (*value) : "cc", "memory"); return add; }
指令xaddl實(shí)現(xiàn)了加法功能,其將%0對(duì)應(yīng)操作數(shù)加到%1對(duì)應(yīng)操作數(shù),函數(shù)最后返回累加之前的舊值。
這里再回到第一小節(jié),cl取值128字節(jié),且注釋表明cl一定要大于等于cache_line_size。cl是什么?三個(gè)共享變量之間的偏移量。那假如去掉這個(gè)限制,由于每個(gè)變量只占8字節(jié),所以三個(gè)變量總共占24字節(jié),假設(shè)cache_line_size即緩存行大小為64字節(jié),即這三個(gè)共享變量可能屬于同一個(gè)緩存行。
那么當(dāng)使用lock指令鎖定ngx_accept_mutex->lock變量時(shí),會(huì)鎖定該變量所在的緩存行,從而導(dǎo)致對(duì)共享變量ngx_connection_counter和ngx_temp_number同樣執(zhí)行了鎖定,此時(shí)其他CPU是無法訪問這兩個(gè)共享變量的。因此這里會(huì)限制cl大于等于緩存行大小。
本文簡要介紹了nginx中鎖的實(shí)現(xiàn)原理,多核高速緩存沖突問題,內(nèi)聯(lián)匯編簡單語法,以及原子比較-交換操作和原子累加操作的實(shí)現(xiàn)。
才疏學(xué)淺,如有錯(cuò)誤或者不足,請(qǐng)指出。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/40184.html
摘要:而對(duì)于堆內(nèi)存,通常需要程序員進(jìn)行管理。我們通常說的內(nèi)存管理亦是只堆空間內(nèi)存管理。內(nèi)存管理整體可以分為個(gè)部分,第一部分是常規(guī)的內(nèi)存池,用于進(jìn)程平時(shí)所需的內(nèi)存管理第二部分是共享內(nèi)存的管理。將內(nèi)存塊按照的整數(shù)次冪進(jìn)行劃分最小為最大為。 施洪寶 一. 概述 應(yīng)用程序的內(nèi)存可以簡單分為堆內(nèi)存,棧內(nèi)存。對(duì)于棧內(nèi)存而言,在函數(shù)編譯時(shí),編譯器會(huì)插入移動(dòng)棧當(dāng)前指針位置的代碼,實(shí)現(xiàn)棧空間的自管理。而對(duì)于...
摘要:前言對(duì)于多進(jìn)程多線程的應(yīng)用程序來說,保證數(shù)據(jù)正確的同步與更新離不開鎖和信號(hào),中的鎖與信號(hào)基本采用系列函數(shù)實(shí)現(xiàn)。中的鎖類型有很多種互斥鎖自旋鎖文件鎖讀寫鎖原子鎖,本節(jié)就會(huì)講解中各種鎖的定義與使用。 前言 對(duì)于多進(jìn)程多線程的應(yīng)用程序來說,保證數(shù)據(jù)正確的同步與更新離不開鎖和信號(hào),swoole 中的鎖與信號(hào)基本采用 pthread 系列函數(shù)實(shí)現(xiàn)。UNIX 中的鎖類型有很多種:互斥鎖、自旋鎖、文...
摘要:本文旨在對(duì)鎖相關(guān)源碼本文中的源碼來自使用場景進(jìn)行舉例,為讀者介紹主流鎖的知識(shí)點(diǎn),以及不同的鎖的適用場景。中,關(guān)鍵字和的實(shí)現(xiàn)類都是悲觀鎖。自適應(yīng)意味著自旋的時(shí)間次數(shù)不再固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定。 前言 Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當(dāng)?shù)膱鼍跋履軌蛘宫F(xiàn)出非常高的效率。本文旨在對(duì)鎖相關(guān)源碼(本文中的源碼來自JDK 8)、使用場景...
摘要:解決上問題在變量前添加版本號(hào),將變成循環(huán)時(shí)間長開銷大,因?yàn)樽孕枰闹荒鼙WC一個(gè)共享變量的原子操作分類二重入鎖支持重進(jìn)入的鎖,排它鎖分類三讀寫鎖一對(duì)鎖,讀鎖,寫鎖,在同一時(shí)刻允許多線程訪問 1、 分類一:樂觀鎖與悲觀鎖 a)悲觀鎖:認(rèn)為其他線程會(huì)干擾本身線程操作,所以加鎖 i.具體表現(xiàn)形式:synchronized關(guān)鍵字和lock實(shí)現(xiàn)類 ...
摘要:而對(duì)于堆內(nèi)存,通常需要程序員進(jìn)行管理。二內(nèi)存池管理說明本部分使用的版本為具體源碼參見文件實(shí)現(xiàn)使用流程內(nèi)存池的使用較為簡單可以分為步,調(diào)用函數(shù)獲取指針。將內(nèi)存塊按照的整數(shù)次冪進(jìn)行劃分最小為最大為。 運(yùn)營研發(fā)團(tuán)隊(duì) 施洪寶 一. 概述 應(yīng)用程序的內(nèi)存可以簡單分為堆內(nèi)存,棧內(nèi)存。對(duì)于棧內(nèi)存而言,在函數(shù)編譯時(shí),編譯器會(huì)插入移動(dòng)棧當(dāng)前指針位置的代碼,實(shí)現(xiàn)棧空間的自管理。而對(duì)于堆內(nèi)存,通常需要程序...
閱讀 2573·2021-09-02 15:40
閱讀 1577·2019-08-30 15:54
閱讀 1090·2019-08-30 12:48
閱讀 3410·2019-08-29 17:23
閱讀 1057·2019-08-28 18:04
閱讀 3675·2019-08-26 13:54
閱讀 617·2019-08-26 11:40
閱讀 2406·2019-08-26 10:15