深入理解mmap--內核程式碼分析及驅動demo示例

mmap是一個很常用的系統調用,無論是分配記憶體、讀寫大文件、鏈接動態庫文件,還是多進程間共享記憶體,都可以看到其身影。本文首先介紹了進程地址空間和mmap,然後分析了內核程式碼以了解其實現,最後通過一個簡單的demo驅動示例,加深對mmap的理解。

本部落格已遷移至CatBro's Blog,那是我自己搭建的個人部落格,歡迎關注。

本文鏈接

進程地址空間及vma

作為前置知識,先來對進程地址空間做個簡單介紹,以便更好地理解後面的內容。現代作業系統的記憶體管理離不開硬體的支援,如分段機制、分頁機制。它們用於實現記憶體的隔離、保護以及高效使用。進程之間地址空間相互隔離,每個進程都有一套頁表,實現線性地址到物理地址的轉換。

虛擬記憶體映射

下面是32位系統(x86)的進程地址空間布局圖

img

0~3G 部分是用戶空間的地址,3G~4G 部分是內核地址空間。虛擬地址從低到高分別為程式碼段、數據段(已初始化的靜態變數)、bss段(未初始化的靜態變數)、heap堆、mmap映射區、棧、命令行參數、環境變數。

從0xc0000000開始就是內核地址空間了。內核地址空間又分為線性記憶體區和高端記憶體區。高端記憶體區是用於vmalloc機制、fixmap等的。在x86體系中,最低16MB物理記憶體是DMA記憶體區,用於執行DMA操作。

64位系統(x86_64)上,記憶體地址可用空間為0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,這是一個非常巨大的地址空間。而Linux實際上只用了低47位(128T),高17位作擴展。實際用到的地址空間為0x0000000000000000 ~ 0x00007FFFFFFFFFFF(用戶空間)和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF(內核空間)。

在64位處理器中,由於有足夠的內核空間可以線性映射物理記憶體,所以就不需要高端記憶體這個管理區了。更詳細的資訊可以參考內核文檔。

VMA

進程地址空間在Linux內核中使用struct vm_area_struct來描述,簡稱VMA。由於這些地址空間歸屬於各個用戶進程,所以在用戶進程的struct mm_struct中也有相應的成員。進程可以通過內核的記憶體管理機制動態地添加或刪除這些記憶體區域。

每個記憶體區域具有相關的許可權,比如可讀、可寫、可執行。如果進程訪問了不在有效範圍內的記憶體區域、或非法訪問了記憶體,那麼處理器會報缺頁異常,嚴重的會出現段錯誤。

// include/linux/mm_types.h

/*
 * This struct defines a memory VMM memory area. There is one of these
 * per VM-area/task.  A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
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 */
    struct vm_area_struct *vm_next, *vm_prev;

    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) */

    atomic_long_t swap_readahead_info;
#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;
} __randomize_layout;

解釋下幾個主要的成員:

  • vm_start和vm_end:表示vma的起始和結束地址,相減就是vma的長度
  • vm_next和vm_prev:鏈表指針
  • vm_rb:紅黑樹節點
  • vm_mm:所屬進程的記憶體描述符mm_struct數據結構
  • vm_page_prot:vma的訪問許可權
  • vm_flags:vma的標誌
  • anon_vma_chain和anon_vma:用於管理RMAP反向映射
  • vm_ops:指向操作方法結構體
  • vm_pgoff:文件映射的偏移量。
  • vm_file:指向被映射的文件

mmap簡介

// 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);
  • addr:指定起始地址,為了可移植性一般設為NULL
  • length:表示映射到進程地址空間的大小
  • prot:讀寫屬性,PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE
  • flags:標誌,如共享映射、私有映射
  • fd:文件描述符,匿名映射時設為-1。
  • offset:文件映射時,表示偏移量

