12.内存映射
使一个磁盘文件与内存中的一个缓冲区相映射,进程可以像访问普通内存一样对文件进行访问,不必再调用read,write,更加高效。
用到的函数
mmap函数
原型:
#include <sys/mman.h> void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
功能:创建共享内存映射
参数:
addr
:指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址。
length
:必须>0。映射地址空间的字节数,它从被映射文件开头offset
个字节开始算起。
prot
:指定共享内存的访问权限。可取如下几个值的可选:
PROT_READ
(可读)
PROT_WRITE
(可写)
PROT_EXEC
(可执行)
PROT_NONE
(不可访问)
flags
:由以下几个常值指定:
MAP_SHARED
(共享映射)
MAP_PRIVATE
(私有映射)
MAP_FIXED
(表示必须使用 start 参数作为开始地址,如果失败不进行修正)
MAP_ANONYMOUS
(匿名映射,用于血缘关系进程间通信)其中,
MAP_SHARED
,MAP_PRIVATE
必选其一,而MAP_FIXED
则不推荐使用。
fd
:表示要映射的文件句柄。如果匿名映射写-1。
offset
:表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射。返回值:
- 成功返回创建的映射区首地址
- 失败返回
MAP_FAILED
,设置errno值
lseek函数
原型:
#include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence);
功能:用于在文件中定位文件指针的位置
参数:
fd
:文件描述符,通常是通过open
或fopen
函数获得的。
offset
:相对于whence
的偏移量,以字节为单位。
whence
:表示参考点的位置,可以是以下值之一::表示参考点的位置,可以是以下值之一:
SEEK_SET
:文件开头SEEK_CUR
:文件当前位置SEEK_END
:文件结尾返回值:
- 成功时,返回新的文件指针位置。
- 失败时,返回-1,并设置
errno
示例1
利用内存映射实现两个无亲缘关系的线程间的通信
write.c
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
void *addr;
int fd;
fd = open("test",O_RDWR);
if(fd<0)
{
perror("open");
return 0;
}
//获取文件大小
int fileSize = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET); // 将文件指针重置到文件开头
//映射文件到内存
addr = mmap(NULL,2000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
close(fd);
return 0;
}
//写入文件
int i = 0;
printf("%d\n",fileSize);
while(i < 2000)
{
printf("%d\n",i);
memcpy(addr+i,"5",1);
sleep(1);
i++;
}
return 0;
}
read.c
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
void *addr;
int fd;
fd = open("test",O_RDWR);
if(fd<0)
{
perror("open");
return 0;
}
//获取文件大小
int fileSize = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET); // 将文件指针重置到文件开头
//映射文件到内存
addr = mmap(NULL,2000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
close(fd);
return 0;
}
//读取
int i = 0;
printf("%d\n",fileSize);
while(i < 2000)
{
printf("%s\n",(char *)addr);
sleep(1);
}
return 0;
}
示例2匿名映射
利用内存映射匿名映射实现两个亲缘线程间的通信
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(){
void *addr;
//匿名映射
addr = mmap(NULL,2048, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
//创建子进程
pid_t pid;
pid = fork();
if(pid<0)
{
perror("fork");
return 0;
}
//父进程
else if(pid>0)
{
memcpy(addr,"1234567890",10);
wait(NULL);//回收子进程
}
//子进程
else
{
sleep(1);
printf("read father val=%s\n",(char *)addr);
}
munmap(addr,2048);//释放内存
}
注意事项
-
创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区。
-
当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护),如果不满足报非法参数(Invalid argument)错误。
当MAP_PRIVATE时候,mmap中的权限是对内存的限制,只需要文件有读权限即可,操作只在内存有效,不会写到物理磁盘,且不能在进程间共享。
-
映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
-
用于映射的文件大小必须>0,当映射文件大小为0时,指定非0大小创建映射区,访问映射地址会报总线错误,指定0大小创建映射区,报非法参数错误(Invalid argument)
-
文件偏移量必须为0或者4K的整数倍(不是会报非法参数Invalid argument错误)
-
映射大小可以大于文件大小,但只能访问文件page的内存地址,否则报总线错误 ,超出映射的内存大小报段错误
-
映射内存大小为4K的整数倍,若申请映射大小不为4K的整数倍,则会自动向上补齐。例如申请2k,实际可以操作的内存大小为4K。
System V共享内存
- 共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝
- 共享内存在内核空间创建,可被进程映射到用户空间访问,使用灵活
- 由于多个进程可同时访问共享内存,因此需要同步和互斥机制配合使用
共享内存使用步骤:
- 生成key
- 创建/打开共享内存
- 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
- 读写共享内存
- 撤销共享内存映射
- 删除共享内存对象
ftok函数
原型:
#include <sys/types.h> #include <sys/ipc.h> key_t **ftok**(const char *pathname, int proj_id);
功能:生成一个唯一的与文件相关的键值(key),这个键值通常用于IPC(进程间通信)中,比如创建共享内存或信号量等。
参数:
pathname
:必须是存在的且可访问的文件路径。它可以是一个普通文件或者目录,但该路径下的某个文件需要具有相应的权限,以便其他进程可以通过该路径访问到相同的键值。proj_id
:是一个8位的整数(即范围在0到255之间)。它与文件的索引节点号结合生成最终的键值。返回值:
- 如果函数执行成功,会返回一个
key_t
类型的键值- 如果执行失败,则返回-1
注意:
确保
pathname
指定的文件存在且可访问。
proj_id
的值虽然作为int
类型传递,但实际上只有8个比特被使用,因此其取值范围是0到255。生成的键值是由
proj_id
的后8个比特、文件所在设备的最后两位和文件的索引节点号的最后四位组成的
shmget函数
原型:
#include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
功能:获取或创建一个共享内存段,并返回一个共享内存标识符。
参数:
key
:共享内存的键值,可以是0(IPC_PRIVATE)以创建一个新的共享内存对象,或者是大于0的32位整数。如果shmflg
中包含了IPC_CREAT
标志,且提供的键值已经存在,则会返回现有的共享内存标识符。size
:指定需要共享的内存容量,以字节为单位。shmflg
:权限标志,与open
函数的mode
参数类似。如果希望在键值指定的共享内存不存在时创建它,可以与IPC_CREAT
进行或操作。返回值:
- 如果成功,
shmget
函数返回一个共享内存标识符,该标识符可以在后续的共享内存相关操作中使用,如shmat
、shmdt
和shmctl
等。- 如果失败,返回-1。
注意:
- 不同进程可以通过
shmget
返回的共享内存标识符访问同一块共享内存。- 当写数据到共享内存时,需要注意同步问题,即在进程间访问共享内存时要加锁,以防止数据竞争。
- 在使用共享内存时,应确保结构体中的缓冲区已经声明了足够的大小,而不是仅使用一个指针并在需要时通过
malloc
分配内存,因为这样分配的地址其他进程无法访问
ipcs 命令
- 格式:
ipcs [options]
- 功能:
ipcs
命令用于提供系统中进程间通信设施的信息,包括消息队列、共享内存段和信号量。- 参数:
-a
或--all
:显示所有类型的 IPC 资源信息。-b
或--broad
:显示 IPC 资源的宽泛信息,包括最大允许的尺寸等。-m
或--shmems
:仅显示共享内存段的信息。-q
或--queues
:仅显示消息队列的信息。-s
或--semaphores
:仅显示信号量的信息。-i <id>
或--identifier <id>
:显示特定 IPC 资源 ID 的详细信息。-l
或--limits
:显示系统限制信息。-u
或--summary
:显示 IPC 资源的摘要信息。-p
或--pid
:显示创建 IPC 资源的进程 ID。-t
或--time
:显示最后操作 IPC 资源的时间。-c
或--creator
:显示 IPC 资源的创建者信息。-h
或--human
:以人类可读的格式显示大小。-v
或--version
:显示ipcs
命令的版本信息。-V
或--help
:显示帮助信息。- 示例:
ipcs -a
:显示所有类型的 IPC 资源信息。ipcs -m
:仅显示共享内存段的信息。ipcs -q
:仅显示消息队列的信息。ipcs -s
:仅显示信号量的信息。ipcs -u
:显示 IPC 资源的摘要信息。ipcs -i 1234
:显示 ID 为 1234 的 IPC 资源的详细信息。
shmat函数
原型:
#include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将创建好的共享内存段连接到某个进程,并指定内存空间
参数:
shmid
:是通过shmget
函数返回的共享内存标识符,它唯一标识了一个共享内存段。shmaddr
:是一个可选参数,用于指定共享内存映射到进程地址空间中的起始地址。如果传递NULL
,则由系统自动选择一个地址进行映射。shmflg
:是一组标志位,用于控制共享内存的访问权限和其他属性。常见的标志包括SHM_RDONLY
(只读访问)和SHM_WRONLY
(只写访问)。返回值:
- 成功,返回一个指向共享内存段在进程地址空间中映射的起始位置的指针。这个指针可以用于访问共享内存中的数据。
- 失败,返回-1
shmdt函数
原型:
#include <sys/shm.h> int shmdt(const void *shmaddr);
功能:从进程的地址空间中分离已经附加的共享内存段
参数:
shmaddr
是一个指向共享内存段的指针,这个指针通常是通过shmat
函数返回的。返回值:
- 成功,
shmdt
函数返回0- 失败,返回 -1,并设置
errno
以指示错误原因。
shmctl函数
原型:
#include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:控制共享内存段,包括删除和修改权限等操作
参数:
shmid
:是通过shmget
函数返回的共享内存标识符,唯一标识了一个共享内存段。cmd
:是控制命令,用于指定要执行的操作
- IPC_STAT(获取共享内存的状态)
- IPC_SET(设置共享内存的状态)
- IPC_RMID(删除共享内存段)
buf
:是一个指向shmid_ds
结构体的指针,用于存储或接收共享内存的状态信息。在某些命令下,如IPC_STAT和IPC_SET,需要提供这个参数。返回值:
- 成功,
shmctl
函数返回0;- 出错,返回 -1,并设置
errno
以指示错误原因
示例
创建和使用共享内存
write.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
int main() {
key_t key; // 定义一个键值变量
int shmid; // 定义一个共享内存标识符变量
char *buf; // 定义一个指向共享内存的指针变量
key = ftok("keytest", 100); // 生成一个唯一的键值
if (key < 0)
{
perror("ftok");
return 0;
}
printf("key=%x\n", key); // 打印生成的键值
shmid = shmget(key, 512, IPC_CREAT | 0666); //创建并获取共享内存段
if (shmid < 0)
{
perror("shmget");
return 0;
}
printf("shmid=%d\n", shmid); // 打印共享内存标识符
buf = shmat(shmid, NULL, 0); // 将共享内存段附加到进程的地址空间中
if (buf < 0)
{
perror("shmat");
return 0;
}
strcpy(buf, "hello world"); // 将字符串"hello world"复制到共享内存中
}
read.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
int main() {
key_t key; // 定义一个键值变量
int shmid; // 定义一个共享内存标识符变量
char *buf; // 定义一个指向共享内存的指针变量
key = ftok("keytest", 100); // 生成一个唯一的键值
if (key < 0)
{
perror("ftok");
return 0;
}
printf("key=%x\n", key); // 打印生成的键值
shmid = shmget(key, 512, 0666); // 获取共享内存段
if (shmid < 0)
{
perror("shmget");
return 0;
}
printf("shmid=%d\n", shmid); // 打印共享内存标识符
buf = shmat(shmid, NULL, 0); // 将共享内存段附加到进程的地址空间中
if (buf < 0)
{
perror("shmat");
return 0;
}
printf("read = %s\n",buf);
if(shmdt(buf) == -1)//共享内存段从进程的地址空间中分离
perror("shmdt");
if(shmctl(shmid, IPC_RMID, NULL) == -1)//删除共享内存段
perror("shmctl");
return 0;
}