用戶空間自旋鎖實現的思考

我頓悟,不論這個結論是否正確,我還是要以它爲指導思想來寫點代碼,因爲我真的想寫點併發場景下的東西來驗證我這七年來對編程及操作系統的思考

於是我又重啓了去年六七月分停止的併發編程大業,打開了虛擬機,重啓了烏班圖,準備大幹一場

可是啊!原本以爲自己對併發編程已經有點了解了,但實際寫代碼出現問題時還是會一時摸不着頭腦,讓人信心大措,我準備在userSpace通過gcc原子操作和共享內存實現一個既可以用於多線程又可用於多進程同步的spinLock,同時對linux提供的信號量和互斥量進行封裝實現一個既可用於多線程的又可用於多進程同步的鎖,對程序員提供一種便利的鎖的使用方式,匿名鎖用於同一個進程中多個線程的同步,命名鎖既可用於多個進程間的同步又可用於多線程間的同步,同時加上因進程奔潰導致的死鎖檢測與清除

 

我開始按自己的想法和對鎖及linux內核對共享內存實現的理解,編寫代碼,並進行測試,起初,一切進行順利,每次測試運行都能得到一個期望的值,但代碼改着改着就出問題了,多個進程對共享內存中的一個整型變量通過userSpace spinLock進行同步,同時進行10億次自增操作時,總是得不到想要的結果,後來連直接使用原子自增也得不到正確的結果了

 

幾經排查欣然發現由於剛開始測試啓動的進程個數比較少,使用一個4字節的int類型做爲共享變量就可以保存自增結果了,後來開到了十幾個進程,int類型溢出了,就換了一個8字節long long類型,問題出現在這裏?我的虛擬機是32位的,當使用gcc提供的原子操作__sync_fetch_and_add((long long*)ptr,1),在32位的系統下操作一個64位8字節的long long類型時,編譯器產生了一大串彙編指令使用進位加法在32位4字節寄存器中來實現對這個64位的8字節進行加1操作,而不是一條原子指令

是這樣?

如圖在32位系統下對一個4字節int類型變量進行原子自增,編譯器產生如下彙編代碼

第8-10行 生成main函數調用的棧幀

第12行 代碼把棧中變量count的地址放入eax寄存器

第13行 lock硬件級鎖,鎖住總線,防止其它cpu核心及其它cpu,同時執行指令,addl直接對eax寄存器中地址指向的值加1,由此完成一次原子加1操作

第14行 把0放到eax寄存器做爲默認返回值

第15行 清理棧幀此爲stdcall

第16行 從棧中彈出ip指令到ip寄存器中返回完成此次函數調用

 

 

如圖在32位系統下對一個8字節long long類型變量進行原子加1,編譯器產生如下彙編代碼

這一長串代碼使用了多個寄存器輔助來完成對long long類型變量進行加1,大眼一看這麼長一串代碼肯定不是原子操作啊,但細分析,它卻實現了原子操作所保證的語義

第47行代碼原子操作lock cmpxchg8b(我們常說的CAS)把計算結果ecx,ebx中的值與開始這輪計算開始時棧中變量count值eax,edx(x86是小端所以,eax中是低32位,edx中高32位)做比較,如果相等把計算結果8個字節放到棧中,esp中存放的地址處即爲count賦值,如果不相等則jne 8048509至第40行代碼,從棧中取count的最新值分別爲高低4字節並保存到edx,eax寄存器中,進行下一輪計算,如此反覆直致成功,可以理解在代碼級別實現了一個自旋的樂觀鎖

 

這也解釋了爲什麼我把int類型換成long long類型後程序慢了很多很多

 

由此可以發現long long類型並不是導致結果不正確的原因

 

然後我在代碼中到處加membar也沒用,我對所有的變量進行檢查,是否有共享的變量,被直接引用但是沒有使用volatile修飾,翻來翻去就發現多進程在共享內存中,共享過兩個變量,一個變量是實現spinlock用到的一個4字節int,一個是用來做累加結果用到的一個8字節long long,這兩個變量,確實是沒使用volatile修飾,但這兩個共享變量是以指針的形式對外提供可見的,編譯器應該無法感知到它的存在,所以就不可能把它做爲一個變量優化到寄存器中,再者我8字節,32位cpu一個寄存器才4字節

 

問題到底出現在了那????

只有看下反彙編代碼了,看下編譯器到底生成了什麼樣的代碼,到底有沒有把變量優化到寄存器

其實是咱寫了一個BUG啊!

 

 