flag標誌

  • MAP_SHARED:創建一個共享的映射區域。多個進程可以這樣映射同一個文件,修改後的內容會同步到磁碟文件中。
  • MAP_PRIVATE:創建寫時複製的私有映射。多個進程可以私有映射同一個文件,修改之後不會同步到磁碟中。
  • MAP_ANONYMOUS:創建匿名映射,即沒有關聯到文件的映射
  • MAP_FIXED:使用參數addr創建映射,如果無法映射指定的地址就返回失敗,addr要求按頁對齊。如果指定的地址空間與已有的VMA重疊,會先銷毀重疊的區域。
  • MAP_POPULATE:對於文件映射,會提前預讀文件內容到映射區域,該特性只支援私有映射。

4類映射

根據prot和flags的不同組合,可以分為以下4種映射類型:

  • 私有匿名:通常用於記憶體分配(大塊)
  • 私有文件:通常用於載入動態庫
  • 共享匿名:通常用於進程間共享記憶體,默認打開/dev/zero這個特殊的設備文件
  • 共享文件:通常用於記憶體映射I/O,進程間通訊

mmap記憶體映射原理

  1. 當用戶空間調用mmap時,系統會尋找一段滿足要求的連續虛擬地址,然後創建一個新的vma插入到mm系統的鏈表和紅黑樹中。
  2. 調用內核空間mmap,建立文件塊/設備物理地址和進程虛擬地址vma的映射關係
    1. 如果是磁碟文件,沒有特別設置標誌的話這裡只是建立映射不會實際分配記憶體。
    2. 如果是設備文件,直接通過remap_pfn_range函數建立設備物理地址到虛擬地址的映射。
  3. (如果是磁碟文件映射)當進程對這片映射地址空間進行訪問時,引發缺頁異常,將數據從磁碟中拷貝到物理記憶體。後續用戶空間就可以直接對這塊內核空間的物理記憶體進行讀寫,省去了用戶空間跟內核空間之間的拷貝過程。

內核程式碼分析

當我們在用戶空間調用mmap時,首先通過系統調用進入內核空間,可以看到這裡將offset轉成了以頁為單位。

// arch/x86/kernel/sys_x86_64.c
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
        unsigned long, prot, unsigned long, flags,
        unsigned long, fd, unsigned long, off)
{
    long error;
    error = -EINVAL;
    if (off & ~PAGE_MASK)
        goto out;

    error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:
    return error;
}

來看系統調用sys_mmap_pgoff,如果是不是匿名映射,會通過fd獲取file結構體。

// mm/mmap.c
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
        unsigned long, prot, unsigned long, flags,
        unsigned long, fd, unsigned long, pgoff)
{
    struct file *file = NULL;
    unsigned long retval;
    if (!(flags & MAP_ANONYMOUS)) {
        // ...
        file = fget(fd);
        // ...
    }
    // ...
    retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
    return retval;
}

接著看vm_mmap_pgoff函數,這裡主要用訊號量對進程地址空間做了一個保護,然後根據populate的值會prefault頁表,如果是文件映射則會對文件進行預讀。

// mm/util.c
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flag, unsigned long pgoff)
{
    unsigned long ret;
    struct mm_struct *mm = current->mm;
    unsigned long populate;
    LIST_HEAD(uf);

    ret = security_mmap_file(file, prot, flag);
    if (!ret) {
        if (down_write_killable(&mm->mmap_sem))
            return -EINTR;
        ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,
                    &populate, &uf);
        up_write(&mm->mmap_sem);
        userfaultfd_unmap_complete(mm, &uf);
        if (populate)
            mm_populate(ret, populate);
    }
    return ret;
}

do_mmap_pgoff只是簡單調用do_mmap

// include/linux/mm.h
static inline unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot, unsigned long flags,
    unsigned long pgoff, unsigned long *populate,
    struct list_head *uf)
{
    return do_mmap(file, addr, len, prot, flags, 0, pgoff, populate, uf);
}

我們來看do_mmap實現:

