Linux隨筆 - 這么解釋mmap可能好理解些

寫在前面

經(jīng)常能刷到講解linux內(nèi)核相關(guān)知識的文章以及課程,大部分是賣課的,給我的感覺就是不太好懂,甚至越講越不懂,越講越復(fù)雜。我今天思考了一下原因:

  1. 只講源代碼不講原理。我一直想要搞懂內(nèi)核,但是隨便搜索得來的文章往往不能深入看,發(fā)現(xiàn)問題越來越多,好不容易今天看懂了,過幾天就忘了,過一個月就全忘了回到起點(diǎn)了;我想根本的原因是,每行代碼我都懂,但是不知道為什么這么寫。
  2. 不講歷史,只講結(jié)果。任何一個工程,不管大小,都是不斷演進(jìn)的,變化的。我們往往看的是結(jié)果,就是他最后的樣子,至于為啥會是這樣,不清楚,所以后面如果技術(shù)發(fā)生了變化,升級,新的代碼還得重新理解,重新學(xué)習(xí),越來越累。

所以,我嘗試換一種方式理解Linux內(nèi)核。就從mmap開始吧。

mmap到底在做什么?

1. 看看man mmap

看官方解釋往往是第一步,因?yàn)闄?quán)威,準(zhǔn)確。

NAME
mmap, munmap - map or unmap files or devices into memory>

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

 See NOTES for information on feature test macro requirements.

DESCRIPTION
mmap() creates a new mapping in the virtual address space of the calling process. The starting >address for the new mapping is specified in addr. The length argument specifies
the length of the mapping (which must be greater than 0).

看文字mmap的功能就是為當(dāng)前進(jìn)程的虛擬內(nèi)存分配一個新的映射,映射的起始地址是addr長度length。還可以傳入一個fd,說明可以將這個虛擬地址綁定到一個文件上。

我的理解

通過看這段文字,我大概腦子里已經(jīng)有一個大概的思路了。因?yàn)槲一撕荛L的時間已經(jīng)搞懂了這些概念是什么:

  1. 虛擬地址在CPU在OS中到底指的是什么?
  2. 映射這個動作到底指的什么?

如果你清楚上面這兩個問題,我來稍微解釋下,理解了的可以跳過。

  1. 這里的地址虛擬內(nèi)存地址。當(dāng)Linux內(nèi)核起來后,CPU就不清楚啥是物理地址了(從real mode到long mode),因?yàn)樗荒芙佑|到虛擬地址。(具體CPU如何將虛擬地址對應(yīng)到物理地址的尋址過程可以參考這里);
  2. 有了虛擬地址以后,地址這個概念發(fā)生了拓展,不再只跟內(nèi)存一一對應(yīng)了,它可以代表:各種連接在總線上的設(shè)備,特定的寄存器等等,可以代表你想要用mov指令訪問的任何位置,任何設(shè)備中的數(shù)據(jù);
  3. 現(xiàn)在虛擬地址是個資源概念,CPU可以訪問到的資源,是個抽象概念了——多一層抽象,構(gòu)架就多一份靈活性,虛擬地址是個偉大的發(fā)明;
  4. 如果我現(xiàn)在說映射指的是將抽象的虛擬地址具體如何綁定到設(shè)備中實(shí)在的數(shù)據(jù)的過程,應(yīng)該好理解些了吧?程序員可以把這個映射理解成——硬件世界的面向?qū)ο蟪橄蟮倪^程。用代碼表示就是:
interface Address {
void access(VirtualAddress address);
}

public class Memory impliment Adress{
  public void access(VirtualAddress address){
  //memory怎么通過虛擬地址訪問數(shù)據(jù)
  }
}

public class File impliment Adress {
public void access(VirtualAddress address){
//如何通過虛擬地址來訪問文件的數(shù)據(jù)
}
}

