1.開場白
我們知道,linux系統(tǒng)中用戶空間和內(nèi)核空間是隔離的,用戶空間程序不能隨意的訪問內(nèi)核空間數(shù)據(jù),只能通過中斷或者異常的方式進入內(nèi)核態(tài),一般情況下,我們使用copy_to_user和copy_from_user等內(nèi)核api來實現(xiàn)用戶空間和內(nèi)核空間的數(shù)據(jù)拷貝,但是像顯存這樣的設(shè)備如果也采用這樣的方式就顯的效率非常底下,因為用戶經(jīng)常需要在屏幕上進行繪制,要消除這種復制的操作就需要應(yīng)用程序直接能夠訪問顯存,但是顯存被映射到內(nèi)核空間,應(yīng)用程序是沒有訪問權(quán)限的,如果顯存也能同時映射到用戶空間那就不需要拷貝操作了,于是字符設(shè)備中提供了mmap接口,可以將內(nèi)核空間映射的那塊物理內(nèi)存再次映射到用戶空間,這樣用戶空間就可以直接訪問不需要任何拷貝操作,這就是我們今天要說的0拷貝技術(shù)。
下面是正常情況下用戶空間和內(nèi)核空間數(shù)據(jù)訪問圖示:
2. 體驗一下
首先我們通過一個例子來感受一下:
驅(qū)動代碼:
注:驅(qū)動代碼中使用misc框架來實現(xiàn)字符設(shè)備,misc框架會處理如創(chuàng)建字符設(shè)備,創(chuàng)建設(shè)備等通用的字符設(shè)備處理,我們只需要關(guān)心我們的實際的邏輯即可(內(nèi)核中大量使用misc設(shè)備框架來使用字符設(shè)備操作集如ioctl接口,像實現(xiàn)系統(tǒng)虛擬化kvm模塊,實現(xiàn)安卓進程間通信的binder模塊等)。
0copy_demo.c
#include
#include
#include
#include
#include
#defineMISC_DEV_MINOR5
staticchar*kbuff;
staticssize_tmisc_dev_read(structfile*filep,char__user*buf,size_tcount,loff_t*offset)
{
intret;
size_tlen=(count>PAGE_SIZE?PAGE_SIZE:count);
pr_info("######%s:%dkbuff:%s######
",__func__,__LINE__,kbuff);
ret=copy_to_user(buf,kbuff,len);//這里使用copy_to_user來進程內(nèi)核空間到用戶空間拷貝
returnlen-ret;
}
staticssize_tmisc_dev_write(structfile*filep,constchar__user*buf,size_tcount,loff_t*offset)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}
staticintmisc_dev_mmap(structfile*filep,structvm_area_struct*vma)
{
intret;
unsignedlongstart;
start=vma->vm_start;
ret=remap_pfn_range(vma,start,virt_to_phys(kbuff)>>PAGE_SHIFT,
PAGE_SIZE,vma->vm_page_prot);//使用remap_pfn_range來映射物理頁面到進程的虛擬內(nèi)存中virt_to_phys(kbuff)>>PAGE_SHIFT作用是將內(nèi)核的虛擬地址轉(zhuǎn)化為實際的物理地址頁幀號創(chuàng)建頁表的權(quán)限為通過mmap傳遞的vma->vm_page_prot映射大小為1頁
returnret;
}
staticlongmisc_dev_ioctl(structfile*filep,unsignedintcmd,unsignedlongargs)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}
staticintmisc_dev_open(structinode*inodep,structfile*filep)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}
staticintmisc_dev_release(structinode*inodep,structfile*filep)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}
staticstructfile_operationsmisc_dev_fops={
.open=misc_dev_open,
.release=misc_dev_release,
.read=misc_dev_read,
.write=misc_dev_write,
.unlocked_ioctl=misc_dev_ioctl,
.mmap=misc_dev_mmap,
};
staticstructmiscdevicemisc_dev={
MISC_DEV_MINOR,
"misc_dev",
&misc_dev_fops,
};
staticint__initmisc_demo_init(void)
{
misc_register(&misc_dev);//注冊misc設(shè)備(讓misc來幫我們處理創(chuàng)建字符設(shè)備的通用代碼,這樣我們就不需要在去做這些和我們的實際邏輯無關(guān)的代碼處理了)
kbuff=(char*)__get_free_page(GFP_KERNEL);//申請一個物理頁面(返回對應(yīng)的內(nèi)核虛擬地址,內(nèi)核初始化的時候會做線性映射,將整個ddr內(nèi)存映射到線性映射區(qū),所以我們不需要做頁表映射)
if(NULL==kbuff)
return-ENOMEM;
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}
staticvoid__exitmisc_demo_exit(void)
{
free_page((unsignedlong)kbuff);
misc_deregister(&misc_dev);
pr_info("######%s:%d######
",__func__,__LINE__);
}
module_init(misc_demo_init);
module_exit(misc_demo_exit);
MODULE_LICENSE("GPL");
應(yīng)用代碼:test.c
#include
#include
#include
#include
#include
#include
#include
intmain(intargc,char**argv)
{
intfd;
char*ptr;
charbuff[32];
fd=open("/dev/misc_dev",O_RDWR);//打開字符設(shè)備
if(fd0)?{
??perror("failtoopen");
return-1;
}
ptr=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//映射字符設(shè)備到進程的地址空間權(quán)限為可讀可寫映射為共享大小為一個頁面
if(ptr==MAP_FAILED){
perror("failtommap");
return-1;
}
memcpy(ptr,"helloworld!!!", 15);//寫mmap映射的內(nèi)存直接操作,不需要進行特權(quán)級別的陷入!
if(read(fd,buff,15)==-1){//讀接口來讀取映射的內(nèi)存,這里會進行內(nèi)核空間到用戶空間的數(shù)據(jù)拷貝(需要調(diào)用系統(tǒng)調(diào)用在內(nèi)核空間進行拷貝,然后才能訪問)
perror("failtoread");
return-1;
}
puts(buff);
pause();
return0;
}
Makefile文件:
exportARCH=arm64
exportCROSS_COMPILE=aarch64-linux-gnu-
KERNEL_DIR?=~/kernel/linux-5.11
obj-m:=0copy_demo.o
modules:
$(MAKE)-C$(KERNEL_DIR)M=$(PWD)modules
app:
aarch64-linux-gnu-gcctest.c-otest
cptest$(KERNEL_DIR)/kmodules
clean:
$(MAKE)-C$(KERNEL_DIR)M=$(PWD)clean
install:
cp*.ko$(KERNEL_DIR)/kmodules
編譯驅(qū)動代碼和應(yīng)用代碼,然后拷貝到qemu中運行:
編譯驅(qū)動模塊代碼:
$makemodules
編譯并拷貝應(yīng)用:
$makeapp
拷貝驅(qū)動模塊到qemu:
$makeinstall
加載驅(qū)動代碼:
#insmod0copy_demo.ko
[23328.532194]######misc_demo_init:91######
查看生成的設(shè)備節(jié)點:
#ls-l/dev/misc_dev
crw-rw----10010,5Apr719:26/dev/misc_dev
后臺運行應(yīng)用程序:
#./test&
#[23415.280501]######misc_dev_open:56######
[23415.281052]######misc_dev_read:20kbuff:helloworld!!!######
helloworld!!!
查看test的pid:
#pidoftest
1768
查看內(nèi)存映射:
#cat/proc/1768/maps
aaaabc5a0000-aaaabc5a1000r-xp0000000000:198666193/mnt/test
aaaabc5b0000-aaaabc5b1000r--p0000000000:198666193/mnt/test
aaaabc5b1000-aaaabc5b2000rw-p0000100000:198666193/mnt/test
aaaacf033000-aaaacf054000rw-p0000000000:000[heap]
ffff8a911000-ffff8aa52000r-xp00000000fe:00152/lib/libc-2.27.so
ffff8aa52000-ffff8aa61000---p00141000fe:00152/lib/libc-2.27.so
ffff8aa61000-ffff8aa65000r--p00140000fe:00152/lib/libc-2.27.so
ffff8aa65000-ffff8aa67000rw-p00144000fe:00152/lib/libc-2.27.so
ffff8aa67000-ffff8aa6b000rw-p0000000000:000
ffff8aa6b000-ffff8aa88000r-xp00000000fe:00129/lib/ld-2.27.so
ffff8aa91000-ffff8aa92000rw-s0000000000:05152/dev/misc_dev//映射設(shè)備文件到用戶空間
ffff8aa92000-ffff8aa94000rw-p0000000000:000
ffff8aa94000-ffff8aa96000r--p0000000000:000[vvar]
ffff8aa96000-ffff8aa97000r-xp0000000000:000[vdso]
ffff8aa97000-ffff8aa98000r--p0001c000fe:00129/lib/ld-2.27.so
ffff8aa98000-ffff8aa9a000rw-p0001d000fe:00129/lib/ld-2.27.so
ffffecb5a000-ffffecb7b000rw-p0000000000:000[stack]
執(zhí)行了以上步驟可以發(fā)現(xiàn)最終內(nèi)核中出現(xiàn)了我在應(yīng)用程序中寫入的“hello world!!!“ 字符串,應(yīng)用程序也能成功讀取到(當然本文講解的0拷貝實現(xiàn)的驅(qū)動接口是mmap,而我們讀取使用的是read接口,里面我們用copy_to_user來實現(xiàn)的,當然我們可以直接操作mmap映射的內(nèi)存不需要任何拷貝操作)。
查看應(yīng)用程序的內(nèi)存映射發(fā)現(xiàn),/dev/misc_dev設(shè)備被映射到了ffff8aa91000-ffff8aa92000這段用戶空間地址范圍,而且權(quán)限為rw-s(可讀可寫共享)。
寫到這里可能大家還是有點不明白那我來解釋下:
1.用戶空間不能直接訪問內(nèi)核空間數(shù)據(jù)(不能直接讀寫),一旦訪問發(fā)生缺頁異常,產(chǎn)生段錯誤,必須通過read這樣的接口來訪問,而read這樣的接口會通過系統(tǒng)調(diào)用的方式寫入到內(nèi)核態(tài),然后通過copy_to_user這樣的內(nèi)核api來拷貝內(nèi)核空間數(shù)據(jù)到用戶空間之后才能正常訪問。
2.通過mmap這種方式之后,用戶進程可以直接訪問這塊內(nèi)存,memcpy訪問的也只不過是用戶空間地址,由于訪問的時候已經(jīng)分配好了物理頁面和建立好了物理頁到虛擬頁的映射,所有不會發(fā)生缺頁異常,也不會發(fā)生用戶態(tài)到內(nèi)核態(tài)的陷入動作。
3.用戶態(tài)進程正常訪問內(nèi)核態(tài)數(shù)據(jù)需要首先通過系統(tǒng)調(diào)用等方式陷入內(nèi)核,進行數(shù)據(jù)拷貝,然后再次回到用戶態(tài),用戶態(tài)和內(nèi)核態(tài)直接的進出需要進行上下文切換,需要2次上下文切換,需要一定的開銷,而mmap映射好之后以后訪問都不需要進行上下文切換。
4.mmap映射這種方法由于物理頁面通過頁面共享更加節(jié)省內(nèi)存,而用戶態(tài)和內(nèi)核態(tài)內(nèi)存拷貝需要兩份物理頁面。
3.實現(xiàn)原理
我們發(fā)現(xiàn)通過mmap映射之后,我們在應(yīng)用程序中可以直接讀寫這段內(nèi)存,不需要任何用戶空間和內(nèi)核空間的拷貝動作,大大提高了內(nèi)存訪問效率,那么就是是如何實現(xiàn)的呢?下面我們來揭開它神秘的面紗:
實現(xiàn)0拷貝功不可沒的是mmap接口中的remap_pfn_range內(nèi)核api,它將內(nèi)核空間映射的物理內(nèi)存重新映射到了用戶空間,下面我們來看這個函數(shù)的實現(xiàn):remap_pfn_range函數(shù)參數(shù)如下:
intremap_pfn_range(structvm_area_struct*vma,unsignedlongaddr,
|unsignedlongpfn,unsignedlongsize,pgprot_tprot)
vma為需要映射的進程的vma(進程調(diào)用mmap的時候內(nèi)核會找到一個合適的vma), addr為vma中的一個起始映射地址(這是用戶空間的一個虛擬地址),pfn為頁幀號(在驅(qū)動的mmap接口中會將內(nèi)核空間的地址轉(zhuǎn)化為物理地址的頁幀號),size為需要映射的大小,prot為映射的權(quán)限(一般取mmap時傳遞的權(quán)限如rw)
remap_pfn_range實現(xiàn)主要如下代碼段:
remap_pfn_range
...
pgd=pgd_offset(mm,addr);
flush_cache_range(vma,addr,end);
do{
next=pgd_addr_end(addr,end);
err=remap_p4d_range(mm,pgd,addr,next,
pfn+(addr>>PAGE_SHIFT),prot);
if(err)
break;
}while(pgd++,addr=next,addr!=end);
解釋下:remap_pfn_range函數(shù)會查找進程的頁表,然后填寫頁表,會將映射的物理頁幀號和訪問權(quán)限填寫到進程的對應(yīng)頁表中,這會遍歷進程的各級頁表找到最終的頁表項然后進行填寫,具體過程自行查看代碼。
我們需要注意的是:
1.一般情況下,用戶程序調(diào)用mmap只是申請?zhí)摂M內(nèi)存(即是獲得一塊沒有使用用戶空間內(nèi)存,使用vma描述),實際的物理頁表都是通過進程訪問的時候缺頁異常的方式來申請的,但是本場景中是物理頁面已經(jīng)申請好了,進程訪問時不會再發(fā)生缺頁異常,不會申請物理頁面。
2.同樣,物理頁面到用戶空間虛擬頁面的映射也在調(diào)用mmap的時候,驅(qū)動調(diào)用mmap接口的remap_pfn_range映射好了,也不需要在訪問的時候發(fā)生缺頁異常來建立映射。所以,只要用戶進程通過mmap映射之后就可以正常訪問,訪問過程中不會發(fā)生缺頁異常,映射虛擬頁對應(yīng)的物理頁面已經(jīng)在驅(qū)動中申請好映射好。
下面給出mmap映射原理的圖示:
4.應(yīng)用場景
最后,我們來看下使用framebuffer的lcd對0拷貝的使用情況:
fbmem_init//drivers/video/fbdev/core/fbmem.c
->register_chrdev(FB_MAJOR,"fb",&fb_fops)//注冊framebuffer字符設(shè)備
->structfile_operationsfb_fops={
->.mmap=fb_mmap
->fb_mmap//framebuffer的實現(xiàn)
->vm_iomap_memory
->io_remap_pfn_range
->remap_pfn_range
->fb_class=class_create(THIS_MODULE,"graphics")//創(chuàng)建設(shè)備類
lcd驅(qū)動代碼中會設(shè)置好最終注冊framebuffer:
xxxfb_probe
->register_framebuffer
->do_register_framebuffer
->fb_info->dev=device_create(fb_class,fb_info->device,
|MKDEV(FB_MAJOR,i),NULL,"fb%d",i);//創(chuàng)建設(shè)備會出現(xiàn)/dev/fdx設(shè)備節(jié)點
可以看到當系統(tǒng)支持framebuffer設(shè)備時,在fbmem_init中會創(chuàng)建framebuffer設(shè)備類關(guān)聯(lián)字符設(shè)備操作集fb_fops,lcd的驅(qū)動代碼中會調(diào)用register_framebuffer創(chuàng)建framebuffer設(shè)備(就會創(chuàng)建出了/dev/fdx 設(shè)備節(jié)點),應(yīng)用程序就可以通過mmap來映射framebuffer設(shè)備到用戶空間,然后進行屏幕繪制操作,不需要任何數(shù)據(jù)拷貝。
5.總結(jié)
可以看的出,通過mmap實現(xiàn)0拷貝非常簡單,只需要在驅(qū)動的mmap接口中調(diào)用remap_pfn_range來將內(nèi)核空間映射的那塊物理頁再次映射到用戶空間即可,這就實現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)共享,這和用戶進程之間的共享內(nèi)存機制非常相似,都需要操作進程的頁表將這段物理內(nèi)存映射到進程虛擬地址空間。
原文標題:5.總結(jié)
文章出處:【微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
處理器
+關(guān)注
關(guān)注
68文章
19885瀏覽量
235057 -
內(nèi)核
+關(guān)注
關(guān)注
3文章
1416瀏覽量
41409 -
Linux
+關(guān)注
關(guān)注
87文章
11509瀏覽量
213709 -
內(nèi)存映射
+關(guān)注
關(guān)注
0文章
15瀏覽量
7540
原文標題:5.總結(jié)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
黑芝麻智能一芯多域零拷貝共享內(nèi)存技術(shù):破解車載大數(shù)據(jù)傳輸效能困局

如何通過高效工程評審EQ流程,實現(xiàn)PCB零缺陷制造?
四頻拷貝遙控器走俏海外

FB08 1對7 U盤拷貝格式化機——高效數(shù)據(jù)復制工具

FB16 1對15 U盤拷貝格式化機——高效數(shù)據(jù)復制工具

零碳節(jié)能工廠是什么?有什么功能?如何實現(xiàn)? ?
M.2硬盤拷貝,該怎么選擇適合的工具?FPGA拷貝機到底有沒有用?

嵌入式學習-飛凌嵌入式ElfBoard ELF 1板卡-LCD顯示圖片編程示例之介紹mmap
批量音頻檔案拷貝最佳方案:解決播放錯誤與拷貝不完全問題


飛凌嵌入式ElfBoard ELF 1板卡-LCD顯示圖片編程示例之介紹mmap
磁盤拷貝機會拷貝刪除的內(nèi)容嗎
編譯例程partition_mmap,報錯no such vaddr range怎么解決?
無線室內(nèi)定位系統(tǒng)是通過什么技術(shù)實現(xiàn)的呢?

評論