『壹』 共享內存原理
linux的2.2.x內核支持多種共享內存方式,如mmap()系統調用,Posix共享內存,以及系統V共享內存。
共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式。兩個不同進程A、B共享內存的意思是,同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時看到進程B對共享內存中數據的更新,反之亦然。由於多個進程共享同一塊內存區域,必然需要某種同步機制,互斥鎖和信號量都可以。
系統V共享內存原理
進程間需要共享的數據被放在一個叫做IPC共享內存區域的地方,所有需要訪問該共享區域的進程都要把該共享區域映射到本進程的地址空間中去。系統V共享內存通過shmget獲得或創建一個IPC共享內存區域,並返回相應的標識符。內核在保證shmget獲得或創建一個共享內存區,初始化該共享內存區相應的shmid_kernel結構注同時,還將在特殊文件系統shm中,創建並打開一個同名文件,並在內存中建立起該文件的相應dentry及inode結構,新打開的文件不屬於任何一個進程(任何進程都可以訪問該共享內存區)。所有這一切都是系統調用shmget完成的。
Linux 有一個系統調用叫 mmap(),這個 mmap() 可以把一個文件映射到進程的地址空間(進程使用的虛擬內存),這樣進程就可以通過讀寫這個進程地址空間來讀寫這個文件。
你可能會覺得奇怪,我明明寫的是內存啊,怎麼會變成寫文件了呢?他們之間是怎麼轉化的呢?
沒錯,你寫的確實是內存,但是你寫的這個內存不是普通的內存,你寫在這個內存上的內容,過段時間後會被內核寫到這個文件上面。而寫文件,其實最後都會變成寫數據到設備里(硬碟、Nand Flash 等)。
mmap的優點主要在為用戶程序隨機的訪問,操作,文件提供了一個方便的操作方法;其次就是為不同進程共享大批量數據提供高效的手段;另外就是對特大文件(無法一次性讀入內存)的處理提供了一種有效的方法。
內核里存在著一個特殊的文件系統,這個文件系統的存儲介質不是別的,正是 RAM。
在 shmget() 調用之後,系統會為你在這個文件系統上創建一個文件,但是這個時候僅僅是創建了這個文件。
然後你就應該調用 shmat() 了,調用 shmat() 之後,內核會使用 mmap 把這個文件映射到你的進程地址空間,這個時候你就能直接讀寫映射後的地址了。
過段時間,內核把你寫的 內容寫到了文件裡面,但是,這個文件的存儲介質是內存,所以他會怎麼做?看明白了吧?
答案:他會寫入內存呀
我們先來看看如果不使用內存映射文件的處理流程是怎樣的,首先我們得先讀出磁碟文件的內容到內存中,然後修改,最後回寫到磁碟上。第一步讀磁碟文件是要經過一次系統調用的,它首先將文件內容從磁碟拷貝到內核空間的一個緩沖區,然後再將這些數據拷貝到用戶空間,實際上是兩次數據拷貝。第三步回寫也一樣也要經過兩次數據拷貝。
所以我們基本上會有四次數據的拷貝了,因為大文件數據量很大,幾十GB甚至更大,所以拷貝的開銷是非常大的。
而內存映射文件是操作系統的提供的一種機制,可以減少這種不必要的數據拷貝,從而提高效率。它由mmap()將文件直接映射到用戶空間,mmap()並沒有進行數據拷貝,真正的數據拷貝是在缺頁中斷處理時進行的,由於mmap()將文件直接映射到用戶空間,所以中斷處理函數根據這個映射關系,直接將文件從硬碟拷貝到用戶空間,所以只進行了一次數據拷貝 ,比read進行兩次數據拷貝要好上一倍,因此,內存映射的效率要比read/write效率高。
一般來說,read write操作可以滿足大多數文件操作的要求,但是對於某些特殊應用領域所需要的幾十GB甚至更大的存儲,這種通常的文件處理方法進行處理顯然是行不通的。
mmap將一個文件或者其它對象映射進內存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最後一個頁不被使用的空間將會清零。munmap執行相反的操作,刪除特定地址區域的對象映射。
當使用mmap映射文件到進程後,就可以直接操作這段虛擬地址進行文件的讀寫等操作,不必再調用read,write等系統調用.但需注意,直接對該段內存寫時不會寫入超過當前文件大小的內容.
參考地址:
『貳』 linux下通過shmget創建的共享內存,是屬於用戶空間還是內核空間
屬於用戶空間. shmat後返回的地址空間屬於用戶空間, 不同進程可以將同一物理內存區域映射到各自的用戶空間中。該空間可以隨意讀寫。note: 一個小屁進程,在用戶態時,是沒有許可權操作內核空間的。
虛擬地址空間=用戶空間+內核空間。
『叄』 架構師進階:Linux進程間如何共享內存
共享內存 IPC 原理
共享內存進程間通信機制主要用於實現進程間大量的數據傳輸,下圖所示為進程間使用共享內存實現大量數據傳輸的示意圖:
640
共享內存是在內存中單獨開辟的一段內存空間,這段內存空間有自己特有的數據結構,包括訪問許可權、大小和最近訪問的時間等。該數據結構定義如下:
from /usr/include/linux/shm.h
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms 操作許可權 */
int shm_segsz; /* size of segment (bytes) 段長度大小 */
__kernel_time_t shm_atime; /* last attach time 最近attach時間 */
__kernel_time_t shm_dtime; /* last detach time 最近detach時間 */
__kernel_time_t shm_ctime; /* last change time 最近change時間 */
__kernel_ipc_pid_t shm_cpid; /* pid of creator 創建者pid */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator 最近操作pid */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */|
};
兩個進程在使用此共享內存空間之前,需要在進程地址空間與共享內存空間之間建立聯系,即將共享內存空間掛載到進程中。
系統對共享內存做了以下限制:
#define SHMMAX 0x2000000 /* max shared seg size (bytes) 最大共享段大小 */
#define SHMMIN 1 /* min shared seg size (bytes) 最小共享段大小 */
#define SHMMNI 4096 /* max num of segs system wide */
#define SHMALL (SHMMAX/getpagesize()*(SHMMNI/16))|
define SHMSEG SHMMNI /* max shared segs per process */
Linux 共享內存管理
1.創建共享內存
#include <sys/ipc.h> #include <sys/shm.h>
/*
* 第一個參數為 key 值,一般由 ftok() 函數產生
* 第二個參數為欲創建的共享內存段大小(單位為位元組)
* 第三個參數用來標識共享內存段的創建標識
*/
int shmget(key_t key, size_t size, int shmflg);
2.共享內存控制
#include <sys/ipc.h> #include <sys/shm.h>
/*
* 第一個參數為要操作的共享內存標識符
* 第二個參數為要執行的操作
* 第三個參數為 shmid_ds 結構的臨時共享內存變數信息
*/
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
3.映射共享內存對象
系統調用 shmat() 函數實現將一個共享內存段映射到調用進程的數據段中,並返回內存空間首地址,其函數聲明如下:
#include <sys/types.h>
#include <sys/shm.h>
/*
* 第一個參數為要操作的共享內存標識符
* 第二個參數用來指定共享內存的映射地址,非0則為此參數,為0的話由系統分配
* 第三個參數用來指定共享內存段的訪問許可權和映射條件
*/
void *shmat(int shmid, const void *shmaddr, int shmflg);
4.分離共享內存對象
在使用完畢共享內存空間後,需要使用 shmdt() 函數調用將其與當前進程分離。函數聲明如下:
#include <sys/types.h>
#include <sys/shm.h>
/*
* 參數為分配的共享內存首地址
*/
int shmdt(const void *shmaddr);
共享內存在父子進程間遵循的約定
1.使用 fork() 函數創建一個子進程後,該進程繼承父親進程掛載的共享內存。
2.如果調用 exec() 執行一個新的程序,則所有掛載的共享內存將被自動卸載。
3.如果在某個進程中調用了 exit() 函數,所有掛載的共享內存將與當前進程脫離關系。
程序實例
申請一段共享內存,父進程在首地址處存入一整數,子進程讀出。
#include
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include
#include
#define SHM_SIZE 1024
int main()
{
int shm_id, pid;
int *ptr = NULL;
/* 申請共享內存 */
shm_id = shmget((key_t)1004, SHM_SIZE, IPC_CREAT | 0600);
/* 映射共享內存到進程地址空間 */
ptr = (int*)shmat(shm_id, 0, 0);
printf("Attach addr is %p ", ptr);
*ptr = 1004;
printf("The Value of Parent is : %d ", *ptr);
if((pid=fork()) == -1){
perror("fork Err");
exit(0);
}
else if(!pid){
printf("The Value of Child is : %d ", *ptr);
exit(0);
}else{
sleep(1);
/* 解除映射 */
shmdt(ptr);
/* 刪除共享內存 */
shmctl(shm_id, IPC_RMID, 0);
}
return 0;
}
輸出結果:
640
『肆』 linux共享內存的內存模型
要使用一塊共享內存,進程必須首先分配它。隨後需要訪問這個共享內存塊的每一個進程都必須將這個共享內存綁定到自己的地址空間中。當完成通信之後,所有進程都將脫離共享內存,並且由一個進程釋放該共享內存塊。
理解 Linux 系統內存模型可以有助於解釋這個綁定的過程。在 Linux 系統中,每個進程的虛擬內存是被分為許多頁面的。這些內存頁面中包含了實際的數據。每個進程都會維護一個從內存地址到虛擬內存頁面之間的映射關系。盡管每個進程都有自己的內存地址,不同的進程可以同時將同一個內存頁面映射到自己的地址空間中,從而達到共享內存的目的。
分配一個新的共享內存塊會創建新的內存頁面。因為所有進程都希望共享對同一塊內存的訪問,只應由一個進程創建一塊新的共享內存。再次分配一塊已經存在的內存塊不會創建新的頁面,而只是會返回一個標識該內存塊的標識符。一個進程如需使用這個共享內存塊,則首先需要將它綁定到自己的地址空間中。這樣會創建一個從進程本身虛擬地址到共享頁面的映射關系。當對共享內存的使用結束之後,這個映射關系將被刪除。當再也沒有進程需要使用這個共享內存塊的時候,必須有一個(且只能是一個)進程負責釋放這個被共享的內存頁面。
所有共享內存塊的大小都必須是系統頁面大小的整數倍。系統頁面大小指的是系統中單個內存頁麵包含的位元組數。在 Linux 系統中,內存頁面大小是4KB,不過您仍然應該通過調用 getpagesize 獲取這個值。
『伍』 如何設置linux的共享內存
首先先使用shmget建立一塊共享內存,然後向該內存中寫入數據並返回該共享內存shmid
使用另一個程序通過上一程序返回的shmid讀該共享內存內的數據
建立共享內存並寫入數據的程序
#include<stdio.h>
#include<string.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdlib.h>
#include<errno.h>
voidget_buf(char*buf)
{
inti=0;
while((buf[i]=getchar())!=' '&&i<1024)
i++;
}
intmain(void)
{
intshmid;
shmid=shmget(IPC_PRIVATE,sizeof(char)*1024,IPC_CREAT|0666);
if(shmid==-1)
{
perror("shmget");
}
char*buf;
if((int)(buf=shmat(shmid,NULL,0))==-1)
{
perror("shmat");
exit(1);
}
get_buf(buf);
printf("%d ",shmid);
return0;
}
讀取數據的程序
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdlib.h>
intmain(intargc,char**argv)
{
intshmid;
shmid=atoi(argv[1]);
char*buf;
if((int)(buf=shmat(shmid,NULL,0))==-1)
{
perror("shmat");
exit(1);
}
printf("%s ",buf);
shmdt(buf);
return0;
}
命令行的第一個參數設為第一個程序輸出的數字
如
使用完以後可以使用
ipcrm -m 19562507
來刪除該共享內存
『陸』 探討一下 Linux 共享內存的 N 種方式
關於 Linux 共享內存,寫得最好的應該是宋寶華的 《世上最好的共享內存》 一文。
本文可以說是對這篇文章的學習筆記,順手練習了一下 rust libc —— shichaoyuan/learn_rust/linux-shmipc-demo
按照宋寶華的總結,當前有四種主流的共享內存方式:
前兩種方式比較符合傳統的用法,共享內存做為進程間通信的媒介。
第三種方式更像是通過傳遞內存「句柄」進行數據傳輸。
第四種方式是為設備間傳遞數據設計,避免內存拷貝,直接傳遞內存「句柄」。
這里嘗試了一下第二種和第三種方式。
這套 API 應該是最普遍的 —— shm_open + mmap,本質上來說 Aeron 也是用的這種方式(關於 Aeron 可以參考 我之前的文章 )。
看一下 glibc 中 shm_open 函數的實現就一清二楚了:
shm_open 函數就是在 /dev/shm 目錄下建文件,該目錄掛載為 tmpfs,至於 tmpfs 可以簡單理解為存儲介質是內存的一種文件系統,更准確的理解可以參考官方文檔 tmpfs.txt 。
然後通過 mmap 函數將 tmpfs 文件映射到用戶空間就可以隨意操作了。
優點:
這種方式最大的優勢在於共享的內存是有「實體」(也就是 tmpfs 中的文件)的,所以多個進程可以很容易通過文件名這個信息構建共享內存結構,特別適合把共享內存做為通信媒介的場景(例如 Aeron )。
缺點:
如果非要找一個缺點的話,可能是,文件本身獨立於進程的生命周期,在使用完畢後需要注意刪除文件(僅僅 close 是不行的),否則會一直佔用內存資源。
memfd_create 函數的作用是創建一個匿名的文件,返回對應的 fd,這個文件當然不普通,它存活在內存中。更准確的理解可以參考官方文檔 memfd_create(2) 。
直觀理解,memfd_create 與 shm_open 的作用是一樣的,都是創建共享內存實體,只是 memfd_create 創建的實體是匿名的,這就帶了一個問題:如何讓其它進程獲取到匿名的實體?shm_open 方式有具體的文件名,所以可以通過打開文件的方式獲取,那麼對於匿名的文件怎麼處理呢?
答案是:通過 Unix Domain Socket 傳遞 fd。
rust 的 UDS 實現:
rust 在 std 中已經提供了 UDS 的實現,但是關於傳遞 fd 的 send_vectored_with_ancillary 函數還屬於 nightly-only experimental API 階段。所以這里使用了一個三方 crate —— sendfd ,坦白說可以自己實現一下,使用 libc 構建好 SCM_RIGHTS 數據,sendmsg 出去即可,不過細節還是挺多,我這里就放棄了。
這套 API 設計更靈活,直接拓展了我的思路,本來還是受限於 Aeron 的用法,如果在這套 API 的加持下,是否可以通過傳遞數據包內存塊(fd)真正實現零拷貝呢?
優點:
靈活。
缺點:
無
『柒』 共享內存 linux下怎麼跑
linux 共享內存實現
說起共享內存,一般來說會讓人想起下面一些方法:
1、多線程。線程之間的內存都是共享的。更確切的說,屬於同一進程的線程使用的是同一個地址空間,而不是在不同地址空間之間進行內存共享;
2、父子進程間的內存共享。父進程以MAP_SHARED|MAP_ANONYMOUS選項mmap一塊匿名內存,fork之後,其子孫進程之間就能共享這塊內存。這種共享內存由於受到進程父子關系的限制,一般較少使用;
3、mmap文件。多個進程mmap到同一個文件,實際上就是大家在共享文件pagecache中的內存。不過文件牽涉到磁碟的讀寫,用來做共享內存顯然十分笨重,所以就有了不跟磁碟扯上關系的內存文件,也就是我們這里要討論的tmpfs和shmem;
tmpfs是一套虛擬的文件系統,在其中創建的文件都是基於內存的,機器重啟即消失。
shmem是一套ipc,通過相應的ipc系統調用shmget能夠以指定key創建一塊的共享內存。需要使用這塊內存的進程可以通過shmat系統調用來獲得它。
雖然是兩套不同的介面,但是在內核裡面的實現卻是同一套。shmem內部掛載了一個tmpfs分區(用戶不可見),shmget就是在該分區下獲取名為"SYSV${key}"的文件。然後shmat就相當於mmap這個文件。
所以我們接下來就把tmpfs和shmem當作同一個東西來討論了。
tmpfs/shmem是一個介於文件和匿名內存之間的東西。
一方面,它具有文件的屬性,能夠像操作文件一樣去操作它。它有自己inode、有自己的pagecache;
另一方面,它也有匿名內存的屬性。由於沒有像磁碟這樣的外部存儲介質,內核在內存緊缺時不能簡單的將page從它們的pagecache中丟棄,而需要swap-out;(參閱《linux頁面回收淺析》)
對tmpfs/shmem內存的讀寫,就是對pagecache中相應位置的page所代表的內存進行讀寫,這一點跟普通的文件映射沒有什麼不同。
如果進程地址空間的相應位置尚未映射,則會建立到pagecache中相應page的映射;
如果pagecache中的相應位置還沒有分配page,則會分配一個。當然,由於不存在磁碟上的源數據,新分配的page總是空的(特別的,通過read系統調用去讀一個尚未分配page的位置時,並不會分配新的page,而是共享ZERO_PAGE);
如果pagecache中相應位置的page被回收了,則會先將其恢復;
對於第三個「如果」,tmpfs/shmem和普通文件的page回收及其恢復方式是不同的:
page回收時,跟普通文件的情況一樣,內核會通過prio_tree反向映射找到映射這個page的每一個pagetable,然後將其中對應的pte清空。
不同之處是普通文件的page在確保與磁碟同步(如果page為臟的話需要刷回磁碟)之後就可以丟棄了,而對於tmpfs/shmem的page則需要進行swap-out。
注意,匿名page在被swap-out時,並不是將映射它的pte清空,而是得在pte上填寫相應的swap_entry,以便知道page被換出到哪裡去,否則再需要這個page的時候就沒法swap-in了。
而tmpfs/shmem的page呢?pagetable中對應的pte被清空,swap_entry會被存放在pagecache的radix_tree的對應slot上。
等下一次訪問觸發pagefault時,page需要恢復。
普通文件的page恢復跟page未分配時的情形一樣,需要新分配page、然後根據映射的位置重新從磁碟讀出相應的數據;
而tmpfs/shmem則是通過映射的位置找到radix_tree上對應的slot,從中得到swap_entry,從而進行swap-in,並將新的page放回pagecache;
這里就有個問題了,在pagecache的radix_tree的某個slot上,怎麼知道裡面存放著的是正常的page?還是swap-out後留下的swap_entry?
如果是swap_entry,那麼slot上的值將被加上RADIX_TREE_EXCEPTIONAL_ENTRY標記(值為2)。swap_entry的值被左移兩位後OR上RADIX_TREE_EXCEPTIONAL_ENTRY,填入slot。
也就是說,如果${slot}&RADIX_TREE_EXCEPTIONAL_ENTRY!=0,則它代表swap_entry,且swap_entry的值是${slot}>>2;否則它代表page,${slot}就是指向page的指針,當然其值可能是NULL,說明page尚未分配。
那麼顯然,page的地址值其末兩位肯定是0,否則就可能跟RADIX_TREE_EXCEPTIONAL_ENTRY標記沖突了;而swap_entry的值最大隻能是30bit或62bit(對應32位或64位機器),否則左移兩位就溢出了。
最後以一張圖說明一下匿名page、文件映射page、tmpfs/shmempage的回收及恢復過程:
『捌』 Linux進程通信實驗(共享內存通信,接上篇)
這一篇記錄一下共享內存實驗,需要linux的共享內存機制有一定的了解,同時也需要了解POSIX信號量來實現進程間的同步。可以參考以下兩篇博客: https://blog.csdn.net/sicofield/article/details/10897091
https://blog.csdn.net/ljianhui/article/details/10253345
實驗要求:編寫sender和receiver程序,sender創建一個共享內存並等待用戶輸入,然後把輸入通過共享內存發送給receiver並等待,receiver收到後把消息顯示在屏幕上並用同樣方式向sender發送一個over,然後兩個程序結束運行。
這個實驗的難點主要在於共享內存的創建和撤銷(涉及到的步驟比較多,需要理解各步驟的功能),以及實現兩個進程間的相互等待(使用信號量來實現,這里使用了有名信號量)
實驗心得:學習理解了linux的共享內存機制以及POSIX信號量機制。
兩個實驗雖然加強了對linux一些機制的理解,但是感覺對linux的學習還不夠,需要繼續學習。