Linux 共享內存使用常見陷阱與分析
所謂共享內存就是使得多個進程可以訪問同一塊內存空間,是最快的可用 IPC 形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。其他進程能把同一段共享內存段 “連接到” 他們自己的地址空間裏去。所有進程都能訪問共享內存中的地址。如果一個進程向這段共享內存寫了數據,所做的改動會即時被有訪問同一段共享內存的其他進程看到。共享內存的使用大大降低了在大規模數據處理過程中內存的消耗,但是共享內存的使用中有很多的陷阱,一不注意就很容易導致程序崩潰。
- 超過共享內存的大小限制?
在一個 linux 服務器上,共享內存的總體大小是有限制的,這個大小通過 SHMMAX 參數來定義(以字節爲單位),您可以通過執行以下命令來確定 SHMMAX 的值:
# cat /proc/sys/kernel/shmmax
如果機器上創建的共享內存的總共大小超出了這個限制,在程序中使用標準錯誤 perror 可能會出現以下的信息:
unable to attach to shared memory
> 解決方法:
1、設置 SHMMAX
SHMMAX 的默認值是 32MB 。一般使用下列方法之一種將 SHMMAX 參數設爲 2GB :
通過直接更改 /proc 文件系統,你不需重新啓動機器就可以改變 SHMMAX 的默認設置。我使用的方法是將以下命令放入 />etc/rc.local 啓動文件中:
echo "2147483648" > /proc/sys/kernel/shmmax
您還可以使用 sysctl 命令來更改 SHMMAX 的值:sysctl -w kernel.shmmax=2147483648
最後,通過將該內核參數插入到 /etc/sysctl.conf 啓動文件中,您可以使這種更改永久有效:echo "kernel.shmmax=2147483648" >> /etc/sysctl.conf
2、設置 SHMMNI
我們現在來看 SHMMNI 參數。這個內核參數用於設置系統範圍內共享內存段的最大數量。該參數的默認值是 4096 。這一數值已經足夠,通常不需要更改。您可以通過執行以下命令來確定 SHMMNI 的值:cat /proc/sys/kernel/shmmni
3、設置 SHMALL
最後,我們來看 SHMALL 共享內存內核參數。該參數控制着系統一次可以使用的共享內存總量(以頁爲單位)。簡言之,該參數的值始終應該至少爲:ceil(SHMMAX/PAGE_SIZE)
SHMALL 的默認大小爲 2097152 ,可以使用以下命令進行查詢:cat /proc/sys/kernel/shmall
SHMALL 的默認設置對於我們來說應該足夠使用。注意:在 i386 平臺上 Red Hat Linux 的 頁面大小 爲 4096 字節。但是,您可以使用 bigpages ,它支持配置更大的內存頁面尺寸。
2. 多次進行 shmat 會出現什麼問題?
當首次創建共享內存段時,它並不能被任何進程所訪問。爲了使共享內存區可以被訪問,則必須通過 shmat 函數將其附加 (attach) 到自己的進程空間中,這樣進程就與共享內存建立了連接。該函數聲明在 linux/shm.h 中:void *shmat(int shmid, const void *shmaddr, int shmflg);
參數 shmid 是 shmget() 的返回值,是個標識符; 參數 shmflg 是存取權限標誌;如果爲 0 ,則不設置任何限制權限。在 中定義了幾個權限:
#define SHM_RDONLY 010000 /* attach read-only else read-write */ #define SHM_RND 020000 /* round attach address to SHMLBA */ #define SHM_REMAP 040000 /* take-over region on attach */
如果指定 SHM_RDONLY ,那麼共享內存區只有讀取權限。參數 shmaddr 是共享內存的附加點,不同的取值有不同的含義:
Ø 如果爲空,則由內核選擇一個空閒的內存區;如果非空,返回地址取決於調用者是否給 shmflg 參數指定 SHM_RND 值,如果沒有指定,則共享內存區附加到由 shmaddr 指定的地址;否則附加地址爲 shmaddr 向下舍入一個共享內存低端邊界地址後的地址 (SHMLBA ,一個常址)。
Ø 通常將參數 shmaddr 設置爲 NULL 。
shmat() 調用成功後返回一個指向共享內存區的指針,使用該指針就可以訪問共享內存區了,如果失敗則返回 -1。其映射關係如下圖所示:
其中,shmaddr 表示的是物理內存空間映射到進程的虛擬內存空間時候,虛擬內存空間中該塊內存的起始地址,在使用中,因爲我們一般不清楚進程中哪些地址沒有被佔用,所以不好指定物理空間的內存要映射到本進程的虛擬內存地址,一般會讓內核自己指定:ptr = shmat(shmid, NULL,0);
這樣掛載一個共享內存如果是一次調用是沒有問題的,但是一個進程是可以對同一個共享內存多次 shmat 進行掛載的,物理內存是指向同一塊,如果 shmaddr 爲 NULL,則每次返回的線性地址空間都不同。而且指向這塊共享內存的引用計數會增加。也就是進程多塊線性空間會指向同一塊物理地址。這樣,如果之前掛載過這塊共享內存的進程的線性地址沒有被 shmdt 掉,即申請的線性地址都沒有釋放,就會一直消耗進程的虛擬內存空間,很有可能會最後導致進程線性空間被使用完而導致下次 shmat 或者其他操作失敗。
> 解決方法:
可以通過判斷需要申請的共享內存指針是否爲空來標識是否是第一次掛載共享內存,若是則使用進行掛載,若不是則退出。
void* ptr = NULL; if (NULL != ptr) return; ptr = shmat(shmid,ptr,0666);
附:
函數 shmat 將標識號爲 shmid 共享內存映射到調用進程的地址空間中,映射的地址由參數 shmaddr 和 shmflg 共同確定,其準則爲: (1) 如果參數 shmaddr 取值爲 NULL,系統將自動確定共享內存鏈接到進程空間的首地址。 (2) 如果參數 shmaddr 取值不爲 NULL 且參數 shmflg 沒有指定 SHM_RND 標誌,系統將運用地址 shmaddr 鏈接共享內存。 (3) 如果參數 shmaddr 取值不爲 NULL 且參數 shmflg 指定了 SHM_RND 標誌位,系統將地址 shmaddr 對齊後鏈接共享內存。其中選項 SHM_RND 的意思是取整對齊,常數 SHMLBA 代表了低邊界地址的倍數,公式 “shmaddr – (shmaddr % SHMLBA)” 的意思是將地址 shmaddr 移動到低邊界地址的整數倍上。
3.shmget 創建共享內存,當 key 相同時,什麼情況下會出錯?
shmget() 用來創建一個共享內存區,或者訪問一個已存在的共享內存區。該函數定義在頭文件 linux/shm.h 中,原型如下:int shmget(key_t key, size_t size, int shmflg);
參數 key 是由 ftok() 得到的鍵值;
參數 size 是以字節爲單位指定內存的大小;
參數 shmflg 是操作標誌位,它的一些宏定義如下:
IPC_CREATE : 調用 shmget 時,系統將此值與其他共享內存區的 key 進行比較,如果存在相同的 key ,說明共享內存區已存在,此時返回該共享內存區的標識符,否則新建一個共享內存區並返回其標識符。
IPC_EXCL : 該宏必須和 IPC_CREATE 一起使用,否則沒意義。當 shmflg 取 IPC_CREATE | IPC_EXCL 時,表示如果發現內存區已經存在則返回 -1,錯誤代碼爲 EEXIST 。
注意,當創建一個新的共享內存區時,size 的值必須大於 0 ;如果是訪問一個已經存在的內存共享區,則置 size 爲 0 。
一般我們創建共享內存的時候會在一個進程中使用 shmget 來創建共享內存: int shmid = shmget(key, size, IPC_CREATE|0666); 而在另外的進程中,使用 shmget 和同樣的 key 來獲取到這個已經創建了的共享內存: shmid = shmget(key, size, IPC_CREATE|0666);
如果創建進程和掛接進程 key 相同,而對應的 size 大小不同,是否會 shmget 失敗?
Ø 已經創建的共享內存的大小是可以調整的,但是已經創建的共享內存的大小隻能調小,不能調大
當多個進程都能創建共享內存的時候,如果 key 出現相同的情況,並且一個進程需要創建的共享內存的大小要比另外一個進程要創建的共享內存小,共享內存大的進程先創建共享內存,共享內存小的進程後創建共享內存,小共享內存的進程就會獲取到大的共享內存進程的共享內存,並修改其共享內存的大小和內容(留意下面的評論補充),從而可能導致大的共享內存進程崩潰。
解決方法:
方法一:在所有的共享內存創建的時候,使用排他性創建,即使用 IPC_EXCL 標記:在共享內存掛接的時候,先使用排他性創建判斷共享內存是否已經創建,如果還沒創建則進行出錯處理,若已經創建,則掛接
shmid = Shmget(key, size,IPC_CREATE|IPC_EXCL); if (-1 != shmid) Printf("error"); shmid = Shmget(key, size,IPC_CREATE);
方法二:雖然都希望自己的程序能和其他的程序預先約定一個唯一的鍵值,但實際上並不是總可能的成行的,因爲自己的程序無法爲一塊共享內存選擇一個鍵值。因此,在此把 key 設爲 IPC_PRIVATE,這樣,操作系統將忽略鍵,建立一個新的共享內存,指定一個鍵值,然後返回這塊共享內存 IPC 標識符 ID。而將這個新的共享內存的標識符 ID 告訴其他進程可以在建立共享內存後通過派生子進程,或寫入文件或管道來實現,即這種方法不使用 key 來創建共享內存,由操作系統來保證唯一性。
4.ftok 是否一定會產生唯一的 key 值?
ftok 原型如下:key_t ftok(char * pathname, int proj_id)
pathname 就時你指定的文件名,proj_id 是子序號。在一般的 UNIX 實現中,是將文件的索引節點號取出,前面加上子序號得到 key_t 的返回值。如指定文件的索引節點號爲 65538,換算成 16 進製爲 0×010002,而你指定的 proj_id 值爲 38,換算成 16 進製爲 0×26,則最後的 key_t 返回值爲 0×26010002。
查詢文件索引節點號的方法是:ls -i。但當刪除重建文件後,索引節點號由操作系統根據當時文件系統的使用情況分配,因此與原來不同,所以得到的索引節點號也不同。根據 pathname 指定的文件(或目錄)名稱,以及 proj_id 參數指定的數字,ftok 函數爲 IPC 對象生成一個唯一性的鍵值。在實際應用中,很容易產生的一個理解是,在 proj_id 相同的情況下,只要文件(或目錄)名稱不變,就可以確保 ftok 返回始終一致的鍵值。然而,這個理解並非完全正確,有可能給應用開發埋下很隱晦的陷阱。因爲 ftok 的實現存在這樣的風險,即在訪問同一共享內存的多個進程先後調用 ftok 函數的時間段中,如果 pathname 指定的文件(或目錄)被刪除且重新創建,則文件系統會賦予這個同名文件(或目錄)新的 i 節點信息,於是這些進程所調用的 ftok 雖然都能正常返回,但得到的鍵值卻並不能保證相同。由此可能造成的後果是,原本這些進程意圖訪問一個相同的共享內存對象,然而由於它們各自得到的鍵值不同,實際上進程指向的共享內存不再一致;如果這些共享內存都得到創建,則在整個應用運行的過程中表面上不會報出任何錯誤,然而通過一個共享內存對象進行數據傳輸的目的將無法實現。
所以如果要確保 key_t 值不變,要麼確保 ftok 的文件不被刪除,要麼不用 ftok,指定一個固定的 key_t 值。如果存在生成 key_t 值的文件被刪除過,則很有可能自己現在使用的共享內存 key_t 值會和另外一個進程的 key_t 值衝突,如下面這種情況:
進程 1 使用文件 1 來 ftok 生成了 key10000,進程 2 使用文件 2 來 ftok 生成了 key 11111,此時如果進程 1 和進程 2 都需要下載文件,並將文件的內容更新到共享內存,此時進程 1 和 2 都需要先下文件,再刪掉之前的共享內存,再使用 ftok 生成新的 key,再用這個 key 去申請新的共享內存來裝載新的問題,但是可能文件 2 比較大,下載慢,而文件 1 比較小,下載比較慢,由於文件 1 和文件 2 都被修改,此時文件 1 所佔用的文件節點號可能是文件 2 之前所佔用的,此時如果下載的文件 1 的 ftok 生成的 key 爲 11111 的話,就會和此時還沒有是否 11111 這個 key 的進程 2 的共享內存衝突,導致出現問題。
解決方法:
方法一:在有下載文件操作的程序中,對下載的文件使用 ftok 獲取 key 的時候,需要進行衝突避免的措施,如使用獨佔的方式獲取共享內存,如果不成功,則對 key 進行加一操作,再進行獲取共享內存,一直到不會產生衝突爲止。
方法二:下載文件之前,將之前的文件進行 mv 一下,先 “佔” 着這個文件節點號,防止其他共享內存申請 key 的時候獲取到。
另外, 創建進程在通知其他進程掛接的時候,建議不使用 ftok 方式來獲取 Key,而使用文件或者進程間通信的方式告知。
- 共享內存刪除的陷阱?
當進程結束使用共享內存區時,要通過函數 shmdt 斷開與共享內存區的連接。該函數聲明在 sys/shm.h 中,其原型如下:shmdt(const void *shmaddr);
參數 shmaddr 是 shmat 函數的返回值。進程脫離共享內存區後,數據結構 shmid_ds 中的 shm_nattch 就會減 1 。但是共享段內存依然存在,只有 shm_attch 爲 0 後,即沒有任何進程再使用該共享內存區,共享內存區纔在內核中被刪除。一般來說,當一個進程終止時,它所附加的共享內存區都會自動脫離。
我們通過 int shmctl(int shmid , int cmd , struct shmid_ds *buf); 來刪除已經存在的共享內存。
第一個參數,shmid,是由 shmget 所返回的標記符。
第二個參數,cmd,是要執行的動作。他可以有三個值:
IPC_STAT 設置 shmid_ds 結構中的數據反射與共享內存相關聯的值。 IPC_SET 如果進程有相應的權限,將與共享內存相關聯的值設置爲 shmid_ds 數據結構中所提供的值。 IPC_RMID 刪除共享內存段。
第三個參數,buf,是一個指向包含共享內存模式與權限的結構的指針,刪除的時候可以默認爲 0。
如果共享內存已經與所有訪問它的進程斷開了連接,則調用 IPC_RMID 子命令後,系統將立即刪除共享內存的標識符,並刪除該共享內存區,以及所有相關的數據結構;如果仍有別的進程與該共享內存保持連接,則調用 IPC_RMID 子命令後,該共享內存並不會被立即從系統中刪除,而是被設置爲 IPC_PRIVATE 狀態,並被標記爲” 已被刪除”(使用 ipcs 命令可以看到 dest 字段);直到已有連接全部斷開,該共享內存纔會最終從系統中消失。
需要說明的是:一旦通過 shmctl 對共享內存進行了刪除操作,則該共享內存將不能再接受任何新的連接,即使它依然存在於系統中!所以,可以確知,在對共享內存刪除之後不可能再有新的連接,則執行刪除操作是安全的;否則,在刪除操作之後如仍有新的連接發生,則這些連接都將可能失敗!
shmdt 和 shmctl 的區別:
Shmdt 是將共享內存從進程空間 detach 出來,使進程中的 shmid 無效化,不可以使用。但是保留空間。而 shmctl(sid,IPC_RMID,0) 則是刪除共享內存,徹底不可用,釋放空間。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/MjJbNISuYmTmLrF3FUS1tg