目前,進程間通信主要集中在管道和共享內存上使用,共享內存是總所周知的直接對內存映射操作,速度最快的通信方式,缺點,可能就是數據同步沒有提供同步機制
共享存儲映射
存儲映射I/O
存儲映射I/O (Memory-mapped I/O) 使一個磁盤文件與存儲空間中的一個緩沖區相映射。於是當從緩沖區中取數據,就相當於讀文件中的相應字節。於此類似,將數據存入緩沖區,則相應的字節就自動寫入文件。這樣,就可在不使用read和write函數的情況下,使用地址(指針)完成I/O操作。
使用這種方法,首先應通知內核,將一個指定文件映射到存儲區域中。這個映射工作可以通過mmap函數來實現。
mmap函數
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:成功:返回創建的映射區首地址;失敗:MAP_FAILED宏
參數:
addr: 建立映射區的首地址,由Linux內核指定。使用時,直接傳遞NULL
length: 欲創建映射區的大小
prot: 映射區權限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 標志位參數(常用於設定更新物理區域、設置共享、創建匿名映射區)
MAP_SHARED: 會將映射區所做的操作反映到物理設備(磁盤)上。
MAP_PRIVATE: 映射區所做的修改不會反映到物理設備。
fd: 用來建立映射區的文件描述符
offset: 映射文件的偏移(4k的整數倍)
munmap函數
同malloc函數申請內存空間類似的,mmap建立的映射區在使用結束后也應調用類似free的函數來釋放。
int munmap(void *addr, size_t length); 成功:0; 失敗:-1
借鑒malloc和free函數原型,嘗試裝自定義函數smalloc,sfree來完成映射區的建立和釋放。思考函數接口該如何設計?
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> void *smalloc(size_t size) { void *p; p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0); if (p == MAP_FAILED) { p = NULL; } return p; } void sfree(void *ptr, size_t size) { munmap(ptr, size); } int main(void) { int *p; pid_t pid; p = smalloc(4); pid = fork(); //創建子進程 if (pid == 0) { *p = 2000; printf("child, *p = %d\n", *p); } else { sleep(1); printf("parent, *p = %d\n", *p); } sfree(p, 4); return 0; }
mmap注意事項
【mmap.c】
#include <stdio.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> void sys_err(char *str) { perror(str); exit(1); } int main(void) { char *mem; int len = 0; int fd = open("hello678", O_RDWR|O_CREAT|O_TRUNC, 0644); if (fd < 0) sys_err("open error"); len = lseek(fd, 3, SEEK_SET); //獲取文件大小,根據文件大小創建映射區 write(fd, "\0", 1); //實質性完成文件拓展 len = lseek(fd, 0, SEEK_END); printf("The length of file = %d\n", len); mem = mmap(NULL, len, PROT_WRITE, MAP_PRIVATE, fd, 0); if (mem == MAP_FAILED) //出錯判斷 sys_err("mmap err: "); close(fd); strcpy(mem, "aaa"); printf("%s\n", mem); if (munmap(mem, len) < 0) sys_err("munmap"); return 0; } //思考: //1. 如果mem++,munmap可否成功? //2. 如果open時O_RDONLY, mmap時PROT參數指定PROT_READ|PROT_WRITE會怎樣? //3. 如果文件偏移量為1000會怎樣? //4. 如果不檢測mmap的返回值,會怎樣? //5. mmap什么情況下會調用失敗? //6. 對mem越界操作會怎樣? //7. 文件描述符先關閉,對mmap映射有沒有影響? //8. 可以open的時候O_CREAT一個新文件來創建映射區嗎?
思考:
1. 如果mem++,munmap可否成功?
2. 如果open時O_RDONLY, mmap時PROT參數指定PROT_READ|PROT_WRITE會怎樣?
3. 如果文件偏移量為1000會怎樣?
5. mmap什么情況下會調用失敗?
6. 對mem越界操作會怎樣?
7. 文件描述符先關閉,對mmap映射有沒有影響?
4. 如果不檢測mmap的返回值,會怎樣?
8. 可以open的時候O_CREAT一個新文件來創建映射區嗎?
總結:使用mmap時務必注意以下事項:
- 創建映射區的過程中,隱含着一次對映射文件的讀操作。
- 當MAP_SHARED時,要求:映射區的權限應 <=文件打開的權限(出於對映射區的保護)。而MAP_PRIVATE則無所謂,因為mmap中的權限是對內存的限制。
- 映射區的釋放與文件關閉無關。只要映射建立成功,文件可以立即關閉。
- 特別注意,當映射文件大小為0時,不能創建映射區。所以:用於映射的文件必須要有實際大小!! mmap使用時常常會出現總線錯誤,通常是由於共享文件存儲空間大小引起的。
- munmap傳入的地址一定是mmap的返回地址。堅決杜絕指針++操作。
- 如果文件偏移量必須為4K的整數倍
- mmap創建映射區出錯概率非常高,一定要檢查返回值,確保映射區建立成功再進行后續操作。
mmap父子進程通信
父子等有血緣關系的進程之間也可以通過mmap建立的映射區來完成數據通信。但相應的要在創建映射區的時候指定對應的標志位參數flags:
MAP_PRIVATE: (私有映射) 父子進程各自獨占映射區;
MAP_SHARED: (共享映射) 父子進程共享映射區;
父進程創建映射區,然后fork子進程,子進程修改映射區內容,而后,父進程讀取映射區內容,查驗是否共享。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/wait.h> int var = 100; int main(void) { int *p; pid_t pid; int fd; fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644); if(fd < 0){ perror("open error"); exit(1); } unlink("temp"); //刪除臨時文件目錄項,使之具備被釋放條件. ftruncate(fd, 4); p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); //p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); if(p == MAP_FAILED){ //注意:不是p == NULL perror("mmap error"); exit(1); } close(fd); //映射區建立完畢,即可關閉文件 pid = fork(); //創建子進程 if(pid == 0){ *p = 2000; var = 1000; printf("child, *p = %d, var = %d\n", *p, var); } else { sleep(1); printf("parent, *p = %d, var = %d\n", *p, var); wait(NULL); int ret = munmap(p, 4); //釋放映射區 if (ret == -1) { perror("munmap error"); exit(1); } } return 0; }
結論:父子進程共享:1. 打開的文件 2. mmap建立的映射區(但必須要使用MAP_SHARED)
匿名映射
通過使用我們發現,使用映射區來完成文件讀寫操作十分方便,父子進程間通信也較容易。但缺陷是,每次創建映射區一定要依賴一個文件才能實現。通常為了建立映射區要open一個temp文件,創建好了再unlink、close掉,比較麻煩。 可以直接使用匿名映射來代替。其實Linux系統給我們提供了創建匿名映射區的方法,無需依賴一個文件即可創建映射區。同樣需要借助標志位參數flags來指定。
使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
"4"隨意舉例,該位置表大小,可依實際需要填寫。
【fork_map_anon_linux.c】
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int *p; pid_t pid; int fd = open("/dev/zero", O_RDWR); //p = mmap(NULL, 400, PROT_READ|PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); p = mmap(NULL, 400, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if(p == MAP_FAILED){ //注意:不是p == NULL perror("mmap error"); exit(1); } close(fd); pid = fork(); //創建子進程 if(pid == 0){ *p = 2000; printf("child, *p = %d\n", *p); } else { sleep(1); printf("parent, *p = %d\n", *p); } munmap(p, 400); //釋放映射區 return 0; }
需注意的是,MAP_ANONYMOUS和MAP_ANON這兩個宏是Linux操作系統特有的宏。在類Unix系統中如無該宏定義,可使用如下兩步來完成匿名映射區的建立。
① fd = open("/dev/zero", O_RDWR);
② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
【fork_map_anon.c】
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int *p; pid_t pid; int fd; fd = open("/dev/zero", O_RDWR); p = mmap(NULL, 400, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); if(p == MAP_FAILED){ //注意:不是p == NULL perror("mmap error"); exit(1); } pid = fork(); //創建子進程 if(pid == 0){ *p = 2000; printf("child, *p = %d\n", *p); } else { sleep(1); printf("parent, *p = %d\n", *p); } munmap(p, 4); //釋放映射區 return 0; }
結論:
MAP_PRIVATE:在父子進程間通信,映射的區域不能互相訪問,或者不能讀取父或子寫入映射內存地址空間的數據
mmap無血緣關系進程間通信
實質上mmap是內核借助文件幫我們創建了一個映射區,多個進程之間利用該映射區完成數據傳遞。由於內核空間多進程共享,因此無血緣關系的進程間也可以使用mmap來完成通信。只要設置相應的標志位參數flags即可。若想實現共享,當然應該使用MAP_SHARED了。
例:
mmap_r.c
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> struct STU { int id; char name[20]; char sex; }; void sys_err(char *str) { perror(str); exit(-1); } int main(int argc, char *argv[]) { int fd; struct STU student; struct STU *mm; if (argc < 2) { printf("./a.out file_shared\n"); exit(-1); } fd = open(argv[1], O_RDONLY); if (fd == -1) sys_err("open error"); mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0); if (mm == MAP_FAILED) sys_err("mmap error"); close(fd); while (1) { printf("id=%d\tname=%s\t%c\n", mm->id, mm->name, mm->sex); usleep(10000); } munmap(mm, sizeof(student)); return 0; }
mmap_w.c
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> struct STU { int id; char name[20]; char sex; }; void sys_err(char *str) { perror(str); exit(1); } int main(int argc, char *argv[]) { int fd; struct STU student = {10, "xiaoming", 'm'}; char *mm; if (argc < 2) { printf("./a.out file_shared\n"); exit(-1); } fd = open(argv[1], O_RDWR | O_CREAT, 0664); ftruncate(fd, sizeof(student)); mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if (mm == MAP_FAILED) sys_err("mmap"); close(fd); while (1) { memcpy(mm, &student, sizeof(student)); student.id++; sleep(1); } munmap(mm, sizeof(student)); return 0; }
以上總結了mmap的用法,現在和ipc system V的共享內存比較一下
shm圖示例:
(1)通過int shmget(key_t key, size_t size, int shmflg);在物理內存創建一個共享內存,返回共享內存的編號。
(2)通過void *shmat(int shmid, constvoid shmaddr,int shmflg);連接成功后把共享內存區對象映射到調用進程的地址空間
(3)通過void *shmdt(constvoid* shmaddr);斷開用戶級頁表到共享內存的那根箭頭。
(4)通過int shmctl(int shmid, int cmd, struct shmid_ds* buf);釋放物理內存中的那塊共享內存。
總結mmap和shm:
1、mmap是在磁盤上建立一個文件,每個進程地址空間中開辟出一塊空間進行映射。
而對於shm而言,shm每個進程最終會映射到同一塊物理內存。shm保存在物理內存,這樣讀寫的速度要比磁盤要快,但是存儲量不是特別大。
2、相對於shm來說,mmap更加簡單,調用更加方便,所以這也是大家都喜歡用的原因。
3、另外mmap有一個好處是當機器重啟,因為mmap把文件保存在磁盤上,這個文件還保存了操作系統同步的映像,所以mmap不會丟失,但是shmget就會丟失。
具體的話請看這篇總結:
https://www.cnblogs.com/stevensfollower/p/4897711.html