一,内存映射
对于磁盘文件和进程:
将一个文件或其它对象映射到进程地址空间,实现文件在磁盘的存储地址和进程地址空间中一段虚拟地址的映射关系。有了这样的映射,进程利用指针直接读写虚拟地址就可以完成对文件的读写操作。这样可以避免进行read/write函数操作。
文件的内存映射示意图:
对于用户进程和内核进程:
将用户进程的一段内存区域映射到内核进程,映射成功后,用户进程对这段内存区域的修改直接反映到内核空间,同样,内核进程对这段内存区域的修改也直接反映到用户空间。
没有内存映射的I/O操作示意图: 磁盘->内核空间->用户空间
有内存映射的I/O操作示意图:少了一个copy操作
内存映射的优点:
减少了拷贝次数,节省I/O操作的开支
用户空间和内核空间可以直接高效交互
进程可以直接操作磁盘文件,用内存读写代替 I/O读写
应用场景:
1.进程间通信
使用内存映射实现进程间通信的两个场景:
场景1.有亲缘关系的进程间通信(父子进程)
step1: 父进程创建内存映射区
step2: 父进程利用fork()创建子进程
step3: 子进程继承了父进程的内存映射区后,父子进程通过内存映射区进行通信
场景2.没有亲缘关系的进程间通信
step1: 准备一个非空的磁盘文件
step2: 进程a通过磁盘文件创建内存映射区
step3: 进程b通过磁盘文件创建内存映射区
step4: 进程a和进程b共同修改内存映射区实现进程通信
*基于内存映射区的进程间通信,是非阻塞的。
*子进程能通过fork继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间中的一部分),但是由于同样的原因,新程序则不能通过exec继承存储映射区。
2.文件读写操作
step1: 读磁盘文件,获得文件描述符
step2: 基于文件描述符建立进程的内存映射区
step3: 利用进程进行内存映射区的读写操作
step4: 释放内存映射区,关闭文件描述符
内存映射的重要函数--mmap/munmap/msync
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
start:用户进程中要映射的某段内存区域的起始地址,通常为NULL(由内核来指定)
length:要映射的内存区域的大小
prot:期望的内存保护标志
flags:指定映射对象的类型
fd:要映射的文件描述符
offset:要映射的用户空间的内存区域在内核空间中已经分配好了的内存区域中的偏移
--prot参数取值:
PROT_READ:映射区可读
PROT_WRITE:映射区可写
PROT_EXEC:映射区可执行
PROT_NONE:映射区不可访问
--flags参数取值:
MAP_SHARED:变动是共享的,内存区域的读写影响到原文件
MAP_PRIVATE:变动是私有的,内存区域的读写不会影响到原文件
返回:若成功,返回指向内存映射区域的指针,若出错,返回MAP_FAILED(-1)。
*使用mmap时需要注意,不是所有文件都可以进行内存映射,一个访问终端或者套接字的描述符只能用read/write这类的函数去访问,用mmap做内存映射会报错。超过文件大小的访问会产生SIGBUS信号。
int munmap(void *start, size_t length);
start:指向内存映射区的指针
length:内存映射区域的大小
返回:若成功,返回0,若出错,返回-1。
int msync(void *start, size_t length, int flags);
start:指向内存映射区的指针
length:内存映射区域的大小
flags:模式的设置
--flags参数取值:
MS_ASYNC:异步写
MS_SYNC:同步写
MS_INVALIDATE:使高速缓存的数据失效
*MS_ASYNC和MS_SYNC的区别,一旦写操作已经由内核排入队列,MS_ASYNC立即返回,MS_SYNC则要等到写操作完成后才返回。
代码样例:
Demo1: 文件操作--利用内存映射读文件的第一行
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
int main(int argc, char *argv[]) {
int fd, index;
char *map;
struct stat file_stats;
if ((fd = open("test.txt", O_RDONLY)) == -1) {
perror("open");
exit(1);
}
if (stat("test.txt", &file_stats) == -1) {
perror("stat");
exit(1);
}
// mmap to read
map = mmap(0, file_stats.st_size, PROT_READ, MAP_SHARED, fd, 0);
// Print the first line
printf("The first line is:\n");
index = 0;
while(1) {
if (map[index] == '\n') {
printf("\n");
break;
} else {
printf("%c", map[index]);
}
index += 1;
}
if (munmap(map, file_stats.st_size) == -1) {
close(fd);
perror("Error un-mmapping the file");
exit(1);
}
close(fd);
return 0;
}
Demo2: 利用内存映射实现进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
void* create_shared_memory(size_t size) {
int prot = PROT_READ | PROT_WRITE;
int flags = MAP_ANONYMOUS | MAP_SHARED;
int fd = 0;
int offset = 0;
return mmap(NULL, size, prot, flags, fd, offset);
}
int main() {
char* parent_message = "hello";
char* child_message = "goodbye";
void* shmem = create_shared_memory(128);
memcpy(shmem, parent_message, sizeof(parent_message));
int pid = fork();
if (pid == 0) {
printf("Child read: %s\n", shmem);
memcpy(shmem, child_message, sizeof(child_message));
printf("Child wrote: %s\n", shmem);
} else {
printf("Parent read: %s\n", shmem);
sleep(3);
printf("After 3s, parent read: %s\n", shmem);
}
return 0;
}
运行结果:
Parent read: hello
Child read: hello
Child wrote: goodbye
After 3s, parent read: goodbye
二,共享内存:
内存映射和共享内存的区别:
1.内存映射与文件关联,共享内存不需要与文件关联,把共享内存理解为内存上的一个匿名片段。
2.内存映射可以通过fork继承给子进程,共享内存不可以。
3.文件打开的函数不同,内存映射文件由open函数打开,共享内存区对象由shm_open函数打开。但是它们被打开后返回的文件描述符都是由mmap函数映射到进程的地址空间。
共享内存允许多个进程共享一个给定的存储区。
对于Client-Server架构,如果服务器进程和客户端进程共享同一块存储区,服务器进程正在将数据写入共享存储区时,在写入操作完成之前,客户端进程不应去取出这些数据。一般用信号量来同步共享内存的访问。
共享内存区在系统存储中的位置:
为什么要用共享内存:
对于涉及到内核操作的,内核和进程之间,经历了四次复制操作,开销很大。
使用共享内存后,客户到服务器之间只需要经历两次复制操作
共享内存常用函数:
Posix标准版本:
1.创建或获取共享内存
int shm_open(const char *name, int oflag, mode_t mode);
--name:共享内存对象的名字
--oflag:与open函数类似,可以是O_RDONLY、O_WRONLY、O_RDWR,还可以按位或O_CREAT、O_EXCL、O_TRUNC等
--mode:如果oflag没有指定O_CREAT,可以指定为0
返回值:若成功,返回文件描述符。若失败,返回-1
2.销毁共享内存
int shm_unlink(const char *name);
3.修改共享内存的大小(还可以修改文件的大小)
int ftruncate(int fd, off_t length)
处理mmap的时候,普通文件或共享内存区对象的大小都可以通过调用ftruncate修改。文件大小如果大于length, 额外的数据就会被丢掉。
System_V标准版本:
1. 创建或获取共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg)
--key:进程间事先约定的key,或者调用key_t ftok( char * fname, int id )获取
--size:共享内存大小,当创建一个新的共享内存区时,size必须大于0,如果是访问一个已经存在的内存共享区,size可以是0
--shmflg:标志位,可以取IPC_CREATE|IPC_EXCL,它的用法和创建文件时使用的mode参数是一样的。
返回值:若成功,返回shmid。若失败,返回-1
2. 将进程附加到已创建的共享内存
#include <sys/types.h>
#include <sys/shm.h>
void * shmat(int shmid, const void *shmaddr, int shmflg)
--shmid:共享内存区的标识id,shmget的返回值
--shmaddr:共享内存附加到本进程后在本进程地址空间的内存地址,若为NULL,由内核分配地址。
--shmflg:一般为0,不设置任何限制权限。如果设置为只读,shmflg=SHM_RDONLY
返回值:若成功,返回指向共享内存区的指针。若失败,返回-1
3. 从已附加的共享内存段中分离进程
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr)
--shmaddr:指向共享内存区的指针
返回值:若成功,返回0。若失败,返回-1
4.控制共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
--shmid:共享内存标识符
--cmd:共享内存控制指令
IPC_STAT:得到共享内存的状态
IPC_SET:改变共享内存的状态
IPC_RMID:删除该共享内存
--shmid_ds: 共享内存管理结构体
返回值:若成功,返回0。若失败,返回-1
两个版本的微小差异:Posix共享内存区对象的大小可在任意时刻由ftruncate函数修改,System V共享内存区对象的大小是在调用shmget创建时固定下来的。
代码样例:
Demo1: POSIX版
Producer:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/mman.h>
int main()
{
/* the size (in bytes) of shared memory object */
const int SIZE = 4096;
/* name of the shared memory object */
const char* name = "OS";
/* strings written to shared memory */
const char* message_0 = "Hello";
const char* message_1 = "World!";
/* shared memory file descriptor */
int shm_fd;
/* pointer to shared memory object */
void* ptr;
/* create the shared memory object */
shm_fd = shm_open(name, O_CREAT | O_RDWR, 0666);
/* configure the size of the shared memory object */
ftruncate(shm_fd, SIZE);
/* memory map the shared memory object */
ptr = mmap(0, SIZE, PROT_WRITE, MAP_SHARED, shm_fd, 0);
/* write to the shared memory object */
sprintf(ptr, "%s", message_0);
ptr += strlen(message_0);
sprintf(ptr, "%s", message_1);
ptr += strlen(message_1);
return 0;
}
consumer:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/mman.h>
int main()
{
/* the size (in bytes) of shared memory object */
const int SIZE = 4096;
/* name of the shared memory object */
const char* name = "OS";
/* shared memory file descriptor */
int shm_fd;
/* pointer to shared memory object */
void* ptr;
/* open the shared memory object */
shm_fd = shm_open(name, O_RDONLY, 0666);
/* memory map the shared memory object */
ptr = mmap(0, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
/* read from the shared memory object */
printf("%s", (char*)ptr);
/* remove the shared memory object */
shm_unlink(name);
return 0;
}
编译方式:
gcc producer.c -pthread -lrt -o producer
gcc consumer.c -pthread -lrt -o consumer
Demo2: System_V版
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *data;
int mode;
key = 1234;
/*if ((key = ftok("shmdemo.c", 'R')) == -1) {
perror("ftok");
exit(1);
}*/
/* connect to the segment: */
if ((shmid = shmget(key, SHM_SIZE, 0644 | IPC_CREAT)) == -1)
{
perror("shmget");
exit(1);
}
/* attach to the segment to get a pointer to it: */
data = shmat(shmid, (void *)0, 0);
if (data == (char *)(-1))
{
perror("shmat");
exit(1);
}
char *str_test = "Send by producer";
printf("Writing to segment: \"%s\"\n", str_test);
strncpy(data, str_test, SHM_SIZE);
/* Reading from the segment*/
printf("Reading form the segment: \"%s\"\n", data);
/* detach from the segment: */
if (shmdt(data) == -1)
{
perror("shmdt");
exit(1);
}
return 0;
}
运行结果:
Writing to segment: "Send by producer"
Reading form the segment: "Send by producer"
参考教程:
《UNIX环境高级编程第3版》
https://code-examples.net/zh-TW/q/564fd2
https://www.geeksforgeeks.org/posix-shared-memory-api/