所以,我的理解是:mmap是個分配物理內(nèi)存的函數(shù),或者說機(jī)制。用戶態(tài)進(jìn)程通過調(diào)用這個函數(shù)向系統(tǒng)申請了一塊連續(xù)的內(nèi)存資源。而且,居然還可以傳入一個文件,那大概就是利用虛擬地址訪問文件資源了吧。大概分成這么幾個步驟:

  1. 既然虛擬地址是個資源,盡管它是虛擬的、不存在的抽象概念,但是是資源肯定要分配。所以第一步就是從進(jìn)程“廣袤”的地址空間中找一段大小合適的,沒有用過的虛擬地址空間來做后面的映射操作;
  2. 找到以后我要存下來,或者說用一個結(jié)構(gòu)保存下來,后面只要找到這個結(jié)構(gòu)就能操作這段虛擬地址空間;這個結(jié)構(gòu)就是vm_area_struct;
  3. 如果要映射到內(nèi)存,我就去找一個頁的物理內(nèi)存,然后操作頁表生成頁表項(xiàng)就行了;物理內(nèi)存維護(hù)在slab中,頁表維護(hù)在task_struct中,登記一下就行了;相當(dāng)于上面?zhèn)未a中Memory.access(virtualAddress)的實(shí)現(xiàn)
  4. 如果是要映射到文件,那么就要實(shí)現(xiàn)File.access(virtualAddress)接口。

這是個高層的理解,也是設(shè)計的初衷。因?yàn)橛辛颂摂M地址就可以做到mmap,也只有虛擬地址才能做到mmap這么靈活的構(gòu)架設(shè)計。這就是所謂的機(jī)制與策略的分離構(gòu)架思想。因?yàn)椋?/p>

  • Linux是個通用的操作系統(tǒng),未來要接入的設(shè)備五花八門,接口形式也不同,要怎么設(shè)計一套足夠靈活構(gòu)架解決這個問題呢?虛擬內(nèi)存提供了答案。
  • 虛擬內(nèi)存擋在CPU與外部設(shè)備之間,對CPU屏蔽了外部設(shè)備與數(shù)據(jù)訪問的復(fù)雜度,用統(tǒng)一的方式去訪問所有的設(shè)備——也就是CPU指提供訪問機(jī)制,no more no less。簡單來說就是,CPU只提供接口,不提供具體實(shí)現(xiàn);
  • 而設(shè)備的復(fù)雜度由設(shè)備的制造商去解決,制造商根據(jù)不同CPU構(gòu)架訪問資源的接口,實(shí)現(xiàn)自己的access實(shí)現(xiàn),并將實(shí)現(xiàn)注入到OS中去就行了。這就是策略由提供商來做,也只能由制造商來做才能繁榮整個生態(tài);
  • 現(xiàn)在大家明白什么是驅(qū)動程序了嗎?很簡單,就是根據(jù)CPU/OS提供的接口,規(guī)范,機(jī)制來實(shí)現(xiàn)自己的策略,然后注入到OS中去跑的過程。

下面看看是不是這么回事,我們現(xiàn)在看看關(guān)鍵的內(nèi)核代碼。

內(nèi)核mmap的代碼

淺看一下,理解意思就行,內(nèi)核的代碼嵌套比較深,其實(shí)了解原理后,只要抓住關(guān)鍵點(diǎn)就行了。

首先就是要找到虛擬內(nèi)存的接口定義

  • vm_area_struct其實(shí)就是虛擬內(nèi)存的class對象,在其中定義了一個跟access十分類似的字段:
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. */

    ......

    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops; //就是這個字段

    .......
} __randomize_layout;

這個字段就是了const struct vm_operations_struct *vm_ops;,我們展開看看:

struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    /* Called any time before splitting to check if it's allowed */
    int (*may_split)(struct vm_area_struct *area, unsigned long addr);
    int (*mremap)(struct vm_area_struct *area, unsigned long flags);
    /*
     * Called by mprotect() to make driver-specific permission
     * checks before mprotect() is finalised.   The VMA must not
     * be modified.  Returns 0 if eprotect() can proceed.
     */
    int (*mprotect)(struct vm_area_struct *vma, unsigned long start,
            unsigned long end, unsigned long newflags);
    vm_fault_t (*fault)(struct vm_fault *vmf);
    vm_fault_t (*huge_fault)(struct vm_fault *vmf,
            enum page_entry_size pe_size);
    void (*map_pages)(struct vm_fault *vmf,
            pgoff_t start_pgoff, pgoff_t end_pgoff);
    ........... //后面還有就不貼了
};