// mm/mmap.c
unsigned long do_mmap(struct file *file, unsigned long addr,
            unsigned long len, unsigned long prot,
            unsigned long flags, vm_flags_t vm_flags,
            unsigned long pgoff, unsigned long *populate,
            struct list_head *uf)
{
    struct mm_struct *mm = current->mm;
    // ...
    len = PAGE_ALIGN(len);
    // ...
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    // ...
    addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
    if (!IS_ERR_VALUE(addr) &&
        ((vm_flags & VM_LOCKED) ||
         (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
        *populate = len;
    return addr;
}

這個函數主要將映射長度頁對齊,對prot屬性和flags標誌進行了檢查和處理,設置了vm_flags。get_unmapped_area函數檢查指定的地址或自動選擇可用的虛擬地址。然後就調用mmap_region,可以看到返回之後,根據調用介面時設置的flags對populate進行了設置。如果設置了MAP_LOCKED,或者設置了MAP_POPULATE但沒有設置MAP_NONBLOCK,就進行前面提到的prefault操作。

然後繼續看mmap_region

// mm/mmap.c
unsigned long mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
        struct list_head *uf)
{
    // ...
    vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
            NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)    // 可以跟之前的映射合併
        goto out;
    
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = vm_get_page_prot(vm_flags);
    vma->vm_pgoff = pgoff;
    INIT_LIST_HEAD(&vma->anon_vma_chain);
    
    if (file) {
        // ...
        vma->vm_file = get_file(file);
        error = call_mmap(file, vma);   // 調用文件的mmap
        //...
    } else if (vm_flags & VM_SHARED) {
        error = shmem_zero_setup(vma);
    }
    
    // ...
    return addr;
    // ...
}

該函數首先做了一些地址空間檢查,接著vma_merge檢查是否可以和老的映射合併,然後就是分配vma並初始化。如果是文件映射,調用call_mmap;如果是匿名共享映射,調用shmem_zero_setup,它裡面會進行/dev/zero文件相關設置。

call_mmap只是簡單地調用文件句柄中的mmap操作函數。

// include/linux/fs.h
static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
    return file->f_op->mmap(file, vma);
}

如果是普通文件系統中的文件的話,我們以ext4為例,裡面主要是設置了vma->vm_ops為ext4_file_vm_ops。

// fs/ext4/file.c
static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
    //...
    vma->vm_ops = &ext4_file_vm_ops;
    //...
    return 0;
}

static const struct vm_operations_struct ext4_file_vm_ops = {
    .fault      = ext4_filemap_fault,
    .map_pages  = filemap_map_pages,
    .page_mkwrite   = ext4_page_mkwrite,
};

後續當訪問這個vma地址空間時,就會調用相應的操作函數進行處理,比如頁錯誤處理函數會調用ext4_filemap_fault,裡面又會調用filemap_fault。

如果是設備文件的話,由相應的設備驅動實現mmap方法,在裡面建立設備物理記憶體到vma地址空間的映射。接下來通過一個簡單的驅動demo來演示。

簡單總結一下

mmap                        // offset轉成頁為單位
+-- sys_mmap_pgoff          // 通過fd獲取file
    +-- vm_mmap_pgoff       // 訊號量保護,映射完成後populate
        +-- do_mmap_pgoff   // 簡單封裝
            +-- do_mmap     // 映射長度頁對齊,prot和flags檢查,設置vm_flags,獲取映射虛擬地址
                +-- mmap_region      // 地址空間檢查,vma_merge,vma分配及初始化
                    |-- call_mmap    // 文件映射,簡單封裝
                    |   +-- file->f_op->mmap    // 調用實際文件的mmap方法
                    |-- shmem_zero_setup       // 匿名共享映射,/dev/zero

驅動demo

我們編寫了一個簡單的misc設備,在驅動載入的時候使用alloc_pages分配設備的物理記憶體(4頁),當然也可以使用kmalloc或vmalloc。然後實現了幾個操作方法,其中最主要的就是mmap方法,為了方便測試我們還實現了read、write、llseek等方法。

{% note default %}
ps: 驅動及測試程式程式碼已上傳github,catbro666/mmap-driver-demo
{% endnote %}

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/gfp.h>          // alloc_page
#include <linux/miscdevice.h>   // miscdevice misc_xxx
#include <linux/uaccess.h>      // copy_from/to_user

#define DEMO_NAME "demo_dev"
#define PAGE_ORDER 2
#define MAX_SIZE (PAGE_SIZE << PAGE_ORDER)

