日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當前位置:首頁 > 單片機 > 程序喵大人

1.開場白

  • 環(huán)境:
    處理器架構:arm64
    內(nèi)核源碼:linux-5.11
    ubuntu版本:20.04.1
    代碼閱讀工具:vim+ctags+cscope

我們知道,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ù)拷貝,但是像顯存這樣的設備如果也采用這樣的方式就顯的效率非常底下,因為用戶經(jīng)常需要在屏幕上進行繪制,要消除這種復制的操作就需要應用程序直接能夠訪問顯存,但是顯存被映射到內(nèi)核空間,應用程序是沒有訪問權限的,如果顯存也能同時映射到用戶空間那就不需要拷貝操作了,于是字符設備中提供了mmap接口,可以將內(nèi)核空間映射的那塊物理內(nèi)存再次映射到用戶空間,這樣用戶空間就可以直接訪問不需要任何拷貝操作,這就是我們今天要說的0拷貝技術。

下面是正常情況下用戶空間和內(nèi)核空間數(shù)據(jù)訪問圖示:

2. 體驗一下

首先我們通過一個例子來感受一下:

驅動代碼:

注:驅動代碼中使用misc框架來實現(xiàn)字符設備,misc框架會處理如創(chuàng)建字符設備,創(chuàng)建設備等通用的字符設備處理,我們只需要關心我們的實際的邏輯即可(內(nèi)核中大量使用misc設備框架來使用字符設備操作集如ioctl接口,像實現(xiàn)系統(tǒng)虛擬化kvm模塊,實現(xiàn)安卓進程間通信的binder模塊等)。

0copy_demo.c


#include?<linux/module.h>
#include?<linux/kernel.h>
#include?<linux/init.h>
#include?<linux/mm.h>
#include?<linux/miscdevice.h>


#define?MISC_DEV_MINOR?5

static?char?*kbuff;


static?ssize_t?misc_dev_read(struct?file?*filep,?char?__user?*buf,?size_t?count,?loff_t?*offset)
{

?int?ret;

?size_t?len?=?(count?>?PAGE_SIZE???PAGE_SIZE?:?count);

?pr_info("######?%s:%d?kbuff:%s?######\n",?__func__,?__LINE__,?kbuff);
?
?ret?=?copy_to_user(buf,?kbuff,?len);??//這里使用copy_to_user??來進程內(nèi)核空間到用戶空間拷貝

?return?len?-?ret;
}

static?ssize_t?misc_dev_write(struct?file?*filep,?const?char?__user?*buf,?size_t?count,?loff_t?*offset)
{
?pr_info("######?%s:%d?######\n",?__func__,?__LINE__);
?return?0;
}

static?int?misc_dev_mmap(struct?file?*filep,?struct?vm_area_struct?*vma)
{
?int?ret;
?unsigned?long?start;

?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)核的虛擬地址轉化為實際的物理地址頁幀號??創(chuàng)建頁表的權限為通過mmap傳遞的?vma->vm_page_prot???映射大小為1頁

?return?ret;
}

static?long?misc_dev_ioctl(struct?file?*filep,?unsigned?int?cmd,?unsigned?long?args)
{
?pr_info("######?%s:%d?######\n",?__func__,?__LINE__);
?return?0;
}



static?int?misc_dev_open(struct?inode?*inodep,?struct?file?*filep)
{
?pr_info("######?%s:%d?######\n",?__func__,?__LINE__);
?return?0;
}

static?int?misc_dev_release(struct?inode?*inodep,?struct?file?*filep)
{
?pr_info("######?%s:%d?######\n",?__func__,?__LINE__);
?return?0;
}


static?struct?file_operations?misc_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,
};

static?struct?miscdevice?misc_dev?=?{
?MISC_DEV_MINOR,
?"misc_dev",
?&misc_dev_fops,
};

static?int?__init?misc_demo_init(void)
{
?misc_register(&misc_dev);??//注冊misc設備?(讓misc來幫我們處理創(chuàng)建字符設備的通用代碼,這樣我們就不需要在去做這些和我們的實際邏輯無關的代碼處理了)

?
?kbuff?=?(char?*)__get_free_page(GFP_KERNEL);??//申請一個物理頁面(返回對應的內(nèi)核虛擬地址,內(nèi)核初始化的時候會做線性映射,將整個ddr內(nèi)存映射到線性映射區(qū),所以我們不需要做頁表映射)
?if?(NULL?==?kbuff)
??return?-ENOMEM;

?pr_info("######?%s:%d?######\n",?__func__,?__LINE__);
?return?0;
}

