mmap核心原始碼分析

FreeeLinux發表於2017-02-27

對於mmap函式,我之前的理解太單一了。這幾天好好複習了一下以前學過的知識,重新對該函式有了新的認識。

之前我的認識是,mmap是用來對映記憶體的,它對映的記憶體來自磁碟上檔案。所以我以為malloc函式底層也對映檔案記憶體。後來一直想不通。

實際上,mmap函式再malloc底層實現中採用了匿名對映(就是這個匿名對映,我之前一直概念不清)。

先說下malloc呼叫mmap一般的形式:

//原型
//mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

addr = mmap(NULL, 4096, PROT_READ|PORT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

對於malloc對映匿名記憶體來說,必須是以頁為單位的,比如上面的4096。使用者程式向核心空間分配記憶體都是直接向夥伴系統要的。在此基礎上glibc將記憶體細化為可以按照位元組分配的方式。

匿名記憶體的顯著特徵,MAP_ANONYMOUS,以及檔案描述符fd傳遞-1。

下面開始剖析原始碼,看看匿名記憶體與檔案對映有什麼不一樣。

mmap的系統呼叫時sys_mmap2,實際上就是一個簡單轉呼叫:

asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
    unsigned long prot, unsigned long flags,
    unsigned long fd, unsigned long pgoff)
{
    return do_mmap2(addr, len, prot, flags, fd, pgoff);
}

do_mmap2,程式碼如下:‘

static inline long do_mmap2(
    unsigned long addr, unsigned long len,
    unsigned long prot, unsigned long flags,
    unsigned long fd, unsigned long pgoff)
{
    int error = -EBADF;
    struct file * file = NULL;  //呵呵,注意這裡file指標初始為NULL

    flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
    if (!(flags & MAP_ANONYMOUS)) {//MAP_ANONYMOUS設成1,表示沒有檔案,實際上只是用來"圈地"
        file = fget(fd);//獲取file結構
        if (!file)
            goto out;
    }

//所以上面的步驟,如果我們設定了MAP_ANONYMOUS,那麼不會用fd獲取實際的檔案,以後file指標仍然為NULL

    down(t->mm->mmap_sem);
    error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);  //傳入file=NULL
    up(¤t->mm->mmap_sem);

    if (file)
        fput(file);
out:
    return error;
}

inline函式do_mmap(),是供核心自己用的,它也是將已開啟檔案對映到當前程式空間。程式碼為:

static inline unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flag, unsigned long offset)
{
    unsigned long ret = -EINVAL;
    if ((offset + PAGE_ALIGN(len)) < offset)
        goto out;
    if (!(offset & ~PAGE_MASK))
        ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);  //沒得說,file還是NULL
out:
    return ret;
}

兩者都呼叫,do_mmap_pgoff,程式碼如下:

unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len,
    unsigned long prot, unsigned long flags, unsigned long pgoff)
{
    struct mm_struct * mm = current->mm;
    struct vm_area_struct * vma;
    int correct_wcount = 0;
    int error;

    .....//各種判斷,先忽略
    if (flags & MAP_FIXED) {
        if (addr & ~PAGE_MASK)
            return -EINVAL;
    } else {//MAP_FIXED為0,就表示指定的對映地址只是一個參考值,不能滿足時可以由核心給分配一個
        addr = get_unmapped_area(addr, len);//當前程式的使用者空間中分配一個起始地址
        if (!addr)
            return -ENOMEM;
    }

    /* Determine the object being mapped and call the appropriate
     * specific mapper. the address has already been validated, but
     * not unmapped, but the maps are removed from the list.
     */
    vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);//對映到一個特定的檔案也是一種屬性,屬性不同的區段不能共存於同一邏輯區間,所以總要為之單獨建立一個邏輯區間
    if (!vma)
        return -ENOMEM;

    vma->vm_mm = mm;
    vma->vm_start = addr;//起始地址
    vma->vm_end = addr + len;//結束地址
    vma->vm_flags = vm_flags(prot,flags) | mm->def_flags;

    if (file) {//設定vma->flags
        VM_ClearReadHint(vma);
        vma->vm_raend = 0;

        if (file->f_mode & FMODE_READ)
            vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
        if (flags & MAP_SHARED) {
            vma->vm_flags |= VM_SHARED | VM_MAYSHARE;

            /* This looks strange, but when we don't have the file open
             * for writing, we can demote the shared mapping to a simpler
             * private mapping. That also takes care of a security hole
             * with ptrace() writing to a shared mapping without write
             * permissions.
             *
             * We leave the VM_MAYSHARE bit on, just to get correct output
             * from /proc/xxx/maps..
             */
            if (!(file->f_mode & FMODE_WRITE))
                vma->vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
        }
    } else {
        vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
        if (flags & MAP_SHARED)
            vma->vm_flags |= VM_SHARED | VM_MAYSHARE;
    }
    vma->vm_page_prot = protection_map[vma->vm_flags & 0x0f];
    vma->vm_ops = NULL;
    vma->vm_pgoff = pgoff;//所對映內容在檔案中的起點,有了這個起點,發生缺頁異常時,就可以根據虛擬地址計算出相應頁面在檔案中的位置
    vma->vm_file = NULL;
    vma->vm_private_data = NULL;

    /* Clear old maps */
    error = -ENOMEM;
    if (do_munmap(mm, addr, len))//檢查目標地址在當前程式的虛擬空間是否已經在使用,如果已經在使用就要將老的對映撤銷,要是這個操作失敗,則goto free_vma。因為flags的標誌位為MAP_FIXED為1時,並未對此檢查。
        goto free_vma;

    /* Check against address space limit. */
    if ((mm->total_vm << PAGE_SHIFT) + len //虛擬空間的使用是否超出了為其設定的下限
        > current->rlim[RLIMIT_AS].rlim_cur)
        goto free_vma;

    /* Private writable mapping? Check memory availability.. */
    if ((vma->vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE &&//物理頁面數是否夠
        !(flags & MAP_NORESERVE)                 &&
        !vm_enough_memory(len >> PAGE_SHIFT))
        goto free_vma;

    if (file) {
        if (vma->vm_flags & VM_DENYWRITE) {
            error = deny_write_access(file);//排斥常規檔案操作,如read write 
            if (error)
                goto free_vma;
            correct_wcount = 1;
        }
        vma->vm_file = file;//重點哦
        get_file(file);
        error = file->f_op->mmap(file, vma);//指向了generic_file_mmap
        if (error)
            goto unmap_and_free_vma;
    } else if (flags & MAP_SHARED) {
        error = shmem_zero_setup(vma);
        if (error)
            goto free_vma;
    }

    /* Can addr have changed??
     *
     * Answer: Yes, several device drivers can do it in their
     *         f_op->mmap method. -DaveM
     */
    flags = vma->vm_flags;
    addr = vma->vm_start;

    insert_vm_struct(mm, vma);//插入到對應的佇列中
    if (correct_wcount)
        atomic_inc(&file->f_dentry->d_inode->i_writecount);

    mm->total_vm += len >> PAGE_SHIFT;
    if (flags & VM_LOCKED) {//僅在加鎖時才呼叫make_pages_present
        mm->locked_vm += len >> PAGE_SHIFT;
        make_pages_present(addr, addr + len);
    }
    return addr;//最後返回的起始虛擬地址,一般是後12位為0

unmap_and_free_vma:
    if (correct_wcount)
        atomic_inc(&file->f_dentry->d_inode->i_writecount);
    vma->vm_file = NULL;
    fput(file);
    /* Undo any partial mapping done by a device driver. */
    flush_cache_range(mm, vma->vm_start, vma->vm_end);
    zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start);
    flush_tlb_range(mm, vma->vm_start, vma->vm_end);