static struct device *mydemodrv_device;
static struct page *page = NULL;
static char *device_buffer = NULL;

static const struct file_operations demodrv_fops = {
    .owner      = THIS_MODULE,
    .open       = demodrv_open,
    .release    = demodrv_release,
    .read       = demodrv_read,
    .write      = demodrv_write,
    .mmap       = demodev_mmap,
    .llseek     = demodev_llseek
};

static struct miscdevice mydemodrv_misc_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = DEMO_NAME,
    .fops = &demodrv_fops,
};

static int __init demo_dev_init(void)
{
    int ret;

    ret = misc_register(&mydemodrv_misc_device);
    if (ret) {
        printk("failed to register misc device");
        return ret;
    }

    mydemodrv_device = mydemodrv_misc_device.this_device;

    printk("succeeded register misc device: %s\n", DEMO_NAME);

    page = alloc_pages(GFP_KERNEL, PAGE_ORDER);
    if (!page) {
        printk("alloc_page failed\n");
        return -ENOMEM;
    }
    device_buffer = page_address(page);
    printk("device_buffer physical address: %lx, virtual address: %px\n",
           page_to_pfn(page) << PAGE_SHIFT, device_buffer);

    return 0;
}


static void __exit demo_dev_exit(void)
{
    printk("removing device\n");

    __free_pages(page, PAGE_ORDER);

    misc_deregister(&mydemodrv_misc_device);
}

module_init(demo_dev_init);
module_exit(demo_dev_exit);
MODULE_AUTHOR("catbro666");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("mmap test module");

這裡主要看一下mmap方法的實現,核心函數是remap_pfn_range,它用於建立實際物理地址到vma虛擬地址的映射。我們來看下它的參數,第一個是要映射的用戶空間vma,第二個是映射起始地址,第三個是內核記憶體的物理頁幀號,第四個是映射區域的大小,第五個是對這個映射的頁保護標誌。

我們用到的大部分參數通過vma獲取,如上一節所看到的,外層函數已經做好了vma初始化工作。因為我們是用alloc_pages分配的記憶體,其物理地址是連續的,所以映射也比較簡單。

static int demodev_mmap(struct file *file, struct vm_area_struct *vma)
{
    struct mm_struct *mm;
    unsigned long size;
    unsigned long pfn_start;
    void *virt_start;
    int ret;

    mm = current->mm;
    pfn_start = page_to_pfn(page) + vma->vm_pgoff;
    virt_start = page_address(page) + (vma->vm_pgoff << PAGE_SHIFT);

    /* 映射大小不超過實際分配的物理記憶體大小 */
    size = min(((1 << PAGE_ORDER) - vma->vm_pgoff) << PAGE_SHIFT,
               vma->vm_end - vma->vm_start);

    printk("phys_start: 0x%lx, offset: 0x%lx, vma_size: 0x%lx, map size:0x%lx\n",
           pfn_start << PAGE_SHIFT, vma->vm_pgoff << PAGE_SHIFT,
           vma->vm_end - vma->vm_start, size);

    if (size <= 0) {
        printk("%s: offset 0x%lx too large, max size is 0x%lx\n", __func__,
               vma->vm_pgoff << PAGE_SHIFT, MAX_SIZE);
        return -EINVAL;
    }

    // 外層vm_mmap_pgoff已經用訊號量保護了
    ret = remap_pfn_range(vma, vma->vm_start, pfn_start, size, vma->vm_page_prot);

    if (ret) {
        printk("remap_pfn_range failed, vm_start: 0x%lx\n", vma->vm_start);
    }
    else {
        printk("map kernel 0x%px to user 0x%lx, size: 0x%lx\n",
               virt_start, vma->vm_start, size);
    }

再來看下read方法的實現,主要就是從設備記憶體中拷貝數據到用戶空間的buf中,然後更新文件偏移。write方法也是類似,這裡就不再展示。

static ssize_t
demodrv_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    int actual_readed;
    int max_read;
    int need_read;
    int ret;
    max_read = PAGE_SIZE - *ppos;
    need_read = max_read > count ? count : max_read;
    if (need_read == 0)
        dev_warn(mydemodrv_device, "no space for read");

    ret = copy_to_user(buf, device_buffer + *ppos, need_read);
    if (ret == need_read)
        return -EFAULT;
    actual_readed = need_read - ret;
    *ppos += actual_readed;

    printk("%s actual_readed=%d, pos=%lld\n", __func__, actual_readed, *ppos);
    return actual_readed;
}