static?void?__exit?misc_demo_exit(void)
{
?free_page((unsigned?long)kbuff);

?misc_deregister(&misc_dev);
?pr_info("######?%s:%d?######\n",?__func__,?__LINE__);
}

module_init(misc_demo_init);
module_exit(misc_demo_exit);
MODULE_LICENSE("GPL");

應用代碼:test.c

#include?<stdio.h>
#include?<string.h>
#include?<unistd.h>
#include?<sys/types.h>
#include?<sys/stat.h>
#include?<fcntl.h>
#include?<sys/mman.h>



int?main(int?argc,?char?**argv)
{
?
?int?fd;
?char?*ptr;
?char?buff[32];

?fd?=?open("/dev/misc_dev",?O_RDWR);??//打開字符設備
?if?(fd?<?0)?{
??perror("fail?to?open");
??return?-1;
?}
??
?ptr?=?mmap(NULL,?4096,?PROT_READ?|?PROT_WRITE,?MAP_SHARED,?fd,?0);?//映射字符設備到進程的地址空間??權限為可讀可寫?映射為共享??大小為一個頁面
?if?(ptr?==?MAP_FAILED)?{
??perror("fail?to?mmap");
??return?-1;
?}


?memcpy(ptr,?"hello?world!!!", 15);???//寫mmap映射的內(nèi)存??直接操作,不需要進行特權級別的陷入!


?if(read(fd,?buff,?15)?==?-1)?{??//讀接口??來讀取映射的內(nèi)存,這里會進行內(nèi)核空間到用戶空間的數(shù)據(jù)拷貝?(需要調(diào)用系統(tǒng)調(diào)用?在內(nèi)核空間進行拷貝,然后才能訪問)
??perror("fail?to?read");
??return?-1;
?}
?puts(buff);??

?pause();
?return?0;
}

Makefile文件:

export?ARCH=arm64
export?CROSS_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-gcc?test.c?-o?test
?cp?test?$(KERNEL_DIR)/kmodules

clean:
?$(MAKE)?-C?$(KERNEL_DIR)?M=$(PWD)?clean

install:
?cp?*.ko?$(KERNEL_DIR)/kmodules

編譯驅動代碼和應用代碼,然后拷貝到qemu中運行:

編譯驅動模塊代碼:
$?make?modules

編譯并拷貝應用:
$?make?app

拷貝驅動模塊到qemu:
$?make?install?

加載驅動代碼:
#?insmod?0copy_demo.ko
[23328.532194]?######?misc_demo_init:91?######

查看生成的設備節(jié)點:
#?ls?-l?/dev/misc_dev?
crw-rw----????1?0????????0??????????10,???5?Apr??7?19:26?/dev/misc_dev

后臺運行應用程序:
#?./test&
#?[23415.280501]?######?misc_dev_open:56?######
[23415.281052]?######?misc_dev_read:20?kbuff:hello?world!!!?######
hello?world!!!

查看test的pid:
#?pidof?test
1768


查看內(nèi)存映射:
#?cat?/proc/1768/maps?
aaaabc5a0000-aaaabc5a1000?r-xp?00000000?00:19?8666193????????????????????/mnt/test
aaaabc5b0000-aaaabc5b1000?r--p?00000000?00:19?8666193????????????????????/mnt/test
aaaabc5b1000-aaaabc5b2000?rw-p?00001000?00:19?8666193????????????????????/mnt/test
aaaacf033000-aaaacf054000?rw-p?00000000?00:00?0??????????????????????????[heap]
ffff8a911000-ffff8aa52000?r-xp?00000000?fe:00?152????????????????????????/lib/libc-2.27.so
ffff8aa52000-ffff8aa61000?---p?00141000?fe:00?152????????????????????????/lib/libc-2.27.so
ffff8aa61000-ffff8aa65000?r--p?00140000?fe:00?152????????????????????????/lib/libc-2.27.so
ffff8aa65000-ffff8aa67000?rw-p?00144000?fe:00?152????????????????????????/lib/libc-2.27.so
ffff8aa67000-ffff8aa6b000?rw-p?00000000?00:00?0?
ffff8aa6b000-ffff8aa88000?r-xp?00000000?fe:00?129????????????????????????/lib/ld-2.27.so
ffff8aa91000-ffff8aa92000?rw-s?00000000?00:05?152????????????????????????/dev/misc_dev??????//映射設備文件到用戶空間
ffff8aa92000-ffff8aa94000?rw-p?00000000?00:00?0?
ffff8aa94000-ffff8aa96000?r--p?00000000?00:00?0??????????????????????????[vvar]
ffff8aa96000-ffff8aa97000?r-xp?00000000?00:00?0??????????????????????????[vdso]
ffff8aa97000-ffff8aa98000?r--p?0001c000?fe:00?129????????????????????????/lib/ld-2.27.so
ffff8aa98000-ffff8aa9a000?rw-p?0001d000?fe:00?129????????????????????????/lib/ld-2.27.so
ffffecb5a000-ffffecb7b000?rw-p?00000000?00:00?0??????????????????????????[stack]

