完全分析 Linux mmap 原理
內(nèi)存映射是一個很有趣的思想,我們都知道操作系統(tǒng)分為用戶態(tài)和內(nèi)核態(tài),用戶態(tài)是不能直接和物理設(shè)備打交道,如果我們用戶空間想訪問硬盤的一塊區(qū)域數(shù)據(jù),則需要兩次拷貝(硬盤->內(nèi)核->用戶),但是內(nèi)存映射的設(shè)計只需要發(fā)生一次拷貝,大大提高了讀取數(shù)據(jù)的效率。那么內(nèi)存映射的原理和內(nèi)核是如何實現(xiàn)的呢?
1. 內(nèi)存映射概念
內(nèi)存映射,簡而言之就是將用戶空間的一段內(nèi)存區(qū)域映射到內(nèi)核空間,映射成功后,用戶對這段內(nèi)存區(qū)域的修改可以直接反映到內(nèi)核空間,同樣,內(nèi)核空間對這段區(qū)域的修改也直接反映給用戶空間,對于用戶空間和內(nèi)核空間兩者之間需要進行大量數(shù)據(jù)傳輸?shù)炔僮鞯脑捫适欠浅8叩?。如下圖所示

實現(xiàn)這樣的映射后,進程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁到對應(yīng)的文件磁盤上,就可以完成對于文件的操作,而不需要再調(diào)用read/write等系統(tǒng)調(diào)用函數(shù)。同時,內(nèi)核空間對于這段區(qū)域的修改也可以直接反映到用戶空間,從而可以實現(xiàn)不同進程間的文件共享。
mmap/munmap?接口是常用的內(nèi)存映射的系統(tǒng)調(diào)用接口,無論是在用戶空間分配內(nèi)存、讀寫大文件、連接動態(tài)庫文件,還是多進程間共享內(nèi)存,都可以看到其身影,其聲明如下:
#include?
void*?mmap(void*?start,?size_t?length,?int?prot,?int?flags,?int?fd,?off_t?offset);
int?munmap(void?*addr,?size_t?length);
條件:
mmap()必須以PAGE_SIZE為單位進行映射,而內(nèi)存也只能以頁為單位進行映射,若要映射非PAGE_SIZE整數(shù)倍的地址范圍,要先進行內(nèi)存對齊,強行以PAGE_SIZE的倍數(shù)大小進行映射。
參數(shù)說明:
start:映射區(qū)的開始地址,設(shè)置為0時表示由系統(tǒng)決定映射區(qū)的起始地址。length:映射區(qū)的長度。//長度單位是 以字節(jié)為單位,不足一內(nèi)存頁按一內(nèi)存頁處理prot:期望的內(nèi)存保護標(biāo)志,不能與文件的打開模式?jīng)_突。是以下的某個值,可以通過or運算合理地組合在一起PROT_EXEC: 表示映射的頁面是可以執(zhí)行的 PROT_READ:表示映射的頁面是可以讀取的 PROT_WRITE :表示映射的頁面是可以寫入的 PROT_NONE :表示映射的頁面是不可訪問的 flags:指定映射對象的類型,映射選項和映射頁是否可以共享。它的值可以是一個或者多個以下位的組合體 MAP_SHARED:創(chuàng)建一個共享映射的區(qū)域,多個進程可以通過共享映射的方式來映射一個文件,這樣其他進程也可以看到映射內(nèi)容的改變,修改后的內(nèi)容會同步到磁盤文件 MAP_PRIVATE:創(chuàng)建一個私有的寫時復(fù)制的映射,多個進程可以通過私有映射方式來映射一個文件,其他的進程不會看到映射文件內(nèi)容的改變,修改后也不會同步到磁盤中 MAP_ANONYMOUS:創(chuàng)建一個匿名映射,即沒有關(guān)聯(lián)到文件的映射 MAP_FIXED: MAP_POPULATE:提前遇到文件內(nèi)容到映射區(qū) fd:mmap映射釋放和文件相關(guān)聯(lián),可以分為匿名映射和文件映射 文件映射:將一個普通文件的全部或者一部分映射到進程的虛擬內(nèi)存中。映射后,進程就可以直接在對應(yīng)的內(nèi)存區(qū)域操作文件內(nèi)容! 匿名映射:匿名映射沒有對應(yīng)的文件或者對應(yīng)的文件時虛擬文件(如:/dev/zero),映射后會把內(nèi)存分頁全部初始化為0。 offset:被映射對象內(nèi)容的起點
返回說明:
成功執(zhí)行時,mmap()返回被映射區(qū)的指針,munmap()返回0。失敗時,mmap()返回MAP_FAILED[其值為(void *)-1],munmap返回-1。
根據(jù)文件關(guān)聯(lián)性和映射區(qū)域示范共享等屬性,其分為
| 私有映射 | 共享映射 | |
|---|---|---|
| 匿名映射 | 私有匿名映射(通常用于內(nèi)存分配),當(dāng)使用大于128K內(nèi)存時 fd = -1 且 flags = MAP_ANONYMOUS|MAP_PRIVATE | 共享匿名映射(通常用于父子進程間共享) fd = -1 且 flags = MAP_ANONYMOUS|MAP_SHARED |
| 文件映射 | 私有文件映射(通常用于動態(tài)庫加載) | 共享文件映射(通常用于內(nèi)存映射IO、進程間通信) |
2. 源碼分析
查看?mmap?的系統(tǒng)調(diào)用的代碼實現(xiàn),其流程為?sys_mmp_pg_off(),最終會調(diào)用達(dá)到?do_mmap_pgoff,該函數(shù)使一個體系結(jié)構(gòu)無關(guān)的代碼,定義在?mm/mmap.c?中,
首先我們來看看?do_mmap(),是整個?mmap()?的具體操作函數(shù)
unsigned?long?do_mmap(struct?file?*file,?unsigned?long?addr,
???unsigned?long?len,?unsigned?long?prot,
???unsigned?long?flags,?vm_flags_t?vm_flags,
???unsigned?long?pgoff,?unsigned?long?*populate)
{
?struct?mm_struct?*mm?=?current->mm;????????//獲取該進程的memory?descriptor
?int?pkey?=?0;
?*populate?=?0;
?//函數(shù)對傳入的參數(shù)進行一系列檢查,?假如任一參數(shù)出錯,都會返回一個errno
?if?(!len)
??return?-EINVAL;
?/*
??*?Does?the?application?expect?PROT_READ?to?imply?PROT_EXEC?
??*
??*?(the?exception?is?when?the?underlying?filesystem?is?noexec
??*??mounted,?in?which?case?we?dont?add?PROT_EXEC.)
??*/
?if?((prot?&?PROT_READ)?&&?(current->personality?&?READ_IMPLIES_EXEC))
??if?(!(file?&&?path_noexec(&file->f_path)))
???prot?|=?PROT_EXEC;
?//假如沒有設(shè)置MAP_FIXED標(biāo)志,且addr小于mmap_min_addr,?因為可以修改addr,?所以就需要將addr設(shè)為mmap_min_addr的頁對齊后的地址
?if?(!(flags?&?MAP_FIXED))
??addr?=?round_hint_to_min(addr);
?/*?Careful?about?overflows..?*/
?len?=?PAGE_ALIGN(len);?????????//進行Page大小的對齊
?if?(!len)
??return?-ENOMEM;
?/*?offset?overflow??*/
?if?((pgoff?+?(len?>>?PAGE_SHIFT))???return?-EOVERFLOW;
?/*?Too?many?mappings??*/
?if?(mm->map_count?>?sysctl_max_map_count)????//判斷該進程的地址空間的虛擬區(qū)間數(shù)量是否超過了限制
??return?-ENOMEM;
?//get_unmapped_area從當(dāng)前進程的用戶空間獲取一個未被映射區(qū)間的起始地址
?addr?=?get_unmapped_area(file,?addr,?len,?pgoff,?flags);
?if?(offset_in_page(addr))????????//檢查addr是否有效
??return?addr;
?if?(prot?==?PROT_EXEC)?{
??pkey?=?execute_only_pkey(mm);
??if?(pkey?0)
???pkey?=?0;
?}
?/*?Do?simple?checking?here?so?the?lower-level?routines?won't?have
??*?to.?we?assume?access?permissions?have?been?handled?by?the?open
??*?of?the?memory?object,?so?we?don't?do?any?here.
??*/
?vm_flags?|=?calc_vm_prot_bits(prot,?pkey)?|?calc_vm_flag_bits(flags)?|
???mm->def_flags?|?VM_MAYREAD?|?VM_MAYWRITE?|?VM_MAYEXEC;
?//假如flags設(shè)置MAP_LOCKED,即類似于mlock()將申請的地址空間鎖定在內(nèi)存中,?檢查是否可以進行l(wèi)ock
?if?(flags?&?MAP_LOCKED)
??if?(!can_do_mlock())
???return?-EPERM;
?if?(mlock_future_check(mm,?vm_flags,?len))
??return?-EAGAIN;
?if?(file)?{??????????//?file指針不為nullptr,?即從文件到虛擬空間的映射
??struct?inode?*inode?=?file_inode(file);???//獲取文件的inode
??switch?(flags?&?MAP_TYPE)?{???????//根據(jù)標(biāo)志指定的map種類,把為文件設(shè)置的訪問權(quán)考慮進去
??case?MAP_SHARED:
???if?((prot&PROT_WRITE)?&&?!(file->f_mode&FMODE_WRITE))
????return?-EACCES;
???/*
????*?Make?sure?we?don't?allow?writing?to?an?append-only
????*?file..
????*/
???if?(IS_APPEND(inode)?&&?(file->f_mode?&?FMODE_WRITE))
????return?-EACCES;
???/*
????*?Make?sure?there?are?no?mandatory?locks?on?the?file.
????*/
???if?(locks_verify_locked(file))
????return?-EAGAIN;
???vm_flags?|=?VM_SHARED?|?VM_MAYSHARE;
???if?(!(file->f_mode?&?FMODE_WRITE))
????vm_flags?&=?~(VM_MAYWRITE?|?VM_SHARED);
???/*?fall?through?*/
??case?MAP_PRIVATE:
???if?(!(file->f_mode?&?FMODE_READ))
????return?-EACCES;
???if?(path_noexec(&file->f_path))?{
????if?(vm_flags?&?VM_EXEC)
?????return?-EPERM;
????vm_flags?&=?~VM_MAYEXEC;
???}
???if?(!file->f_op->mmap)
????return?-ENODEV;
???if?(vm_flags?&?(VM_GROWSDOWN|VM_GROWSUP))
????return?-EINVAL;
???break;
??default:
???return?-EINVAL;
??}
?}?else?{
??switch?(flags?&?MAP_TYPE)?{
??case?MAP_SHARED:
???if?(vm_flags?&?(VM_GROWSDOWN|VM_GROWSUP))
????return?-EINVAL;
???/*
????*?Ignore?pgoff.
????*/
???pgoff?=?0;
???vm_flags?|=?VM_SHARED?|?VM_MAYSHARE;
???break;
??case?MAP_PRIVATE:
???/*
????*?Set?pgoff?according?to?addr?for?anon_vma.
????*/
???pgoff?=?addr?>>?PAGE_SHIFT;
???break;
??default:
???return?-EINVAL;
??}
?}
?/*
??*?Set?'VM_NORESERVE'?if?we?should?not?account?for?the
??*?memory?use?of?this?mapping.
??*/
?if?(flags?&?MAP_NORESERVE)?{
??/*?We?honor?MAP_NORESERVE?if?allowed?to?overcommit?*/
??if?(sysctl_overcommit_memory?!=?OVERCOMMIT_NEVER)
???vm_flags?|=?VM_NORESERVE;
??/*?hugetlb?applies?strict?overcommit?unless?MAP_NORESERVE?*/
??if?(file?&&?is_file_hugepages(file))
???vm_flags?|=?VM_NORESERVE;
?}
?//一頓檢查和配置,調(diào)用核心的代碼mmap_region
?addr?=?mmap_region(file,?addr,?len,?vm_flags,?pgoff);
?if?(!IS_ERR_VALUE(addr)?&&
?????((vm_flags?&?VM_LOCKED)?||
??????(flags?&?(MAP_POPULATE?|?MAP_NONBLOCK))?==?MAP_POPULATE))
??*populate?=?len;
?return?addr;
}
do_mmap()?根據(jù)用戶傳入的參數(shù)做了一系列的檢查,然后根據(jù)參數(shù)初始化?vm_area_struct?的標(biāo)志?vm_flags,vma->vm_file = get_file(file)?建立文件與vma的映射,?mmap_region()?負(fù)責(zé)創(chuàng)建虛擬內(nèi)存區(qū)域:
unsigned?long?mmap_region(struct?file?*file,?unsigned?long?addr,
??unsigned?long?len,?vm_flags_t?vm_flags,?unsigned?long?pgoff)
{
?struct?mm_struct?*mm?=?current->mm;?????//獲取該進程的memory?descriptor
?struct?vm_area_struct?*vma,?*prev;
?int?error;
?struct?rb_node?**rb_link,?*rb_parent;
?unsigned?long?charged?=?0;
?/*?檢查申請的虛擬內(nèi)存空間是否超過了限制?*/
?if?(!may_expand_vm(mm,?vm_flags,?len?>>?PAGE_SHIFT))?{
??unsigned?long?nr_pages;
??/*
???*?MAP_FIXED?may?remove?pages?of?mappings?that?intersects?with
???*?requested?mapping.?Account?for?the?pages?it?would?unmap.
???*/
??nr_pages?=?count_vma_pages_range(mm,?addr,?addr?+?len);
??if?(!may_expand_vm(mm,?vm_flags,
?????(len?>>?PAGE_SHIFT)?-?nr_pages))
???return?-ENOMEM;
?}
?/*?檢查[addr,?addr+len)的區(qū)間是否存在映射空間,假如存在重合的映射空間需要munmap??*/
?while?(find_vma_links(mm,?addr,?addr?+?len,?&prev,?&rb_link,
?????????&rb_parent))?{
??if?(do_munmap(mm,?addr,?len))
???return?-ENOMEM;
?}
?/*
??*?Private?writable?mapping:?check?memory?availability
??*/
?if?(accountable_mapping(file,?vm_flags))?{
??charged?=?len?>>?PAGE_SHIFT;
??if?(security_vm_enough_memory_mm(mm,?charged))
???return?-ENOMEM;
??vm_flags?|=?VM_ACCOUNT;
?}
?//檢查是否可以合并[addr,?addr+len)區(qū)間內(nèi)的虛擬地址空間vma
?vma?=?vma_merge(mm,?prev,?addr,?addr?+?len,?vm_flags,
???NULL,?file,?pgoff,?NULL,?NULL_VM_UFFD_CTX);
?if?(vma)???????//假如合并成功,即使用合并后的vma,?并跳轉(zhuǎn)至out
??goto?out;
?//如果不能和已有的虛擬內(nèi)存區(qū)域合并,通過Memory?Descriptor來申請一個vma
?vma?=?kmem_cache_zalloc(vm_area_cachep,?GFP_KERNEL);
?if?(!vma)?{
??error?=?-ENOMEM;
??goto?unacct_error;
?}
?//初始化vma
?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;
?INIT_LIST_HEAD(&vma->anon_vma_chain);
?if?(file)?{?//假如指定了文件映射?
??if?(vm_flags?&?VM_DENYWRITE)?{???//映射的文件不允許寫入,調(diào)用deny_write_accsess(file)排斥常規(guī)的文件操作
???error?=?deny_write_access(file);
???if?(error)
????goto?free_vma;
??}
??if?(vm_flags?&?VM_SHARED)?{???//映射的文件允許其他進程可見,?標(biāo)記文件為可寫
???error?=?mapping_map_writable(file->f_mapping);
???if?(error)
????goto?allow_write_and_free_vma;
??}
??//遞增File的引用次數(shù),返回File賦給vma
??vma->vm_file?=?get_file(file);
??error?=?file->f_op->mmap(file,?vma);??//調(diào)用文件系統(tǒng)指定的mmap函數(shù)
??if?(error)
???goto?unmap_and_free_vma;
??/*?Can?addr?have?changed??
???*
???*?Answer:?Yes,?several?device?drivers?can?do?it?in?their
???*?????????f_op->mmap?method.?-DaveM
???*?Bug:?If?addr?is?changed,?prev,?rb_link,?rb_parent?should
???*??????be?updated?for?vma_link()
???*/
??WARN_ON_ONCE(addr?!=?vma->vm_start);
??addr?=?vma->vm_start;
??vm_flags?=?vma->vm_flags;
?}?else?if?(vm_flags?&?VM_SHARED)?{
??error?=?shmem_zero_setup(vma);??//假如標(biāo)志為VM_SHARED,但沒有指定映射文件,需要調(diào)用shmem_zero_setup(),實際映射的文件是dev/zero
??if?(error)
???goto?free_vma;
?}
?//將申請的新vma加入mm中的vma鏈表
?vma_link(mm,?vma,?prev,?rb_link,?rb_parent);
?/*?Once?vma?denies?write,?undo?our?temporary?denial?count?*/
?if?(file)?{
??if?(vm_flags?&?VM_SHARED)
???mapping_unmap_writable(file->f_mapping);
??if?(vm_flags?&?VM_DENYWRITE)
???allow_write_access(file);
?}
?file?=?vma->vm_file;
out:
?perf_event_mmap(vma);
?//更新進程的虛擬地址空間mm
?vm_stat_account(mm,?vm_flags,?len?>>?PAGE_SHIFT);
?if?(vm_flags?&?VM_LOCKED)?{
??if?(!((vm_flags?&?VM_SPECIAL)?||?is_vm_hugetlb_page(vma)?||
?????vma?==?get_gate_vma(current->mm)))
???mm->locked_vm?+=?(len?>>?PAGE_SHIFT);
??else
???vma->vm_flags?&=?VM_LOCKED_CLEAR_MASK;
?}
?if?(file)
??uprobe_mmap(vma);
?/*
??*?New?(or?expanded)?vma?always?get?soft?dirty?status.
??*?Otherwise?user-space?soft-dirty?page?tracker?won't
??*?be?able?to?distinguish?situation?when?vma?area?unmapped,
??*?then?new?mapped?in-place?(which?must?be?aimed?as
??*?a?completely?new?data?area).
??*/
?vma->vm_flags?|=?VM_SOFTDIRTY;
?vma_set_page_prot(vma);
?return?addr;
unmap_and_free_vma:
?vma->vm_file?=?NULL;
?fput(file);
?/*?Undo?any?partial?mapping?done?by?a?device?driver.?*/
?unmap_region(mm,?vma,?prev,?vma->vm_start,?vma->vm_end);
?charged?=?0;
?if?(vm_flags?&?VM_SHARED)
??mapping_unmap_writable(file->f_mapping);
allow_write_and_free_vma:
?if?(vm_flags?&?VM_DENYWRITE)
??allow_write_access(file);
free_vma:
?kmem_cache_free(vm_area_cachep,?vma);
unacct_error:
?if?(charged)
??vm_unacct_memory(charged);
?return?error;
}
mmap_region()?調(diào)用了?call_mmap(file, vma),call_mmap?根據(jù)文件系統(tǒng)的類型選擇適配的?mmap()?函數(shù),我們選擇目前常用的ext4,ext4_file_mmap()?是ext4對應(yīng)的mmap, 功能非常簡單,更新了file的修改時間(file_accessed(flie)),將對應(yīng)的operation賦給?vma->vm_flags,后面的文件系統(tǒng)章節(jié)在學(xué)習(xí)這塊。
通過分析mmap的源碼我們發(fā)現(xiàn)在調(diào)用?mmap()?的時候僅僅申請一個?vm_area_struct?來建立文件與虛擬內(nèi)存的映射,并沒有建立虛擬內(nèi)存與物理內(nèi)存的映射。假如沒有設(shè)置?MAP_POPULATE?標(biāo)志位,Linux并不在調(diào)用?mmap()?時就為進程分配物理內(nèi)存空間,直到下次真正訪問地址空間時發(fā)現(xiàn)數(shù)據(jù)不存在于物理內(nèi)存空間時,觸發(fā)?Page Fault?即缺頁中斷,Linux才會將缺失的Page換入內(nèi)存空間。其代碼流程圖如下所示

3. 應(yīng)用場景
對于傳統(tǒng)的linux系統(tǒng)文件操作是如何的呢?首選我們來看看工作流是如何的,其流程如下圖所示

其特點為
使用頁緩存機制,提高讀寫效率和保護磁盤 讀文件時,先將文件從磁盤拷貝到緩存,由于頁緩存區(qū)是在內(nèi)核空間,不能被用戶空間直接訪問,所以需要將頁緩存區(qū)數(shù)據(jù)再次拷貝到用戶空間,有2次文件拷貝工作
下面來看看使用內(nèi)存映射文件讀/寫的流程,其流程圖如下圖所示

其特點為:
用戶空間與內(nèi)核空間的交互式通過映射的區(qū)域直接交互,用內(nèi)存的讀取代替I/O讀寫,文件讀寫效率高 數(shù)據(jù)拷貝次數(shù)少,對文件的讀取操作跨過頁緩存,減少了數(shù)據(jù)拷貝一次,效率提高 可實現(xiàn)高效的大規(guī)模數(shù)據(jù)傳輸
在Linux系統(tǒng)中,根據(jù)內(nèi)存映射的本質(zhì)和特點,其應(yīng)用場景在于
1.實現(xiàn)內(nèi)存共享,如跨進程通信
2.提高數(shù)據(jù)讀/寫效率:如讀寫操作
對于進程間的通信,其工作流程如下圖所示

創(chuàng)建一塊共享的接收區(qū),實現(xiàn)地址映射關(guān)系
發(fā)送進程數(shù)據(jù)到自身的虛擬內(nèi)存區(qū)域,數(shù)據(jù)拷貝1次
由于發(fā)送進程的虛擬地址空間與接收進程的虛擬內(nèi)存地址存在映射關(guān)系,所以發(fā)送到的數(shù)據(jù)也存放到接收進程的虛擬內(nèi)存中,即實現(xiàn)了跨進程間通信
4. 總結(jié)
內(nèi)存映射的讀寫操作主要的過程如下:
創(chuàng)建虛擬映射區(qū)域,其在當(dāng)前進程的虛擬地址空間中,尋找一段滿足大小要求的虛擬地址,并且為此虛擬地址分配一個虛擬內(nèi)存區(qū)域(vm_area_struct結(jié)構(gòu)),初始化該虛擬內(nèi)存區(qū)域,插入到進程虛擬地址區(qū)域的鏈表和紅黑樹中 實現(xiàn)地址映射關(guān)系,建立頁表,該過程在mmap函數(shù)中并未實現(xiàn),此時只是創(chuàng)建了映射關(guān)系,并不將任何文件數(shù)據(jù)拷貝至主存中,真正的數(shù)據(jù)拷貝是通過進程發(fā)起讀寫操作時 進程訪問該映射空間,實現(xiàn)文件內(nèi)容到物理內(nèi)存的數(shù)據(jù)拷貝,當(dāng)進程讀寫訪問該映射地址時,如果進程寫操作改變了內(nèi)容,并不會立即更新,而是一定時間后系統(tǒng)會自動會寫臟數(shù)據(jù)到對應(yīng)硬盤的地址空間
使用mmap來創(chuàng)建文件映射,由于只建立了進程地址空間VMA,并沒有馬上分配page cache和建立映射關(guān)系。那么就會導(dǎo)致一個問題,當(dāng)創(chuàng)建一個很大的VMA,會頻繁發(fā)生缺頁中斷。
內(nèi)存映射機制mmap是POSIX標(biāo)準(zhǔn)的系統(tǒng)調(diào)用,有匿名映射和文件映射兩種。
匿名映射使用進程的虛擬內(nèi)存空間,它和malloc(3)類似,實際上有些malloc實現(xiàn)會使用mmap匿名映射分配內(nèi)存,不過匿名映射不是POSIX標(biāo)準(zhǔn)中規(guī)定的。 文件映射有MAP_PRIVATE和MAP_SHARED兩種。前者使用COW的方式,把文件映射到當(dāng)前的進程空間,修改操作不會改動源文件。后者直接把文件映射到當(dāng)前的進程空間,所有的修改會直接反應(yīng)到文件的page cache,然后由內(nèi)核自動同步到映射文件上。
相比于IO函數(shù)調(diào)用,基于文件的mmap的一大優(yōu)點是把文件映射到進程的地址空間,避免了數(shù)據(jù)從用戶緩沖區(qū)到內(nèi)核page cache緩沖區(qū)的復(fù)制過程;當(dāng)然還有一個優(yōu)點就是不需要頻繁的read/write系統(tǒng)調(diào)用。
原文:?https://blog.csdn.net/u012489236/article/details/109709724