測試程式

安裝驅動

我們首先編譯安裝驅動,設備節點文件已經自動創建。查看內核日誌可以看到已經成功創建了設備,並分配了記憶體。起始物理地址為0x5b1558000,內核虛擬地址為0xffff8d1ab1558000。

$ sudo insmod mydemodev.ko
$ ll /dev|grep demo
crw-------   1 root root     10,  58 12月 12 23:33 demo_dev
$ dmesg | tail -n 2
[110047.799513] succeeded register misc device: demo_dev
[110047.799517] device_buffer physical address: 5b1558000, virtual address: ffff8d1ab1558000

測試程式1

接下來我們寫了幾個測試程式來對這個驅動進行測試。首先來看第一個測試程式,我們打開驅動設備文件/dev/demo_dev,然後mmap映射了1頁的大小,這裡前後分別sleep了5秒,是為了提供觀察的時間。然後通過映射的用戶空間虛擬地址進行讀寫測試,驗證mmap是否正確映射了。首先通過虛擬地址寫,隨後用read讀取進行比對檢查。然後通過write寫,隨後用虛擬地址讀取進行比對檢查。

// test1.c
#include <stdio.h>      // printf
#include <fcntl.h>      // open
#include <unistd.h>     // read, close, getpagesize
#include <sys/mman.h>   // mmap
#include <string.h>     // memcmp, strlen
#include <assert.h>     // assert

#define DEMO_DEV_NAME   "/dev/demo_dev"

int main()
{
    char buf[64];
    int fd;
    char *addr = NULL;
    int ret;
    char *message = "Hello World\n";
    char *message2 = "I'm superman\n";

    fd = open(DEMO_DEV_NAME, O_RDWR);
    if (fd < 0) {
        printf("open device %s failed\n", DEMO_DEV_NAME);
        return -1;
    }
    
    sleep(5);
    addr = mmap(NULL, (size_t)getpagesize(), PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_LOCKED, fd, 0);
    sleep(5);
    
    /* 測試映射正確 */
    /* 寫到mmap映射的虛擬地址中,通過read讀取設備文件 */
    ret = sprintf(addr, "%s", message);
    assert(ret == strlen(message));

    ret = read(fd, buf, 64);
    assert(ret == 64);
    assert(!memcmp(buf, message, strlen(message)));

    /* 通過write寫入設備文件,修改體現在mmap映射的虛擬地址 */
    ret = write(fd, message2, strlen(message2));

    assert(ret == strlen(message2));
    assert(!memcmp(addr + 64, message2, strlen(message2)));

    munmap(addr, (size_t)getpagesize());
    close(fd);
    return 0;
}

我們編譯運行測試程式,結果如我們預期。從內核日誌可以看到映射起始物理地址0x5b1558000,偏移為0,vma大小是1頁,映射大小也是1頁。將內核空間虛擬地址0xffff8d1ab1558000映射到了用戶空間0x7f21c0f58000。

$ sudo ./test1
$ dmesg|tail -n 4
[110691.745381] phys_start: 0x5b1558000, offset: 0x0, vma_size: 0x1000, map size:0x1000
[110691.745388] map kernel 0xffff8d1ab1558000 to user 0x7f21c0f58000, size: 0x1000
[110696.745816] demodrv_read actual_readed=64, pos=64
[110696.745822] demodrv_write actual_written=13, pos=77

與此同時,我們使用pmap觀察mmap前後的進程的地址空間

{% fold 點擊展開進程地址空間 %}

