文章目录
主要参考了《深入linux内核》和《Linux内核深度解析》,另外简单浅析了一下相关内容
内存映射原理及mmap
物理地址空间(设备和内存)
物理地址是处理器在系统总线上看到的地址。
- 使用精简指令集(Reduced Instruction Set Computer,RISC)的处理器通常只实现一个物理地址空间,外围设备和物理内存使用统一的物理地址空间。
- 有些处理器架构把分配给外围设备的物理地址区域称为设备内存。
**处理器通过外围设备控制器的寄存器访问外围设备,寄存器分为控制寄存器,状态寄存器和数据寄存器三大类。**外围设备的寄存器通常被连续地编址,处理器对外围设备寄存编址方式分为两种:
- I/O映射方式(I/O-mapped):I/O映射方式(I/O-mapped):英特尔的x86处理器为外围设备专门实现了一个单独的地址空间,称为“I/O地址空间”或“I/O端口空间”,处理器通过专门的I/O指令(如x86的in和out指令)来访问这一空间中的地址单元。
- 内存映射方式(memory-mapped) :使用精简指令集的处理器通常只实现一个物理地址空间,外围设备和物理内存使用统一的物理地址空间,
处理器可以像访问一个内存单元那样访问外围设备,不需要提供专门的I/O指令。
应用程序只能通过虚拟地址访问外设寄存器,内核提供API函数来把外设寄存器的物理地址映射到虚拟地址空间。
- 函数ioremap()把外设寄存器的物理地址映射到内核虚拟地址空间
- 函数io_remap_pfn_range()把外设寄存器的物理地址映射到进程的用户虚拟地址空间
ARM64架构 (物理地址宽度最大支持48位) 分为两种内存类型:
- 正常内存(Normal Memory):包括物理内存和只读存在器(ROM) ;
- 设备内存(Device Memory):指分配给外围设备寄存器的物理地址区域。
设备内存共享属性总是外部共享,缓存属性总是不可缓存(必须绕过处理器的缓存)。
内存映射原理
内存映射即在进程的虚拟地址空间中创建一个映射,分为两种:
(1)文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件。
(2)匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,没有数据源。
通常把文件映射的物理页称为文件页,把匿名映射的物理页称为匿名页
根据修改是否对其他进程可见和是否传递到底层文件,内存映射分为共享映射和私有映射。
(1)共享映射:修改数据时映射相同区域的其他进程可以看见,如果是文件支持的映射,修改会传递到底层文件。
(2)私有映射:第一次修改数据时会从数据源复制一个副本,然后修改副本,其他进程看不见,不影响数据源。
【内存映射的原理】
- 创建内存映射时,在进程的用户虚拟地址空间中分配一个虚拟内存区域。内核采用延迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。
- Linux内核采用延迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。
- 如果是文件映射,那么分配物理页,把文件指定区间的数据读到物理页中,然后在页表中把虚拟页映射到物理页。
- 如果是匿名映射,就分配物理页,然后在页表中把虚拟页映射到物理页。
进程中的文件映射和匿名映射
-
两个进程可以使用共享的文件映射实现共享内存。
-
匿名映射通常是私有映射,共享的匿名映射只可能出现在父进程和子进程之间。(非 System V 的共享内存)
-
在进程的虚拟地址空间中,代码段和数据段是私有的文件映射。未初始化数据段、堆栈是私有的匿名映射。
修改过的脏页面不会立即更新到文件中,可以调用msync来强制同步写入文件。
vm_area_struc
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking.
8 + 8 + 16 + 24 + 8 = 64
*/
// 这两个成员分别用来保存该虚拟内存空间的首地址和末地址后第一个字节的地址。
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev; // 分别为VMA链表的前后成员连接操作
// 创建一棵红黑树,将VMA作为一个节点加入到红黑树中,加速搜索
struct rb_node vm_rb;
/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
struct mm_struct *vm_mm; // 指向内存描述符,即虚拟内存区域所属的用户地址空间
pgprot_t vm_page_prot; // 保护位
/* 标志位 */
unsigned long vm_flags; /* Flags, see mm.h. */
/* 为了支持查询一个文件区间被映射到哪些虚拟内存区域,把一个文件映射到的所有虚拟内存加入该文件地址空间结构
address_space的成员i_mmap指向的区域树 */
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
/* 把虚拟内存区域关联的所有anon_yma实例串联起来,一个虚拟内存区域会关联到父进程的anon_vma实例
和自己的anon_vma实例*/
struct list_head anon_vma_chain;
/* 指向一个anon_vma实例,结构anon_vma用来组织匿名页被映射到的所有的虚拟地址空间 */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* 虚拟内存操作集合 */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; // 文件偏移,单位是页
struct file * vm_file; // 文件指针,如果是私有的匿名映射则为空
void * vm_private_data; // 指向内存区的私有数据
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};
mm.h
#define VM_NONE 0x00000000
#define VM_READ 0x00000001 /* currently active flags */
#define VM_WRITE 0x00000002
#define VM_EXEC 0x00000004
#define VM_SHARED 0x00000008
...
vm_operations_struct
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area); // 在创建虚拟内存区域时调用open方法
void (*close)(struct vm_area_struct * area); // 在删除虚拟内存区域时调用close方法
int (*mremap)(struct vm_area_struct * area); // 使用系统调用mremap移动虚拟内存区域时调用mremap方法
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); // 访问文件映射的虚拟页时,如果没有映射到物理页,生产缺页异常,异常处理程序调用fault就去来把文件的数据读到文件页缓存当中
// 与fault类似,区别是huge_fault方法针对使用透明巨型页的文件映射
int (*huge_fault)(struct vm_fault *vmf, enum page_entry_size pe_size);
/* 读文件映射的虚拟页时,如果没有映射到物理页,生成缺页异常,异常处理程序除了读入正在访问的文件页,还会预读后续的文件页,调用map_pages方法在文件的页缓存中分配物理页 */
void (*map_pages)(struct vm_fault *vmf,
pgoff_t start_pgoff, pgoff_t end_pgoff);
/* 第一次写私有的文件映射时,生成页错误异常,异常处理程序执行写时复制, 调用page_mkwrite方法
以通知文件系统页即将变成可写,以便文件系统检查是否允许写,或者等待页进入合适的状态。 */
int (*page_mkwrite)(struct vm_fault *vmf);
/* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
int (*pfn_mkwrite)(struct vm_fault *vmf);
/* called by access_process_vm when get_user_pages() fails, typically
* for use by special VMAs that can switch between memory and hardware
*/
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write);
/* Called by the /proc/PID/maps code to ask the vma whether it
* has a special name. Returning non-NULL will also cause this
* vma to be dumped unconditionally. */
const char *(*name)(struct vm_area_struct *vma);
...
};
文件映射的虚拟内存区域
(1)成员vm_file指向文件的一个打开实例(file)。索引节点代表一个文件,描述文件的属性。
(2)成员vm_pgoff存放文件的以页为单位的偏移。
(3)成员vm_ops指向虚拟内存操作集合,创建文件映射的时候调用文件操作集合中的mmap方法(file->f_op->mmap)以注册虚拟内存操作集合。
例如:假设文件属于EXT4文件系统,文件操作集合中的mmap方法是函数ext4_file_mmap,该函数把虚拟内存区域的成员vm_ops设置为ext4_file_vm_ops。
共享匿名映射的虚拟内存区域
共享匿名映射的实现原理和文件映射相同,区别是共享匿名映射关联的文件是内核创建的内部文件。在内存文件系统tmpfs中创建一个名为“/dev/zero”的文件,名字没有意义,创建两个共享匿名映射就会创建两个名为“/dev/zero”的文件,两个文件是独立的,毫无关系。
(1)成员vm_file指向文件的一个打开实例(file)。
(2)成员vm_pgoff存放文件的以页为单位的偏移。
(3)成员vm_ops指向共享内存的虚拟内存操作集合shmem_vm_ops。
共享匿名映射的创建方式
当使用参数 fd = -1 且 flags = MAP_ANONYMOUS | MAP_SHARED 时,创建的mmap 映射是共享匿名映射.
共享匿名映射让相关进程共享一块内存区域,通常用于父子进程之间的通信.
创建共享匿名映射有如下两种方式,这两种方法最终都调用shmem 模块来创建共享匿名映射
1. fd = -1,且 使用这个文件句柄来创建mmap(lags = MAP_ANONYMOUS | MAP_SHARED) .
在这种情况下,do_mmap_pgoff()->mmap_region() 函数
最终会调用shmem_zero_setup 来打开一个 "/dev/zero"
2. fd = 打开 "/dev/zero"返回的 fd ,且 使用这个文件句柄来创建mmap(flags = MAP_ANONYMOUS | MAP_SHARED)
mmap 做共享文件映射的时候可以用 磁盘文件,也可以用 内存文件。
mmap 做共享匿名映射的时候 ,本质上用的是 /dev/zero(其文件是内存文件/dev/zero) 。
私有匿名映射的虚拟内存区域