執(zhí)行了以上步驟可以發(fā)現(xiàn)最終內(nèi)核中出現(xiàn)了我在應用程序中寫入的“hello world!!!“ ?字符串,應用程序也能成功讀取到(當然本文講解的0拷貝實現(xiàn)的驅動接口是mmap,而我們讀取使用的是read接口,里面我們用copy_to_user來實現(xiàn)的,當然我們可以直接操作mmap映射的內(nèi)存不需要任何拷貝操作)。

查看應用程序的內(nèi)存映射發(fā)現(xiàn),/dev/misc_dev設備被映射到了ffff8aa91000-ffff8aa92000這段用戶空間地址范圍,而且權限為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映射之后,我們在應用程序中可以直接讀寫這段內(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ù)如下:

int?remap_pfn_range(struct?vm_area_struct?*vma,?unsigned?long?addr,?????????
????????????????|???unsigned?long?pfn,?unsigned?long?size,?pgprot_t?prot)???

vma為需要映射的進程的vma(進程調(diào)用mmap的時候內(nèi)核會找到一個合適的vma), addr為vma中的一個起始映射地址(這是用戶空間的一個虛擬地址),pfn為頁幀號(在驅動的mmap接口中會將內(nèi)核空間的地址轉化為物理地址的頁幀號),size為需要映射的大小,prot為映射的權限(一般取mmap時傳遞的權限如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ù)會查找進程的頁表,然后填寫頁表,會將映射的物理頁幀號和訪問權限填寫到進程的對應頁表中,這會遍歷進程的各級頁表找到最終的頁表項然后進行填寫,具體過程自行查看代碼。

我們需要注意的是:

1.一般情況下,用戶程序調(diào)用mmap只是申請?zhí)摂M內(nèi)存(即是獲得一塊沒有使用用戶空間內(nèi)存,使用vma描述),實際的物理頁表都是通過進程訪問的時候缺頁異常的方式來申請的,但是本場景中是物理頁面已經(jīng)申請好了,進程訪問時不會再發(fā)生缺頁異常,不會申請物理頁面。

2.同樣,物理頁面到用戶空間虛擬頁面的映射也在調(diào)用mmap的時候,驅動調(diào)用mmap接口的remap_pfn_range映射好了,也不需要在訪問的時候發(fā)生缺頁異常來建立映射。所以,只要用戶進程通過mmap映射之后就可以正常訪問,訪問過程中不會發(fā)生缺頁異常,映射虛擬頁對應的物理頁面已經(jīng)在驅動中申請好映射好。

下面給出mmap映射原理的圖示:



4.應用場景

最后,我們來看下使用framebuffer的lcd對0拷貝的使用情況

fbmem_init????//drivers/video/fbdev/core/fbmem.c
->register_chrdev(FB_MAJOR,?"fb",?&fb_fops)??//注冊framebuffer字符設備
???
???->?struct?file_operations?fb_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)建設備類

lcd驅動代碼中會設置好最終注冊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)建設備??會出現(xiàn)/dev/fdx?設備節(jié)點

可以看到當系統(tǒng)支持framebuffer設備時,在fbmem_init中會創(chuàng)建framebuffer設備類關聯(lián)字符設備操作集fb_fops,lcd的驅動代碼中會調(diào)用register_framebuffer創(chuàng)建framebuffer設備(就會創(chuàng)建出了/dev/fdx 設備節(jié)點),應用程序就可以通過mmap來映射framebuffer設備到用戶空間,然后進行屏幕繪制操作,不需要任何數(shù)據(jù)拷貝。

5.總結

可以看的出,通過mmap實現(xiàn)0拷貝非常簡單,只需要在驅動的mmap接口中調(diào)用remap_pfn_range來將內(nèi)核空間映射的那塊物理頁再次映射到用戶空間即可,這就實現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)共享,這和用戶進程之間的共享內(nèi)存機制非常相似,都需要操作進程的頁表將這段物理內(nèi)存映射到進程虛擬地址空間。


本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權益,請及時聯(lián)系本站刪除。
關閉