引言 - 背景 2016 年寫過一篇關於 linux 共用記憶體 shm api 掃盲文. C擴展 從共用記憶體shm到memcache外部記憶體 比較簡單. 沒有深入分析(能力有限, 也深入分析不了). 3年(2019)過去了. 本質而言共用記憶體這種編程活化石般 雙刃劍, 像 "redis" 這種分散式 ...
引言 - 背景
2016 年寫過一篇關於 linux 共用記憶體 shm api 掃盲文. C擴展 從共用記憶體shm到memcache外部記憶體
比較簡單. 沒有深入分析(能力有限, 也深入分析不了). 3年(2019)過去了. 本質而言共用記憶體這種編程活化石般
雙刃劍, 像 "redis" 這種分散式記憶體資料庫完全可以替代它做想做的業務(硬體過剩). 這裡為什麼繼續鞭屍呢?
想要為這種最快的數據交互 IPC 方式, 做個多平臺移植實戰代碼. 更加詳細底層原理和麵試問題, 請自行搜索.
好的疑惑和思索可以分享個博主, 共同提升.
前言 - shm api 介紹
先帶大家熟悉 linux 和 winds 共用記憶體 shm 相關的 api 介紹. 編程很簡單, 無外乎學習和實踐. 當然不寫
代碼, 狂看原理的, 當我沒說. 畢竟身居高位, 指點江山, 都曾經有過夢想, 也流過無數智慧 ~
1. linux shm 相關 api 熟悉
linux shm 有兩套 api 這裡還是只說 System V 標準的 4 個系統 api
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> /* * System V 風格的 IPC 函數實現共用記憶體介面 */ // // shmctl - 共用記憶體控制操作 // __shmid : 共用記憶體標識符, shmget () 返回返回值 // __cmd : IPC_STAT -> 得到共用記憶體的狀態, 把共用記憶體的 shmid_ds 結構複製到 buf 中 // : IPC_SET -> 改變共用記憶體的狀態, 把 buf 複製到共用記憶體的 shmid_ds 結構內 // : IPC_RMID -> 刪除這片共用記憶體 // __buf : 共用記憶體管理結構體 // return : 0 正確, 出錯 -1, 錯誤原因查 errno 表 // : EINVAL -> 參數 size 小於 SHMMIN 或大於 SHMMAX // : EEXIST -> 預建立 key 所致的共用記憶體, 但已經存在 // : EIDRM -> 參數 key 所致的共用記憶體已經刪除 // : ENOSPC -> 超過了系統允許建立的共用記憶體的最大值 SHMALL // : ENOENT -> key 所指的共用記憶體不存在, 參數 shmflg 也未設 IPC_CREAT 位 // : EACCES -> 沒有許可權 // : ENOMEM -> 核心記憶體不足 // // extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW; // // shmget - 得到共用記憶體段 // __key : 新建或者獲取已經存在的共用記憶體的 key // __size : 創建共用記憶體的大小, 獲取設置預設 0 // __shmflg : 標誌集合 // : IPC_CREAT -> 若不存在則創建, 需要在 shmflg 中 '|許可權信息' |0664 若存在則打開 // : IPC_EXCL -> 與 IPC_CREAT 搭配使用, 若存在則創建失敗 errno == EEXIST // : 0 -> 獲取已經存在的共用記憶體 // return : __shmid, 出錯 -1 // extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW; // // shmat - 附加共用記憶體段 // __shmid : 共用記憶體標識符, shmget () 返回返回值 // __shmaddr: NULL 表示由系統選擇 // : 非 NULL 且 shmflg 是 SHM_RND, 會按照頁對齊的原則從 shmaddr 開始 // 找最近的地址開始分配, 否則 shmaddr 指定的地址必須是頁對齊的 // __shmflg : 0 -> 預設操作標誌 // : SHM_RDONLY -> 表示掛接到該共用記憶體的進程必須有讀許可權 // : SHM_REMAP -> 表示如果要映射的共用記憶體已經有現存的記憶體, 那麼就將舊的替換 // return : 成功返回映射記憶體的地址, 失敗返回 (void *)-1 設 errno // extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg) __THROW; // // shmdt - 分離共用記憶體段 // __shmaddr: 共用記憶體標識符, shmat() 返回值 // return : 成功返回 0, 失敗返回 -1 設 errno // extern int shmdt (const void *__shmaddr) __THROW;
對於 shm_ctl 用到的相關結構和巨集截取部分如下
/* Mode bits for `msgget', `semget', and `shmget'. */ #define IPC_CREAT 01000 /* Create key if key does not exist. */ #define IPC_EXCL 02000 /* Fail if key exists. */ #define IPC_NOWAIT 04000 /* Return error on wait. */ /* Control commands for `msgctl', `semctl', and `shmctl'. */ #define IPC_RMID 0 /* Remove identifier. */ #define IPC_SET 1 /* Set `ipc_perm' options. */ #define IPC_STAT 2 /* Get `ipc_perm' options. */ #ifdef __USE_GNU # define IPC_INFO 3 /* See ipcs. */ #endif /* Special key values. */ #define IPC_PRIVATE ((__key_t) 0) /* Private key. */ /* Data structure used to pass permission information to IPC operations. */ struct ipc_perm { __key_t __key; /* Key. */ __uid_t uid; /* Owner's user ID. */ __gid_t gid; /* Owner's group ID. */ __uid_t cuid; /* Creator's user ID. */ __gid_t cgid; /* Creator's group ID. */ unsigned short int mode; /* Read/write permission. */ unsigned short int __pad1; unsigned short int __seq; /* Sequence number. */ unsigned short int __pad2; __syscall_ulong_t __glibc_reserved1; __syscall_ulong_t __glibc_reserved2; }; /* Permission flag for shmget. */ #define SHM_R 0400 /* or S_IRUGO from <linux/stat.h> */ #define SHM_W 0200 /* or S_IWUGO from <linux/stat.h> */ /* Flags for `shmat'. */ #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 */ #define SHM_EXEC 0100000 /* execution access */ /* Commands for `shmctl'. */ #define SHM_LOCK 11 /* lock segment (root only) */ #define SHM_UNLOCK 12 /* unlock segment (root only) */ __BEGIN_DECLS /* Segment low boundary address multiple. */ #define SHMLBA (__getpagesize ()) extern int __getpagesize (void) __THROW __attribute__ ((__const__)); /* Type to count number of attaches. */ typedef __syscall_ulong_t shmatt_t; /* Data structure describing a shared memory segment. */ struct shmid_ds { struct ipc_perm shm_perm; /* operation permission struct */ size_t shm_segsz; /* size of segment in bytes */ __time_t shm_atime; /* time of last shmat() */ #ifndef __x86_64__ unsigned long int __glibc_reserved1; #endif __time_t shm_dtime; /* time of last shmdt() */ #ifndef __x86_64__ unsigned long int __glibc_reserved2; #endif __time_t shm_ctime; /* time of last change by shmctl() */ #ifndef __x86_64__ unsigned long int __glibc_reserved3; #endif __pid_t shm_cpid; /* pid of creator */ __pid_t shm_lpid; /* pid of last shmop */ shmatt_t shm_nattch; /* number of current attaches */ __syscall_ulong_t __glibc_reserved4; __syscall_ulong_t __glibc_reserved5; };
(滿屏的惡意, 直接從系統 include 拔下來, 木有強迫症) 基於上面大概. 寫個 demo 帶大家熟悉其用法
#include <stdio.h> #include <errno.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> // str_hash_rs - Robert Sedgwicks Hash Function unsigned str_hash_rs(const char * s) { register unsigned hash = 0; for (unsigned c, a = 63689; (c = *s); ++s) { hash = hash * a + c; a *= 378551; } return hash & 0x7FFFFFFF; } #define STR_SHM "shm_1_to_2" #define INT_SHM 4096 // shm_init - // shm_init - init 操作, -1 error, 0 init create, 1 exists int shm_init(key_t key, size_t size); // // 文件 : shm.c // 目標 : // 1' 熟悉 Linux System V shm 機制 // 2' 第一次創建共用記憶體, 寫入數據 // 3' 第二次讀取共用記憶體, 隨後銷毀 // int main(int argc, char * argv[]) { char cmd[BUFSIZ]; key_t key = (key_t)str_hash_rs(STR_SHM); printf("name = %s -> key = %d\n", STR_SHM, key); // shm init int ret = shm_init(key, INT_SHM); if (ret < 0) { perror("shm_init "STR_SHM); return EXIT_FAILURE; } if (ret == 0) { // shm 剛創建 return EXIT_SUCCESS; } // ret == 1 shm 已經存在我們開始獲取共用記憶體 int shmid = shmget(key, 0, 0); if (shmid < 0) { perror("shmget"); return EXIT_FAILURE; } printf("key = %d -> shmid = %d\n", key, shmid); // 開始附加共用記憶體段 void * shmaddr = shmat(shmid, NULL, 0); if ((intptr_t)shmaddr < 0) { perror("shmat shmid "STR_SHM); return EXIT_FAILURE; } printf("shmid = %d, shmaddr = %p\n", shmid, shmaddr); snprintf(cmd, sizeof cmd, "ipcs -m -i %d", shmid); // 開始操作記憶體 long * ptr = shmaddr; printf("ptr = %p, *ptr = %ld\n", ptr, *ptr); long old = __sync_lock_test_and_set(ptr, 1); if (old) { // 共用記憶體分離 printf("__sync_lock_test_and_set old = %ld, *ptr = %ld\n", old, *ptr); return shmdt(shmaddr); } printf("Second run now old = %ld, *ptr = %ld\n", old, *ptr); __sync_lock_release(ptr); printf("shmid = %d, shmdt after\n> %s\n", shmid, cmd); system(cmd); // 記憶體分離 shmdt(shmaddr); // 刪除共用記憶體 shmctl(shmid, IPC_RMID, NULL); // 最後測試共用記憶體狀態 printf("shmid = %d, shmdt after\n> %s\n", shmid, cmd); system(cmd); return EXIT_SUCCESS; } // shm_init - init 操作, -1 error, 0 init create, 1 exists int shm_init(key_t key, size_t size) { // 通過 key 創建共用記憶體 or 打開共用記憶體 int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL|0666); if (shmid < 0) { if (errno == EEXIST) { // 當前的共用記憶體已經存在 return 1; } return shmid; } // create shm and shm at void * shmaddr = shmat(shmid, NULL, 0); if ((intptr_t)shmaddr < 0) { return -1; } // memset zero memset(shmaddr, 0, size); // shm dt return shmdt(shmaddr); }
代碼極其適合感受和練習. 寫完後相應的 api 操作就熟悉了. 練習代碼的設計思路是, 跑第一次初始化共用記憶體.
跑第二次輸出共用記憶體中數據. 附帶說一點 linux 可以通過下麵命令查看和刪除共用記憶體
ipcs -m ipcrm -m [shmid] ipcs -m | awk -F' ' 'NR!=1{print $2}'
詳細展示是這樣的
wang@zhi:~$ ipcs -m ------------ 共用記憶體段 -------------- 鍵 shmid 擁有者 許可權 位元組 連接數 狀態 0x00000000 753664 wang 600 524288 2 目標 0x00000000 622593 wang 600 524288 2 目標 ipcrm -m [shmid]
順帶看 demo.c 編譯完執行後輸出結果
wang@zhi:~/code/shm$ gcc -g -Wall demo.c wang@zhi:~/code/shm$ ./a.out name = shm_1_to_2 -> key = 1294752001 wang@zhi:~/code/shm$ ./a.out name = shm_1_to_2 -> key = 1294752001 key = 1294752001 -> shmid = 242122773 shmid = 242122773, shmaddr = 0x7f3ec7fde000 ptr = 0x7f3ec7fde000, *ptr = 0 Second run now old = 0, *ptr = 1 shmid = 242122773, shmdt after > ipcs -m -i 242122773 共用記憶體段 shmid=242122773 uid=1000 gid=1000 cuid=1000 cgid=1000 模式=0666 訪問許可權=0666 bytes=4096 lpid=7282 cpid=7280 nattch=1 附加時間=Wed Dec 26 21:06:33 2018 脫離時間=Wed Dec 26 21:06:33 2018 更改時間=Wed Dec 26 21:06:32 2018 shmid = 242122773, shmdt after > ipcs -m -i 242122773 ipcs: id 242122773 not found
2. winds shm 相關 api 熟悉
跨平臺是個悲傷的話題. 這隻是小人物單純的站隊.
// // http://www.office-cn.net/t/api/createfile.htm // https://docs.microsoft.com/zh-cn/windows/desktop/api/fileapi/nf-fileapi-createfilea // CreateFile - 創建或打開一個對象的句柄 // lpFileName : 創建或打開的對象的名字 // dwDesiredAccess : 指明對象的控制模式. 一個應用程式可以包含讀控制, 寫控制, 讀/寫控制, 設備查詢控制 // dwShareMode : 指定對象的共用模式 // lpSecurityAttributes : 一個指向 SECURITY_ATTRIBUTES 結構對象的指針, 決定返回的句柄是否被子進程所繼承 // dwCreationDisposition : 指明當打開的對象存在或不存在的時候各需怎樣處理 // dwFlagsAndAttributes : 指定文件屬性和標誌 // hTemplateFile : 把具有 GENERIC_READ 許可權的句柄指定為一個模板文件 // return : 對象句柄 // HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ); // // http://www.office-cn.net/t/api/deletefile.htm // https://docs.microsoft.com/zh-cn/windows/desktop/api/fileapi/nf-fileapi-deletefilea // DeleteFileA - 刪除指定文件 // lpFileName : 欲刪除文件的名字 // return : 非零表示成功,零表示失敗 // BOOL DeleteFile( LPCSTR lpFileName ); // // http://www.office-cn.net/t/api/createfilemapping.htm // https://docs.microsoft.com/zh-cn/windows/desktop/api/winbase/nf-winbase-createfilemappinga // CreateFileMapping - 創建一個新的文件映射對象 // hFile : 指定欲在其中創建映射的一個文件句柄. INVALID_HANDLE_VALUE 表示在記憶體中創建一個文件映射 // lpFileMappingAttributes : 指定一個安全對象, 在創建文件映射時使用. 如果為 NULL 表示使用預設安全對象 // flProtect : 指定文件映射對象的頁面保護 // dwMaximumSizeHigh : 文件映射的最大長度高32位 // dwMaximumSizeLow : 文件映射的最大長度低32位 // lpName : 指定文件映射對象的名字. 如存在這個名字的一個映射, 函數就會打開它, NULL 表示沒有名字 // return : 新建文件映射對象的句柄, NULL 意味著出錯 // HANDLE CreateFileMapping( HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCSTR lpName ); // // http://www.office-cn.net/t/api/closehandle.htm // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx // CloseHandle - 關閉一個內核對象。其中包括文件、文件映射、進程、線程、安全和同步對象等 // hObject : 欲關閉的一個對象的句柄 // return : 非零表示成功,零表示失敗 // BOOL WINAPI CloseHandle( _In_ HANDLE hObject ); // // http://www.office-cn.net/t/api/index.html?mapviewoffile.htm // https://msdn.microsoft.com/en-us/library/windows/desktop/aa366761(v=vs.85).aspx // MapViewOfFile - 將一個文件映射對象映射到當前應用程式的地址空間 // hFileMappingObject : 文件映射對象的句柄 // dwDesiredAccess : 映射對象的文件數據的訪問方式 // dwFileOffsetHigh : 表示文件映射起始偏移的高32位. // dwFileOffsetLow : 表示文件映射起始偏移的低32位 // dwNumberOfBytesToMap : 指定映射文件的位元組數 // return : 文件映射在記憶體中的起始地址, NULL 表示出錯 // LPVOID WINAPI MapViewOfFile( _In_ HANDLE hFileMappingObject, _In_ DWORD dwDesiredAccess, _In_ DWORD dwFileOffsetHigh, _In_ DWORD dwFileOffsetLow, _In_ SIZE_T dwNumberOfBytesToMap ); // // http://www.office-cn.net/t/api/unmapviewoffile.htm // https://msdn.microsoft.com/en-us/library/windows/desktop/aa366882(v=vs.85).aspx // UnmapViewOfFile - 在當前應用程式的記憶體地址空間解除對一個文件映射對象的映射 // lpBaseAddress : 指定要解除映射的一個文件映射的基準地址, 這個地址是早先用 MapViewOfFile 函數獲得的 // return : 非零表示成功,零表示失敗 // BOOL WINAPI UnmapViewOfFile( _In_ LPCVOID lpBaseAddress );
閱讀完我整理註釋和URL. 就沒有其它了.
正文 - 共用記憶體封裝
前戲做完, 是時候進入介面設計正式封裝環節.