可以看到很多接口——C語言就是函數(shù)指針。這個就是抽象類。我們可以看到有個叫做fault的接口函數(shù),這個函數(shù)就是x86中斷中“第14名”,大名鼎鼎的pagefault exception的處理點(diǎn)了。(要理解Linux內(nèi)核,必須先了解CPU,要了解CPU只要了解內(nèi)存怎么管理,中斷怎么處理其實(shí)就夠了,一點(diǎn)點(diǎn)題外話)

看到這里其實(shí)我們就能猜測這個過程了:
1、在mmap系統(tǒng)調(diào)用中l(wèi)inux其實(shí)不用分配實(shí)際的物理內(nèi)存,只要給進(jìn)程分配一段資源——一段沒有映射的虛擬地址空間——vma結(jié)構(gòu);
2、對vma結(jié)構(gòu)做初始化,主要是為CPU機(jī)制——pagefault exception——準(zhǔn)備具體的實(shí)現(xiàn)類——映射文件還是映射內(nèi)存;設(shè)置到這里就可以了,因?yàn)镃OW(copy on write)機(jī)制會把物理內(nèi)存的分配推遲到最后一刻——中斷發(fā)生的時候;
3、在pagefault exception中肯定會做兩個事情,也只需要做兩個事情:
1. 根據(jù)中斷進(jìn)程,找到發(fā)生中斷的虛擬內(nèi)存——vma 結(jié)構(gòu);
2. 調(diào)用vma->fault接口進(jìn)行處理就行了。

我們先看內(nèi)核代碼,看看pagefault exception是否真的是這么處理的。(先證實(shí)第3點(diǎn)猜測)
先看看pagefault的入口:

/*
address就是引發(fā)pagefault中斷處的虛擬地址
regs是用戶態(tài)進(jìn)程的CPU上下文
*/
void do_page_fault(unsigned long address, struct pt_regs *regs)
{
    struct vm_area_struct *vma = NULL;
    struct task_struct *tsk = current;
    struct mm_struct *mm = tsk->mm;
    int sig, si_code = SEGV_MAPERR;
    unsigned int write = 0, exec = 0, mask;
    vm_fault_t fault = VM_FAULT_SIGSEGV;    /* handle_mm_fault() output */
    unsigned int flags;         /* handle_mm_fault() input */

    ........ //這里就是根據(jù)引發(fā)中斷的地址找到對應(yīng)的vma結(jié)構(gòu)
    vma = find_vma(mm, address);

        ......... //找到vma以后就開始調(diào)用vma相應(yīng)的處理方法了
    fault = handle_mm_fault(vma, address, flags, regs);

    .........
}

最后調(diào)用 __do_fault函數(shù)處理。

static vm_fault_t __do_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret;
    if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) {
        vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
        if (!vmf->prealloc_pte)
            return VM_FAULT_OOM;
        smp_wmb(); /* See comment in __pte_alloc() */
    }
        //這里就開始調(diào)用fault了。跟我們猜測一致。
    ret = vma->vm_ops->fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
                VM_FAULT_DONE_COW)))
        return ret;

..................

    return ret;
}

看到這里ret = vma->vm_ops->fault(vmf);,確實(shí)調(diào)了fault來處理缺頁,這個fault其實(shí)是個接口,是不是很像多態(tài)?所以說,面相對象是個概念,任何語言都可以實(shí)現(xiàn)的。

嗯嗯,非常好!跟我的猜測是一致的?,F(xiàn)在就是要確認(rèn)1,2兩個地方了,具體分配頁表是在缺頁中斷處,mmap系統(tǒng)調(diào)用就是實(shí)現(xiàn)多態(tài)函數(shù)的綁定咯,我們看看?;氐?code>mmap系統(tǒng)調(diào)用處開始找。

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 = ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:
    return error;
}

再找ksys_mmap_pgoff函數(shù)

unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
                  unsigned long prot, unsigned long flags,
                  unsigned long fd, unsigned long pgoff)
{
    ............\\前面都是校驗(yàn)

    retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:
    if (file)
        fput(file);
    return retval;
}

進(jìn)入vm_mmap_pgoff函數(shù),再到do_mmap函數(shù),linux代碼嵌套是很深的。

/*
 * 這個函數(shù)完成了file->vma的綁定。
 */