$ sudo pmap -x $(pgrep test1)
[sudo] password for ssl:
30830:   ./test1
Address           Kbytes     RSS   Dirty Mode  Mapping
0000557b19475000       4       4       0 r-x-- test1
0000557b19475000       0       0       0 r-x-- test1
0000557b19676000       4       4       4 r---- test1
0000557b19676000       0       0       0 r---- test1
0000557b19677000       4       4       4 rw--- test1
0000557b19677000       0       0       0 rw--- test1
00007f21c0941000    1948     888       0 r-x-- libc-2.27.so
00007f21c0941000       0       0       0 r-x-- libc-2.27.so
00007f21c0b28000    2048       0       0 ----- libc-2.27.so
00007f21c0b28000       0       0       0 ----- libc-2.27.so
00007f21c0d28000      16      16      16 r---- libc-2.27.so
00007f21c0d28000       0       0       0 r---- libc-2.27.so
00007f21c0d2c000       8       8       8 rw--- libc-2.27.so
00007f21c0d2c000       0       0       0 rw--- libc-2.27.so
00007f21c0d2e000      16       8       8 rw---   [ anon ]
00007f21c0d2e000       0       0       0 rw---   [ anon ]
00007f21c0d32000     156     156       0 r-x-- ld-2.27.so
00007f21c0d32000       0       0       0 r-x-- ld-2.27.so
00007f21c0f41000       8       8       8 rw---   [ anon ]
00007f21c0f41000       0       0       0 rw---   [ anon ]
00007f21c0f59000       4       4       4 r---- ld-2.27.so
00007f21c0f59000       0       0       0 r---- ld-2.27.so
00007f21c0f5a000       4       4       4 rw--- ld-2.27.so
00007f21c0f5a000       0       0       0 rw--- ld-2.27.so
00007f21c0f5b000       4       4       4 rw---   [ anon ]
00007f21c0f5b000       0       0       0 rw---   [ anon ]
00007ffdacdf1000     132       8       8 rw---   [ stack ]
00007ffdacdf1000       0       0       0 rw---   [ stack ]
00007ffdacf3c000      12       0       0 r----   [ anon ]
00007ffdacf3c000       0       0       0 r----   [ anon ]
00007ffdacf3f000       4       4       0 r-x--   [ anon ]
00007ffdacf3f000       0       0       0 r-x--   [ anon ]
ffffffffff600000       4       0       0 --x--   [ anon ]
ffffffffff600000       0       0       0 --x--   [ anon ]
---------------- ------- ------- -------
total kB            4376    1120      68

$ sudo pmap -x $(pgrep test1)
30830:   ./test1
Address           Kbytes     RSS   Dirty Mode  Mapping
0000557b19475000       4       4       0 r-x-- test1
0000557b19475000       0       0       0 r-x-- test1
0000557b19676000       4       4       4 r---- test1
0000557b19676000       0       0       0 r---- test1
0000557b19677000       4       4       4 rw--- test1
0000557b19677000       0       0       0 rw--- test1
00007f21c0941000    1948     888       0 r-x-- libc-2.27.so
00007f21c0941000       0       0       0 r-x-- libc-2.27.so
00007f21c0b28000    2048       0       0 ----- libc-2.27.so
00007f21c0b28000       0       0       0 ----- libc-2.27.so
00007f21c0d28000      16      16      16 r---- libc-2.27.so
00007f21c0d28000       0       0       0 r---- libc-2.27.so
00007f21c0d2c000       8       8       8 rw--- libc-2.27.so
00007f21c0d2c000       0       0       0 rw--- libc-2.27.so
00007f21c0d2e000      16       8       8 rw---   [ anon ]
00007f21c0d2e000       0       0       0 rw---   [ anon ]
00007f21c0d32000     156     156       0 r-x-- ld-2.27.so
00007f21c0d32000       0       0       0 r-x-- ld-2.27.so
00007f21c0f41000       8       8       8 rw---   [ anon ]
00007f21c0f41000       0       0       0 rw---   [ anon ]
00007f21c0f58000       4       0       0 rw-s- demo_dev
00007f21c0f58000       0       0       0 rw-s- demo_dev
00007f21c0f59000       4       4       4 r---- ld-2.27.so
00007f21c0f59000       0       0       0 r---- ld-2.27.so
00007f21c0f5a000       4       4       4 rw--- ld-2.27.so
00007f21c0f5a000       0       0       0 rw--- ld-2.27.so
00007f21c0f5b000       4       4       4 rw---   [ anon ]
00007f21c0f5b000       0       0       0 rw---   [ anon ]
00007ffdacdf1000     132       8       8 rw---   [ stack ]
00007ffdacdf1000       0       0       0 rw---   [ stack ]
00007ffdacf3c000      12       0       0 r----   [ anon ]
00007ffdacf3c000       0       0       0 r----   [ anon ]
00007ffdacf3f000       4       4       0 r-x--   [ anon ]
00007ffdacf3f000       0       0       0 r-x--   [ anon ]
ffffffffff600000       4       0       0 --x--   [ anon ]
ffffffffff600000       0       0       0 --x--   [ anon ]
---------------- ------- ------- -------
total kB            4380    1120      68

