1、内存映射原理​

参考文献:  
深入理解Linux内存子系统 (qq.com)
经典|图解Linux内存性能优化核心思想 (qq.com)

CPU对外设端口物理地址的编址方式有两种:一种是IO映射方式,另一种是内存映射方式。mmap是一种内存映射文件的方法。

(1)IO映射方式:CPU是i386架构的情况在i386系列的处理中,内存和外部IO是独立编址,也是独立寻址的。MEM的内存空间是32位可以寻址到4G,IO空间是16位可以寻址到64K。

(2)内存映射方式:arm,powerpc在这一类的嵌入式处理器中,IO Port的寻址方式是采用内存映射,也就是IO bus就是Mem bus。系统的寻址能力如果是32位,IO Port+Mem(包括IO Mem)可以达到4G。Linux将基于IO映射方式的和内存映射方式的IO端口统称为IO区域(IO region)。

(3)内存映射即在进程的虚拟地址空间中创建一个映射表,主要分为两种:

①文件映射,文件支持的内存映射是把文件的一个区间映射到进程的虚拟地址空间,数据源就是文件系统设备上的文件。--mmap

②还有就是匿名映射:没有文件对应的内存映射,把物理内存直接映射到虚拟地址空间,这里没有数据源。也是mmap实现。

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

(4)内存映射的原理:

创建内存映射时,在进程的用户虚拟地址空间中分配一个虚拟内存空间。内核采用延迟分配物理内存的策略,在进程第一个访问虚拟页的时候,产生缺页异常

如果是文件映射,那么直接分配物理页,把文件指定区间的数据读到物理页中,然后在页表中把虚拟页映射到物理页。

如果是匿名映射,就分配物理页,然后在页表中把虚拟页映射到物理页。有点类似裸机开发时,对硬件GPIO资源的访问一样,由于C函数不能直接读写GPIO对应的寄存器,只能进行ioremap映射后,把物理地址转换成虚拟地址。

在裸板开发的程序中,如何把GPIO寄存器的物理地址映射到虚拟地址的呢?
int led_open(struct inode *node, struct file *filp)
{
    led_config = ioremap(LEDCON,4);
    writel(0x11110000,led_config);
    
    led_data = ioremap(LEDDAT,4);
    
    return 0;
}

 

2、虚拟内存的数据结构体

有个问题请教大家?? 底下这段代码是满足一段需求,在内核中申请一个vma,然后对它进行其他操作。实验发现,这个驱动current->mm->mmap->vm_next->vm_start + 1到vm_end的长度正好是2K的页面的大小。试着通过下面另一段代码:

问题1、用户态并没有申请内存内存,通过ioctl后,在内核调用vma的时候,发现有三段vma用双向链表连接起来。为什么是三段???谁能解释一下???

        mm=current->mm;
        printk("addr start= 0x%lx\n", mm->mmap->vm_start+1);
        printk("addr end  = 0x%lx\n", mm->mmap->vm_end+1);

        temp = mm->mmap->vm_next;
        while(temp != NULL)
        {
                printk("this vma start=0x%lx, end=0x%lx\n", temp->vm_start, temp->vm_end);
                temp=temp->vm_next;
        }
打印的结果:

addr start=0x8001 end = 0x7b001
this vma start=0x82000, end=0x84000  这段空间正好是2K,这里应该是用来保存task_struct中内核的一段数据。在内核设计里面有谈到。
this vma start=0x84000, end=0xa7000
this vma start=0xbe9b0000, end=0xbe9c5000

  

#include <linux/security.h>
#include <linux/mm.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <asm/current.h>

MODULE_LICENSE("GPL");
static int __init find_vma_init(void);
static void __exit find_vma_exit(void);

int __init find_vma_init(void)
{
    struct mm_struct *mm ;
    unsigned long addr ;
    struct vm_area_struct * vma ;
    //struct vm_area_struct *va;

    mm= current->mm;
    addr = mm->mmap->vm_next->vm_start + 1;
    printk("addr = 0x%lx\n", addr);

    vma = find_vma(mm, addr);
    if(vma != NULL )
    {
        printk("vma->vm_start = 0x%lx\n", vma->vm_start);
        printk("vma->vm_end = 0x%lx\n", vma->vm_end);
    }
    return 0;
}
void __exit find_vma_exit(void)
{
    printk("exit! \n");
}

module_init(find_vma_init);
module_exit(find_vma_exit);

 

虚拟内存空间分配给进程的一个虚拟地址范围,在内核使用一个结构体来描述:vm_area_struct描述虚拟内存区域

struct mm_struct {
    struct vm_area_struct *mmap;/* 虚拟内存区域链表,每个进程都有list of VMAs */
struct vm_area_struct {
    /* The first cache line has the info for VMA tree walking. */
 用这两个成员来保存虚拟内存空间的首地址和末地址的第一个字节的地址
    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*/
    分别表示VAM链表的前后成员连接操作
    struct vm_area_struct *vm_next, *vm_prev;

如果采用链表组织化,会影响它的搜索速度,解决此问题则采用红黑树,在每个进程结构体
mm_struct中都会创建一颗红黑树,将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;    /* The address space we belong to. */
    pgprot_t vm_page_prot;        /* Access permissions of this VMA. */
    unsigned long vm_flags;        /* Flags, see mm.h. */