free_vma:
    kmem_cache_free(vm_area_cachep, vma);
    return error;
}

哈哈,我關注的重點來了,這個函式get_unmapped_area,是用來給程式找到一塊VMA的,來看看它幹了什麼:

unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
        unsigned long pgoff, unsigned long flags)
{
    unsigned long (*get_area)(struct file *, unsigned long,
                  unsigned long, unsigned long, unsigned long);

    get_area = current->mm->get_unmapped_area;  //預設使用當前程式虛存管理對應的get_unmapped_ared函式,這裡只是從程式地址空間獲得VMA
    if (file && file->f_op && file->f_op->get_unmapped_area)   //匿名對映檔案指標依舊為空
        get_area = file->f_op->get_unmapped_area;  //如果檔案指標不為NULL,使用檔案對應的get_unmapped_area函式,這裡就是從檔案獲得VMA
    addr = get_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

    if (addr > TASK_SIZE - len)
        return -ENOMEM;
    if (addr & ~PAGE_MASK)  //PAGE_MASK低12位都是0,這裡是用來檢測是否是整頁面,如果不是則出錯
        return -EINVAL;

    return arch_rebalance_pgtables(addr, len);
}

唉,真相大白。匿名對映就是file=NULL。

get_unmmaped_ared函式解開了我的疑惑,該函式實現的核心對於匿名檔案對映和檔案對映的選擇。使用函式指標,要麼賦值mm->get_unmapped_area從程式地址空間獲得VMA,要麼從file->f_op->get_unmapped_ared獲得VMA。

並沒有做實際的記憶體分配,只是簡單的獲取了一片VMA。獲取VMA是呼叫find_vma()函式在vma_ared_struct的雙連結串列中查詢,並且匿名對映還有可能和緊挨著的VMA合併。

當CPU第一個引用mmap區域的頁面時,會引發缺頁中斷,核心會在物理儲存器中找到一個合適的犧牲頁面,如果該頁面被修改過,就將這個頁面換出來,用二進位制零覆蓋犧牲頁面並更新頁表,將這個頁面標記為是駐留在儲存器中的。注意在磁碟和儲存器之間並沒有實際的資料傳送。因為這個原因,對映到匿名檔案的區域的頁面有時也叫做請求二進位制零的頁(demand zero page)。

另外,由於使用mmap分配記憶體,核心需要清零頁面,並且mmap分配的記憶體都是頁對齊的,所以使用mmap有一定的消耗。所以glibc才設定大於128k使用mmap,一般使用sbrk()函式分配記憶體。

參考:

  1. Linux核心原始碼情景分析-系統呼叫mmap()
  2. Linux核心分析之程式地址空間
  3. linux mmap函式詳解
  4. CSAPP, Randal E.Byant David R.O.Hallaron著,龔奕利,雷迎春譯。

相關文章