## Linux 核心專題: 虛擬攝影機裝置 > 執行人: hungyuhang > [專題解說錄影](https://youtu.be/M5ZO42bix0M) ### Reviewed by 'ollieni' ``` Skipping BTF generation for /home/hungyuhang/linux2024/vcam/vcam.ko due to unavailability of vmlinux ``` 不是很確定這個訊息代表什麼。 根據[BPF Type Format (BTF)](https://www.kernel.org/doc/html/next/bpf/btf.html) >BTF (BPF Type Format) is the metadata format which encodes the debug info related to BPF program/map BTF file 是和 BPF(Berkerly packet filter)相關的訊息,請問這樣的 warning 對你的專案有影響嗎? > [name=hungyuhang] > 依我目前的理解,如果 BTF generation 被跳過的話,影響應該是 Linux 無法在我的核心模組裡面插入一些 debug message ,而這些 debug message 是當我們在用 [eBPF](https://ebpf.io/what-is-ebpf/) 技術去對核心模組進行 debug 的時候會被用到。 ### Reviewed by `st10740` > 試著將 vcam 運行久一點再來看結果(使用這個測試來播放單色輸出的影片約十分鐘),從結果發現隨著時間拉長, vcam 在呼叫 __check_object_size() 跟 _copy_from_user() 的時候也開始出現 memory leak 的狀況。 想請問 __check_object_size() 和 _copy_from_user() 之間是否有關連性呢? 因為在你提供的 stack trace 上似乎沒有看到 __check_object_size()。 > [name=hungyuhang] > 在 vcam [程式碼](https://github.com/sysprog21/vcam/blob/d39b6ea5d76f95965569e81e5d912a8eb7bc122d/fb.c#L137)裡面,當把使用者傳入 Framebuffer API 的資料從 user-space 複製到 kernel-sapce 的時候會使用 [copy_from_user()](https://elixir.bootlin.com/linux/latest/source/include/linux/uaccess.h#L180) 這個函式,以下式這個函式的程式碼: > ```c > static __always_inline unsigned long __must_check > copy_from_user(void *to, const void __user *from, unsigned long n) > { > if (check_copy_size(to, n, false)) > n = _copy_from_user(to, from, n); > return n; > } > ``` > 其中, `check_copy_size()` 會間接的呼叫到 [__check_object_size()](https://elixir.bootlin.com/linux/latest/source/mm/usercopy.c#L213) ,所以當程式碼呼叫 `copy_from_user()` 的時候,在 `check_copy_size()` 回傳 `True` 的前提下, `__check_object_size()` 跟 `_copy_from_user()` 就會依序被呼叫到。 ## 任務簡介 改進 [vcam](https://github.com/sysprog21/vcam) 裝置驅動程式,使其可在新版 Linux 核心運作、通過 V4L2 測試,並支援 DMABUF。 [vcam](https://github.com/sysprog21/vcam) 是個針對 Linux 核心開發的虛擬攝影機裝置,全部程式碼僅 1 千 5 百行,從而可理解 V4L2 (video fro Linux APIs, version 2) 的使用和 Linux 多媒體架構。開發一個虛擬的攝影機裝置除了理解 Linux 核心設計外,也有資訊安全的幫助,例如你可以安插相關程式碼,紀錄有哪些應用程式偷偷啟動攝影機,但過程中又不會揭露真正的隱私。 * 相關資訊: * [2020 年開發紀錄](https://hackmd.io/@eecheng/B16rQ3GjU) * [2021 年開發紀錄](https://hackmd.io/@WayneLin1992/HkDBmLUDO) * [2022 年開發紀錄](https://hackmd.io/@Masamaloka/linux2022-vcam) * [2023 年開發紀錄](https://hackmd.io/@sysprog/rJEhcgoSn) ## 建立背景知識 理解 vcam 的運作原理: [筆記](https://hackmd.io/@hungyuhang/linux2024-final) ## TODO: 在 Linux v6.8+ 運作 > 提交 pull request 並針對近期 Linux 核心進行修正 ### Patch 1 使用 `make` 命令編譯核心模組,會出現以下錯誤: ``` $ make make -C /lib/modules/6.5.0-35-generic/build M=/home/hungyuhang/linux2024/vcam modules make[1]: Entering directory '/usr/src/linux-headers-6.5.0-35-generic' warning: the compiler differs from the one used to build the kernel The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0 You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0 CC [M] /home/hungyuhang/linux2024/vcam/module.o CC [M] /home/hungyuhang/linux2024/vcam/control.o In file included from ./include/linux/linkage.h:7, from ./arch/x86/include/asm/cache.h:5, from ./include/linux/cache.h:6, from ./arch/x86/include/asm/current.h:9, from ./include/linux/sched.h:12, from ./include/linux/ratelimit.h:6, from ./include/linux/dev_printk.h:16, from ./include/linux/device.h:15, from /home/hungyuhang/linux2024/vcam/control.c:3: /home/hungyuhang/linux2024/vcam/control.c: In function ‘create_control_device’: ./include/linux/export.h:29:22: error: passing argument 1 of ‘class_create’ from incompatible pointer type [-Werror=incompatible-pointer-types] 29 | #define THIS_MODULE (&__this_module) | ~^~~~~~~~~~~~~~~ | | | struct module * /home/hungyuhang/linux2024/vcam/control.c:267:38: note: in expansion of macro ‘THIS_MODULE’ 267 | ctldev->dev_class = class_create(THIS_MODULE, dev_name); | ^~~~~~~~~~~ In file included from ./include/linux/device.h:31: ./include/linux/device/class.h:230:54: note: expected ‘const char *’ but argument is of type ‘struct module *’ 230 | struct class * __must_check class_create(const char *name); | ~~~~~~~~~~~~^~~~ /home/hungyuhang/linux2024/vcam/control.c:267:25: error: too many arguments to function ‘class_create’ 267 | ctldev->dev_class = class_create(THIS_MODULE, dev_name); | ^~~~~~~~~~~~ ./include/linux/device/class.h:230:29: note: declared here 230 | struct class * __must_check class_create(const char *name); | ^~~~~~~~~~~~ cc1: some warnings being treated as errors make[3]: *** [scripts/Makefile.build:251: /home/hungyuhang/linux2024/vcam/control.o] Error 1 make[2]: *** [/usr/src/linux-headers-6.5.0-35-generic/Makefile:2039: /home/hungyuhang/linux2024/vcam] Error 2 make[1]: *** [Makefile:234: __sub-make] Error 2 make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-35-generic' make: *** [Makefile:14: kmod] Error 2 ``` 觀察錯誤訊息,可以發現錯誤是來自於 `class_create()` 呼叫。這個函式只需要一個引數,但是程式碼中卻對這個函式傳入了兩個引數。 會出現這樣的錯誤訊息,是因為 Linux Kernel API 在 Linux v6.4 之後的版本更新了對 `create_class()` 函式的呼叫方式。 更新前的 API 呼叫方式: ```c class_create(owner, name) ``` 更新後的 API 呼叫方式: ```c class_create(name) ``` 對於 Linux Kernel API 的詳細更改可以參考 [Linux kernel commit 1aaba11](https://github.com/torvalds/linux/commit/1aaba11da9aa7d7d6b52a74d45b31cac118295a1#diff-bf5afba571cf825f63da3977a19a898d0d724fa37f0f5fbe31f4770a9ca9e39b) 。 為了因應以上變動,需要在 vacm 專案的 `control.c` 內加上以下程式碼: ```diff + #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0) + ctldev->dev_class = class_create(dev_name); + #else ctldev->dev_class = class_create(THIS_MODULE, dev_name); + #endif ``` 當核心版本大於或等於 v6.4 的時候,就會用新版 Kernel API 的方式去呼叫 `create_class()` ,反之則使用原來的方式去呼叫。 提交紀錄: [sysprog21#38](https://github.com/sysprog21/vcam/pull/38) ### Patch 2 將 Linux kernel 版本更新到 `6.8.0` 之後,編譯 vcam 核心組件時又出現了新的問題: ``` make -C /lib/modules/6.8.0-35-generic/build M=/home/hungyuhang/linux2024/vcam modules make[1]: Entering directory '/usr/src/linux-headers-6.8.0-35-generic' warning: the compiler differs from the one used to build the kernel The kernel was built by: x86_64-linux-gnu-gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0 You are using: gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0 CC [M] /home/hungyuhang/linux2024/vcam/module.o CC [M] /home/hungyuhang/linux2024/vcam/control.o CC [M] /home/hungyuhang/linux2024/vcam/device.o CC [M] /home/hungyuhang/linux2024/vcam/videobuf.o /home/hungyuhang/linux2024/vcam/videobuf.c: In function ‘vcam_out_videobuf2_setup’: /home/hungyuhang/linux2024/vcam/videobuf.c:138:6: error: ‘struct vb2_queue’ has no member named ‘min_buffers_needed’ 138 | q->min_buffers_needed = 2; | ^~ make[3]: *** [scripts/Makefile.build:243: /home/hungyuhang/linux2024/vcam/videobuf.o] Error 1 make[2]: *** [/usr/src/linux-headers-6.8.0-35-generic/Makefile:1926: /home/hungyuhang/linux2024/vcam] Error 2 make[1]: *** [Makefile:240: __sub-make] Error 2 make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-35-generic' make: *** [Makefile:14: kmod] Error 2 ``` 錯誤來自於 `struct vb2_queue` 結構體沒有 `min_buffers_needed` 這個成員。 參考 [Linux kernel commit 80c2b40](https://github.com/torvalds/linux/commit/80c2b40a51393add616a1fd186a1cc10bd676a3f#diff-9610c74d0c30f4dbbc55b8e8dc9d066a01afbcf03fa10008cb12387d285786dc) ,可以發現 Linux 核心在 v6.8 之後就把 `min_buffers_needed` 改名成 `min_queued_buffers` 。所以只要將成員名稱做修改就好。 排除上述問題之後,重新執行了一次 make ,但編譯時出現另一個問題: ``` hungyuhang@UX434FQ:~/linux2024/vcam$ make make -C /lib/modules/6.8.0-35-generic/build M=/home/hungyuhang/linux2024/vcam modules make[1]: Entering directory '/usr/src/linux-headers-6.8.0-35-generic' warning: the compiler differs from the one used to build the kernel The kernel was built by: x86_64-linux-gnu-gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0 You are using: gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0 CC [M] /home/hungyuhang/linux2024/vcam/module.o CC [M] /home/hungyuhang/linux2024/vcam/control.o CC [M] /home/hungyuhang/linux2024/vcam/device.o CC [M] /home/hungyuhang/linux2024/vcam/videobuf.o CC [M] /home/hungyuhang/linux2024/vcam/fb.o /home/hungyuhang/linux2024/vcam/fb.c: In function ‘vcamfb_init’: /home/hungyuhang/linux2024/vcam/fb.c:405:19: error: ‘FBINFO_FLAG_DEFAULT’ undeclared (first use in this function); did you mean ‘FAULT_FLAG_DEFAULT’? 405 | info->flags = FBINFO_FLAG_DEFAULT; | ^~~~~~~~~~~~~~~~~~~ | FAULT_FLAG_DEFAULT /home/hungyuhang/linux2024/vcam/fb.c:405:19: note: each undeclared identifier is reported only once for each function it appears in make[3]: *** [scripts/Makefile.build:243: /home/hungyuhang/linux2024/vcam/fb.o] Error 1 make[2]: *** [/usr/src/linux-headers-6.8.0-35-generic/Makefile:1926: /home/hungyuhang/linux2024/vcam] Error 2 make[1]: *** [Makefile:240: __sub-make] Error 2 make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-35-generic' make: *** [Makefile:14: kmod] Error 2 ``` 這次的錯誤是來自於 Liunx 核心在 v6.6 之後就將 `FBINFO_FLAG_DEFAULT` 巨集給移除了,詳細可以參考 [Linux kernel commit 0444fa3](https://github.com/torvalds/linux/commit/0444fa357c16a4c36c6c6e3ef13e690875fad208) 。 關於如何更改程式碼,我參考了[這個範例](https://elixir.bootlin.com/linux/latest/source/drivers/gpu/drm/drm_fbdev_generic.c#L71)在 `FBINFO_FLAG_DEFAULT` 被移除時所做的[更動](https://github.com/torvalds/linux/commit/40e324e0d859d7645f9331d10986b2b96aed8e10),這邊的作法是直接不賦予 `fb_info.flags` 值。但他們能這樣做的前提都是他們在創立 `fb_info` 的 struct 的時候是使用 [framebuffer_alloc()](https://elixir.bootlin.com/linux/latest/source/drivers/video/fbdev/core/fb_info.c#L22) ,這個函式會直接把整個 `fb_info` 的 struct 裡面各個成員的值給設為預設值。 但是在 vcam 裡面, `fb_info` 是用 `vmalloc()` 去分配記憶體位址,所以無法確保 `fb_info` 裡面的所有欄位被設定為適當的預設值。所以我在這次的 vcam 程式碼更動也使用了 [framebuffer_alloc()](https://elixir.bootlin.com/linux/latest/source/drivers/video/fbdev/core/fb_info.c#L22) 函式來初始化 `fb_info` 的 struct 。 修改之後再重新執行 make ,可以編譯成功,但是在編譯的輸出裡面有一行訊息: ``` Skipping BTF generation for /home/hungyuhang/linux2024/vcam/vcam.ko due to unavailability of vmlinux ``` 不是很確定這個訊息代表什麼。 提交紀錄:[sysprog21#39](https://github.com/sysprog21/vcam/pull/39) 如何快速找到核心程式碼在什麼時候被更改? :::warning 善用 git log 和 git blame ::: ## 通過 V4L2 測試 > 若遇到測試案例失敗的狀況,修正並提交 pull request。 > 更新相關的文件 先使用 vcam 專案內的 `vcam-util` 工具程式確認 video 裝置的名稱: ``` $ sudo ./vcam-util -l Available virtual V4L2 compatible devices: 1. fb1(640,480,1/1,rgb24) -> /dev/video4 ``` 使用以下命令對 vcam 做測試: ``` $ sudo v4l2-compliance -d /dev/video4 -f ``` 以下節錄自測試的輸出結果,可以看到所有測試都有通過: ``` ... Total for vcam device /dev/video4: 48, Succeeded: 48, Failed: 0, Warnings: 0 ``` ## TODO: 修正記憶體操作 > 使用 [kmodleak](https://github.com/tzussman/kmodleak) 找出記憶體操作的缺失並修正 首先照著 [kmodleak](https://github.com/tzussman/kmodleak) readme 裡面的指引進行編譯。 編譯完後,執行 kmodleak : ``` $ sudo ./kmodleak vcam ``` 並且在另一個終端將 vcam 掛載到核心上面: ``` $ sudo insmod vcam.ko ``` 當想要停止測試時,將 vcam 卸載: ``` $ sudo rmmod vcam ``` vcam 被卸載後, kmodleak 就會自動停止,並且產生報告。 以下節錄自 kmodleak 產生的報告: > 關於怎麼看報告可以看[這篇](https://github.com/iovisor/bcc/blob/master/tools/memleak_example.txt) ``` $ sudo ./kmodleak vcam using page size: 4096 Tracing module memory allocs... Unload module (or hit Ctrl-C) to end module 'vcam' loaded module 'vcam' unloaded 27 stacks with outstanding allocations: 32768 bytes in 1 allocations from stack addr = 0x15fb48 size = 32768 0 [<ffffffffaf64edf4>] __alloc_pages+0x264 1 [<ffffffffaf64edf4>] __alloc_pages+0x264 2 [<ffffffffaf657b18>] allocate_slab+0xa8 3 [<ffffffffaf657e28>] new_slab+0x38 4 [<ffffffffaf65ae35>] ___slab_alloc+0x435 5 [<ffffffffaf65d2c0>] kmalloc_trace+0x310 6 [<ffffffffafbc5142>] do_register_framebuffer+0x272 7 [<ffffffffafbc51f1>] register_framebuffer+0x21 8 [<ffffffffc51024a2>] crypto_ccm_module_init+0x5492 9 [<ffffffffc5101191>] crypto_ccm_module_init+0x4181 10 [<ffffffffc50ff307>] crypto_ccm_module_init+0x22f7 11 [<ffffffffc5251045>] __kstrtabns_nvKmsKapiGetFunctionsTable+0x8a02 12 [<ffffffffaf2019bb>] do_one_initcall+0x5b 13 [<ffffffffaf3f4380>] do_init_module+0xc0 14 [<ffffffffaf3f6281>] load_module+0xba1 ... ``` 上面訊息的意思是說,有一個 32768 bytes 的記憶體空間被配置但是卻沒有被釋放。而在這次測試期間,有偵測到 27 個沒有被釋放掉的記憶體配置。 :::info 在這些 stack trace 都沒有看到 vcam 的函式,不確定原因。 ::: 經過觀察, vcam 大多數的 memory leak 都來自於下面兩種: - 載入模組時或是卸載時所配置的記憶體無法被釋放 - 這些記憶體都是在呼叫例如 `video_register_device()` 這類 Linux 提供的 driver 函式時所配置的。 - 在 framebuffer 的 callback function 呼叫 `copy_from_user()` 的時候會產生一些無法被釋放的記憶體 而且大多數 memory leak 並不是每次都發生,上面那個 32768 bytes 的 memory leak 就是一個例子。 跟 akvcam 做比對 akvcam : ``` $ sudo ./kmodleak akvcam using page size: 4096 Tracing module memory allocs... Unload module (or hit Ctrl-C) to end module 'akvcam' loaded module 'akvcam' unloaded 44 stacks with outstanding allocations: ``` vcam : ``` $ sudo ./kmodleak vcam using page size: 4096 Tracing module memory allocs... Unload module (or hit Ctrl-C) to end module 'vcam' loaded module 'vcam' unloaded 27 stacks with outstanding allocations: ``` 本來想要以 akvcam 作為範本來改善 vcam 在載入以及卸載模組時所產生的 memory leak ,但是發現 akvcam 也有一樣的問題,例如這兩個專案都無法釋放在呼叫 `__video_register_device` 時所配置的記憶體。 :::info 目前這邊遇到的問題,是從 stack trace 只能得知 vcam driver 在呼叫哪個 kernel api call 的時候配置的記憶體最後沒被釋放掉,但是沒有被釋放掉的原因卻難以追蹤。 以 `__video_register_device` 為例,我們可以得知核心在建立 video device 的時候配置了一些記憶體,但是當 vcam 呼叫相對應的 `video_unregister_device` 的時候,這些相對應的記憶體卻沒被釋放掉,推測有可能是下列原因: - 設定 video device 的時候有些設定沒有被正確的設定到 - 建立 / 消滅 video device 的 api call 順序不對 - 核心的程式碼本來就會造成 memory leak 但這些都只是推測,要實際知道原因的話可能需要花時間去看原始碼,或是有其他方式? ::: 另外也有注意到配置的記憶體位置有分兩種: ``` 32768 bytes in 1 allocations from stack addr = 0x8b070 size = 32768 0 [<ffffffff8284edf4>] __alloc_pages+0x264 1 [<ffffffff8284edf4>] __alloc_pages+0x264 2 [<ffffffff82857b18>] allocate_slab+0xa8 ... ``` ``` 128 bytes in 1 allocations from stack addr = 0xffff98be4329d400 size = 128 0 [<ffffffff8285ceb3>] kmem_cache_alloc+0x253 1 [<ffffffff8285ceb3>] kmem_cache_alloc+0x253 2 [<ffffffff829bac55>] __kernfs_new_node+0x65 3 [<ffffffff829bc4c6>] kernfs_new_node+0x56 ... ``` 依照[這份文件](https://docs.kernel.org/arch/x86/x86_64/mm.html)來看,數字小的記憶體位置(例如上面的 `0x8b070` )是 user-space virtual memory ,數字大的(例如上面的 `0xffff98be4329d400` )是 Kernel-space virtual memory 。 關於 memory mapping 的概念可以參考[這篇](https://hackmd.io/@sysprog/linux-memory)以及[這篇](https://linux-kernel-labs.github.io/refs/heads/master/labs/memory_mapping.html)。 ### 嘗試追蹤 memory leak 的原因 試著去追蹤其中一個 memory leak ,這邊選擇了呼叫 `register_framebuffer()` 時所產生的 memory leak : ``` 32768 bytes in 1 allocations from stack addr = 0x8b070 size = 32768 0 [<ffffffff8284edf4>] __alloc_pages+0x264 1 [<ffffffff8284edf4>] __alloc_pages+0x264 2 [<ffffffff82857b18>] allocate_slab+0xa8 3 [<ffffffff82857e28>] new_slab+0x38 4 [<ffffffff8285ae35>] ___slab_alloc+0x435 5 [<ffffffff8285d2c0>] kmalloc_trace+0x310 6 [<ffffffff82dc5142>] do_register_framebuffer+0x272 7 [<ffffffff82dc51f1>] register_framebuffer+0x21 8 [<ffffffffc54bc3d9>] rfcomm_tty_driver+0xb2d1 9 [<ffffffffc54bb0bd>] rfcomm_tty_driver+0x9fb5 10 [<ffffffffc54b92d7>] rfcomm_tty_driver+0x81cf 11 [<ffffffffc54c5045>] rfcomm_tty_driver+0x13f3d 12 [<ffffffff824019bb>] do_one_initcall+0x5b 13 [<ffffffff825f4380>] do_init_module+0xc0 14 [<ffffffff825f6281>] load_module+0xba1 ``` 以下是推測的程式執行方式: [do_register_framebuffer()](https://elixir.bootlin.com/linux/latest/source/drivers/video/fbdev/core/fbmem.c#L412) 會用 [kmalloc_trace()](https://elixir.bootlin.com/linux/latest/source/mm/slub.c#L3996) 為 `fb_info->pixmap` 配置一個記憶體位置,其中 `kmalloc_trace()` 會使用 slab 來取得記憶體。 :::info 從 slab 取得的記憶體應該是在 kernel memory space ,但是這個 memory leak 的 address 卻是 `0x8b070` (位於 user-sapce )。 所以說這個 memory leak 的原因會是因為 slab allocator 不小心配置了錯誤的記憶體位置而導致的嗎? ::: 經過觀察,如果配置記憶體的函式(例如 `kmalloc()` `vmalloc()` )的後面有接著呼叫 `__alloc_pages()` 的話,那它 leak 的 memory address 就會是在 user-space 裡面的,反之如果沒有的話,那麼 leak 的 memory address 就會是在 kernel space 裡面。 ### 嘗試改善呼叫 `copy_from_user()` 時產生的 memory leak 試著將 vcam 運行久一點再來看結果(使用[這個測試](#輸出單色畫面)來播放單色輸出的影片約十分鐘),從結果發現隨著時間拉長, vcam 在呼叫 [\_\_check_object_size()](https://elixir.bootlin.com/linux/latest/source/mm/usercopy.c#L213) 跟 [\_copy_from_user()](https://elixir.bootlin.com/linux/latest/source/lib/usercopy.c#L11) 的時候也開始出現 memory leak 的狀況。 ``` 3072 bytes in 3 allocations from stack addr = 0xffff98bebedd8800 size = 1024 addr = 0xffff98be55403800 size = 1024 addr = 0xffff98be75e7e400 size = 1024 0 [<ffffffff8285d212>] kmalloc_trace+0x262 1 [<ffffffff8285d212>] kmalloc_trace+0x262 2 [<ffffffff82f7ec97>] free_iova_fast+0x167 3 [<ffffffff82f7a7d0>] fq_ring_free_locked+0x50 4 [<ffffffff82f7b30d>] fq_flush_timeout+0x6d 5 [<ffffffff82601d87>] call_timer_fn+0x27 6 [<ffffffff82602132>] __run_timers+0x262 7 [<ffffffff826021fd>] run_timer_softirq+0x1d 8 [<ffffffff8363fbce>] __do_softirq+0xde 9 [<ffffffff825090b7>] __irq_exit_rcu+0xd7 10 [<ffffffff8250941e>] irq_exit_rcu+0xe 11 [<ffffffff83624882>] sysvec_apic_timer_interrupt+0x92 12 [<ffffffff83800f4b>] asm_sysvec_apic_timer_interrupt+0x1b 13 [<ffffffff82c60c17>] _copy_from_user+0x27 14 [<ffffffffc54bbde3>] rfcomm_tty_driver+0xacdb 15 [<ffffffff82dcba8f>] fb_write+0x6f 16 [<ffffffff828e168d>] vfs_write+0xfd 17 [<ffffffff828e1dd3>] ksys_write+0x73 18 [<ffffffff828e1e89>] __x64_sys_write+0x19 19 [<ffffffff824040be>] x64_sys_call+0x7e 20 [<ffffffff8361dc3f>] do_syscall_64+0x7f 21 [<ffffffff83800130>] entry_SYSCALL_64_after_hwframe+0x78 ``` :::info 為什麼 calling stack 的記憶體位址不是遞減的? ::: [\_copy_from_user()](https://elixir.bootlin.com/linux/latest/source/lib/usercopy.c#L11) 的功能(參考[這個網址](https://docs.kernel.org/kernel-hacking/hacking.html?highlight=copy_from_user#copy-to-user-copy-from-user-get-user-put-user)):就是把資料從 user-space 複製到 kernel space 。 但是為什麼 `copy_from_user()` 需要去配置記憶體? 從 stack trace 來看,在呼叫 `copy_from_user()` 之後,會做以下的事情: 1. APIC(Advanced Programmable Interrupt Controller) 觸發 timer interrupt 2. DMA 模組會把某些 page frame 釋放掉?也就是 flush 掉(透過 [fq_ring_free_locked()](https://elixir.bootlin.com/linux/latest/source/drivers/iommu/dma-iommu.c#L147) ) - fq = flush queue - 從 calling stack 的函式名稱來看,這邊應該跟 [RCU](https://hackmd.io/@sysprog/linux-rcu) 有點關係 3. 而上述「釋放掉」的過程,會把釋放掉的 page frame 的一些東西資訊放到快取(rcache)裡面,方便下次要申請 iova 的時候可以快速取得,所以會在快取裡面去建立記憶體。 - 參考 [free_iova_fast()](https://elixir.bootlin.com/linux/latest/source/drivers/iommu/iova.c#L424) - 對於 iova 的概念可以參考[這篇](https://blog.csdn.net/flyingnosky/article/details/116771018) - pfn: page frame number 但是在 vcam 的使用情境, framebuffer 理論上並沒有用到 DMA ,那為什麼會呼叫到 DMA 相關的東西?猜想: 1. 在設定 framebuffer 的時候有東西沒有設定好 2. 或許在呼叫 fb_write 會啟動 dma 3. 還是呼叫 copy_from_user 的時候會用到? - 目前會猜是這個,因為 framebuffer 框架就我查到的資訊跟 DMA 是無關的。 - [reference](https://lwn.net/Articles/695991/) 有突發奇想把配置 framebuffer 記憶體位置的 `vmalloc(size)` 換成 `kmalloc(size, GFP_KERNEL)` 但是 memory leak 依然存在。 :::danger 找不到 memory leak 的原因,這個部份先暫緩。 ::: ### 改善載入跟卸載模組時產生的 memory leak 參考 [HotMercury](https://github.com/HotMercury) 以及 [jason50123](https://github.com/jason50123) 的[筆記](https://hackmd.io/@sysprog/r1e08fg7A)中[排除記憶體處理的缺失]( https://hackmd.io/@sysprog/r1e08fg7A#TODO-%E6%8E%92%E9%99%A4%E8%A8%98%E6%86%B6%E9%AB%94%E8%99%95%E7%90%86%E7%9A%84%E7%BC%BA%E5%A4%B1)章節,他們使用了 [rcu_barrier()](https://elixir.bootlin.com/linux/latest/source/kernel/rcu/tree.c#L4062) 來解決 memory leak 的問題。( rcu_barrier 的介紹: [rcubarrier.txt](https://www.kernel.org/doc/Documentation/RCU/rcubarrier.txt)) 具體來說,他們在呼叫 [kmem_cache_destroy()](https://elixir.bootlin.com/linux/latest/source/mm/slab_common.c#L479) 後,又再呼叫了一個 rcu_barrier() 來解決記憶體來不及被釋放掉的問題。實際改動可以參考 [RoyWFHuang的這個commit]( https://github.com/RoyWFHuang/simplefs/commit/1c3736589ea6d35249aba9485277bda568a9e4b8) 參照上述的更動,我在將 vcam 卸載時會呼叫到的部份函式呼叫之後加上了 rcu_barrier() 。以下是程式碼改動: ```diff // function vcamfb_destroy in fb.c info = fb_data->info; if (info) { unregister_framebuffer(info); + rcu_barrier(); fb_dealloc_cmap(&info->cmap); framebuffer_release(info); } ``` ```diff // function destroy_vcam_device in device.c if (vcam->sub_thr_id) kthread_stop(vcam->sub_thr_id); vcamfb_destroy(vcam); + rcu_barrier(); mutex_destroy(&vcam->vcam_mutex); video_unregister_device(&vcam->vdev); v4l2_device_unregister(&vcam->v4l2_dev); + rcu_barrier(); kfree(vcam); } ``` ```diff // function free_control_device in control.c static void free_control_device(struct control_device *dev) { size_t i; for (i = 0; i < dev->vcam_device_count; i++) destroy_vcam_device(dev->vcam_devices[i]); kfree(dev->vcam_devices); device_destroy(dev->dev_class, dev->dev_number); class_destroy(dev->dev_class); + rcu_barrier(); cdev_del(&dev->cdev); unregister_chrdev_region(dev->dev_number, 1); kfree(dev); } ``` 使用[這個測試](https://hackmd.io/@hungyuhang/linux2024-final#輸出單色畫面)來播放單色輸出的影片約五分鐘來測試兩個不同的版本。 以下是兩個不同版本程式碼用 kmodleak 做測量的部份結果: - 原版 ``` $ sudo ./kmodleak vcam using page size: 4096 Tracing module memory allocs... Unload module (or hit Ctrl-C) to end module 'vcam' loaded module 'vcam' unloaded 31 stacks with outstanding allocations: ... ``` - 加了 rcu_barrier() 的版本 ``` $ sudo ./kmodleak vcam using page size: 4096 Tracing module memory allocs... Unload module (or hit Ctrl-C) to end module 'vcam' loaded module 'vcam' unloaded 10 stacks with outstanding allocations: ... ``` 可以看到,加了 rcu_barrier() 的版本的 memory leak 的數量明顯減少了,但為什麼會減少的原因還有待研究。 ## TODO: 支援 DMABUF > 參考 [akvcam](https://github.com/webcamoid/akvcam) 兩專案差異: akvcam 可以把 `VB2_DMABUF` 加入 `struct vb2_queue` 的 `io_modes` 成員(使用 `|` 運算子)。 vcam 直接把 `struct vb2_queue` 的 `io_modes` 成員的值設為 `VB2_MMAP | VB2_USERPTR | VB2_READ` 先嘗試將 vcam 的 vb2_queue.io_modes 加入 `VB2_DMABUF` : ```diff - q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ; + q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ | VB2_DMABUF; ``` 然後運行 vcam 核心模組,核心模組可以正常運作。 > 如何確認 vb2 模組真的有用 DMA ?