{% endfold %}

可以看到mmap之後多了一個叫做demo_dev的段,其起始地址就是我們映射的用戶空間地址0x7f21c0f58000。

00007f21c0f58000       4       0       0 rw-s- demo_dev
00007f21c0f58000       0       0       0 rw-s- demo_dev

測試程式2

測試程式2差別不大,打開同一個設備文件,mmap建立相同的映射,然後分別通過read和虛擬地址讀取前一個程式寫的內容。

// test.2
int main()
{
    char buf[64];
    int fd;
    char *addr = NULL;
    int ret;
    char *message = "Hello World\n";
    char *message2 = "I'm superman\n";

    /* 另一進程打開同一設備文件,然後用mmap映射 */
    fd = open(DEMO_DEV_NAME, O_RDWR);
    if (fd < 0) {
        printf("open device %s failed\n", DEMO_DEV_NAME);
        return -1;
    }
    addr = mmap(NULL, (size_t)getpagesize(), PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_LOCKED, fd, 0);

    /* 通過read讀取設備文件 */
    ret = read(fd, buf, sizeof(buf));

    assert(ret == sizeof(buf));
    assert(!memcmp(buf, message, strlen(message)));

    /* 通過mmap映射的虛擬地址讀取 */
    assert(!memcmp(addr + sizeof(buf), message2, strlen(message2)));

    munmap(addr, (size_t)getpagesize());
    close(fd);
    return 0;
}

編譯運行,測試結果如我們預期。同一個內核虛擬地址現在映射到了不同的用戶空間虛擬地址。通過mmap我們實現了進程間通訊。

$ sudo ./test2
$ dmesg|tail -n 3
[111333.818374] phys_start: 0x5b1558000, offset: 0x0, vma_size: 0x1000, map size:0x1000
[111333.818378] map kernel 0xffff8d1ab1558000 to user 0x7f015ee94000, size: 0x1000
[111333.818381] demodrv_read actual_readed=64, pos=64

測試程式3

這次我們來測試一些特殊情況,映射的大小改成了1個位元組,根據前面的程式碼分析,映射是需要頁對齊的,所以預期實際會映射一個頁。在一頁的範圍內是可以正常讀寫的。然後嘗試寫到vma映射範圍之外,預期會出現段錯誤。

int main()
{
    char buf[64];
    int fd;
    char *addr = NULL;
    off_t offset;
    int ret;
    char *message = "Hello World\n";
    char *message2 = "I'm superman\n";

    fd = open(DEMO_DEV_NAME, O_RDWR);
    if (fd < 0) {
        printf("open device %s failed\n", DEMO_DEV_NAME);
        return -1;
    }
    /* 映射1個位元組 */
    addr = mmap(NULL, 1, PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_LOCKED, fd, 0);

    /* 寫到mmap映射的虛擬地址中,通過read讀取設備文件 */
    ret =sprintf(addr, "%s", message);
    assert(ret == strlen(message));

    ret = read(fd, buf, sizeof(buf));
    assert(ret == sizeof(buf));
    assert(!memcmp(buf, message, strlen(message)));

    /* 寫到一頁的尾部 */
    ret = sprintf(addr + getpagesize() - sizeof(buf), "%s", message2);
    assert(ret == strlen(message2));

    offset = lseek(fd, getpagesize() - sizeof(buf), SEEK_SET);
    assert(offset == getpagesize() - sizeof(buf));

    ret = read(fd, buf, sizeof(buf));
    assert(ret == sizeof(buf));
    assert(!memcmp(buf, message2, strlen(message2)));
    
    /* 寫到一頁之後,超出映射範圍 */
    printf("expect segment error\n");
    ret = sprintf(addr + getpagesize(), "something");
    printf("never reach here\n");

    munmap(addr, 1);
    close(fd);
    return 0;
}

