linux的mmap
2015/07/22 16:40
瀏覽1,850
迴響0
推薦0
引用0
一.前言
mmap的具體實現以前在學習內核時學習過,但是對於其中的很多函數是一知半解的,有些只能根據其函數名來猜測其具體的功能,在本文中,一起來重新深入理解其具體的實現。
二.mmap的用戶層應用
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
具體參數含義
start : 指向欲映射的內存起始地址,通常設為 NULL,代表讓系統自動選定地址,映射成功後返回該地址。
length: 代表將文件中多大的部分映射到內存。
prot : 映射區域的保護方式。可以為以下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取
flags : 影響映射區域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此旗標。
MAP_SHARED 對映射區域的寫入數據會復制回文件內,而且允許其他映射該文件的進程共享。
MAP_PRIVATE 對映射區域的寫入操作會產生一個映射文件的復制,即私人的“寫入時復制”(copy on write)對此區域作的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS建立匿名映射。此時會忽略參數fd,不涉及文件,而且映射區域無法和其他進程共享。
MAP_DENYWRITE只允許對映射區域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。
fd : 要映射到內存中的文件描述符。如果使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統不支持匿名內存映射,則可以使用fopen打開/dev/zero文件,
然後對該文件進行映射,可以同樣達到匿名內存映射的效果。
offset:文件映射的偏移量,通常設置為0,代表從文件最前方開始對應,offset必須是PAGE_SIZE的整數倍。
返回值:
若映射成功則返回映射區的內存起始地址,否則返回MAP_FAILED(-1),錯誤原因存於errno 中。
錯誤代碼:
EBADF 參數fd 不是有效的文件描述詞
EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內存被鎖住。
ENOMEM 內存不足。
用戶層的調用很簡單,其具體功能就是直接將物理內存直接映射到用戶虛擬內存,使用戶空間可以直接對物理空間操作。但是對於內核層而言,其具體實現比較復雜。
三.mmap的內核實現
linux mmap 詳解
謹以此文紀念過往的歲月
一.前言
mmap的具體實現以前在學習內核時學習過,但是對於其中的很多函數是一知半解的,有些只能根據其函數名來猜測其具體的功能,在本文中,一起來重新深入理解其
具體的實現。
二.mmap的用戶層應用
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
具體參數含義
start : 指向欲映射的內存起始地址,通常設為 NULL,代表讓系統自動選定地址,映射成功後返回該地址。
length: 代表將文件中多大的部分映射到內存。
prot : 映射區域的保護方式。可以為以下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取
flags : 影響映射區域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此旗標。
MAP_SHARED 對映射區域的寫入數據會復制回文件內,而且允許其他映射該文件的進程共享。
MAP_PRIVATE 對映射區域的寫入操作會產生一個映射文件的復制,即私人的“寫入時復制”(copy on write)對此區域作的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS建立匿名映射。此時會忽略參數fd,不涉及文件,而且映射區域無法和其他進程共享。
MAP_DENYWRITE只允許對映射區域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。
fd : 要映射到內存中的文件描述符。如果使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統不支持匿名內存映射,則可以使用fopen打開/dev/zero文件,
然後對該文件進行映射,可以同樣達到匿名內存映射的效果。
offset:文件映射的偏移量,通常設置為0,代表從文件最前方開始對應,offset必須是PAGE_SIZE的整數倍。
返回值:
若映射成功則返回映射區的內存起始地址,否則返回MAP_FAILED(-1),錯誤原因存於errno 中。
錯誤代碼:
EBADF 參數fd 不是有效的文件描述詞
EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內存被鎖住。
ENOMEM 內存不足。
用戶層的調用很簡單,其具體功能就是直接將物理內存直接映射到用戶虛擬內存,使用戶空間可以直接對物理空間操作。但是對於內核層而言,其具體實現比較復雜。
三.mmap的內核實現
對於mmap的內核有了解的都會知道用戶層的mmap到內核層的mmap其中多了一個參數vma_struct這個結構體,在開始時對於這個參數很疑惑就是這個參數的值是哪兒來的,
在這裏我們會一一來講述。
還是從do_mmap開始吧。
3.1 do_mmap
參數說明:
file :就是用戶層想要映射的file
addr :欲映射的起始地址,即用戶層的start
prot :用戶層傳入的port
flag :同上
offset:同上
從這裏可以知道,這裏面的參數幾乎均是用戶層傳入的參數。
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) --頁對齊len,檢測傳入參數是否有誤。
goto out;
if (!(offset & ~PAGE_MASK)) --檢測offset是否頁對齊。映射時只能映射頁對齊的長度。
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
3.2 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; --當前用戶進程的mm
struct inode *inode;
unsigned int vm_flags;
int error;
int accountable = 1;
unsigned long reqprot = prot;
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC)) --是否隱藏了可執行屬性。
if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;
if (!len)
return -EINVAL;
if (!(flags & MAP_FIXED)) -
addr = round_hint_to_min(addr); --判斷輸入的欲映射的起始地址是否小於最小映射地址,如果小於,將addr修改為最小地址,不過前提是MAP_FIXED旗標沒有設置。
error = arch_mmap_check(addr, len, flags); --不同平臺對於mmap參數的不同檢測。這裏之間返回0
if (error)
return error;
len = PAGE_ALIGN(len); --檢測len是否越界,len的範圍在0~TASK_SIZE之間。
if (!len || len > TASK_SIZE)
return -ENOMEM; --錯誤值為nomem
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) --再次檢測是否越界。我們這裏不得不小心哪個暈頭了傳入一個莫名其妙的值
return -EOVERFLOW;
if (mm->map_count > sysctl_max_map_count) --在一個進程中對於mmap個數是有限制的。超出了還是nomem的錯誤。
return -ENOMEM;
addr = get_unmapped_area(file, addr, len, pgoff, flags); --獲取沒有映射的地址,這個是查詢mm中空閑的內存地址,這個在下面理解。
if (addr & ~PAGE_MASK)
return addr;
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags |
VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; --設置vm_flags,根據傳入的port和flags以及mm本身自有的旗標來設置。
if (flags & MAP_LOCKED) {
if (!can_do_mlock())
return -EPERM;
vm_flags |= VM_LOCKED;
}
if (vm_flags & VM_LOCKED) {
unsigned long locked, lock_limit;
locked = len >> PAGE_SHIFT;
locked += mm->locked_vm;
lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
lock_limit >>= PAGE_SHIFT;
if (locked > lock_limit && !capable(CAP_IPC_LOCK))
return -EAGAIN;
}
--關於鎖定的內存區在以後學習中再看,這裏就不細看。
inode = file ? file->f_path.dentry->d_inode : NULL; --判斷是否匿名映射,如果不是則賦值inode
if (file) {
switch (flags & MAP_TYPE) { --MAP_TYPE = 0x0F type的掩碼
case MAP_SHARED:
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE)) --file應該被打開並允許寫入。
return -EACCES;
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE)) --不能寫入一個只允許寫追加的文件
return -EACCES;
if (locks_verify_locked(inode)) --確保文件沒有被強制鎖定。
return -EAGAIN;
vm_flags |= VM_SHARED | VM_MAYSHARE; --嘗試允許其他進程共享。
if (!(file->f_mode & FMODE_WRITE)) --如果file不允許寫就算了,共享也沒有用啊,因為file就一直固定死了,共享也沒有意義。
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}
if (is_file_hugepages(file))
accountable = 0;
if (!file->f_op || !file->f_op->mmap)
return -ENODEV;
break;
default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}
--上面就是對一些旗標進行檢測,防止出現旗標沖突,比如我欲映射的文件不允許寫,而我映射的旗標卻設定是可寫並可以共享的,這個就沖突了。
error = security_file_mmap(file, reqprot, prot, flags, addr, 0); --這個函數就忽略了。
if (error)
return error;
return mmap_region(file, addr, len, flags, vm_flags, pgoff,accountable); --最後一個參數為是否為大頁,如果是的就為0.其余的參數都好理解。
}
3.3 get_unmapped_area
這個是獲取沒有被映射的內存區
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;
if (file && file->f_op && file->f_op->get_unmapped_area)
get_area = file->f_op->get_unmapped_area;
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)
return -EINVAL;
return arch_rebalance_pgtables(addr, len);
}
對於get_area函數我們以arch_get_unmapped_area為例來看如何查找一個空閑的mmap area
unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,unsigned long len, unsigned long pgoff, unsigned long flags)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
unsigned long start_addr;
if (len > TASK_SIZE)
return -ENOMEM;
if (flags & MAP_FIXED) --還記否這個MAP_FIXED是什麽含義不?
return addr;
if (addr) {
addr = PAGE_ALIGN(addr);
vma = find_vma(mm, addr); --vma為NULL即addr的地址不在任一個VMA(vma->vm_start~vma->vm_end) addr的地址沒有被映射,
而且空洞足夠我們這次的映射,那麽返回addr以準備這次的映射
if (TASK_SIZE - len >= addr &&(!vma || addr + len <= vma->vm_start))
return addr;
}
if (len > mm->cached_hole_size) { --如果所需的長度大於當前vma之間的空洞長度
start_addr = addr = mm->free_area_cache;
} else {
start_addr = addr = TASK_UNMAPPED_BASE; --需要的長度小於當前空洞,為了不至於時間浪費,那麽從0開始搜尋,
這裏的搜尋基地址TASK_UNMAPPED_BASE很重要,用戶mmap的地址的基地址必須在TASK_UNMAPPED_BASE之上,
但是一定這樣嚴格 嗎?看上面的if (addr)判斷,如果用戶給了一個地址在TASK_UNMAPPED_BASE之下,
映射實際上還是會發生的。
mm->cached_hole_size = 0;
}
full_search:
for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
if (TASK_SIZE - len < addr) {
if (start_addr != TASK_UNMAPPED_BASE) {
addr = TASK_UNMAPPED_BASE;
start_addr = addr;
mm->cached_hole_size = 0;
goto full_search;
}
return -ENOMEM;
}
if (!vma || addr + len <= vma->vm_start) { --如果第一次find_vma返回值即為NULL ,vma沒有被映射並且空洞足夠映射
!vma的條件只有可能在循環的第一次滿足,在其後不可能滿足,在其後的判斷條件即為
vma->vma_end~vma->vma_next->vma_start之間的空洞大小大於所需要映射的長度即可,
下面判斷條件中的addr為vma->vma_end,而vma->vm_start為vma->vma_next->vma_start
mm->free_area_cache = addr + len;
return addr;
}
if (addr + mm->cached_hole_size < vma->vm_start) --在循環的第一次如果vma不為NULL,不會滿足下面的條件,在以後循環中mm->cached_hole_size
則為該次vma->vm_start 與上一次的vma->vm_end之間的差值
mm->cached_hole_size = vma->vm_start - addr;
addr = vma->vm_end;
}
}
還記否以前看的紅黑樹,這裏就現實的用了紅黑樹的算法。關於這個我們就不看了。
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
struct rb_node * rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node,struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
3.4 mmap_region
unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, unsigned long flags,
unsigned int vm_flags, unsigned long pgoff,int accountable)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
struct vm_area_struct *merged_vma;
int correct_wcount = 0;
int error;
struct rb_node **rb_link, *rb_parent;
unsigned long charged = 0;
struct inode *inode = file ? file->f_path.dentry->d_inode : NULL;
/* Clear old maps */
error = -ENOMEM;
munmap_back:
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); --函數find_vma_prepare()與find_vma()基本相同,它掃描當前進程地址空間的vm_area_struct
結構所形成的紅黑樹,試圖找到結束地址高於addr的第一個區間;如果找到了一個虛擬區,
說明addr所在的虛擬區已經在使用,也就是已經有映射存在,因此要調用do_munmap()
把這個老的虛擬區從進程地址空間中撤銷,如果撤銷不成功,就返回一個負數;
如果撤銷成功,就繼續查找,直到在紅黑樹中找不到addr所在的虛擬區
if (vma && vma->vm_start < addr + len) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
if (!may_expand_vm(mm, len >> PAGE_SHIFT)) -- 頁數和超過限定值返回 0 ,不超過返回1
return -ENOMEM;
if (flags & MAP_NORESERVE) -- 如果flags參數中沒有設置MAP_NORESERVE標誌,新的虛擬區含有私有的可寫頁,空閑頁面數小於要映射的虛擬區
的大小;則函數終止並返回一個負數;其中函數security_vm_enough_memory()用來檢查一個
進程的地址空間中是否有足夠的內存來進行一個新的映射
vm_flags |= VM_NORESERVE;
if (accountable && (!(flags & MAP_NORESERVE) ||
sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
if (vm_flags & VM_SHARED) {
/* Check memory availability in shmem_file_setup? */
vm_flags |= VM_ACCOUNT;
} else if (vm_flags & VM_WRITE) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory(charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
}
if (!file && !(vm_flags & VM_SHARED)) { --如果是匿名映射(file為空),並且這個虛擬區是非共享的,則可以把這個虛擬區和與它緊挨的前一個虛擬區進行合並;
虛擬區的合並是由vma_merge()函數實現的。如果合並成功,則轉out處,請看後面out處的代碼。
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, NULL, pgoff, NULL);
if (vma)
goto out;
}
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
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;
if (file) {
error = -EINVAL;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
goto free_vma;
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file);
error = file->f_op->mmap(file, vma); -- (⊙o⊙)哦 ,終於可以調用設備文件中真正的mmap
if (error)
goto unmap_and_free_vma;
if (vm_flags & VM_EXECUTABLE)
added_exe_file_vma(mm);
} else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
如果建立的是從文件到虛存區間的映射,則:
1.當參數flags中的VM_GROWSDOWN或VM_GROWSUP標誌位為1時,說明這個區間可以向低地址或高地址擴展,但從文件映射的區間不能進行擴展,因此轉到free_vma,釋放給vm_area_struct分配的Slab,並返回一個錯誤;
2.當flags中的VM_DENYWRITE標誌位為1時,就表示不允許通過常規的文件操作訪問該文件,所以要調用deny_write_access()排斥常規的文件操作(參見第八章)。
3.get_file()函數的主要作用是遞增file結構中的共享計數;
4.每個文件系統都有個fiel_operation數據結構,其中的函數指針mmap提供了用來建立從該類文件到虛存區間進行映射的操作,這是最具有實質意義的函數;對於大部分文件系統,這個函數為generic_file_mmap( )函數實現的,該函數執行以下操作:
(1)初始化vm_area_struct結構中的vm_ops域。如果VM_SHARED標誌為1,就把該域設置成file_shared_mmap,否則就把該域設置成file_private_mmap。從某種意義上說,這個步驟所做的事情類似於打開一個文件並初始化文件對象的方法。
(2)從索引節點的i_mode域(參見第八章)檢查要映射的文件是否是一個常規文件。如果是其他類型的文件(例如目錄或套接字),就返回一個錯誤代碼。
(3)從索引節點的i_op域中檢查是否定義了readpage( )的索引節點操作。如果沒有定義,就返回一個錯誤代碼。
(4)調用update_atime( )函數把當前時間存放在該文件索引節點的i_atime域中,並將這個索引節點標記成臟。
5.如果flags參數中的MAP_SHARED標誌位為1,則調用shmem_zero_setup()進行共享內存的映射。
if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
vma->vm_flags &= ~VM_ACCOUNT;
addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
if (vma_wants_writenotify(vma))
vma->vm_page_prot = vm_get_page_prot(vm_flags & ~VM_SHARED);
merged_vma = NULL;
if (file)
merged_vma = vma_merge(mm, prev, addr, vma->vm_end,
vma->vm_flags, NULL, file, pgoff, vma_policy(vma));
if (merged_vma) {
mpol_put(vma_policy(vma));
kmem_cache_free(vm_area_cachep, vma);
fput(file);
if (vm_flags & VM_EXECUTABLE)
removed_exe_file_vma(mm);
vma = merged_vma;
} else {
vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file;
}
此時,把新建的虛擬區插入到進程的地址空間,這是由函數vma_link()完成的,該函數具有三方面的功能:
(1)把vma 插入到虛擬區鏈表中
(2)把vma插入到虛擬區形成的紅黑樹中
(3)把vam插入到索引節點(inode)共享鏈表中
函數atomic_inc(x)給*x加1,這是一個原子操作。在內核代碼中,有很多地方調用了以atomic為前綴的函數。原子操作,在操作過程中不會被中斷。
if (correct_wcount)
atomic_inc(&inode->i_writecount);
out:
mm->total_vm += len >> PAGE_SHIFT;
vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
long nr_pages = mlock_vma_pages_range(vma, addr, addr + len);
if (nr_pages < 0)
return nr_pages; /* vma gone! */
mm->locked_vm += (len >> PAGE_SHIFT) - nr_pages;
} else if ((flags & MAP_POPULATE) && !(flags & MAP_NONBLOCK))
make_pages_present(addr, addr + len);
return addr;
unmap_and_free_vma:
if (correct_wcount)
atomic_inc(&inode->i_writecount);
vma->vm_file = NULL;
fput(file);
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = 0;
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}
ok!到此mmap的內核核心就可以了,關於具體的mmap的實現,以後再看。
四.總結
mmap的實質是什麽,其實就是從每一個進程中的用戶空間分配一段空間用於映射。這裏面的機關重重,需要好好理解,不過謹記一點,進程的vma_struct是采用了紅黑樹來管理的。對於每一段的內存區都會有一個vma_struct來描述,比如數據區,code區等等,以及mmap所需要的一段內存區。
mmap的具體實現以前在學習內核時學習過,但是對於其中的很多函數是一知半解的,有些只能根據其函數名來猜測其具體的功能,在本文中,一起來重新深入理解其具體的實現。
二.mmap的用戶層應用
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
具體參數含義
start : 指向欲映射的內存起始地址,通常設為 NULL,代表讓系統自動選定地址,映射成功後返回該地址。
length: 代表將文件中多大的部分映射到內存。
prot : 映射區域的保護方式。可以為以下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取
flags : 影響映射區域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此旗標。
MAP_SHARED 對映射區域的寫入數據會復制回文件內,而且允許其他映射該文件的進程共享。
MAP_PRIVATE 對映射區域的寫入操作會產生一個映射文件的復制,即私人的“寫入時復制”(copy on write)對此區域作的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS建立匿名映射。此時會忽略參數fd,不涉及文件,而且映射區域無法和其他進程共享。
MAP_DENYWRITE只允許對映射區域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。
fd : 要映射到內存中的文件描述符。如果使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統不支持匿名內存映射,則可以使用fopen打開/dev/zero文件,
然後對該文件進行映射,可以同樣達到匿名內存映射的效果。
offset:文件映射的偏移量,通常設置為0,代表從文件最前方開始對應,offset必須是PAGE_SIZE的整數倍。
返回值:
若映射成功則返回映射區的內存起始地址,否則返回MAP_FAILED(-1),錯誤原因存於errno 中。
錯誤代碼:
EBADF 參數fd 不是有效的文件描述詞
EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內存被鎖住。
ENOMEM 內存不足。
用戶層的調用很簡單,其具體功能就是直接將物理內存直接映射到用戶虛擬內存,使用戶空間可以直接對物理空間操作。但是對於內核層而言,其具體實現比較復雜。
三.mmap的內核實現
linux mmap 詳解
謹以此文紀念過往的歲月
一.前言
mmap的具體實現以前在學習內核時學習過,但是對於其中的很多函數是一知半解的,有些只能根據其函數名來猜測其具體的功能,在本文中,一起來重新深入理解其
具體的實現。
二.mmap的用戶層應用
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
具體參數含義
start : 指向欲映射的內存起始地址,通常設為 NULL,代表讓系統自動選定地址,映射成功後返回該地址。
length: 代表將文件中多大的部分映射到內存。
prot : 映射區域的保護方式。可以為以下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取
flags : 影響映射區域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此旗標。
MAP_SHARED 對映射區域的寫入數據會復制回文件內,而且允許其他映射該文件的進程共享。
MAP_PRIVATE 對映射區域的寫入操作會產生一個映射文件的復制,即私人的“寫入時復制”(copy on write)對此區域作的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS建立匿名映射。此時會忽略參數fd,不涉及文件,而且映射區域無法和其他進程共享。
MAP_DENYWRITE只允許對映射區域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。
fd : 要映射到內存中的文件描述符。如果使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統不支持匿名內存映射,則可以使用fopen打開/dev/zero文件,
然後對該文件進行映射,可以同樣達到匿名內存映射的效果。
offset:文件映射的偏移量,通常設置為0,代表從文件最前方開始對應,offset必須是PAGE_SIZE的整數倍。
返回值:
若映射成功則返回映射區的內存起始地址,否則返回MAP_FAILED(-1),錯誤原因存於errno 中。
錯誤代碼:
EBADF 參數fd 不是有效的文件描述詞
EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內存被鎖住。
ENOMEM 內存不足。
用戶層的調用很簡單,其具體功能就是直接將物理內存直接映射到用戶虛擬內存,使用戶空間可以直接對物理空間操作。但是對於內核層而言,其具體實現比較復雜。
三.mmap的內核實現
對於mmap的內核有了解的都會知道用戶層的mmap到內核層的mmap其中多了一個參數vma_struct這個結構體,在開始時對於這個參數很疑惑就是這個參數的值是哪兒來的,
在這裏我們會一一來講述。
還是從do_mmap開始吧。
3.1 do_mmap
參數說明:
file :就是用戶層想要映射的file
addr :欲映射的起始地址,即用戶層的start
prot :用戶層傳入的port
flag :同上
offset:同上
從這裏可以知道,這裏面的參數幾乎均是用戶層傳入的參數。
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) --頁對齊len,檢測傳入參數是否有誤。
goto out;
if (!(offset & ~PAGE_MASK)) --檢測offset是否頁對齊。映射時只能映射頁對齊的長度。
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
3.2 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; --當前用戶進程的mm
struct inode *inode;
unsigned int vm_flags;
int error;
int accountable = 1;
unsigned long reqprot = prot;
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC)) --是否隱藏了可執行屬性。
if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;
if (!len)
return -EINVAL;
if (!(flags & MAP_FIXED)) -
addr = round_hint_to_min(addr); --判斷輸入的欲映射的起始地址是否小於最小映射地址,如果小於,將addr修改為最小地址,不過前提是MAP_FIXED旗標沒有設置。
error = arch_mmap_check(addr, len, flags); --不同平臺對於mmap參數的不同檢測。這裏之間返回0
if (error)
return error;
len = PAGE_ALIGN(len); --檢測len是否越界,len的範圍在0~TASK_SIZE之間。
if (!len || len > TASK_SIZE)
return -ENOMEM; --錯誤值為nomem
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) --再次檢測是否越界。我們這裏不得不小心哪個暈頭了傳入一個莫名其妙的值
return -EOVERFLOW;
if (mm->map_count > sysctl_max_map_count) --在一個進程中對於mmap個數是有限制的。超出了還是nomem的錯誤。
return -ENOMEM;
addr = get_unmapped_area(file, addr, len, pgoff, flags); --獲取沒有映射的地址,這個是查詢mm中空閑的內存地址,這個在下面理解。
if (addr & ~PAGE_MASK)
return addr;
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags |
VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; --設置vm_flags,根據傳入的port和flags以及mm本身自有的旗標來設置。
if (flags & MAP_LOCKED) {
if (!can_do_mlock())
return -EPERM;
vm_flags |= VM_LOCKED;
}
if (vm_flags & VM_LOCKED) {
unsigned long locked, lock_limit;
locked = len >> PAGE_SHIFT;
locked += mm->locked_vm;
lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
lock_limit >>= PAGE_SHIFT;
if (locked > lock_limit && !capable(CAP_IPC_LOCK))
return -EAGAIN;
}
--關於鎖定的內存區在以後學習中再看,這裏就不細看。
inode = file ? file->f_path.dentry->d_inode : NULL; --判斷是否匿名映射,如果不是則賦值inode
if (file) {
switch (flags & MAP_TYPE) { --MAP_TYPE = 0x0F type的掩碼
case MAP_SHARED:
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE)) --file應該被打開並允許寫入。
return -EACCES;
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE)) --不能寫入一個只允許寫追加的文件
return -EACCES;
if (locks_verify_locked(inode)) --確保文件沒有被強制鎖定。
return -EAGAIN;
vm_flags |= VM_SHARED | VM_MAYSHARE; --嘗試允許其他進程共享。
if (!(file->f_mode & FMODE_WRITE)) --如果file不允許寫就算了,共享也沒有用啊,因為file就一直固定死了,共享也沒有意義。
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}
if (is_file_hugepages(file))
accountable = 0;
if (!file->f_op || !file->f_op->mmap)
return -ENODEV;
break;
default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}
--上面就是對一些旗標進行檢測,防止出現旗標沖突,比如我欲映射的文件不允許寫,而我映射的旗標卻設定是可寫並可以共享的,這個就沖突了。
error = security_file_mmap(file, reqprot, prot, flags, addr, 0); --這個函數就忽略了。
if (error)
return error;
return mmap_region(file, addr, len, flags, vm_flags, pgoff,accountable); --最後一個參數為是否為大頁,如果是的就為0.其余的參數都好理解。
}
3.3 get_unmapped_area
這個是獲取沒有被映射的內存區
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;
if (file && file->f_op && file->f_op->get_unmapped_area)
get_area = file->f_op->get_unmapped_area;
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)
return -EINVAL;
return arch_rebalance_pgtables(addr, len);
}
對於get_area函數我們以arch_get_unmapped_area為例來看如何查找一個空閑的mmap area
unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,unsigned long len, unsigned long pgoff, unsigned long flags)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
unsigned long start_addr;
if (len > TASK_SIZE)
return -ENOMEM;
if (flags & MAP_FIXED) --還記否這個MAP_FIXED是什麽含義不?
return addr;
if (addr) {
addr = PAGE_ALIGN(addr);
vma = find_vma(mm, addr); --vma為NULL即addr的地址不在任一個VMA(vma->vm_start~vma->vm_end) addr的地址沒有被映射,
而且空洞足夠我們這次的映射,那麽返回addr以準備這次的映射
if (TASK_SIZE - len >= addr &&(!vma || addr + len <= vma->vm_start))
return addr;
}
if (len > mm->cached_hole_size) { --如果所需的長度大於當前vma之間的空洞長度
start_addr = addr = mm->free_area_cache;
} else {
start_addr = addr = TASK_UNMAPPED_BASE; --需要的長度小於當前空洞,為了不至於時間浪費,那麽從0開始搜尋,
這裏的搜尋基地址TASK_UNMAPPED_BASE很重要,用戶mmap的地址的基地址必須在TASK_UNMAPPED_BASE之上,
但是一定這樣嚴格 嗎?看上面的if (addr)判斷,如果用戶給了一個地址在TASK_UNMAPPED_BASE之下,
映射實際上還是會發生的。
mm->cached_hole_size = 0;
}
full_search:
for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
if (TASK_SIZE - len < addr) {
if (start_addr != TASK_UNMAPPED_BASE) {
addr = TASK_UNMAPPED_BASE;
start_addr = addr;
mm->cached_hole_size = 0;
goto full_search;
}
return -ENOMEM;
}
if (!vma || addr + len <= vma->vm_start) { --如果第一次find_vma返回值即為NULL ,vma沒有被映射並且空洞足夠映射
!vma的條件只有可能在循環的第一次滿足,在其後不可能滿足,在其後的判斷條件即為
vma->vma_end~vma->vma_next->vma_start之間的空洞大小大於所需要映射的長度即可,
下面判斷條件中的addr為vma->vma_end,而vma->vm_start為vma->vma_next->vma_start
mm->free_area_cache = addr + len;
return addr;
}
if (addr + mm->cached_hole_size < vma->vm_start) --在循環的第一次如果vma不為NULL,不會滿足下面的條件,在以後循環中mm->cached_hole_size
則為該次vma->vm_start 與上一次的vma->vm_end之間的差值
mm->cached_hole_size = vma->vm_start - addr;
addr = vma->vm_end;
}
}
還記否以前看的紅黑樹,這裏就現實的用了紅黑樹的算法。關於這個我們就不看了。
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
struct rb_node * rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node,struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
3.4 mmap_region
unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, unsigned long flags,
unsigned int vm_flags, unsigned long pgoff,int accountable)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
struct vm_area_struct *merged_vma;
int correct_wcount = 0;
int error;
struct rb_node **rb_link, *rb_parent;
unsigned long charged = 0;
struct inode *inode = file ? file->f_path.dentry->d_inode : NULL;
/* Clear old maps */
error = -ENOMEM;
munmap_back:
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); --函數find_vma_prepare()與find_vma()基本相同,它掃描當前進程地址空間的vm_area_struct
結構所形成的紅黑樹,試圖找到結束地址高於addr的第一個區間;如果找到了一個虛擬區,
說明addr所在的虛擬區已經在使用,也就是已經有映射存在,因此要調用do_munmap()
把這個老的虛擬區從進程地址空間中撤銷,如果撤銷不成功,就返回一個負數;
如果撤銷成功,就繼續查找,直到在紅黑樹中找不到addr所在的虛擬區
if (vma && vma->vm_start < addr + len) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
if (!may_expand_vm(mm, len >> PAGE_SHIFT)) -- 頁數和超過限定值返回 0 ,不超過返回1
return -ENOMEM;
if (flags & MAP_NORESERVE) -- 如果flags參數中沒有設置MAP_NORESERVE標誌,新的虛擬區含有私有的可寫頁,空閑頁面數小於要映射的虛擬區
的大小;則函數終止並返回一個負數;其中函數security_vm_enough_memory()用來檢查一個
進程的地址空間中是否有足夠的內存來進行一個新的映射
vm_flags |= VM_NORESERVE;
if (accountable && (!(flags & MAP_NORESERVE) ||
sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
if (vm_flags & VM_SHARED) {
/* Check memory availability in shmem_file_setup? */
vm_flags |= VM_ACCOUNT;
} else if (vm_flags & VM_WRITE) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory(charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
}
if (!file && !(vm_flags & VM_SHARED)) { --如果是匿名映射(file為空),並且這個虛擬區是非共享的,則可以把這個虛擬區和與它緊挨的前一個虛擬區進行合並;
虛擬區的合並是由vma_merge()函數實現的。如果合並成功,則轉out處,請看後面out處的代碼。
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, NULL, pgoff, NULL);
if (vma)
goto out;
}
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
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;
if (file) {
error = -EINVAL;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
goto free_vma;
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file);
error = file->f_op->mmap(file, vma); -- (⊙o⊙)哦 ,終於可以調用設備文件中真正的mmap
if (error)
goto unmap_and_free_vma;
if (vm_flags & VM_EXECUTABLE)
added_exe_file_vma(mm);
} else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
如果建立的是從文件到虛存區間的映射,則:
1.當參數flags中的VM_GROWSDOWN或VM_GROWSUP標誌位為1時,說明這個區間可以向低地址或高地址擴展,但從文件映射的區間不能進行擴展,因此轉到free_vma,釋放給vm_area_struct分配的Slab,並返回一個錯誤;
2.當flags中的VM_DENYWRITE標誌位為1時,就表示不允許通過常規的文件操作訪問該文件,所以要調用deny_write_access()排斥常規的文件操作(參見第八章)。
3.get_file()函數的主要作用是遞增file結構中的共享計數;
4.每個文件系統都有個fiel_operation數據結構,其中的函數指針mmap提供了用來建立從該類文件到虛存區間進行映射的操作,這是最具有實質意義的函數;對於大部分文件系統,這個函數為generic_file_mmap( )函數實現的,該函數執行以下操作:
(1)初始化vm_area_struct結構中的vm_ops域。如果VM_SHARED標誌為1,就把該域設置成file_shared_mmap,否則就把該域設置成file_private_mmap。從某種意義上說,這個步驟所做的事情類似於打開一個文件並初始化文件對象的方法。
(2)從索引節點的i_mode域(參見第八章)檢查要映射的文件是否是一個常規文件。如果是其他類型的文件(例如目錄或套接字),就返回一個錯誤代碼。
(3)從索引節點的i_op域中檢查是否定義了readpage( )的索引節點操作。如果沒有定義,就返回一個錯誤代碼。
(4)調用update_atime( )函數把當前時間存放在該文件索引節點的i_atime域中,並將這個索引節點標記成臟。
5.如果flags參數中的MAP_SHARED標誌位為1,則調用shmem_zero_setup()進行共享內存的映射。
if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
vma->vm_flags &= ~VM_ACCOUNT;
addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
if (vma_wants_writenotify(vma))
vma->vm_page_prot = vm_get_page_prot(vm_flags & ~VM_SHARED);
merged_vma = NULL;
if (file)
merged_vma = vma_merge(mm, prev, addr, vma->vm_end,
vma->vm_flags, NULL, file, pgoff, vma_policy(vma));
if (merged_vma) {
mpol_put(vma_policy(vma));
kmem_cache_free(vm_area_cachep, vma);
fput(file);
if (vm_flags & VM_EXECUTABLE)
removed_exe_file_vma(mm);
vma = merged_vma;
} else {
vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file;
}
此時,把新建的虛擬區插入到進程的地址空間,這是由函數vma_link()完成的,該函數具有三方面的功能:
(1)把vma 插入到虛擬區鏈表中
(2)把vma插入到虛擬區形成的紅黑樹中
(3)把vam插入到索引節點(inode)共享鏈表中
函數atomic_inc(x)給*x加1,這是一個原子操作。在內核代碼中,有很多地方調用了以atomic為前綴的函數。原子操作,在操作過程中不會被中斷。
if (correct_wcount)
atomic_inc(&inode->i_writecount);
out:
mm->total_vm += len >> PAGE_SHIFT;
vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
long nr_pages = mlock_vma_pages_range(vma, addr, addr + len);
if (nr_pages < 0)
return nr_pages; /* vma gone! */
mm->locked_vm += (len >> PAGE_SHIFT) - nr_pages;
} else if ((flags & MAP_POPULATE) && !(flags & MAP_NONBLOCK))
make_pages_present(addr, addr + len);
return addr;
unmap_and_free_vma:
if (correct_wcount)
atomic_inc(&inode->i_writecount);
vma->vm_file = NULL;
fput(file);
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = 0;
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}
ok!到此mmap的內核核心就可以了,關於具體的mmap的實現,以後再看。
四.總結
mmap的實質是什麽,其實就是從每一個進程中的用戶空間分配一段空間用於映射。這裏面的機關重重,需要好好理解,不過謹記一點,進程的vma_struct是采用了紅黑樹來管理的。對於每一段的內存區都會有一個vma_struct來描述,比如數據區,code區等等,以及mmap所需要的一段內存區。
你可能會有興趣的文章:
限會員,要發表迴響,請先登入