## 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 ?