--摘自窮佐羅的Linux書
共享內存用處
使用文件或者管道進行進程間通信會有很多局限性。管道只能在父進程和子進程間使用;通過文件共享,在處理效率上又差一些,而且訪問文件描述符不如訪問內存地址方便。
Linux系統在編程上提供的共享內存方案有三種:
- mmap內存共享映射
- XSI共享內存
- POSIX共享內存
mmap內存共享映射
mmap本來是存儲映射功能。它可以將一個文件映射到內存中,在程序里就可以直接使用內存地址對文件內容進行訪問。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int port, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
Linux通過系統調用fork派生出的子進程和父進程共用內存地址空間,Linux的mmap實現了一種可以在父子進程之間共享內存地址的方式。
- 父進程將flags參數設置MAP_SHARED方式通過mmap申請一段內存。內存可以映射某個具體文件(fd),也可以不映射具體文件(fd置為-1,flag設置為MAP_ANONYMOUS).
- 父進程調用fork產生子進程,之后在父子進程內都可以訪問到mmap所返回的地址,就可以共享內存了。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <sys/mman.h>
#define COUNT 100
int do_child(int *count)
{
int interval;
// critical section
interval = *count;
interval++;
usleep(1);
*count = interval;
// critical section
exit(0);
}
int main()
{
pid_t pid;
int count;
int *shm_p;
shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if(MAP_FAILED == shm_p) {
perror("mmap()");
exit(1);
}
*shm_p = 0;
for(count = 0; count < COUNT; count++) {
pid = fork();
if(pid < 0) {
perror("fork()");
exit(1);
}
if(pid == 0) {
do_child(shm_p);
}
}
for(count = 0; count < COUNT; count++) {
wait(NULL);
}
printf("shm_p: %d\n", *shm_p);
munmap(shm_p, sizeof(int));
exit(0);
}
這段共享內存的使用是有競爭條件的。進程間通信不僅僅是通信這么簡單,還要處理類似的這樣的臨界區代碼。在這里,可以采用文件鎖進行處理。但是共享內存使用文件鎖顯得不太協調。除了不方便和效率低下以外,文件鎖還不能進行更高級的進程控制。這里可以使用信號量這種更高級的進程同步控制原語來實現相關功能。
下面這段程序用來幫助理解mmap的內存占用情況。
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
#include<sys/file.h>
#include<sys/wait.h>
#include<sys/mman.h>
#define COUNT 100
#define MEMSIZE 1024*1024*1023*2
int main()
{
pid_t pid;
int count;
void *shm_p;
shm_p = mmap(NULL, MEMSIZE, PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if(MAP_FAILED == shm_p) {
perror("mmap()");
exit(1);
}
bzero(shm_p, MEMSIZE);
sleep(3000);
munmap(shm_p, MEMSIZE);
exit(0);
}
申請了一段近2G的內存,並置0.觀察內存變化
[zorro@zorrozou-pc0 sharemem]$ free -g
total used free shared buff/cache available
Mem: 15 2 2 0 10 11
Swap: 31 0 31
[zorro@zorrozou-pc0 sharemem]$ ./mmap_mem &
[1] 32036
[zorro@zorrozou-pc0 sharemem]$ free -g
total used free shared buff/cache available
Mem: 15 2 0 2 12 9
Swap: 31 0 31
可以看出,這段內存被記錄到shared和buff/cache中了。
mmap有一個缺點,那就是共享的內存只能在父進程和fork產生的子進程間使用,除此之外的其它進程無法得到共享內存段的地址。
XSI共享內存
XSI是X/Open組織對UNIX定義的一套接口標准(X/Open System Interface)。XSI共享內存在Linux底層的實現實際上跟mmap沒有什么本質不同,只是在使用方法上有所區別。
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
#include<sys/types.h>
#include<sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
shmget的第三個參數,指定創建標志。支持的標志為:IPC_CREAT、IPC_EXCL。從Linux 2.6之后,還引入了支持大頁的共享內存,標志為:SHM_HUGETLB、SHM_HUGE_2MB等。shemget除了可以創建一個新的共享內存外,還可以訪問一個已經存在的內存,此時可以將shmflg置為0,不加任何標志打開。
shmget返回的int類型的shmid類似於文件描述符,注意只是類似,而並非同樣的實現,所以,不能用select、poll、epoll這樣的方法去控制一個XSI共享內存。對於一個XSI共享內存,其key是系統全局唯一的,這就方便其它進程使用同樣的key,打開同樣一段共享內存,以便進行進程間通信。而是用fork產生的子進程,可以直接通過shmid訪問到相關共享內存段。這就是key的本質:系統中對XSI共享內存的全局唯一表示符。
#include<sys/types.h>
#include<sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
key是通過ftok函數,使用一個約定好的文件名和proj_id生成的。ftok不會創建文件,所以必須指定一個存在並且進程可以訪問的pathname路徑。另外,ftok並不是根據文件的路徑和文件名生成key的,在具體實現上,它使用的是指定文件的inode編號和文件所在設備的設備編號。所以,不同的文件名也可能得到同一個key(不同的文件名指向同一個inode,硬鏈接)。同樣的文件名也不一定就能得到相同的key,一個文件名有可能被刪除重建,這種行為會導致inode變化。
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
#include<sys/file.h>
#include<sys/wait.h>
#include<sys/mman.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#define COUNT 100
#define PATHNAME "/etc/passwd"
int do_child(int proj_id)
{
int interval;
int *shm_p, shm_id;
key_t shm_key;
if((shm_key = ftok(PATHNAME, proj_id)) == -1) {
perror("ftok()");
exit(1);
}
shm_id = shmget(shm_key, sizeof(int), 0);
if(shm_id < 0)
{
perror("shmget()");
exit(1);
}
//使用shmat將相關共享內存映射到本進程的內存地址
shm_p = (int *)shmat(shm_id, NULL, 0);
if((void *)shm_p == (void *)-1)
{
perror("shmat()");
exit(1);
}
// critical section
interval = *shm_p;
interval++;
usleep(1);
*shm_p = interval;
// critical section
//使用shmdt解除本進程內存對共享內存的地址映射,本操作不會刪除共享內存
if(shmdt(shm_p) < 0){
perror("shmdt()");
exit(1);
}
exit(0);
}
int main()
{
pid_t pid;
int count;
int *shm_p;
int shm_id, proj_id;
key_t shm_key;
proj_id = 1234;
if((shm_key = ftok(PATHNAME, proj_id)) == -1)
{
perror("ftok()");
exit(1);
}
//使用shm_key創建一個共享內存,如果系統中已經存在此共享內存,則報錯退出。創建出來的共享內存權限為0600
shm_id = shmget(shm_key, sizeof(int), IPC_CREAT|IPC_EXCL|0600);
if(shm_id < 0) {
perror("shmget()");
exit(1);
}
shm_p = (int *)shmat(shm_id, NULL, 0);
if((void *)shm_p == (void *) -1)
{
perror("shmat()");
exit(1);
}
*shm_p = 0;
for(count = 0; count < COUNT; count++) {
pid = fork();
if(pid < 0) {
perror("fork()");
exit(1);
}
if(pid == 0) {
do_child(proj_id);
}
}
for(count = 0; count < COUNT; count ++) {
wait(NULL);
}
printf("shm_p: %d\n", *shm_p);
if(shmdt(shm_p) < 0) {
perror("shmdt()");
exit(1);
}
if(shmctl(shm_id, IPC_RMID, NULL) < 0) {
perror("shmctl");
exit(1);
}
exit(0);
}
在某些情況下,也可以不通過一個key來創建共享內存。此時可以在key的參數所在位置填IPC_PRIVATE,這樣內核會在保證不沖突的共享內存段id的情況下新建一段共享內存。因為只能是創建,所以flag位一定是IPC_CREAT。可以將shmid傳給子進程。
當獲取到shmid之后,就可以使用shmat來進行地址映射。shmat之后,通過訪問返回的當前進程的虛擬地址就可以訪問到共享內存段了。注意使用之后要調用shmdt解除映射,否則對於長期運行的程序,可能會造成虛擬內存地址泄露。shmdt並不能刪除共享內存段,只是解除共享內存段和進程虛擬地址的映射關系。只要shmid對應的共享內存段還存在,就可以使用shmat繼續映射使用。想要刪除一個共享內存段,需要使用shmctl的IPC_RMID指令處理,或者在命令行中使用ipcrm刪除指定的共享內存id或key。
shmctl還可以查看、修改共享內存的相關屬性,可以在man 2 shmctl中查看。在系統中還可以使用ipcs -m 命令查看系統中所有共享內存的信息。
ipcs - provide information on ipc facilities
ipcs [-asmq] [-tclup]
ipcs [-smq] -i id
-m 共享內存
-q 消息隊列
-s 信號量數組
-a all(缺省)
輸出選項:
-t time
-p pid
-c creator
-l limits
-u summary
在Linux系統中,使用XSI共享內存調用shmget時,可以通過設置shmflg參數來申請大頁內存(huge pages)。
SHM_HUGETLB(since Linux 2.6)
SHM_HUGE_2MB, SHM_HUGE_1GB(since Linux 3.8)
使用大頁內存的好處是提高內核對內存管理的處理效率。因為在相同內存大小的情況下,使用大頁內存(2M一頁)將比使用一般內存頁(4K一頁)的內存頁管理的數量大大減少,從而減少內存頁表項的緩存壓力和CPU cache緩存內存地址的映射壓力。但是需要注意一些地方:
- 大頁內存不能交換(SWAP)
- 使用不當時可能造成更大的內存泄露
- 大頁內存需要使用root權限
- 需要修改系統配置
shm_id = shmget(IPC_PRIVATE, MEMSIZE, SHM_HUGETLB|0600)
如果要申請2G以下的大頁內存,需要系統預留2G以上的大頁內存。
echo 2048 > /proc/sys/vm/nr_hugepages
cat /proc/meminfo | grep -i huge
AnonHugePages: 841728 KB
HugePages_Total: 2020
HugePages_Free: 2020
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
2048是頁數,每頁2M。
還需要注意共享內存的限制:
echo 2147483648 > /proc/sys/kernel/shmmax
echo 33554432 > /proc/sys/kernel/shmall
/proc/sys/kernel/shmall:限制系統用在共享內存上的內存頁總數。一頁一般是4k(可以通過getconf PAGE_SIZE查看)
/proc/sys/kernel/shmmax:限制一個共享內存段的最大長度,單位是字節
/proc/sys/kernel/shmmni:限制整個系統可以創建的最大的共享內存段的個數
POSIX共享內存
POSIX共享內存實際上毫無新意,它本質上是mmap對文件的共享方式映射,只不過映射的是tmpfs文件系統上的文件。
tmpfs是將一部分內存空間用作文件系統,一般掛在/dev/shm目錄。
Linux提供的POSIX共享內存,實際上就是在/dev/shm下創建一個文件,並將其mmap之后映射其內存地址即可。可以通過man shm_overview查看使用方法。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <sys/mman.h>
#define COUNT 100
#define SHMPATH "shm"
int do_child(char * shmpath)
{
int interval, shmfd, ret;
int *shm_p;
// 使用shm_open訪問一個已經創建的POSIX共享內存
shmfd = shm_open(shmpath, O_RDWR, 0600);
if (shmfd < 0) {
perror("shm_open()");
exit(1);
}
// 用mmap將對應的tmpfs文件映射到本進程內存 */
shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED, shmfd, 0);
if (MAP_FAILED == shm_p) {
perror("mmap()");
exit(1);
}
/* critical section */
interval = *shm_p;
interval++;
usleep(1);
*shm_p = interval;
/* critical section */
munmap(shm_p, sizeof(int));
close(shmfd);
exit(0);
}
int main()
{
pid_t pid;
int count, shmfd, ret;
int *shm_p;
/* 創建一個POSIX共享內存 */
shmfd = shm_open(SHMPATH, O_RDWR|O_CREAT|O_TRUNC, 0600);
if (shmfd < 0) {
perror("shm_open()");
exit(1);
}
/* 使用ftruncate設置共享內存段大小 */
ret = ftruncate(shmfd, sizeof(int));
if (ret < 0) {
perror("ftruncate()");
exit(1);
}
/* 使用mmap將對應的tmpfs文件映射到本進程內存 */
shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED, shmfd, 0);
if (MAP_FAILED == shm_p) {
perror("mmap()");
exit(1);
}
*shm_p = 0;
for (count=0;count<COUNT;count++) {
pid = fork();
if (pid < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
do_child(SHMPATH);
}
}
for (count=0;count<COUNT;count++) {
wait(NULL);
}
printf("shm_p: %d\n", *shm_p);
munmap(shm_p, sizeof(int));
close(shmfd);
shm_unlink(SHMPATH);
exit(0);
}
編譯該段代碼的時候需要指定一個庫,-lrt,這是linux的real time庫。
- shm_open的SHMPATH參數是一個路徑,這個路徑默認放在系統的/dev/shm目錄下。這是shm_open封裝好的,保證文件一定在tmpfs下。
- 使用ftruncate改變共享內存的大小,實際就是改變文件的長度。
- shm_unlink實際就是unlink系統調用的封裝。如果不做unlink操作,那么文件會一直存在/dev/shm目錄下。
- 關閉共享內存描述符,使用close.
修改共享內存內核配置
- SHMMAX
一個進程可以在它的虛擬地址空間分配給一個共享內存端的最大大小(單位是字節)
echo 2147483648 > /proc/sys/kernel/shmmax
或
sysctl -w kernel.shmmax=2147483648
或
echo "kenerl.shmmax=2147483648" >> /etc/sysctl.conf
- SHMMNI
系統范圍內共享內存段的數量
echo 4096 > /proc/sys/kernel/shmmni
或
sysctl -w kernel.shmmni=4096
或
echo "kernel.shmmni=4096" >> /etc/sysctl.conf
- SHMALL
這個參數設置了系統范圍內共享內存可以使用的頁數。單位是PAGE_SIZE(通常是4096,可以通過getconf PAGE_SIZE
獲得)。
echo 2097152 > /proc/sys/kernel/shmall
或
sysctl -w kernel.shmall=2097152
或
echo "kernel.shmall=2097152" >> /etc/sysctl.conf
- 移除共享內存
執行ipcs -m
查看系統所有的共享內存。如果status
字段是dest
,表明這段共享內存需要被刪除。
ipcs -m -i $shmid