● 成员vm_file没有意义,是空指针。
● 成员vm_pgoff没有意义。
● 成员vm_ops是空指针。
应用编程接口mmap
应用程序通常使用C标准库提供的函数malloc()申请内存。glibc库的内存分配器ptmalloc使用brk或mmap向内核以页为单位申请虚拟内存,然后把页划分成小内存块分配给应用程序。默认的阈值是128kb,如果应用程序申请的内存长度小阈值,ptmalloc分配器使用brk向内核申请虚拟内存,否则ptmalloc分配器使用mmap向内核申请虚拟内存。
应用程序可以直接使用mmap向内核申请虚拟内存。
1、进程启动映射过程,并且在虚拟地址空间中为映射创建虚拟映射区域;
2、调用内核空间的函数mmap (不同于用户空间函数),实现文件物理地址和进程虚拟地址的 一 一 映射关系;
3、进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
mmap相关系统调用
1、mmap()----创建内存映射
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_toffset);
- 进程创建匿名的内存映射,把内存的物理页映射到进程的虚拟地址空间。
- 进程把文件映射到进程的虚拟地址空间,可以像访问内存一样访问文件,不需要调用系统调用read()/write()访问文件,从而避免用户模式和内核模式之间的切换,提高读写文件速度。
- 两个进程针对同一个文件创建共享的内存映射,实现共享内存。
2、munmap()----删除内存映射
int munmap(void *addr, size_t len);
3、mprotect()----设置虚拟内存区域的访问权限
int mprotect(void *addr, size_t len, int prot);
- PROT_EXEC:页可执行。
- PROT_READ:页可读。
- PROT_WRITE:页可写。
- PROT_NONE:页不可访问。
4、madvise() —— 用来向内核提出内存使用的建议,应用程序告诉内核期望怎样使用指定的虚拟内存区域,以便内核可以选择合适的预读和缓存技术。
int madvise(void *addr, size_t length, int advice);
POSIX标准定义的建议值如下:
- MADV_NORMAL:不需要特殊处理,这是默认值。
- MADV_RANDOM:预期随机访问指定范围的页,预读的用处比较小。
- MADV_SEQUENTIAL:预期按照顺序访问指定范围的页,所以可以激进地预读指定范围的页,并且进程在访问页以后很快释放。
- MADV_WILLNEED:预期很快就会访问指定范围的页,所以可以预读指定范围的页。
- MADV_DONTNEED:预期近期不会访问指定范围的页,即进程已经处理完指定范围的页,内核可以释放相关的资源。
另外还有Linux私有的建议值
使用两个进程通过映射普通文件实现共享内存通信
mmap1.c
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
typedef struct
{
/* data */
char name[4];
int age;
}people;
void main(int argc,char**argv)
{
int fd,i;
people *p_map;
char temp;
fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
lseek(fd,sizeof(people)*5-1,SEEK_SET);
write(fd,"",1);
p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); // 可被进程共享的文件映射
if(p_map==(void*)-1)
{
fprintf(stderr,"mmap : %s \n",strerror(errno));
return ;
}
close(fd);
temp='A';
for(i=0;i<10;i++)
{
temp=temp+1;
(*(p_map+i)).name[1]='\0';
memcpy((*(p_map+i)).name,&temp,1);
(*(p_map+i)).age=30+i;
}
printf("Initialize.\n");
sleep(15);
munmap(p_map,sizeof(people)*10);
printf("UMA OK.\n");
}
mmap2.c
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
typedef struct
{
/* data */
char name[4];
int age;
}people;
void main(int argc,char**argv)
{
int fd,i;
people *p_map;
fd=open(argv[1],O_CREAT|O_RDWR,00777);
p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p_map==(void*)-1)
{
fprintf(stderr,"mmap : %s \n",strerror(errno));
return ;
}
for(i=0;i<10;i++)
{
printf("name:%s age:%d\n",(*(p_map+i)).name,(*(p_map+i)).age);
}
munmap(p_map,sizeof(people)*10);
}
mptroect.c
#include <unistd.h>
#include <signal.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
#define handle_error(msg) do{ perror(msg); exit(EXIT_FAILURE);}while(0)
static char *buffer;
static void handler(int sig, siginfo_t *si, void *unused) // 信号处理函数
{
printf("Get SIGSEGV at address : %p\n",si->si_addr);
exit(EXIT_FAILURE);
}
int main(int argc,char *argv[])
{
int pagesize;
struct sigaction sa;
sa.sa_flags=SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sa.sa_sigaction=handler;
if(sigaction(SIGSEGV,&sa,NULL)==-1) // 注册信号处理函数
handle_error("siaction");
pagesize=sysconf(_SC_PAGE_SIZE);
if(pagesize==-1)
handle_error("sysconf");
buffer=memalign(pagesize,4*pagesize);
if(buffer==NULL)
handle_error("memalign");
printf("start of region : %p\n",buffer);
if(mprotect(buffer+pagesize*2,pagesize,PROT_READ)==-1)
handle_error("mprotect");
for(char *p=buffer;;) // 触发信号
*(p++)='A';
printf("for completed.\n");
exit(EXIT_SUCCESS);
return 0;
}