进程间通信(IPC) 系列 | mmap

什么是 共享内存

共享内存区是把文件或者内存对象映射到多个进程的虚拟地址空间中,这样这些进程可以共享这些数据。但是从这些共享的内存区写入或者读取时通常需要某种形式的同步。当这种映射关系建立起来后,进程可以不再通过执行任何进入内核的系统调用就可直接操作这些内存区。

共享内存是IPC形式中最快的一种方式。

mmap操作接口

#include <sys/mman.h> void* mmap(void *addr, size_t len,int prot, int flags, int fd, off_t offset); 返回:若成功则为被映射区的起始地址,若出错则为MAP_FAILED

addr可以指定fd应被映射到进程虚拟内存空间的起始地址,还可以为空,让内核自己去选择其实地址,一般都指定为空。

len 是被映射到进程地址空间的字节数,它是被映射文件开头起第offset个字节处开始算起, offset一般设置为 0。

内存映射区的保护机制有prot参数指定,可以有如下组合

PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不可访问
flags可以指定如下组合,通常MAP_SHARED或MAP_PRIVATE会指定一个,并可有选择的或上MAP_FIXED。

MAP_SHARED 往映射区域的修改会同步到文件内,其他共享进程可见
MAP_PRIVATE 往映射区域的修改不会同步到文件内,其他共享进程不可见
MAP_FIXED 若addr所指的地址无法成功建立映射时,不对地址做修改。从移植的角度考虑,不建议指定MAP_FIXED,且addr设置为空。

映射关系如下图:

在这里插入图片描述

从某个进程地址空间中删除一个映射关系

#include <sys/mman.h> int munmap(void *addr, size_t len);

addr为mmap返回的地址,len是映射区的大小。再次访问这些地址时系统会给调用进程返回一个SIGSEV信号。

mmap 怎么使用

mmap的使用例子

r.c #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> int main() { int fd; char *mm; fd = open("/temp", O_RDONLY); mm = mmap(NULL, 1024, PROT_READ, MAP_SHARED); printf(“mm addr : %p\n”, mm); printf(“read : %d\n”, *(int *)mm); printf(“str : %s\n”, mm+sizeof(int)); close(fd); munmap(mm, 1024); return 0; } # ./rmm addr : 0x0x7f5d3f327000read : 20str : hello

w.c #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> int main() { int fd; char *mm; char str[20] = {‘h’, ‘e’, ‘l’, ‘l’, ‘o’}; int temp = 20; fd = open("/temp", O_RDWR | O_CREAT, 0664); ftruncate(fd, 1024); mm = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); printf(“mm addr : %p\n”, mm); memcpy(mm, &temp, sizeof(temp)); memcpy(mm+sizeof(temp), str, sizeof(str)); close(fd); munmap(mm, 1024); return 0; } # ./wmm addr : 0x7f5d350c2000

mmap 实现原理

Linux的虚拟内存管理是基于mmap来实现的。

vm_area_struct是在mmap的时候创建的,vm_area_strcut代表了一段连续的虚拟地址,这些虚拟地址相应地映射到一个文件或者一个匿名文件的虚拟页。一个vm_area_struct映射到一组连续的页表项。页表项又指向物理内存page,这样就把一个文件和物理内存页进行相映射。

Linux通过下图的方式来组织虚拟内存的:

在这里插入图片描述

理解一下虚拟地址映射的过程:拿到一个虚拟地址,根据已有的vm_area_struct看这个虚拟地址是否属于某个vm_area_struct

如果没有匹配到,就报段错误,访问了一个没有分配的虚拟地址。

如果匹配到了vm_area_struct,根据虚拟地址和页表的映射关系,找到对应的页表项PTE,如果PTE没有分配,就报一个缺页异常,去加载相应的文件数据到物理内存,如果PTE分配,就去相应的物理页的偏移位置读取数据

所以虚拟页的三种状态的实际含义如下:

未分配虚拟页,指的是没有使用mmap建立vm_area_struct,所以也就没有对应到具体的页表项

已分配虚拟页,未映射到物理页,指的是已经使用了mmap建立的vm_area_struct,可以映射到对应的页表项,但是页表项没有指向具体的物理页

已分配虚拟页,已映射到物理页,指的是已经使用了mmap建立的vm_area_struct,可以映射到对应的页表项,并且页表项指向具体的物理页

文件的共享映射可以用作内存映射IO来对大文件进行操作,比普通IO减少一次复制。需要注意的是内存映射IO涉及到内核的很多操作,比如vm_area_struct的创建,页表的修改等等,比普通IO的操作更复杂。小文件的读写使用普通IO更合适。

本次分析mmap的源码 版本为 1.2.13

mmap实现如下:

asmlinkage int sys_mmap(unsigned long *buffer) { int error; unsigned long flags; struct file * file = NULL; … return do_mmap(file, get_fs_long(buffer), get_fs_long(buffer+1), get_fs_long(buffer+2), flags, get_fs_long(buffer+5)); }

unsigned long do_mmap(struct file * file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long off) { int error; struct vm_area_struct * vma; // 长度为0,直接返回 if ((len = PAGE_ALIGN(len)) == 0) return addr; // 地址不在用户空间 if (addr > TASK_SIZE || len > TASK_SIZE || addr > TASK_SIZE-len) return -EINVAL; /* offset overflow? */ if (off + len < off) return -EINVAL; // 文件映射 if (file != NULL) { // 映射文件的方式 switch (flags & MAP_TYPE) { // 共享方式,每个进程都可见,并且修改会同步到硬盘 case MAP_SHARED: // 映射时设置了写,但是文件不可写则报错,如果是共享只读是可以的 if ((prot & PROT_WRITE) && !(file->f_mode & 2)) return -EACCES; case MAP_PRIVATE: // 私有映射,修改文件不会同步到硬盘 if (!(file->f_mode & 1)) // 不可读 return -EACCES; break; default: return -EINVAL; } if ((flags & MAP_DENYWRITE) && (file->f_inode->i_wcount > 0)) return -ETXTBSY; } else if ((flags & MAP_TYPE) != MAP_PRIVATE) // 匿名映射需要是私有映射,匿名无法共享 return -EINVAL; //MAP_FIXED 方式 if (flags & MAP_FIXED) { // 页未对齐 ,返回 if (addr & ~PAGE_MASK) return -EINVAL; //校验范围 if (len > TASK_SIZE || addr > TASK_SIZE - len) return -EINVAL; } else { //映射时addr为空,则用户使用内核返回的地址 addr = get_unmapped_area(len); //获取一个未使用的虚拟地址 if (!addr) return -ENOMEM; } if (file && (!file->f_op || !file->f_op->mmap)) return -ENODEV; // 申请一个 vma 结构,用于管理 addr vma = (struct vm_area_struct )kmalloc(sizeof(struct vm_area_struct),GFP_KERNEL); if (!vma) return -ENOMEM; vma->vm_task = current; // 记录地址 vma->vm_start = addr; vma->vm_end = addr + len; // 设置读写执行标记 vma->vm_flags = prot & (VM_READ | VM_WRITE | VM_EXEC); // 设置其他标记的值 vma->vm_flags |= flags & (VM_GROWSDOWN | VM_DENYWRITE | VM_EXECUTABLE); // 文件映射 if (file) { //若可读 if (file->f_mode & 1) // 可以修改读写执行位 vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; if (flags & MAP_SHARED) { // 若共享 // 设置共享位,并且设置可以修改共享位 vma->vm_flags |= VM_SHARED | VM_MAYSHARE; // 文件不可写,设置vma属性为不可写,不能共享 if (!(file->f_mode & 2)) vma->vm_flags &= ~(VM_MAYWRITE | VM_SHARED); } } else // 若是匿名映射,设置默认属性 vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; vma->vm_page_prot = protection_map[vma->vm_flags & 0x0f]; vma->vm_ops = NULL; vma->vm_offset = off; // 初始化为NULL,在文件系统的mmap函数中进行设置 vma->vm_inode = NULL; vma->vm_pte = 0; // 解除addr旧的映射 do_munmap(addr, len); / Clear old maps */ if (file) error = file->f_op->mmap(file->f_inode, file, vma); // 对应函数为 generic_mmap() else // 匿名映射 error = anon_map(NULL, NULL, vma); if (error) { kfree(vma); return error; } // 把vm加入链表和 inode 的共享list中 insert_vm_struct(current, vma); merge_segments(current, vma->vm_start, vma->vm_end); return addr; }

上述操作过程如下:

获取一个未使用的虚拟地址 addr, 生成一个 vma 结构 然后和 addr 进行关联。
若是文件映射则调用 generic_mmap();
若是匿名映射,则调用 anon_map()。
把 vma 加入到链表和 inode 的共享链表中。

首先分析下 文件映射 generic_mmap():

int generic_mmap(struct inode * inode, struct file * file, struct vm_area_struct * vma) { struct vm_operations_struct * ops; //校验 … // 私有映射 ops = &file_private_mmap; // 共享映射 if (vma->vm_flags & VM_SHARED) { if (vma->vm_flags & (VM_WRITE | VM_MAYWRITE)) { static int nr = 0; ops = &file_shared_mmap; #ifndef SHARED_MMAP_REALLY_WORKS /* it doesn’t, yet */ if (nr++ < 5) printk("%s tried to do a shared writeable mapping\n", current->comm); return -EINVAL; #endif } } if (!IS_RDONLY(inode)) { inode->i_atime = CURRENT_TIME; inode->i_dirt = 1; } // 建立文件和vma的关系 vma->vm_inode = inode; // inode引用数加一 inode->i_count++; // 设置操作函数集 vma->vm_ops = ops; return 0; }

该函数就是完成了对vma设置有关file的操作接口,同时把vma和file对应inode进行关联。

有关操作接口如下:

static struct vm_operations_struct file_shared_mmap = { NULL, /* open / filemap_close, / close / filemap_unmap, / unmap / NULL, / protect / filemap_sync, / sync / NULL, / advise / filemap_nopage, / nopage / NULL, / wppage / filemap_swapout, / swapout / NULL, / swapin */ };

static struct vm_operations_struct file_private_mmap = { NULL, /* open / NULL, / close / NULL, / unmap / NULL, / protect / NULL, / sync / NULL, / advise / filemap_nopage, / nopage / NULL, / wppage / NULL, / swapout / NULL, / swapin */ };

匿名映射 anon_map()

//匿名映射 static int anon_map(struct inode *ino, struct file * file, struct vm_area_struct * vma) { if (zeromap_page_range(vma->vm_start, vma->vm_end - vma->vm_start, vma->vm_page_prot)) return -ENOMEM; return 0; }

匿名映射处理接口为 zeromap_page_range,该函数的作用就是层层处理全局页表中的中间目录项,处理中间目录项中的页表项,处理页表项中的页框。

在32位系统中采用3级分页机制,也即是页全局目录(PGD)、页中间目录(PMD)、页表项(PTE),三个关系如下:

页全局目录包含若干页中间目录的地址;

而页中间目录又包含若干页表的地址;

每一个页表项指向一个页框。

所以在zeromap_page_range 中层层处理这些关系,处理细节如下:

// 设置[address,address+size]范围内的页中间目录 int zeromap_page_range(unsigned long address, unsigned long size, pgprot_t prot) { int error = 0; pgd_t * dir; unsigned long end = address + size; pte_t zero_pte; // 新的页表项内容全为0,设置写保护,以后访问时写时复制机制被激活 zero_pte = pte_wrprotect(mk_pte(ZERO_PAGE, prot)); // 获取pmd表的起始地址 dir = pgd_offset(current, address); // 设置地址范围内的页中间目录、页表内容为zero_pte while (address < end) { pmd_t *pmd = pmd_alloc(dir, address); error = -ENOMEM; if (!pmd) break; error = zeromap_pmd_range(pmd, address, end - address, zero_pte); if (error) break; // 下一个最高级页目录项 address = (address + PGDIR_SIZE) & PGDIR_MASK; dir++; } // 刷新快表 invalidate(); return error; } // 重新设置[address,address+size]范围内的中间目录项中的页表项 static inline int zeromap_pmd_range(pmd_t * pmd, unsigned long address, unsigned long size, pte_t zero_pte) { unsigned long end; address &= ~PGDIR_MASK; end = address + size; if (end > PGDIR_SIZE) end = PGDIR_SIZE; do { pte_t * pte = pte_alloc(pmd, address); if (!pte) return -ENOMEM; // 设置中间目录项中的页表项 zeromap_pte_range(pte, address, end - address, zero_pte); address = (address + PMD_SIZE) & PMD_MASK; pmd++; } while (address < end); return 0; } // 重新设置[address,address+size]范围内的页表项的页框内容并释放旧的物理地址 static inline void zeromap_pte_range(pte_t * pte, unsigned long address, unsigned long size, pte_t zero_pte) { unsigned long end; // 屏蔽高位 address &= ~PMD_MASK; // 获取末地址 end = address + size; // 末地址是否超过了该页目录项管理的地址范围 if (end > PMD_SIZE) end = PMD_SIZE; do { pte_t oldpage = *pte; // 设置页表项的新内容 *pte = zero_pte; // 释放旧的物理页 forget_pte(oldpage); address += PAGE_SIZE; pte++; //获取下一个页表项 } while (address < end); } // 释放页表项对应虚拟地址的物理页 static inline void forget_pte(pte_t page) //若page不为空,调用free_pte将其释放。 { if (pte_none(page)) return; // 页表项已经映射了物理内存 if (pte_present(page)) { // 物理页引用数减一 free_page(pte_page(page)); // 是保留页则直接返回 if (mem_map[MAP_NR(pte_page(page))] & MAP_PAGE_RESERVED) return; if (current->mm->rss <= 0) return; // 进程驻留内存的页数减一 current->mm->rss–; return; } // 释放交换区 swap_free(pte_val(page)); }

以上匿名映射就处理完了,它的作用就是根据addr更新页表,把页框填充为内容全为0的且是写保护的页,但没有映射物理页面。等到进程访问这个虚拟地址范围的时候。在缺页中断处理函数do_no_page中会处理。

缺页异常

上述内存映射不管时文件映射还是匿名映射,都没有完成虚拟内存到物理页面的完整映射,因此当范围这段虚拟地址是会发生缺页中断。

//当页从未被访问时则调用do_no_page( )函数 void do_no_page(struct vm_area_struct * vma, unsigned long address, int write_access) { pte_t * page_table; pte_t entry; unsigned long page; // 在进程页表里获取address对应的页表项地址 page_table = get_empty_pgtable(vma->vm_task,address); // 分配失败则返回 if (!page_table) return; entry = *page_table; // 已经建立了虚拟地址到物理地址的映射,返回 if (pte_present(entry)) return; // 还没有建立映射 if (!pte_none(entry)) { do_swap_page(vma, address, page_table, entry, write_access); return; } // 屏蔽低12位,得到真正虚拟地址 address &= PAGE_MASK; /*没有nopage说明不是文件mmap ,如果页与文件建立起了映射关系,则nopage 域就指向一个把所缺的页从磁盘装入到RAM 的函数. 1、vma->vm_ops->nopage 域不为NULL。在这种情况下,某个虚拟区映射一个磁盘文件,nopage 域指向从磁盘读入的函数。这种情况涉及到磁盘文件的低层操作。 2、vm_ops 域为NULL,或者vma->vm_ops->nopage 域为NULL。在这种情况下,虚拟区没有映射磁盘文件,也就是说,它是一个匿名映射 */ if (!vma->vm_ops || !vma->vm_ops->nopage) { //说明是匿名映射,进入处理 // 记录分配给进程的页面数目,加1 ++vma->vm_task->mm->rss; // 缺页次数加一 ++vma->vm_task->mm->min_flt; // 获取一个物理页并且把物理页地址写到页表项page_table中 get_empty_page(vma, page_table); // return; } … }

当 do_no_page 走到 if (!vma->vm_ops || !vma->vm_ops->nopage) 处说明是你匿名映射,进行匿名映射处理。

到此处先分析下匿名映射处理:

// 获取一个新的物理页并记录到页表项里 static inline void get_empty_page(struct vm_area_struct * vma, pte_t * page_table) { unsigned long tmp; // 获取一个内容全为0的物理page if (!(tmp = get_free_page(GFP_KERNEL))) { oom(vma->vm_task); put_page(page_table, BAD_PAGE); return; } // 建立虚拟地址到物理地址的映射 put_page(page_table, pte_mkwrite(mk_pte(tmp, vma->vm_page_prot))); } // 把页表项插入页表中 static void put_page(pte_t * page_table, pte_t pte) { // 页表项已经保存了映射信息 if (!pte_none(*page_table)) { printk(“put_page: page already exists %08lx\n”, pte_val(page_table)); free_page(pte_page(pte)); return; } / no need for invalidate */ //插入页表 *page_table = pte; }

关于匿名映射的缺页异常处理就是获取一个内容全为0的物理页,然后把该物理页的页表项插入页表中,到此完成整虚拟地址到物理内存的完整映射。

接下来继续分析 do_no_page

//当页从未被访问时则调用do_no_page( )函数 void do_no_page(struct vm_area_struct * vma, unsigned long address, int write_access) { pte_t * page_table; pte_t entry; unsigned long page; // 在进程页表里获取address对应的页表项地址 page_table = get_empty_pgtable(vma->vm_task,address); // 分配失败则返回 if (!page_table) return; entry = *page_table; // 已经建立了虚拟地址到物理地址的映射,返回 if (pte_present(entry)) return; // 还没有建立映射 if (!pte_none(entry)) { do_swap_page(vma, address, page_table, entry, write_access); return; } // 屏蔽低12位,得到真正虚拟地址 address &= PAGE_MASK; /*没有nopage说明不是文件mmap ,如果页与文件建立起了映射关系,则nopage 域就指向一个把所缺的页从磁盘装入到RAM 的函数. 1、vma->vm_ops->nopage 域不为NULL。在这种情况下,某个虚拟区映射一个磁盘文件,nopage 域指向从磁盘读入的函数。这种情况涉及到磁盘文件的低层操作。 2、vm_ops 域为NULL,或者vma->vm_ops->nopage 域为NULL。在这种情况下,虚拟区没有映射磁盘文件,也就是说,它是一个匿名映射 */ if (!vma->vm_ops || !vma->vm_ops->nopage) { //说明是匿名映射,进入处理 //匿名映射处理逻辑 … } // 申请一页 page = get_free_page(GFP_KERNEL); // 判断是否已经加载过这个页面的数据 if (share_page(vma, address, write_access, page)) { // 缺页但是不需要从硬盘加载数据,min_flt加一 ++vma->vm_task->mm->min_flt; // 分配给进程的页面数目 加1 ++vma->vm_task->mm->rss; return; } if (!page) { oom(current); put_page(page_table, BAD_PAGE); return; } // 缺页并且需要从硬盘加载数据,maj_flt次数加一 ++vma->vm_task->mm->maj_flt; // 分配给进程的页面数目 加一 ++vma->vm_task->mm->rss; // 调文件系统的no_page函数(filemap_nopage),address是虚拟地址,page是物理地址 page = vma->vm_ops->nopage(vma, address, page, write_access && !(vma->vm_flags & VM_SHARED)); if (share_page(vma, address, write_access, 0)) { free_page(page); return; } // 新建一个页表项,page是物理地址 entry = mk_pte(page, vma->vm_page_prot); // 设置写保护或者可写 if (write_access) { entry = pte_mkwrite(pte_mkdirty(entry)); } else if (mem_map[MAP_NR(page)] > 1 && !(vma->vm_flags & VM_SHARED)) entry = pte_wrprotect(entry); // 把物理地址和相关信息写入页表项 put_page(page_table, entry); }

上述是文件映射流程,该流程的主要内容就是申请一个物理内存页,然后文件系统函数 filemap_nopage 函数,该函数的作用就是根据文件的inode节点获取数据块 block,然后计算物理内存页对应的几个数据块,把数据块内容拷贝到物理内存页中。

然后把创建页表项,把物理内存页的物理地址写到页表项中,完成虚拟地址到物理地址的映射关系。

filemap_nopage 操作就是把磁盘文件内容写到物理内存中,具体实现如下:

//从磁盘文件读取数据到相应的物理内存页面 static unsigned long filemap_nopage(struct vm_area_struct * area, unsigned long address, unsigned long page, int no_share) { struct inode * inode = area->vm_inode; unsigned int block; int nr[8]; int i, *p; // 屏蔽掉低12位 address &= PAGE_MASK; // vm_offset代表文件从该处开始被映射,对于vm_start的值,所以算出相对的块大小,还有加上偏移,得到绝对大小 block = address - area->vm_start + area->vm_offset; // 算出是文件中的第几块 block >>= inode->i_sb->s_blocksize_bits; // 一页包括多少块,即读进来的数据要是页的整数倍,假设是2,则至少要读两块 i = PAGE_SIZE >> inode->i_sb->s_blocksize_bits; p = nr; do { // 计算出当前块对应的硬盘块号,存在p中,也即是nr数组中 *p = bmap(inode,block); // 一页包括多少块则多读多少块,即循环多少次 i–; // 需要读的下一块块号 block++; // 指向数组下一个元素的地址 p++; } while (i > 0); // 把磁盘文件内容读到物理page中 return bread_page(page, inode->i_dev, nr, inode->i_sb->s_blocksize, no_share); }

到此也就完成了整个内存映射分析。

总结一下映射过程

:.

内存映射可分为匿名映射和文件映射,进行内存映射时只是完成了基本的构建,比如页表,中间目录项,页表项的创建,但都未进行物理内存的页表项构建。所以从虚拟内存到物理内存的映射未完全打通。
当访问某个虚拟地址空间时,当进行虚拟地址到物理地址的转换时,由于不存在对应的物理内存则发生缺页异常中断,通过缺页异常中断完成后续的内存映射关系,从而完成整个的虚拟内存到物理内存的映射。

共享内存的优缺点

优点

使用共享内存可以减少调用 read/write 等系统调用的开销,同时可以减少用户空间和内核空间的内存拷贝。
提供进程间共享内存的通信的方式。不管是父子进程还是无亲缘关系的进程,都可以通过映射的方式进行通信,但要做到进程间的同步。

缺点

对变长文件不适合,文件无法完成拓展,因为mmap的时候就确定了操作范围。
若文件的操作很频繁,会触发大量的脏页回写操作引起性能问题。
若文件过小,会照成内存浪费,内存映射以页为单位,若映射100个字节,但物理内存也会分配一个物理页。

原文 链接
进程间通信(IPC) 系列 | mmap

喜欢本文的朋友,欢迎关注公众号 Linux码农,获取更多干货
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值