我們編譯運行測試,結果如我們預期,實際映射了1頁的大小,當嘗試超出映射範圍寫時,出現了段錯誤(SIGSEGV)。

$ sudo ./test3
expect segment error
Segmentation fault
$ dmesg|tail -n 6
[111762.605089] phys_start: 0x5b1558000, offset: 0x0, vma_size: 0x1000, map size:0x1000
[111762.605093] map kernel 0xffff8d1ab1558000 to user 0x7f96b5d08000, size: 0x1000
[111762.605105] demodrv_read actual_readed=64, pos=64
[111762.605110] demodrv_read actual_readed=64, pos=4096
[111762.605165] test3[31001]: segfault at 7f96b5d09000 ip 0000560c0fd3ad25 sp 00007ffc5a515330 error 7 in test3[560c0fd3a000+2000]
[111762.605170] Code: e8 80 fb ff ff 48 8d 3d 1a 02 00 00 e8 14 fb ff ff e8 cf fb ff ff 48 63 d0 48 8b 45 80 48 01 d0 48 bb 73 6f 6d 65 74 68 69 6e <48> 89 18 66 c7 40 08 67 00 c7 85 7c ff ff ff 09 00 00 00 48 8d 3d

測試程式4

這次我們又修改了mmap的參數,這次映射了2頁的大小,偏移設置為3頁。因為我們設備分配的物理記憶體大小是4頁,所以映射的第2頁已經超出了實際的設備物理記憶體。預期映射的第一頁可以正常讀寫,第二頁會出現bus錯誤。

int main()
{
    char buf[64];
    int fd;
    char *addr = NULL;
    off_t offset;
    int ret;
    char *message = "Hello World\n";
    char *message2 = "I'm superman\n";

    fd = open(DEMO_DEV_NAME, O_RDWR);
    if (fd < 0) {
        printf("open device %s failed\n", DEMO_DEV_NAME);
        return -1;
    }
    /* 映射2頁,offset 3頁 */
    addr = mmap(NULL, getpagesize() * 2, PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_LOCKED, fd, getpagesize() * 3);

    /* 寫到mmap映射的虛擬地址中,通過read讀取設備文件 */
    ret =sprintf(addr, "%s", message);
    assert(ret == strlen(message));

    offset = lseek(fd, getpagesize() * 3, SEEK_SET);
    ret = read(fd, buf, sizeof(buf));
    assert(ret == sizeof(buf));
    assert(!memcmp(buf, message, strlen(message)));

    /* 寫到一頁之後,超出實際物理記憶體範圍 */
    printf("expect bus error\n");
    ret = sprintf(addr + getpagesize(), "something");
    printf("never reach here\n");

    munmap(addr, getpagesize() * 2);
    close(fd);
    return 0;
}

編譯運行測試程式,結果如預期。雖然vma的大小為2頁,但是實際只映射了1頁的物理記憶體,當嘗試寫到第二頁時出現了bus錯誤(SIGBUS)。

$ sudo ./test4
expect bus error
Bus error
$ dmesg|tail -n 3
[112105.841706] phys_start: 0x5b155b000, offset: 0x3000, vma_size: 0x2000, map size:0x1000
[112105.841710] map kernel 0xffff8d1ab155b000 to user 0x7fe662ec4000, size: 0x1000
[112105.841723] demodrv_read actual_readed=64, pos=12352

參考資料