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

寫在前面

經常能刷到講解linux內核相關知識的文章以及課程贸宏,大部分是賣課的,給我的感覺就是不太好懂常摧,甚至越講越不懂搅吁,越講越復雜。我今天思考了一下原因:

  1. 只講源代碼不講原理落午。我一直想要搞懂內核似芝,但是隨便搜索得來的文章往往不能深入看,發(fā)現(xiàn)問題越來越多板甘,好不容易今天看懂了,過幾天就忘了详炬,過一個月就全忘了回到起點了盐类;我想根本的原因是,每行代碼我都懂呛谜,但是不知道為什么這么寫在跳。
  2. 不講歷史,只講結果隐岛。任何一個工程猫妙,不管大小,都是不斷演進的聚凹,變化的割坠。我們往往看的是結果,就是他最后的樣子妒牙,至于為啥會是這樣彼哼,不清楚,所以后面如果技術發(fā)生了變化湘今,升級敢朱,新的代碼還得重新理解,重新學習,越來越累拴签。

所以孝常,我嘗試換一種方式理解Linux內核。就從mmap開始吧蚓哩。

mmap到底在做什么构灸?

1. 看看man mmap

看官方解釋往往是第一步,因為權威杖剪,準確冻押。

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的功能就是為當前進程的虛擬內存分配一個新的映射,映射的起始地址是addr長度length盛嘿。還可以傳入一個fd洛巢,說明可以將這個虛擬地址綁定到一個文件上。

我的理解

通過看這段文字次兆,我大概腦子里已經有一個大概的思路了稿茉。因為我花了很長的時間已經搞懂了這些概念是什么:

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

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

  1. 這里的地址虛擬內存地址渺蒿。當Linux內核起來后,CPU就不清楚啥是物理地址了(從real mode到long mode)彪薛,因為它只能接觸到虛擬地址茂装。(具體CPU如何將虛擬地址對應到物理地址的尋址過程可以參考這里);
  2. 有了虛擬地址以后,地址這個概念發(fā)生了拓展善延,不再只跟內存一一對應了少态,它可以代表:各種連接在總線上的設備,特定的寄存器等等易遣,可以代表你想要用mov指令訪問的任何位置彼妻,任何設備中的數據;
  3. 現(xiàn)在虛擬地址是個資源概念豆茫,CPU可以訪問到的資源侨歉,是個抽象概念了——多一層抽象,構架就多一份靈活性揩魂,虛擬地址是個偉大的發(fā)明为肮;
  4. 如果我現(xiàn)在說映射指的是將抽象的虛擬地址具體如何綁定到設備中實在的數據的過程,應該好理解些了吧肤京?程序員可以把這個映射理解成——硬件世界的面向對象抽象的過程颊艳。用代碼表示就是:
interface Address {
void access(VirtualAddress address);
}

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

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

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

  1. 既然虛擬地址是個資源,盡管它是虛擬的窥浪、不存在的抽象概念祖很,但是是資源肯定要分配。所以第一步就是從進程“廣袤”的地址空間中找一段大小合適的漾脂,沒有用過的虛擬地址空間來做后面的映射操作假颇;
  2. 找到以后我要存下來,或者說用一個結構保存下來骨稿,后面只要找到這個結構就能操作這段虛擬地址空間笨鸡;這個結構就是vm_area_struct
  3. 如果要映射到內存坦冠,我就去找一個頁的物理內存形耗,然后操作頁表生成頁表項就行了;物理內存維護在slab中辙浑,頁表維護在task_struct中激涤,登記一下就行了;相當于上面?zhèn)未a中Memory.access(virtualAddress)的實現(xiàn)
  4. 如果是要映射到文件判呕,那么就要實現(xiàn)File.access(virtualAddress)接口昔期。

這是個高層的理解,也是設計的初衷佛玄。因為有了虛擬地址就可以做到mmap,也只有虛擬地址才能做到mmap這么靈活的構架設計累澡。這就是所謂的機制與策略的分離構架思想梦抢。因為:

  • Linux是個通用的操作系統(tǒng),未來要接入的設備五花八門愧哟,接口形式也不同奥吩,要怎么設計一套足夠靈活構架解決這個問題呢?虛擬內存提供了答案蕊梧。
  • 虛擬內存擋在CPU與外部設備之間霞赫,對CPU屏蔽了外部設備與數據訪問的復雜度,用統(tǒng)一的方式去訪問所有的設備——也就是CPU指提供訪問機制肥矢,no more no less端衰。簡單來說就是叠洗,CPU只提供接口,不提供具體實現(xiàn)旅东;
  • 而設備的復雜度由設備的制造商去解決灭抑,制造商根據不同CPU構架訪問資源的接口,實現(xiàn)自己的access實現(xiàn)抵代,并將實現(xiàn)注入到OS中去就行了腾节。這就是策略由提供商來做,也只能由制造商來做才能繁榮整個生態(tài)荤牍;
  • 現(xiàn)在大家明白什么是驅動程序了嗎案腺?很簡單,就是根據CPU/OS提供的接口康吵,規(guī)范劈榨,機制來實現(xiàn)自己的策略,然后注入到OS中去跑的過程涎才。

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

內核mmap的代碼

淺看一下耍铜,理解意思就行邑闺,內核的代碼嵌套比較深,其實了解原理后棕兼,只要抓住關鍵點就行了陡舅。