unsigned long do_mmap(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)
{
    ............

    /* Obtain the address to map to. we verify (or select) it and ensure
     * that it represents a valid section of the address space.
     */

  //獲取一個沒有映射的起始地址。應(yīng)該是4k對齊的地址
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

............................ 
//實(shí)際綁定的函數(shù)
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
    ..............
    return addr;
}

實(shí)際的綁定函數(shù)是mmap_region,怎么綁定的呢?

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)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev, *merge;
    ......................
    /*
     * 這里會拿到address對應(yīng)的vma對象
     */
    vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
            NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)
        goto out;

    /*
          也可能在這里拿到address對應(yīng)的vma對象。不管在哪里拿到vma,到這里肯定拿到了。
     */
    vma = vm_area_alloc(mm);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }
        //vma起始位置
    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;

    .....
//vma跟file綁定
        vma->vm_file = get_file(file);

//這里就是完成綁定的地方了!
        error = call_mmap(file, vma);
.............................
}

可以看到call_mmapyou兩個參數(shù)filevma,可見這個函數(shù)就是將兩個對象綁定起來的地方了:

static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
    return file->f_op->mmap(file, vma);
}

這里會調(diào)用file描述符中的mmap(file,vma)函數(shù)完成綁定。如果我們用的文件系統(tǒng)是ext4則應(yīng)該去找找這個文件系統(tǒng)的mmap函數(shù)的實(shí)現(xiàn)。

const struct file_operations ext4_file_operations = {
    .llseek     = ext4_llseek,
    .read_iter  = ext4_file_read_iter,
    .write_iter = ext4_file_write_iter,
    .iopoll     = iomap_dio_iopoll,
    .unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = ext4_compat_ioctl,
#endif
//在這里定義
    .mmap       = ext4_file_mmap,
    .mmap_supported_flags = MAP_SYNC,
    .open       = ext4_file_open,
    .release    = ext4_release_file,
    .fsync      = ext4_sync_file,
    .get_unmapped_area = thp_get_unmapped_area,
    .splice_read    = generic_file_splice_read,
    .splice_write   = iter_file_splice_write,
    .fallocate  = ext4_fallocate,
};

ext4_file_operations就是file->f_op,可見文件系統(tǒng)也是用機(jī)制策略分離的構(gòu)架建立的。任何文件系統(tǒng),都要實(shí)現(xiàn)struct file_operations接口。上面的file->f_op->mmap(file, vma);調(diào)用的就是ext4_file_mmap(file,vma)。

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

這里我們就看到了對于新找到的vma,如果是映射到文件,會把vma的vm_ops抽象接口改成文件定制的vm_ops——ext4_file_vm_ops。

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

我們可以看到,對于ext4文件系統(tǒng)的vm_ops接口的實(shí)現(xiàn)有fault。至此,我們就找到了mmap到文件的地方了,到時候pagefault發(fā)生的時候,會執(zhí)行ext4_filemap_fault函數(shù)進(jìn)行物理內(nèi)存映射,步驟是將vma對應(yīng)的虛擬內(nèi)存地址映射到物理地址,然后這個物理地址填充上相應(yīng)文件的內(nèi)容。是不是很容易理解了。

接著找找將匿名映射,也就是將vma映射到普通的物理內(nèi)存。

后來我翻了下代碼發(fā)現(xiàn)其實(shí)我想多了,Linux對于匿名映射,是沒有填充fault函數(shù)的......do_fault直接從slab中找空閑頁面就行了。這里是證據(jù):

static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
    return !vma->vm_ops;
}

如果是匿名映射的話vma->vm_ops是空的。
到這里就結(jié)束了,結(jié)論就是,mmap確實(shí)是根據(jù)不同的映射條件將虛擬內(nèi)存空間映射到不同的資源上來統(tǒng)一訪問的。

總結(jié)

我感覺,單純閱讀Linux源代碼其實(shí)對開發(fā)幫助有限,而且對一般的非內(nèi)核開發(fā)人員,一段時間不用,就會忘記,但是如果你理解了代碼的機(jī)制,知道了Linux為什么要這么寫,你可能長時間的記住,并且運(yùn)用到自己的工作中,學(xué)習(xí)Linux其實(shí)就是要學(xué)習(xí)它的工程經(jīng)驗(yàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容