寫在前面
經常能刷到講解linux內核相關知識的文章以及課程贸宏,大部分是賣課的,給我的感覺就是不太好懂常摧,甚至越講越不懂搅吁,越講越復雜。我今天思考了一下原因:
- 只講源代碼不講原理落午。我一直想要搞懂內核似芝,但是隨便搜索得來的文章往往不能深入看,發(fā)現(xiàn)問題越來越多板甘,好不容易今天看懂了,過幾天就忘了详炬,過一個月就全忘了回到起點了盐类;我想根本的原因是,每行代碼我都懂呛谜,但是不知道為什么這么寫在跳。
- 不講歷史,只講結果隐岛。任何一個工程猫妙,不管大小,都是不斷演進的聚凹,變化的割坠。我們往往看的是結果,就是他最后的樣子妒牙,至于為啥會是這樣彼哼,不清楚,所以后面如果技術發(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
洛巢,說明可以將這個虛擬地址綁定到一個文件上。
我的理解
通過看這段文字次兆,我大概腦子里已經有一個大概的思路了稿茉。因為我花了很長的時間已經搞懂了這些概念是什么:
- 虛擬地址在CPU在OS中到底指的是什么?
- 映射這個動作到底指的什么芥炭?
如果你清楚上面這兩個問題漓库,我來稍微解釋下,理解了的可以跳過园蝠。
- 這里的
地址
是虛擬內存地址
渺蒿。當Linux內核起來后,CPU就不清楚啥是物理地址了(從real mode到long mode)彪薛,因為它只能接觸到虛擬地址茂装。(具體CPU如何將虛擬地址對應到物理地址的尋址過程可以參考這里); - 有了虛擬地址以后,地址這個概念發(fā)生了拓展善延,不再只跟內存一一對應了少态,它可以代表:各種連接在總線上的設備,特定的寄存器等等易遣,可以代表你想要用
mov
指令訪問的任何位置彼妻,任何設備中的數據; - 現(xiàn)在
虛擬地址
是個資源概念豆茫,CPU可以訪問到的資源侨歉,是個抽象概念了——多一層抽象,構架就多一份靈活性揩魂,虛擬地址是個偉大的發(fā)明为肮; - 如果我現(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ù)的內存資源白修。而且,居然還可以傳入一個文件重斑,那大概就是利用虛擬地址訪問文件資源了吧兵睛。大概分成這么幾個步驟:
- 既然虛擬地址是個資源,盡管它是虛擬的窥浪、不存在的抽象概念祖很,但是是資源肯定要分配。所以第一步就是從進程“廣袤”的地址空間中找一段大小合適的漾脂,沒有用過的虛擬地址空間來做后面的映射操作假颇;
- 找到以后我要存下來,或者說用一個結構保存下來骨稿,后面只要找到這個結構就能操作這段虛擬地址空間笨鸡;這個結構就是
vm_area_struct
; - 如果要映射到內存坦冠,我就去找一個頁的物理內存形耗,然后操作頁表生成頁表項就行了;物理內存維護在slab中辙浑,頁表維護在
task_struct
中激涤,登記一下就行了;相當于上面?zhèn)未a中Memory.access(virtualAddress)
的實現(xiàn) - 如果是要映射到文件判呕,那么就要實現(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_mmap
you兩個參數file
與vma
墙牌,可見這個函數就是將兩個對象綁定起來的地方了:
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其實就是要學習它的工程經驗磺平。