李樂nginx
多線程或者多進程程序訪問同一個變量時,須要加鎖才能實現變量的互斥訪問,不然結果多是沒法預期的,即存在併發問題。解決併發問題一般有兩種方案:
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等文件針對不一樣操做系統實現了若干原子性操做函數。fetch
可經過內聯彙編向C代碼中嵌入彙編語言。原子操做函數內部都使用到了內聯彙編,所以這裏須要作簡要介紹;
內聯彙編格式以下,須要瞭解如下6個概念:優化
asm ( 彙編指令 : 輸出操做數(可選) : 輸入操做數(可選) : 寄存器列表(代表哪些寄存器被修改,可選) );
1)寄存器一般有一些簡稱;ui
2)彙編指令;atom
" popl %0 " " movl %1, %%esi " " movl %2, %%edi "
3)輸入操做數,一般格式爲——"寄存器簡稱/內存簡稱"(值);這種稱爲寄存器約束或者內存約束,代表輸入或者輸出須要藉助寄存器或者內存實現。spa
: "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中鎖的實現原理,多核高速緩存衝突問題,內聯彙編簡單語法,以及原子比較-交換操做和原子累加操做的實現。才疏學淺,若有錯誤或者不足,請指出。