首先就是要找到虛擬內存的接口定義

  • vm_area_struct其實就是虛擬內存的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語言就是函數指針靶衍。這個就是抽象類。我們可以看到有個叫做fault的接口函數茎芋,這個函數就是x86中斷中“第14名”颅眶,大名鼎鼎的pagefault exception的處理點了。(要理解Linux內核田弥,必須先了解CPU涛酗,要了解CPU只要了解內存怎么管理,中斷怎么處理其實就夠了偷厦,一點點題外話)

看到這里其實我們就能猜測這個過程了:
1商叹、在mmap系統(tǒng)調用中l(wèi)inux其實不用分配實際的物理內存,只要給進程分配一段資源——一段沒有映射的虛擬地址空間——vma結構只泼;
2剖笙、對vma結構做初始化,主要是為CPU機制——pagefault exception——準備具體的實現(xiàn)類——映射文件還是映射內存请唱;設置到這里就可以了弥咪,因為COW(copy on write)機制會把物理內存的分配推遲到最后一刻——中斷發(fā)生的時候过蹂;
3、在pagefault exception中肯定會做兩個事情酪夷,也只需要做兩個事情:
1. 根據中斷進程榴啸,找到發(fā)生中斷的虛擬內存——vma 結構;
2. 調用vma->fault接口進行處理就行了晚岭。

我們先看內核代碼鸥印,看看pagefault exception是否真的是這么處理的。(先證實第3點猜測)
先看看pagefault的入口:

/*
address就是引發(fā)pagefault中斷處的虛擬地址
regs是用戶態(tài)進程的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 */

    ........ //這里就是根據引發(fā)中斷的地址找到對應的vma結構
    vma = find_vma(mm, address);

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

    .........
}

最后調用 __do_fault函數處理坦报。

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() */
    }
        //這里就開始調用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);片择,確實調了fault來處理缺頁潜的,這個fault其實是個接口,是不是很像多態(tài)字管?所以說啰挪,面相對象是個概念,任何語言都可以實現(xiàn)的嘲叔。

嗯嗯亡呵,非常好!跟我的猜測是一致的×蚋辏現(xiàn)在就是要確認1锰什,2兩個地方了,具體分配頁表是在缺頁中斷處丁逝,mmap系統(tǒng)調用就是實現(xiàn)多態(tài)函數的綁定咯汁胆,我們看看∷祝回到mmap系統(tǒng)調用處開始找嫩码。

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函數

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

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

進入vm_mmap_pgoff函數,再到do_mmap函數罪既,linux代碼嵌套是很深的铸题。

/*
 * 這個函數完成了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.
     */

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

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

實際的綁定函數是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對應的vma對象
     */
    vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
            NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)
        goto out;

    /*
          也可能在這里拿到address對應的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兩個參數filevma墙牌,可見這個函數就是將兩個對象綁定起來的地方了:

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

這里會調用file描述符中的mmap(file,vma)函數完成綁定。如果我們用的文件系統(tǒng)是ext4則應該去找找這個文件系統(tǒng)的mmap函數的實現(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)也是用機制策略分離的構架建立的。任何文件系統(tǒng)撤防,都要實現(xiàn)struct file_operations接口虽风。上面的file->f_op->mmap(file, vma);調用的就是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接口的實現(xiàn)有fault厂抖。至此,我們就找到了mmap到文件的地方了克懊,到時候pagefault發(fā)生的時候忱辅,會執(zhí)行ext4_filemap_fault函數進行物理內存映射,步驟是將vma對應的虛擬內存地址映射到物理地址谭溉,然后這個物理地址填充上相應文件的內容墙懂。是不是很容易理解了。

接著找找將匿名映射夜只,也就是將vma映射到普通的物理內存垒在。

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

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

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

總結

我感覺粘茄,單純閱讀Linux源代碼其實對開發(fā)幫助有限签舞,而且對一般的非內核開發(fā)人員,一段時間不用柒瓣,就會忘記儒搭,但是如果你理解了代碼的機制,知道了Linux為什么要這么寫芙贫,你可能長時間的記住搂鲫,并且運用到自己的工作中,學習Linux其實就是要學習它的工程經驗磺平。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末魂仍,一起剝皮案震驚了整個濱河市拐辽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌擦酌,老刑警劉巖俱诸,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赊舶,居然都是意外死亡睁搭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門锯岖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來介袜,“玉大人捏萍,你說我怎么就攤上這事合搅【ǎ” “怎么了怔鳖?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵局齿,是天一觀的道長棚壁。 經常有香客問我遗淳,道長锌介,這世上最難降的妖魔是什么秋麸? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任渐排,我火速辦了婚禮,結果婚禮上灸蟆,老公的妹妹穿的比我還像新娘驯耻。我一直安慰自己,他們只是感情好炒考,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布可缚。 她就那樣靜靜地躺著,像睡著了一般斋枢。 火紅的嫁衣襯著肌膚如雪帘靡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天瓤帚,我揣著相機與錄音描姚,去河邊找鬼。 笑死戈次,一個胖子當著我的面吹牛轩勘,可吹牛的內容都是我干的。 我是一名探鬼主播怯邪,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼绊寻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起榛斯,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搂捧,沒想到半個月后驮俗,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡允跑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年王凑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片聋丝。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡索烹,死狀恐怖,靈堂內的尸體忽然破棺而出弱睦,到底是詐尸還是另有隱情百姓,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布况木,位于F島的核電站垒拢,受9級特大地震影響,放射性物質發(fā)生泄漏火惊。R本人自食惡果不足惜求类,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望屹耐。 院中可真熱鬧尸疆,春花似錦、人聲如沸惶岭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俗他。三九已至脖捻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間兆衅,已是汗流浹背地沮。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留羡亩,地道東北人摩疑。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像畏铆,于是被迫代替她去往敵國和親雷袋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內容