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