Linux的共享內存(shared memory)是進程間通信的一種機制之一,用於在多個進程之間共享數據。
共享的數據屬於多個進程的共同資源,類似於多線程的數據一樣,也要使用鎖來保護。
線程是使用pthread_mutex來保護,進程可以使用fcntl()對共享內存的文件描述符加鎖,也可以把共享內存的前4位元組當作自旋鎖spinlock來使用。
自旋鎖,可以像之前的文章提到的,以xchg指令為基礎來實現。
「上古風格」的shmget()系列的共享內存API比較麻煩,已經幾乎不再使用了。
現在,共享內存被實現為內核的tmpfs文件系統,可以像一般的文件一樣使用,貫徹了「unix一切皆是文件」的理念。
1,打開共享內存使用的API是shm_open(),它與打開普通文件的open()函數類似,參數和返回值的意義也差不多。
這是它的man手冊,連接時需要加-lrt。
第一個參數為共享內存的名字字符串,類似open的文件名。
第二個參數是打開時的標誌,O_RDWR表示可讀寫,O_CREAT表示如果不存在則創建,O_TRUNC表示如果存在則把大小截斷為0,也與open函數的這個參數含義一樣,一般是聯合使用這三個。
第三個參數是mode,表示Linux的權限,即讀、寫、執行,為3個八進位數字,一般寫為0666,即所有用戶都可讀寫,不可執行。
它的返回值,就是共享內存對應的文件描述符fd,-1表示出錯,可查看errno分析出錯原因。
2,獲得了共享內存的文件描述符fd之後,要用ftruncate()函數把它設置到需要的大小,第一個參數是fd文件描述符,第二個參數是字節大小。
3,最後一步是把共享內存用mmap()函數映射到當前進程裡,獲得一個共享內存的指針,就可以像正常的內存一樣讀寫它了。
區別就是它是被其他進程共享的,而正常的內存是歸所屬進程獨佔的。
上圖屬於簡單的用戶態自旋鎖spinlock的實現,以xchg指令為基礎,0表示沒加鎖,1表示加鎖,返回0表示獲取鎖成功,返回1表示獲取鎖失敗,配合著sched_yield()進程調度函數使用。
下圖main()函數的前半部分,是共享內存的使用,跟前面提到的一樣分3步。
這裡我們定義了一個shm_t的結構來管理共享內存,它包含一個作為自旋鎖的volatile int lock變量,用於在多進程並發環境裡保護共享內存的數據。
在寫shm_t的其他數據之前要先獲取鎖。
其他數據就是一個int count作測試用,加volatile是防止編譯器優化。
畢竟這一項是共享數據,有可能被其他進程修改,volatile可以要求編譯器必須去讀內存,而不是讀寄存器保存的上次使用的值。
mmap()函數的參數較多,可以具體看man手冊,但常用的方式就是這裡寫的這樣。
第二項為要mmap的字節數,最後一項為要mmap的偏移量,倒第二項為要mmap的文件描述符fd。
可見,共享內存對象也是被當作文件來用的。
PROT_READ表示mmap的內存可讀,PROT_WRITE表示可寫,與打開時的權限選擇是一致的,MAP_SHARED表示以共享方式映射,多個進程對這個內存的讀寫是互相影響的。
fork一個子進程測試一下,100萬次++之後看看shm->count是200萬嗎。
如果不加鎖的情況下,因為++屬於「讀-更新-寫」運算,是存在同步問題的,不會是200萬。
賦值=是原子操作,所以解鎖只需要*lock = 0就行。
++運算也可以寫成原子操作:
asm volatile(「lock; inc %0」
: 「=r」(*p)::);
符號、引號「」都是英文的。
這樣就不需要加鎖了,intel允許在一條指令前面加個lock;表示下一條指令執行時要鎖定內存總線,禁止其他CPU訪問內存,從而讓lock的下一條指令成為原子操作。
在只需要做簡單運算時,原子操作的粒度比鎖更小,可以讓代碼儘快離開互斥區,提高代碼效率。
nginx保護監聽socket,避免epoll驚群的那個鎖,也是優先使用共享內存來實現。在共享內存不被支持時,才使用文件鎖。