一,内存映射:
对于磁盘文件和进程:
将一个文件或其它对象映射到进程地址空间,实现文件在磁盘的存储地址和进程地址空间中一段虚拟地址的映射关系。有了这样的映射,进程利用指针直接读写虚拟地址就可以完成对文件的读写操作。这样可以避免进行read/write函数操作。
文件的内存映射示意图:
内存映射的优点:
减少了拷贝次数,节省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 <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;
}
这段代码实现了父子进程之间共享数据的功能,具体步骤如下:
1. `create_shared_memory`函数用于创建一个共享内存区域,返回一个指向共享内存的指针。在这里,使用`mmap`函数创建一个具有读写权限的共享内存区域。
2. 在`main`函数中,定义了两个字符串常量`parent_message`和`child_message`,分别存储"hello"和"goodbye"这两个消息。
3. 调用`create_shared_memory`函数创建一个128字节大小的共享内存区域,并将返回的指针存储在`shmem`变量中。
4. 使用`memcpy`函数将`parent_message`中的内容复制到共享内存区域中。需要注意的是,`sizeof(parent_message)`实际上只会返回指针的大小,而不是字符串的长度,因此这里复制的内容可能不完整。
5. 调用`fork`函数创建一个子进程,返回值`pid`为0代表子进程,大于0代表父进程。
6. 在子进程中,首先打印出父进程写入的消息,然后使用`memcpy`将`child_message`中的内容写入共享内存区域,并打印出子进程写入的消息。
7. 在父进程中,首先打印出父进程写入的消息,然后调用`sleep`函数等待3秒,再次读取共享内存中的消息,并打印出来。
总的来说,这段代码展示了如何在父子进程之间共享数据,以及如何使用共享内存来实现进程间通信。
运行结果:
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标准版本:此处略详细可参考原文。
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: 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;
}
这段代码演示了使用System V共享内存进行进程间通信的过程。具体步骤如下:
1. 包含所需的头文件,并定义了共享内存的大小为1024字节。
2. 定义了共享内存的键值`key`,共享内存标识符`shmid`,以及指向共享内存区域的指针`data`,还有一个`mode`变量。
3. 将键值设为1234(也可以使用`ftok`函数根据文件路径和项目ID生成键值)。
4. 使用`shmget`函数创建或打开一个共享内存段,如果失败则输出错误信息并退出程序。
5. 使用`shmat`函数将共享内存段连接到进程的地址空间,得到指向共享内存的指针。如果连接失败则输出错误信息并退出程序。
6. 将一个测试字符串"Send by producer"复制到共享内存中,并输出到控制台。
7. 从共享内存中读取数据,并输出到控制台。
8. 使用`shmdt`函数将共享内存从当前进程中分离,如果失败则输出错误信息并退出程序。
总的来说,这段代码展示了如何创建、连接、写入数据、读取数据以及断开与System V共享内存的连接。这种方式可以实现进程间的数据共享。
运行结果:
Writing to segment: "Send by producer"
Reading form the segment: "Send by producer"