關於多進程SpinLock進程異常崩潰退出,代碼bug導致進程正常退出未釋放鎖導致的死鎖解決,同樣也適用於多線程代碼bug導致線程正常退出未釋放鎖導致的死鎖

 

在C++中可利用RAII通過對象的生命週期來解決資源泄漏導致的問題,如內存泄漏導致的oom,鎖未釋放導致的deadlock

  1. 線程崩潰,整個進程都玩完了不用考慮,操作系統會回收整個進程已擁有的資源

 

  1. 在使用RAII的情況下由於異常,或正常return,編譯器都會保證對象的析構函數被調用,從而釋放資源,而調用longjmp在從一個函數棧楨跳到另一個函數棧幀並不會調用當前棧幀中臨時對象的析構函數

 

  1. 進程崩潰使用文件鎖,文件鎖是與文件關聯的,進程無論以何種方式退出總會從內核的exit函數退出做最後的清理工作並釋放此進程擁有的文件鎖

 

  1. 進程崩潰使用信號量,只需對信號量指定SEM_UNDO標誌即可

 

5.多進程使用userSpace SpinLock進程異常崩潰退出,代碼bug導致進程正常退出未釋放鎖,多線程使用userSpace SpinLock或pthread_mutext,代碼bug導致線程正常退出未釋放鎖,則需要進行死鎖檢測與清除

 

 

死鎖檢測清除方法

  1. 對鎖對象增加owner成員變量,在進行lock時爲其賦值,值爲進程pid,或線程tid,unlock時賦值爲0
  2. 每創建一個鎖就將其註冊到DeadLockMonitor中
  3. 主進程對子進程/線程進行wait,pthread_join,當有任務退出後進行死鎖檢測
  4. 遍歷所有註冊的鎖,取出其owner判斷其是否存活或是否是退出任務的pid/tid
  5. 如owner已dead則解鎖

 

 

 

關於無父子關係的多個進程併發創建鎖對象,重複初始化鎖,導致多個進程同時獲取鎖的問題解決

多進程鎖對象存在於共享內存中,無父子關係的多個進程在創建鎖時,鎖的內部會通過共享內存把鎖文件內容映射到自己的虛擬地址空間,然後對鎖進行初始化,此實現可運行在有父子關係的多進程中,父進程創建鎖對象,子進程可不創建鎖對象直接使用父進程鎖對象的副本即可

在有父子關係的多進程環境中子進程按序依次創建,而在無父子關係的多個進程環境中,子進程可能被同時創建,存在併發,創建鎖的情況,

對於重複初始化鎖的解決,在鎖對象的構造函數中,去除鎖的初始化動作,鎖的初始化改爲在創建共享內存對象時,初始化鎖文件時使用fallocate初始化鎖文件大小的同時初始化文件內容爲0

對於併發創建鎖對象,在鎖對象的構造函數中使用文件鎖進行同步即可

 

SharedMem(char *filePath,int size=sizeof(Type)){

              this->size=size+sizeof(int);

              strncpy(this->filePath,filePath,256);

              int fd=open(filePath,O_RDWR|O_CREAT);

              if(fd==-1){

                     printf("open file %s error:%m\n",filePath);

                     exit(-1);

              }

              struct stat status;

              if(fstat(fd,&status)<0){

                     printf("stat file %s error:%m\n",filePath);

                     exit(-1);

              }

              if(status.st_size<this->size){

                     if(fallocate(fd,0,0,this->size)<0){

                            printf("fallocate file %s error:%m\n",filePath);

                            exit(-1);

                     }

              }

       obj=(Type*)mmap(NULL,this->size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

              if(obj==NULL){

                     printf("mmap file %s error:%m\n",filePath);

                     exit(-1);

              }

              ref=(int*)obj;

              obj=(Type*)((char*)obj+sizeof(int));

              incRef();

       }

 

在多進程環境下鎖文件被提前重複刪除的問題解決

在有父子關係的多個進程中,父進程創建鎖,多個子進程得到鎖對象的副本,當一個子進程退出時,鎖對象副本的生命週期結束,鎖對象的析構函數被調用,析構函數會調用mumap卸載共享內存,刪除鎖文件。由於此時其它子進程還未退出,不能刪除鎖文件,對共享內存使用引用計數進行管理,當使用共享內存類創建共享內存對象時,把其申請的內存大小加4個字節做爲引用計數,返回的內存地址爲((char*)ptr+4),

在鎖對象的構造函數使用原子操作進行引用計數加1,在析構函數中使用原子操作對引用計數減1,當引用計數爲0刪除鎖文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章