四、共享内存mmap
内核和用户空间,共享内存。数据copy到内核区后,只需要把地址共享给应用程序即可,无需再copy一次数据到用户空间。
优点:
- 用户程序可以读取和修改共享内存的数据,就像读取用户空间自己的数据一样。
- 无需由内核copy数据到用户空间。
缺点:
- 不支持和其他应用并发访问共享内存,会报非法访问错误
应用:
kafka生产者发送消息到broker的时候,broker的网络接收到数据后,copy到broker的内核空间。然后通过mmap技术,broker会修改消息头,添加一些元数据。所以,写入数据很快。当然顺序IO也是关键技术。
函数原型:
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
mmap的内存即不在堆也不在栈上,是一块独立的空间。
4.1mmap()
mmap()在调用进程的虚拟地址空间中创建一个新的映射。新映射的起始地址在addr中指定。length参数指定映射的长度。
如果addr为空,则内核选择创建映射的地址;这是创建新映射的最可移植方法。如果addr不为空,则内核将其作为一个提示,提示将映射放置在何处;在Linux上,映射将在附近的页面边界处创建。新映射的地址作为调用的结果返回。
文件映射的内容(与匿名映射相反;参见下面的MAP_MAP_ANONYMOUS)使用文件描述符fd所引用的文件(或其他对象)中从偏移量offset开始的length字节进行初始化。offset必须是sysconf(_SC_PAGE_SIZE)返回的页面大小的倍数。
prot参数描述了映射所需的内存保护(不得与文件的打开模式冲突)。它是PROT_NONE或以下一个或多个标志的位OR:
flags参数确定映射的更新是否对映射相同区域的其他进程可见,以及更新是否传递到基础文件。通过在标志中包含以下值中的一个来确定此行为:
此外,以下值中的零个或多个可以在flag中进行“或”运算:
返回值:
成功后,mmap()返回指向映射区域的指针。错误时,返回值MAP_FAILED(即,(void*)-1),并设置errno以指示错误原因。
4.2munmap()
munmap()系统调用删除指定地址范围的映射,并导致对该范围内地址的进一步引用生成无效内存引用。当进程终止时,区域也会自动取消映射。另一方面,关闭文件描述符不会取消区域映射。
地址addr必须是页面大小的倍数(但长度不必是)。包含指定范围一部分的所有页面均未映射,对这些页面的后续引用将生成SIGSEGV。如果指示的范围不包含任何映射页,则不是错误。
返回值:
成功时,munmap()返回0。失败时,它返回-1,errno被设置为指示错误原因(可能是EINVAL)。
错误代码
使用映射区域可产生以下信号:
流程
(1)打开文件
(2)取文件大小
(3)把文件映射成虚拟内存
(4)通过对内存的读写来实现对文件的读写
(5)卸载映射
(6)关闭文件
示例代码:
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while (0) int main(int argc, char *argv[]) { char *addr; int fd; struct stat sb; off_t offset, pa_offset; size_t length; ssize_t s; if (argc < 3 || argc > 4) { fprintf(stderr, "%s file offset [length]\n", argv[0]); exit(EXIT_FAILURE); } fd = open(argv[1], O_RDONLY); if (fd == -1) handle_error("open"); if (fstat(fd, &sb) == -1) /* To obtain file size */ handle_error("fstat"); offset = atoi(argv[2]); pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1); /* offset for mmap() must be page aligned */ if (offset >= sb.st_size) { fprintf(stderr, "offset is past end of file\n"); exit(EXIT_FAILURE); } if (argc == 4) { length = atoi(argv[3]); if (offset + length > sb.st_size) length = sb.st_size - offset; /* Can't display bytes past end of file */ } else { /* No length arg ==> display to end of file */ length = sb.st_size - offset; } addr = mmap(NULL, length + offset - pa_offset, PROT_READ, MAP_PRIVATE, fd, pa_offset); if (addr == MAP_FAILED) handle_error("mmap"); s = write(STDOUT_FILENO, addr + offset - pa_offset, length); if (s != length) { if (s == -1) handle_error("write"); fprintf(stderr, "partial write"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }
shm*接口
共享内存就是允许两个不相关的进程访问同一个内存块。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以,通常需要用其他的机制来同步对共享内存的访问,例如信号量。
shmget()
创建共享内存。函数原型:
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
描述:
shmget()返回与参数key的值关联的System V共享内存段的标识符。如果key的值为IPC_PRIVATE或key不是IPC_PRIVATE,不存在与key对应的共享内存段,并且在shmflg中指定了IPC_CREAT,则会创建一个大小等于size值的新共享内存段(向上舍入为PAGE_SIZE的倍数)。
如果shmflg同时指定IPC_CREAT和IPC_ EXCL,并且key已经存在共享内存段,则shmget()将失败,错误号设置为EEXIST。【这类似于open()的组合O_CREAT|O_EXCL的效果。】
值shmflg由以下组成:
除上述标志外,shmflg的最低有效9位指定授予所有者、组和其他人的权限。这些位的格式和含义与open()的模式参数相同。目前,系统不使用执行权限。
返回值:
成功后,将返回有效的共享内存标识符。出现错误时,返回-1,并设置errno以指示错误。
错误:
失败时,错误号设置为以下之一:
hmat()
启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间,函数原型:
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
描述:
shmat()将由shmid标识的System V共享内存段附加到调用进程的地址空间。附加地址由shmaddr根据以下标准之一指定:
- (1)如果shmaddr为空,系统将选择一个合适的(未使用的)地址来连接段。
- (2)如果shmaddr不为空,并且在shmflg中指定了SHM_RND,则附加发生在等于shmaddr的地址处,向下舍入到SHMLBA的最近倍数。
- (3)否则,shmaddr必须是发生附加的页对齐地址。
除了SHM_RND,还可以在shmflg位掩码参数中指定以下标志:
呼叫进程的brk()值不被附加改变。该段将在进程退出时自动分离。同一段可以作为读写段附加在进程的地址空间中,并且可以多次附加。
成功的shmat()调用更新与共享内存段相关联的shmid_ds结构的成员【参见shmctl()】,如下所示:
- shm_ atime被设置为当前时间。
- shm_ lpid被设置为调用进程的进程ID。
- shm_natch递增1。
返回值:
成功时,shmat()返回附加共享内存段的地址;错误时,返回(void*)-1,并设置errno以指示错误原因。
错误:
当shmat()失败时,errno设置为以下之一:
shmdt()
将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。函数原型:
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
描述:
shmdt()将位于shmaddr指定地址的共享内存段从调用进程的地址空间中分离。要分离的段当前附加的shmaddr必须等于附加的shmat()调用返回的值。
参数shmaddr是shmat()函数返回的地址指针。
在成功调用shmdt()时,系统更新与共享内存段关联的shmid_ds结构的成员,如下所示:
- shm_ atime被设置为当前时间。
- shm_ lpid被设置为调用进程的进程ID。
- shm_natch减1。
返回值:
成功时,shmdt()返回0;在出现错误时,返回-1,并设置errno以指示错误原因。
错误:
当shmdt()失败时,errno设置如下:
shmctl()
控制共享内存。函数原型:
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
描述:
shmctl()对系统V共享内存段执行cmd指定的控制操作,该段的标识符在shmid中给出。
buf参数是指向shmid_ds结构的指针,如下:
struct shmid_ds { struct ipc_perm shm_perm; /* Ownership and permissions */ size_t shm_segsz; /* Size of segment (bytes) */ time_t shm_atime; /* Last attach time */ time_t shm_dtime; /* Last detach time */ time_t shm_ctime; /* Last change time */ pid_t shm_cpid; /* PID of creator */ pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */ shmatt_t shm_nattch; /* No. of current attaches */ ... };
ipc_perm结构定义如下:
struct ipc_perm { key_t __key; /* Key supplied to shmget(2) */ uid_t uid; /* Effective UID of owner */ gid_t gid; /* Effective GID of owner */ uid_t cuid; /* Effective UID of creator */ gid_t cgid; /* Effective GID of creator */ unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */ unsigned short __seq; /* Sequence number */ };
返回值:
成功的IPC_INFO或SHM_INFO操作将返回内核内部数组中记录所有共享内存段信息的最高使用项的索引。(此信息可与重复的SHM_STAT操作一起使用,以获得有关系统上所有共享内存段的信息。)成功的SHM_STAT操作返回其索引在shmid中给出的共享内存段标识符。其他操作成功时返回0。
出现错误时,返回-1,并适当设置errno。
流程
共享内存,可以大大加快对文件或设备的读写操作。共享内存的方式有mmap和shmget 、 shmat。
所谓的零拷贝,就是不需要CPU的参与,而不是其他的意思,mmap内部其实是一个DMA技术。
精选文章推荐阅读:
- 掌握GDB调试工具,轻松排除bug
- 从零开始学习 Linux 内核套接字:掌握网络编程的必备技能
- 解密Linux内核神器:内存屏障的秘密功效与应用方法
- Linux内核文件系统:比万物之神还要强大的存储魔法!
- 牛客网论坛考研计算机组成原理笔记,GitHub已下载量已过百万
- 探索网络通信核心技术,手写TCP/IP用户态协议栈,让性能飙升起来!
- 万字总结简化跨平台编译利器CMake,从入门到项目实战演练!
- 牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万