    /*
     * For areas with an address space and backing store,
     * linkage into the address_space->i_mmap interval tree.
     */
    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.
     */
    struct list_head anon_vma_chain; /* Serialized by mmap_sem & * page_table_lock */
    struct anon_vma *anon_vma;    /* Serialized by page_table_lock */

    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops;
    /* Information about our backing store: */
    unsigned long vm_pgoff;        /* Offset (within vm_file) in PAGE_SIZE units */
    struct file * vm_file;        /* File we map to (can be NULL). */
    void * vm_private_data;        /* was vm_pte (shared mem) */

#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;
};

3、系统调用

        内存管理子系统提供如下的系统调用函数:mmap、 munmap、mprotect函数

1)mmap--创建内存映射

系统调用mmap:进程创建匿名内存映射,把内存的物理页映射到及进城的虚拟地址空间。进程把文件映射到进程的虚拟地址空间,这样就可以访问内存一样访问文件,不需要调用系统调用read、write等访问文件,从而避免用户模式和内核模式之间的频繁切换,提供读写文件速度。---也是实现进程间通信的一种方式。

void *mmap(void * addr, size_t length, int prot, int flag, int fd, off_t offset);

prot的解释:PROT_EXEC、PROT_READ、PROT_WRITE选择这三种权限

flag:MAP_SHARED、MAP_PRIVATE等等还有很多其他选项。

2)munmap--删除内存映射

int munmap(void *addr, size_t len)

3)mprotect() --- 设置内存区域的访问权限

应用程序通常使用C标准提供的函数malloc申请内存。glibc库的内存分配器ptmalloc使用brk或者mmap向内核以页为单位扇区虚拟内存,mmap函数该如何填参数????然后把页划分成小内存块分配给应用程序。默认的阈值是128K,

如果应用程序申请的内存长度小于阈值(128K):ptmalloc分配器使用brk向内核申请虚拟内存,

如果应用程序申请的内存长度大于阈值(128K):ptmalloc分配器使用mmap向内核申请虚拟内存。

4)mmap内存映射的三个过程:

①、进程启动映射过程,并且在虚拟地址空间中为映射创建虚拟映射区域vma。没理解???

②、调用内核空间的系统调用mmap(不是用户态mmap),实现文件物理地址和进程虚拟地址的映射。

③、进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存的拷贝。(延迟分配物理内存技术)

 

5)实现进程间通信--通过普通文件内存映射的方式

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);//设置文件的大小为5个people
    write(fd,"",1);

    p_map=(people*)mmap(NULL,sizeof(people)*10, PROT_READ|PROT_WRITE,  MAP_SHARED, fd, 0);

    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);   

}

 6)内存映射权限分析mprotect函数的使用

Linux信号(signal) 机制分析 - h13 - 博客园
linux中sigaction函数详解_魏波-的博客-CSDN博客_sigaction
Linux中mprotect()函数详解_qiu.s.z的博客-CSDN博客_mprotect
Linux程序异常退出用backtrace定位分析_qiu.s.z的博客-CSDN博客Linux程序内存越界定位分析总结_qiu.s.z的博客-CSDN博客_linux 内存越界

#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 sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,
可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,
将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,
在结构sigaction的实例中,指定了对特定信号的处理,可以为空,
进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,
可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、
信号处理函数执行过程中应屏蔽掉哪些信号等等。
sigaction结构定义如下:
struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}
*/
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)/*SIGSEGV 段错误信号*/
        handle_error("siaction");

    pagesize=sysconf(_SC_PAGE_SIZE);//4096
    if(pagesize==-1)
        handle_error("sysconf");
/*函数:void * memalign (size_t boundary, size_t size) 
函数memalign将分配一个由size指定大小,地址是boundary的倍数的内存块
*/
    buffer=memalign(pagesize,4*pagesize);
    if(buffer==NULL)
        handle_error("memalign");

    printf("start of region : %p\n",buffer);
/*mprotect()函数可以修改调用进程内存页的保护属性,
如果调用进程尝试以违反保护属性的方式访问该内存,
则内核会发出一个SIGSEGV信号给该进程。
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,页大小(一般4KB)整数倍。
len:被修改保护属性区域的长度,最好为页大小整数倍。修改区域范围[addr, addr+len-1]。
prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
1)PROT_READ:内存段可读;
2)PROT_WRITE:内存段可写;
3)PROT_EXEC:内存段可执行;
4)PROT_NONE:内存段不可访问。
返回值:0;成功,-1;失败(并且errno被设置)

*/
    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;
}

 

7)虚拟内存结构体以及内存分析