# LKMPG 學習紀錄
**閱讀 lkmpg 的學習筆記**
若有錯誤歡迎各位協助提醒,感謝
[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/#scheduling-tasks)
# Introduction
linux 核心模組的定義: 一段可以在需要的時候**動態加載或卸載**的程式碼片段。通常這些模組可以增強 linux kernel 的功能,而且不需要重新啟動系統
查看當前 kernel 中有哪些模組
```shell
$ lsmod
```
搜尋指定模組,例如 fat 模組
```shell
$ lsmod | grep fat
```
### SecureBoot
現今的電腦大多在出廠時都設置了 UEFI SecureBoot,這是一項必備的安全標準,目的是為確保說系統只透過原使設備製造商認可的軟體進行啟動。
關閉方式可以直接到 BIOS 中關閉 SecureBoot 選項,或是使用 `mokutil` 來停用 SecureBoot
```shell
sudo apt install mokutil
sudo mokutil --disable-validation
```
:::info
當 Secure Boot 是 啟用(Enabled) 的時候,Linux 核心只允許載入具有有效「數位簽章」的 kernel module(.ko)。
而我們手動編譯或修改的模組,預設是「沒有被簽章」的,因此會被拒絕掛載(insmod / modprobe 會失敗)。
:::
關閉後我們才能去測試跟掛載模組。
## Headers
在開始前,需要先安裝 Kernel 的 header file
```shell
sudo apt-get install linux-headers-`uname -r`
```
## 撰寫 Hello World! 模組
建立 `hello.c`
```c
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, world\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
```
建立 `Makefile`
```
obj-m := hello.o
clean:
rm -rf *.o *.ko *.mod.* *.symvers *.order *.mod.cmd *.mod
```
make 指令
```shell
$ make -C /lib/modules/`uname -r`/build M=`pwd` modules
```
編譯成功後,應該會產生一個 `hello.ko` 模組,可以用命令來查看它
```shell
$ modinfo hello.ko
```
```
filename: /home/user/kmod/hello.ko
license: Dual BSD/GPL
srcversion: F387861272F5CA27DA088DC
depends:
retpoline: Y
name: hello
vermagic: 6.11.0-19-generic SMP preempt mod_unload modversions
```
接著可以將模組掛載
```shell
$ sudo insmod hello.ko
```
使用 `lsmod` 可以找到掛載上去的模組
接著在 /var/log/kern.log 中確認一下輸出結果
```
2025-05-10T18:20:22.957234+08:00 user kernel: Hello, world
```
卸載核心模組
```shell
$ sudo rmmod hello
```
在 /var/log/kern.log 中確認一下輸出結果
```
2025-05-10T18:22:26.210266+08:00 user kernel: Goodbye, curl world
```
`module_init()` 與 `module_exit()` 都是 kernel module 中兩個基本需要的函數,當模組被掛載到核心時,會呼叫 `module_init()`,為核心註冊一個處理程序,或是用自己的程式碼替換核心的某一個函式,而在模組被移除前會呼叫 `module_exit()`,撤銷 `module_init()` 所做的任何操作,以便模組可以安全的卸載。
:::info
Linux 的 coding style 應該要使用 tab 來縮排而不是 space
:::
## printk
[關於 printk 的說明](https://huenlil.pixnet.net/blog/post/23271426)
一開始,kernel 是使用 `printk` 搭配優先級(priority)來輸出訊息,像是 `KERN_INFO` 等,`KERN_INFO` 就是 log 的嚴重程度(info = 資訊),還有像是 `KERN_ERR`(錯誤)、`KERN_WARNING`(警告)等。
```c
printk(KERN_INFO "Hello from kernel\n");
```
後來,這些可以用簡寫的列印巨集來寫,例如 `pr_info` 或 `pr_debug`。
```c
pr_info("Hello from kernel\n");
```
## LICENSE(許可)
我們可以透過幾個 macro 來標示模組的授權方式,舉例像是 "GPL", "Dual BSD/GPL" 等等,這些 macro 定義在 `include/linux/module.h` 中。
`MODULE_LICENSE` 這個 macro 可以引用我們所使用的授權方式
```
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LKMPG");
MODULE_DESCRIPTION("A sample driver");
```
## 傳遞命令行參數給模組
模組也可以接受命令行參數的形式,為了讓你的模組可以接收參數,你要先宣告一個全域變數,這個變數會用來儲存從命令列傳進來的參數值。
`module_param()` 能夠讓我們在 `insmod` 的時後附上參數,這個 macro 可以接受 3 個參數,變數名稱、資料型態、以及在 sysfs 中對應檔案的權限。
```c
int myint = 3;
module_param(myint, int, 0);
```
Array 也可以支援,但他多了第 3 個參數為傳遞的參數數量。
而 `MODULE_PARM_DESC()` 則是讓我們給予不同的參數附上說明,使用 `modinfo` 時可以列出該模組的說明文件。
使用範例:
<details>
<summary>完整程式碼</summary>
```c
#include <linux/init.h>
#include <linux/kernel.h> /* for ARRAY_SIZE() */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/printk.h>
#include <linux/stat.h>
MODULE_LICENSE("GPL");
static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "blah";
static int myintarray[2] = { 420, 420 };
static int arr_argc = 0;
/* module_param(foo, int, 0000)
* The first param is the parameter's name.
* The second param is its data type.
* The final argument is the permissions bits,
* for exposing parameters in sysfs (if non-zero) at a later stage.
*/
module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myshort, "A short integer");
module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(myint, "An integer");
module_param(mylong, long, S_IRUSR);
MODULE_PARM_DESC(mylong, "A long integer");
module_param(mystring, charp, 0000);
MODULE_PARM_DESC(mystring, "A character string");
/* module_param_array(name, type, num, perm);
* The first param is the parameter's (in this case the array's) name.
* The second param is the data type of the elements of the array.
* The third argument is a pointer to the variable that will store the number
* of elements of the array initialized by the user at module loading time.
* The fourth argument is the permission bits.
*/
module_param_array(myintarray, int, &arr_argc, 0000);
MODULE_PARM_DESC(myintarray, "An array of integers");
static int __init hello_5_init(void)
{
int i;
pr_info("Hello, world 5\n=============\n");
pr_info("myshort is a short integer: %hd\n", myshort);
pr_info("myint is an integer: %d\n", myint);
pr_info("mylong is a long integer: %ld\n", mylong);
pr_info("mystring is a string: %s\n", mystring);
for (i = 0; i < ARRAY_SIZE(myintarray); i++)
pr_info("myintarray[%d] = %d\n", i, myintarray[i]);
pr_info("got %d arguments for myintarray.\n", arr_argc);
return 0;
}
static void __exit hello_5_exit(void)
{
pr_info("Goodbye, world 5\n");
}
module_init(hello_5_init);
module_exit(hello_5_exit);
```
</details>
在執行掛載時給予參數:
```shell
$ sudo insmod hello-5.ko mystring="babe" myintarray=12,-1
$ sudo dmesg | tail -7
```
```
[214907.640336] myshort is a short integer: 1
[214907.640338] myint is an integer: 420
[214907.640339] mylong is a long integer: 9999
[214907.640341] mystring is a string: babe
[214907.640343] myintarray[0] = 12
[214907.640345] myintarray[1] = -1
[214907.640346] got 2 arguments for myintarray.
```
# 前置知識
## module begin
一般在 User space 運行的程式是從 `main()` 作為進入點,但在核心模組中,模組都是通過 `module_init` 函數開始,這個函數作為模組的進入點,向核心傳送模組資料,並準備好核心在需要時使用的模組功能,完成任務後,這個入口函數將會 return ,模組處於 inactivce 狀態,直到核心需要他。
## module end
所有的模組都必須調用 `cleanup_module` or `module_exit` 來調用指定的函數作為該模組的退出函數。
## function available for module
在撰寫核心模組時,當中再調用一些函式像是 `pr_info` ,他跟一般寫的程式利用 include 引入函式庫去連結的方式不同,這是因為模組是一個物件檔案,它們只能呼叫 kernel 導出的 symbols(如 `printk()` 或 `pr_info()`),這些 symbols 的定義來自正在執行的 kernel,而不是自己。
/proc/kallsyms 提供了目前可供 module 使用的 symbol 清單。
## Name Space
當撰寫核心模組時,即便是最小的模組也會連結到整個核心,跟 kernel 共用一個 Symbol table,而這時候如果你的模組內有某些函式命名和核心內其他模組撞名,那可能會導致其被覆蓋,因此針對這個問題的最佳解決方式,就是將所有變數宣告為 `static`,因為 `static` 在 C 中代表「此變數或函式的範圍只限於這個檔案」。
## Device Driver
其中一種模組類型稱為 Device driver,他為一些硬體提供功能,像是序列埠,在 UNIX 中,每種硬體都位於 /dev 底下並以檔案表示,稱為 device file。
Device driver 讓使用者可以用 user program 進行互動,例如,es1370.ko 音校卡設備驅動程式可能會將 /dev/sound 設備檔案連接到 Ensoniq IS1370 音效卡。
```shell
$ ls -l /dev/sda[1-3]
brw-rw---- 1 root disk 8, 1 Apr 9 2025 /dev/sda1
brw-rw---- 1 root disk 8, 2 Apr 9 2025 /dev/sda2
brw-rw---- 1 root disk 8, 3 Apr 9 2025 /dev/sda3
```
第一個數字稱為設備的主要編號(major number)。第二個數字是次要編號(minor number)。主要編號告訴你用來存取硬體的驅動程式是哪一個。每個驅動程式都分配了一個獨一無二的主要編號。
所有具有相同主要編號的設備檔案都由同一個驅動程式控制。上述的所有主要編號都是 8,因為它們都由同一個驅動程式控制。
次要編號用來讓驅動程式區分它控制的各種硬體。回到上面的例子,雖然所有三個設備都由同一個驅動程式處理,但因為驅動程式認為它們是不同的硬體,所以它們具有獨特的次要編號。
Device 又可以分為兩種: **character devices** and **block devices**。
區別在於 block devices 有 buffer 來儲存 request,因此他可以去從中選擇最佳的 respond 順序,這種功能對儲存設備來說非常重要,因為會涉及到鄰近區域的寫入和存取效率,再來是它只能以 block 的形式接收輸入和回傳輸出。
character devices 可以使用任意數量的 bytes,大部分的 devices 都屬於這種。
可以透過 `ls -l` 來查看輸出的第一個字符來判斷該設備是何種。
```shell
user@user:~/kmod$ ls -l /dev/tty1
crw--w---- 1 root tty 4, 1 May 8 12:22 /dev/tty1
```
第一個是 'c' , is character devices.
透過 `cat /proc/devices` 指令,我們可以檢視當前系統中的主設備號和設備類型名稱
系統在安裝的時候,所有的 device files 都是由 `mknod`。要建立一個名為 coffee 的新 character device,其 major / minor number 為 12 和 2,我們只需要執行
```shell!
$ mknod /dev/coffee c 12 2
```
:::info
不一定要將 device files 放在 /dev,但這是慣例
:::
# Character Device drivers
## The file_operations Structure
file_operations 結構體定義在 [include/linux/fs.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fs.h) 中,結構體中的每個欄位都是函式指標,指向你自己在驅動程式中定義的函式,用來對裝置做各種操作(例如:讀寫、開啟、關閉等)。 這裡是這個結構體的[完整定義](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fs.h#n2129)。
也可以對結構體內的成員去做初始化,參考 C99 的 [designated initializers](https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html)
```c
struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};
```
其他未賦值的成員,GCC會將其初始化為 NULL。
## Registering A Device 註冊裝置
將 driver 加到系統,意味著將其註冊到 kernel 中,相當於在模組初始化時去分配一個 major number,這是由剛剛的 fs.h 介面中的 `register_chrdev ` 所完成。
```c
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
```
其中,major 代表的是我們想要請求的設備的 major number,name 是設備的名稱,這個名稱將會出現在 `/proc/devices` 中,fops 是指向 driver的 file_operations 表。如果回傳值是負數,則表示註冊失敗。
而要如何挑選一個不會被其他程式占用的 major number? 可以向 kernel 請求一個動態的 major number。
若將 major number 設為 0 傳給 `register_chrdev`,其返回值將會是動態分配的 major number,但缺點是無法事先製作 devices files,因為不知道 major number 會是多少。有幾種方法可以解決這個問題
首先,device driver 本身可以印出新分配的編號,然後我們可以手動製作 device files。
其次,新註冊的裝置會在 /proc/devices 中出現,我們可以手動製作 device files,或寫一個 shell 腳本來讀取該檔案並製作 device files。
第三種方法是我們可以讓 device driver 在成功註冊後使用 `device_create` 函式,並且呼叫 `cleanup_module` 時使用 `device_destroy` 清除。
但 `register_chrdev()` 會占用與給定 major number 相關聯的一系列的 minor number,會有浪費資源的問題。 推薦使用 cdev 介面
## cdev
首先,一樣先註冊裝置編號,可以使用下面任一個函式完成
```c
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
```
再來我們初始化我們的 char device 的結構體 cdev,並將其與設備編號給相關聯
```c
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
```
再來需要將其嵌入到自己的特定結構中
```c
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
```
初始化完成后,我們可以使用 `cdev_add` 將 char device 加入系統。
```c
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
```
## Unregistering A Device
我們不允許 kernel 在 device file 正被其他 process 開啟的時候做 rmmod,這樣代表模組突然被移除,所以它會呼叫原本對應的 driver function 記憶體位置,而這時候假如剛好那塊記憶體位址被其他的模組載入,那將會出現不可預測的行為。
而通常要對這類情況作出防範,我們可能會在最後結束前的函式做檢查,如果有不符的條件就回傳負數之類的,但偏偏 `cleanup_module` 型態是 `void`,不過 kernel 有一個 counter,它會追蹤目前有多少 process 正在使用你的模組。
可以通過執行 cat /proc/modules 或 lsmod 命令查看該 counter 的值,如過這個數字不為 0 , rmmod 將會失敗。
[include/linux/module.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/module.h) 有提供可以增加、減少、和顯示這個 counter 的函式。
`try_module_get(THIS_MODULE)` : 增加目前模組的 count。
`module_put(THIS_MODULE)` : 減少目前模組的 count。
`module_refcount(THIS_MODULE)` : 回傳目前模組的 count。
## chardev.c
<details>
<summary>完整程式碼</summary>
```c
// chardev.c: Create a read-only char device that says how many times
//you have read fron the dev file
#include <linux/atomic.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h> /* for sprint() */
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/uaccess.h> /* for get_user and put_user */
#include <linux/version.h>
#include <asm/errno.h>
/* Prototypes - this would normally gp in a .h file */
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char __user *, size_t,
loff_t *);
#define DEVICE_NAME "chardev" /* Dev name as it appears in /proc/devices */
#define BUF_LEN 80 /* Max length of the message from the device */
/* Global variables are declared as static, so are global within file */
static int major; /* major number assigned to our device driver */
enum{
CDEV_NOT_USED,
CDEV_EXCLUSIVE_OPEN,
};
/* Is device open? Used to prevent multiple access to device */
static atomic_t already_open = ATOMIC_INIT(CDEV_NOT_USED);
static char msg[BUF_LEN + 1]; /* The msg the device will give when asked */
static struct class *cls;
static struct file_operations chardev_fop = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release,
};
static int __init chardev_init(void)
{
major = register_chrdev(0, DEVICE_NAME, &chardev_fop);
if (major < 0) {
pr_alert("Registering char device failed with %d\n", major);
return major;
}
pr_info("I was assigned major number %d.\n", major);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
cls = class_create(DEVICE_NAME);
#else
cls = class_create(THIS_MODULE, DEVICE_NAME);
#endif
device_create(cls, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
pr_info("Device created in /dev/%s\n", DEVICE_NAME);
return 0;
}
static void __exit chardev_exit(void)
{
device_destroy(cls, MKDEV(major, 0)); /* clear device file */
class_destroy(cls);
/* Unregister the device */
unregister_chrdev(major, DEVICE_NAME);
}
/* Methods */
/* Called when a process tries to open the device file, like
* "sudo car /dev/chardev"
*/
static int device_open(struct inode *inode, struct file *file)
{
static int counter = 0;
if (atomic_cmpxchg(&already_open, CDEV_NOT_USED, CDEV_EXCLUSIVE_OPEN))
return -EBUSY;
sprintf(msg, "I alreadt told you %d times Hello world!\n", counter++);
try_module_get(THIS_MODULE);
return 0;
}
/* Called when a process closes the device file */
static int device_release(struct inode *inode, struct file *file)
{
/* We 're not ready for our next caller */
atomic_set(&already_open, CDEV_NOT_USED);
/* Decrement the usage count, or else once you opened the file, you will
* never get rid of the module.
*/
module_put(THIS_MODULE);
return 0;
}
/* Called when a process, which already opened the dec file, attempts to
* read from it.
*/
static ssize_t device_read(struct file *filp, /* see include/linux/fs.h */
char __user *buffer, /* buffer to fill with data */
size_t length, /* length of the buffer */
loff_t *offset)
{
/* Number of bytes actually written to the buffer */
int bytes_read = 0;
const char *msg_ptr = msg;
if (!*(msg_ptr + *offset)){ /* we are at the end of message */
*offset = 0; /* reset the offset */
return 0; /* signify end of file */
}
msg_ptr += *offset;
/* Actually put the data into the buffer */
while (length && *msg_ptr){
/* The buffer is in the user data segment, not the kernel
* segment so "*" assignment won't work. We have to use
* put_user which copies data from the kernel data segment to
* the user data segment.
*/
put_user(*(msg_ptr++), buffer++);
length--;
bytes_read++;
}
*offset += bytes_read;
/* Most read functions return the number of bytes put into the buffer */
return bytes_read;
}
/* Called when a process writes */
static ssize_t device_write(struct file *filp, const char __user *buff,
size_t len, loff_t *off)
{
pr_alert("Sorry, this operation is not supported.\n");
return -EINVAL;
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
```
</details>
# The /proc File System
proc filesystem 是一種傳送資訊給 processes 的方式,像是提供 module list 的 /proc/modules 和 收集記憶體使用資料的 /proc/meminfo。
使用方法和 device driver 很類似,要建立一個結構體,包含了所有指向 handler function 的指標,然後 `init_module` 會跟 kernel 註冊,`cleanup_module` 則會取消註冊。
下面有個範例是如何使用 /proc file,我們以 HelloWorld 示範,當中分為 3 個部分
- 透過 `init_module` 建立 /proc/hellowrold 檔案
- 當使用 `procfile_read` 讀取 /proc/helloworld 時回傳一個值以及一個 buffer。
- 使用 `cleanup_module` 刪除 /proc/helloworld。
當使用 `proc_create` 函式載入模組時會建立 /proc/helloworld 檔案,回傳值是一個指向結構體 `proc_dir_entry` 的指標,這個結構體代表剛剛創建出來的 /proc 檔案,可以使用這個指標來進一步設定這個檔案的屬性,如果回傳為 `NULL` 則表示建立 /proc/helloworld 失敗。
每次讀取 /proc/helloworld 時都會呼叫 procfile_read 函式,此函式的兩個參數 buffer 和 offset,第二個參數 `char *buffer` 要把輸出內容寫進這裡,寫進 buffer 的東西會被傳回給使用者空間程式(例如 cat),這樣使用者才會看到 /proc/helloworld 的內容,offset 告訴你現在讀到了檔案的哪裡。你可以用這個來控制是否要傳回更多資料,還是該結束這次的讀取。
如果在 `procfile_read()` 中回傳的 值不為 0,那麼核心會再度呼叫這個函式。
<details>
<summary> 完整程式碼 </summary>
```c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define procfs_name "helloworld"
static struct proc_dir_entry *our_proc_file;
static ssize_t procfile_read(struct file *file_pointer, char __user *buffer,
size_t buffer_length, loff_t *offset)
{
char s[13] = "HelloWorld!\n";
int len = sizeof(s);
ssize_t ret = len;
if (*offset >= len || copy_to_user(buffer, s, len)) {
pr_info("copy_to_user failed\n");
ret = 0;
} else {
pr_info("procfile read %s\n", file_pointer->f_path.dentry->d_name.name);
*offset += len;
}
return ret;
}
#ifdef HAVE_PROC_OPS
static const struct proc_ops proc_file_fops = {
.proc_read = procfile_read,
};
#else
static const struct file_operations proc_file_fops = {
.read = procfile_read,
};
#endif
static int __init procfs1_init(void)
{
our_proc_file = proc_create(procfs_name, 0644, NULL, &proc_file_fops);
if (NULL == our_proc_file) {
pr_alert("Error:Could not initialize /proc/%s\n", procfs_name);
return -ENOMEM;
}
pr_info("/proc/%s created\n", procfs_name);
return 0;
}
static void __exit procfs_exit(void)
{
proc_remove(our_proc_file);
pr_info("/proc/%s removed\n", procfs_name);
}
module_init(procfs_init);
module_exit(procfs_exit);
MODULE_LICENSE("GPL");
```
</details>
其中來講一下 [copy_to_user](https://manpages.debian.org/stretch-backports/linux-manual-4.11/__copy_to_user.9)
```C
unsigned long __copy_to_user(void __user * to, const void * from, unsigned long n);
```
作用是將資料從 kernel space 複製到 user space,**成功時回傳 0**。
而在上面程式中的用法,就是一次將整個 "HelloWorld!\n"(13 bytes)全部複製到 user space 的 buffer 裡,再來會因為 `*offset += len = 13`,進而停止讀資料。
掛載 helloworld 後
```$ cat /proc/helloworld ```,將會看到 HelloWorld!,說明成功建立 proc file
## Read and Write a /proc file
在前一個例子中示範了讀取 /proc/hellworld,當然也可以做到寫入 /proc 檔案,而原理和讀取相同,當中會使用到 `copy_from_user` 函式,因為要寫入的資料來自於使用者空間,因此必須將資料移動到核心空間內。
但因為 user process 只能存取自己的記憶體區段(segment),所以今天當我們想要撰寫核心模組時,通常會需要存取核心的記憶體區段,而這會由系統自動處理。
然而,如果需要把某個使用者空間的 buffer 傳給核心模組,那核心函式拿到的會是一個位於該 process 的記憶體區段中的指標,這時就不能直接使用這個指標!
必須使用 `put_user` 和 `get_user` 這兩個巨集來存取來自使用者空間的記憶體。
- get_user:從使用者空間讀取一個字元到核心
- put_user:從核心寫入一個字元到使用者空間
如果想要一次處理多個字元(例如整個字串或結構),那就要使用 `copy_to_user`(寫出)或 `copy_from_user`(讀入)。
也就是說,在讀取的時候,因為資料本來就在核心空間中,所以不用額外處理,但是在寫入的時候,資料時從使用者空間傳進來的,所以要用 `copy_from_user` 將資料安全移動到核心空間內。
[copy_from_user](https://manpages.debian.org/testing/linux-manual-4.8/__copy_from_user.9.en.html)
```c
unsigned long __copy_from_user(void * to, const void __user * from, unsigned long n);
```
接下來對原本的程式碼做擴充,讓這個 procfile 能夠有寫入功能
```c
#define PROCFS_MAX_SIZE 1024
static unsigned long procfs_buffer_size = 0;
// 當 /proc file 被寫入時呼叫此函數
static ssize_t procfile_write(struct file *file, const char __user *buff,
size_t len, loff_t *off)
{
procfs_buffer_size = len;
if (procfs_buffer_size >= PROCFS_MAX_SIZE)
procfs_buffer_size = PROCFS_MAX_SIZE - 1;
if (copy_from_user(procfs_buffer, buff, procfs_buffer_size))
return -EFAULT;
procfs_buffer[procfs_buffer_size] = '\0';
*off += procfs_buffer_size;
pr_info("procfile write %s\n", procfs_buffer);
return procfs_buffer_size;
}
```
最後記得跟前面一樣要在 proc_ops 和 file_operations 介面中再加上 write 操作。
掛載後,當我們輸入 `echo "abc" | sudo tee /proc/helloworld` 候,輸入 `sudo dmesg` 查看,即能成功看到輸出。
## Manage /proc file with standard filesystem
前面提到如何用 /proc 的介面去建立可讀寫的檔案,除此之外也可以透過 **inode** 來管理 /proc file,在 Linux 內,每個檔案系統都要自自己的函式來處理 inode 和 檔案操作,這些函式會被集中在
- `struct inode_operations`:inode 層級的操作(例如建立、刪除)
- `struct proc_ops/file operations`: file 層級的操作(read/write 等)
而 `inode_operations` 裡面會包含指向 `proc_ops` 的指標。
`file operations` 處理的是對檔案內容本身的操作,`inode operatopns` 負責的則是檔案的引用或是建立連結的方式等等,當在 /proc 建立新檔案時,可以指定這個新檔案要用哪個 `inode_operations`,這個 `inode_operations` 會指向我們的 `proc_ops`,而 `proc_ops` 又會指定我們的 `read/write` 函式。
```c
static int procfs_open(struct inode *inode, struct file *file)
{
try_module_get(THIS_MODULE);
return 0;
}
static int procfs_close(struct inode *inode, struct file *file)
{
module_put(THIS_MODULE);
return 0;
}
```
[關於 `try_module_get` 和 `module_put`](https://www.kernel.org/doc/html/next/driver-api/basics.html#)
文件中提到,這兩個函式分別是加 / 減少 kernel module 的 reference count(引用計數),防止模組在使用中被卸載,或在不使用時讓它被安全卸載,前面有提到,若是卸載模組時 reference count 不為 0 的話,系統就會認定模組正在被使用,沒辦法卸載,而若是沒使用這兩個函式,很有可能導致 refcnt 錯亂,衍生上述情況,明明已沒在使用模組卻無法移除的情況,所以在 open 和 close 使用,做一個保險。
```c
proc_set_size(our_proc_file, 80);
proc_set_user(our_proc_file, GLOBAL_ROOT_UID, GLOBAL_ROOT_GID);
```
這兩個函式是用來設定 proc 檔案的屬性,讓它在 /proc 底下表現得更像普通檔案(例如 ls -l /proc/xxx 時會看到正確的大小和擁有者)
<details>
<summary> 完整程式碼</summary>
```c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
#include <linux/minmax.h>
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define PROCFS_MAX_SIZE 1024
#define PROCFS_NAME "buffer1k"
static struct proc_dir_entry *our_proc_file;
static char procfs_buffer[PROCFS_MAX_SIZE];
static unsigned long procfs_buffer_size = 0;
//呼叫此函數並讀取 /proc 檔案
static ssize_t procfs_read(struct file *filp, char __user *buffer,
size_t length, loff_t *offset)
{
if (*offset || procfs_buffer_size == 0){
pr_info("procfs_read: END\n");
*offset = 0;
return 0;
}
procfs_buffer_size = min(procfs_buffer_size, length);
if (copy_to_user(buffer, procfs_buffer, procfs_buffer_size))
return -EFAULT;
*offset += procfs_buffer_size;
pr_info("procfs_read: read %lu bytes\n", procfs_buffer_size);
return procfs_buffer_size;
}
static ssize_t procfs_write(struct file *file, const char __user *buffer,
size_t len, loff_t *off)
{
procfs_buffer_size = min(PROCFS_MAX_SIZE, len);
if (copy_from_user(procfs_buffer, buffer, procfs_buffer_size))
return -EFAULT;
*off += procfs_buffer_size;
pr_info("procfs_write: write %lu bytes\n", procfs_buffer_size);
return procfs_buffer_size;
}
static int procfs_open(struct inode *inode, struct file *file)
{
pr_info("procfs is opened");
try_module_get(THIS_MODULE);
return 0;
}
static int procfs_close(struct inode *inode, struct file *file)
{
pr_info("procfs is closed");
module_put(THIS_MODULE);
return 0;
}
#ifdef HAVE_PROC_OPS
static const struct proc_ops proc_file_fops = {
.proc_read = procfs_read,
.proc_write = procfs_write,
.proc_open = procfs_open,
.proc_release = procfs_close,
};
#else
static const struct file_operations proc_file_fops = {
.proc_read = procfs_read,
.proc_write = procfs_write,
.proc_open = procfs_open,
.proc_release = procfs_close,
};
#endif
static int __init procfs_init(void)
{
our_proc_file = proc_create(PROCFS_NAME, 0644, NULL, &proc_file_fops);
if (our_proc_file == NULL) {
pr_alert("Error:Could not initialize /proc/%s\n", PROCFS_NAME);
return -ENOMEM;
}
proc_set_size(our_proc_file, 80);
proc_set_user(our_proc_file, GLOBAL_ROOT_UID, GLOBAL_ROOT_GID);
pr_debug("/proc/%s created\n", PROCFS_NAME);
return 0;
}
static void __exit procfs_exit(void)
{
remove_proc_entry(PROCFS_NAME, NULL);
pr_debug("/proc/%s removed\n", PROCFS_NAME);
}
module_init(procfs_init);
module_exit(procfs_exit);
MODULE_LICENSE("GPL");
```
</details>
---
## seq_file API
[Driver porting: The seq_file interface](https://lwn.net/Articles/22355/)
當要在 /proc 中顯示一大堆資料(例如一堆 process、module、設備狀態),自己管理讀取 offset、格式化、分段傳輸會變得 很複雜又容易出錯,所以 kernel 提供了一個 `seq_file` API 來簡化這個過程。
seq_file 介面可以透過 `linux/seq_file` 調用,其中值得注意的有 3 個部分
### The iterator interface
在使用 `seq_file` 建立虛擬檔案的時候,必須實做一個簡單的迭代器來逐筆顯示資料,這個迭代器需要能根據特定的位置移動,就像檔案一樣可以掃描,而這個位置怎麼定義可以由使用者決定,但位置 0 必須是檔案的開頭。
而一個 iterator 必須要有四個函式來讓 `seq_file` 正常運作,以下示範操作
- start()
```c
static void *ct_seq_start(struct seq_file *s, loff_t *pos)
{
loff_t *spos = kmalloc(sizeof(loff_t), GFP_KERNEL);
if (! spos)
return NULL;
*spos = *pos;
return spos;
}
```
此函式是用 `kmalloc` 配置出一塊記憶體空間來存放目前的 position 值,通常還需要檢查 position 是否已經超過資料範圍,超過時要回傳 `NULL` 讓 `seq_file` 知道檔案結束了。
- next()
```c
static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
loff_t *spos = (loff_t *) v;
*pos = ++(*spos);
return spos;
}
```
此函式用來移動到下一筆資料的,這邊沒什麼特別的,就只是將位置 `+1`,如果已經走到資料結尾,則回傳 `NULL`。
- stop()
```c
static void ct_seq_stop(struct seq_file *s, void *v)
{
kfree(v);
}
```
此函式是清除 iterator 用完的資源,也就是說如果有用到 `kmalloc`,則苦以在這邊做 `kfree()`,否則可以空著不做事。
- show()
```c
static int ct_seq_show(struct seq_file *s, void *v)
{
loff_t *spos = (loff_t *) v;
seq_printf(s, "%Ld\n", *spos);
return 0;
}
```
此函式為輸出一筆資料,會先從 `v` 拿到目前位置或資料,然後用 `seq_printf()` 把格式化後的資料寫進供使用者讀取的 buffer 中。
然後用 `seq_operations` 去榜定四個函式
```c
static struct seq_operations ct_seq_ops = {
.start = ct_seq_start,
.next = ct_seq_next,
.stop = ct_seq_stop,
.show = ct_seq_show
};
```
所以流程大致為: 一開使先呼叫 `start()` ,如果 `start()` 回傳的不是 `NULL` ,就會繼續呼叫 `next()`,如果 `start()` 一開始就回傳 `NULL` 代表沒有任何資料,系統會直接呼叫 `stop()` 做結尾,其中每次呼叫 `next()` 時,也會呼叫一次 `show()` 把目前資料寫進 user buffer 中。

<details>
<summary> 完整程式碼 </summary>
```c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h> /* for seq_file */
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define PROC_NAME "iter"
/* This function is called at the beginning of a sequence.
* ie, when:
* - the /proc file is read (first time)
* - after the function stop (end of sequence)
*/
static void *my_seq_start(struct seq_file *s, loff_t *pos)
{
if (*pos == 0)
return pos;
return NULL;
}
static void *my_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
(*pos)++;
if (*pos < 10){
return pos;
}
return NULL;
}
static void my_seq_stop(struct seq_file *s, void *v)
{
/* nothing to do */
}
static int my_seq_show(struct seq_file *s, void *v)
{
loff_t *spos = (loff_t *)v;
seq_printf(s, "%lld\n", *spos);
return 0;
}
/* This structure gather "function" to manage the sequence */
static struct seq_operations my_seq_ops = {
.start = my_seq_start,
.next = my_seq_next,
.stop = my_seq_stop,
.show = my_seq_show,
};
/* This function is called when the /proc file is open. */
static int my_open(struct inode *inode, struct file *file)
{
return seq_open(file, &my_seq_ops);
};
/* This structure gather "function" that manage the /proc file */
#ifdef HAVE_PROC_OPS
static const struct proc_ops my_file_ops = {
.proc_open = my_open,
.proc_read = seq_read,
.proc_lseek = seq_lseek,
.proc_release = seq_release,
};
#else
static const struct file_operations my_file_ops = {
.open = my_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release,
};
#endif
static int __init seq1_init(void)
{
struct proc_dir_entry *entry;
entry = proc_create(PROC_NAME, 0, NULL, &my_file_ops);
if (entry == NULL) {
pr_debug("Error: Could not initialize /proc/%s\n", PROC_NAME);
return -ENOMEM;
}
return 0;
}
static void __exit seq1_exit(void)
{
remove_proc_entry(PROC_NAME, NULL);
pr_info("/proc/%s removed\n", PROC_NAME);
}
module_init(seq1_init);
module_exit(seq1_exit);
MODULE_LICENSE("GPL");
```
</details>
# sysfs: Interacting with your module
sysfs 是一種虛擬檔案系統,掛載在 /sys,可以讓你從 使用者空間(userspace)讀寫核心物件的屬性(attributes),以此做到觀察裝置狀態、控制硬體開關、debug 核心模組等等需求。
## kobject
參考資料: [The zen of kobjects](https://lwn.net/Articles/51437/)
在 Linux 核心中,kobject(kernel object)是一種統一的物件模型(object model),Kobject主要提供如下功能:
- 通過 parent 指標,可以將所有 kobject 以層次結構的形式組合起來。
- 使用一個引用計數(reference count),來記錄 `kobject` 被引用的次數,並在引用次數變為0時把它釋放(這是 `Kobject` 誕生時的唯一功能)。
- 和 `sysfs` 虛擬文件系統配合,將每一個 `kobject` 及其特性,以文件的形式,開放到 user space。
通常不會單獨使用 `kobject`,而是嵌入在其他結構體中,例如:
```c
struct cdev {
struct kobject kobj;
struct module *owner;
struct file_operations *ops;
struct list_head list;
};
```
這裡 kobj 嵌入在 `cdev` 中。你可以使用 `container_of()` 巨集來從 `kobject` 回推包含它的原始結構:
```c
struct cdev *device = container_of(kp, struct cdev, kobj);
```
### 初始化 kobject
透過呼叫 `kobject_init()` 來建立 `kobject`,並且此函式會將 kobj 的 `reference count` 設定為 1,
```c
void kobject_init(struct kobject *kobj);
```
除此之外,還需要設定 `kobject` 的名稱,而這將是會在 `sysfs` 目錄中顯示的名稱
```c
int kobject_set_name(struct kobject *kobj, const char *format, ...);
```
### Reference counts
只要對該 kobject 的 reference 存在,則 kobject 就會持續存在,用於管理 reference counts 的函數如下:
```c
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
```
當 `kobject_get` 被成功呼叫時,`reference counts` 將會加 1,並且回傳指向 `kobject` 的指標,若 kobject 已經不存在,則回傳 `NULL`。
當對 `kobject` 的 reference 已經不存在時,呼叫 `kobject_put` 以將 `reference counts` 減 1,前面有提到 `kobject_init` 會將 `counts` 設定為 1,因此在程式碼最後須要記得呼叫 `kobject_put`。
### Hooking into sysfs
要讓 `kobject` 能夠出現在 `/sys` 中,需要呼叫 `kobject_add` 並將 `kobject` 傳遞進去
```c
int kobject_add(struct kobject *kobj);
```
而 `kobject_del` 將會把 `kobject` 從 `sysfs` 中移除。
```c
void kobject_del(struct kobject *kobj);
```
### 釋放 kobject
假設有一個 `kobject` 綁定在 `sysfs` 上的某個檔案,如果有一個 user space program 把這個檔案打開(例如 cat 或 echo),即使 kernel 那邊已經「不需要」這個物件了,`reference count` 也不會變成 0,直到這個檔案被關閉,所以不能在 `
reference count` 還沒歸零前就釋放記憶體。
而釋放時就需要用到 `release` 函式,下面是一個實作 `release` 的範例
```c
void my_object_release(struct kobject *kobj)
{
struct my_object *mine = container_of(kobj, struct my_object, kobj);
/* Perform any additional cleanup on this object, then... */
kfree (mine);
}
```
每一個 `kobject` 都必須有一個 `release()`,並且這個 `kobject` 必須保持有,直到該方法被呼叫為止。如果無法滿足這些條件,代表程式碼有缺陷,特別的是,`release()` 並不是直接儲存在 `kobject` 裡,而是儲存在 `ktype`(`struct kobj_type`)這個結構體。
```c
struct kobj_type {
void (*release)(struct kobject *);
struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
};
```
這個結構體用來描述一種 `kobject` 的類型,每一個 `kobject` 都必須對應到一個 `kobj_type`,這個連結通常在初始化時設定 `kobject->ktype` 指向這 `kobj_type`,或者由它所屬的 `kset` 自動指定,`release` 成員函式是用來放置清除記憶體的 callback function (當 `referencr count` 歸零時呼叫),`sysfs_ops` 和 `default_attrs` 兩個欄位,則是控制這些 `kobject` 如何顯示在 `sysfs`。
### kset
`kset` 是一組**相同類型**的 `kobject` 的集合,也就是同種 type,`kset` 看起來像是 `kobj_type` 結構體的一種擴充,但兩者的焦點不同,`struct kobj_type` 關注的是「物件的類型」,而 `struct kset` 則關注於「物件的聚集與集合」。
`kset` 用途如下:
- 核心可以用一個 `kset` 來追蹤所有的 block 裝置或所有的 PCI 驅動程式。
- 每個 `kset` 都包含一個 `kobject`,這個 `kobject` 可以設為其他 `kobject` 的 `parent`,因此 `kset` 是用來建立整個 `device model` 階層結構的關鍵。
- 當有 `kobject` 動態加入或移除時,`kset` 可以決定該怎麼把這些事件通知給 user space。
:::info
用物件導向觀點總結,`kset` 是「最高層級的容器類別」,每個 `kset` 自己就擁有一個內建的 `kobject`,所以它本身也可以當作一個 `kobject` 來使用,也就是說你可以把它丟進 `sysfs` 裡,變成一個 `/sys/xxx` 目錄。
:::
一個 `kset` 會使用標準的 kernel linked list 來保存它的子項(`kobject`),每個 `kobject` 透過其 `kset` 欄位,知道自己是屬於哪個 `kset` 的,這些 `kobject` 還會透過 `parent` 欄位指向該 `kset`(更準確地說,是 `kset` 裡面內建的那個 `kobject`)

圖中的那些 `kobject` 實際上是嵌在其他結構裡的(例如某個裝置結構體,甚至可能是另一個 `kset`),也並不是所有 `kobject` 的 `parent` 一定都要設為所屬的 `kset`。
`kset` 的初始化與設置方式和 `kobject` 非常類似。提供了以下函式:
```c
void kset_init(struct kset *kset);
int kset_add(struct kset *kset); /* 把初始化完的 kset 加入到 sysfs */
int kset_register(struct kset *kset); /* 等同於 kset_init() + kset_add() */
void kset_unregister(struct kset *kset); /* 移除並釋放記憶體 */
```
我們可以利用
```c
kobj->kset = the_kset;
kobject_add(kobj);
```
就會自動幫你加入對應的 `kset`。
## attributes
接下來讓我們講回 `sysfs`,它提供一種介面,讓使用者可以從 user space 讀取或設定 核心 kernel 的 `kobject` 屬性。
基本的 attributes 宣告:
```c
struct attribute {
char *name; /* sysfs檔案的檔名 */
struct module *owner; /* 所屬模組 */
umode_t mode; /* 權限,例如 0444 (read-only) */
};
```
這個結構對應一個 `sysfs` 檔案,等於你這個屬性要創建一個 `/sys/.../xxx` 檔案,這個檔案可以被讀取或寫入,你可以決定名稱和權限,而要使用這個結構,還需要再透過 kernel 提供的函式:
```c
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr);
```
這兩個是往 `sysfs` 註冊或移除檔案用的函式,`kobject` 代表你要嵌入的物件。
在 device driver 中更常使用的結構體為 `struct device_attribute`
```c
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf, size_t count);
};
```
- `show()`:當有人執行 `cat /sys/xxx/attrfile` 會執行這個 function,它需要把你要顯示的資料寫入 buf 中。
- `store()`:當有人執行 `echo value > /sys/xxx/attrfile`,會呼叫這個 function,你要從 buf 收到用戶寫的資料。
使用的時候還需要註冊與移除
```c
int device_create_file(struct device *, const struct device_attribute *);
void device_remove_file(struct device *, const struct device_attribute *);
```
<details>
<summary> 完整程式碼 </summary>
```c
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kobject.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/sysfs.h>
static struct kobject *mymodule;
/* the variable you want to be able to change */
static int myvariable = 0;
static ssize_t myvariable_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", myvariable);
}
static ssize_t myvariable_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count)
{
sscanf(buf, "%d", &myvariable);
return count;
}
static struct kobj_attribute myvariable_attributes =
__ATTR(myvariable, 0660, myvariable_show, myvariable_store);
static int __init mymodule_init(void)
{
int error = 0;
pr_info("mymodule: initialized\n");
mymodule = kobject_create_and_add("mymodule", kernel_kobj);
if (!mymodule)
return -ENOMEM;
error = sysfs_create_file(mymodule, &myvariable_attributes.attr);
if (error) {
kobject_put(mymodule);
pr_info("failed to create the myvariable file "
"in /sys/kernel/mymodule\n");
}
return error;
}
static void __exit mymodule_exit(void)
{
pr_info("Module release\n");
kobject_put(mymodule);
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");
```
</details>
# Blocking Processes and threads
## sleep
如果一個核心模組,不想被某個 process 不斷打擾,我們可以選擇讓這個 process 進入 sleep 狀態,等到準備好再將它喚醒,這就是為什麼即使只有一個 CPU,看起來好像有多個 process 同時執行的原因。
以下有一個範例做演飾,首先創建一個檔案 `/proc/sleep`,目標功能是 **這個檔案一次只能被一個 process 打開**,如果已經有程序打開了這個檔案,模組就會呼叫 `wait_event_interruptible` 讓後來的 process 進入等待狀態。
### wait_event_interruptible
```c
#define wait_event_interruptible(wq_head,condition)
({ int __ret = 0; might_sleep(); if (!(condition)) __ret =
__wait_event_interruptible(wq_head, condition); __ret; })
```
- `wq` : 一個 wait queue,例如 `DECLARE_WAIT_QUEUE_HEAD(myqueue)`
- `condition` : 等待的條件,例如 `flag == 1` 或 `buffer_len > 0`
process 會進入到休眠狀態,將 process 放到 `wait queue` 中等待,直到 `condition` 為 true 或是接收到一個 signal 時醒來,每次 wq 這個 `wait queue` 被喚醒的時候,`condition` 條件都會被重新檢查一次。
而必須在明確改變 `condition` 後呼叫 `wake_up(&wq)`,否則等待者永遠醒不來,例如
```c
flag = 1;
wake_up(&myqueue); // 必須這樣明確叫醒 wait_event_interruptible
```
醒來後回傳
- 0 表示 `condition` 成立
- `-ERESTARTSYS` 表示醒來是因為收到 signal(不是因為條件成立)
---
我們可以用 `tail -f` 來持續開啟一個檔案,這個指令會持續讀取檔案末尾,使檔案保持開啟狀態,當呼叫 `wait_event_interruptible`,它會將 Task(在核心中代表一個 `process` 的資料結構)設為 `TASK_INTERRUPTIBLE` 狀態,表示這個任務會睡眠,直到被某種方式喚醒。
這個任務會被加入到 `wait queue`,也就是等著存取該檔案的所有任務的佇列中。
接下來,函式會呼叫排程器,context switch 到其他可以用 CPU 的 `process`,也就是說讓其他程式先執行。
程式中重要的關鍵便是 **compare-and-exchange**(atomic_cmpxchg)來取得資源
```c
/* 1 if the file is currently open by somebody */
static atomic_t already_open = ATOMIC_INIT(0);
```
宣告一個 atomic 變數 `already_open`,初始化為 0,代表沒有 process 開啟此檔案。
### atomic_cmpxchg
atomic 一詞來表示「不可再拆分的」,於是 "atomic operation" 寓意為「不可再拆分的執行步驟」,也就是「最小操作」,即某個動作執行時,中間沒有辦法分割,也就是說該操作在執行完畢前不會被其他的操作或任務打斷,`atomic_t` 結構體定義如下
```c
typedef struct {
int counter;
} atomic_t;
```
```c
static inline int atomic_cmpxchg(atomic_t *v, int old, int new)
atomic_cmpxchg() - atomic compare and exchange with full ordering
@v: pointer to atomic_t
@old: int value to compare with
@new: int value to assign
If (@v == @old), atomically updates @v to @new with full ordering.
Unsafe to use in noinstr code; use raw_atomic_cmpxchg() there.
Return: The original value of @v.
```
`atomic_cmpxchg` 是一個原子操作函數,用於在多執行緒環境中進行比較並交換。 它會比較目標記憶體位置的值與期望的值,如果相等,則將新的值寫入該位置,並返回目標記憶體位置的舊值。 這個過程是 atomic 的,意味著在操作期間不會被其他執行緒中斷,避免了 race condition 問題。
接下來看他在這段程式中如何使用
再 `open` 函式中可以看到
```c
/* Try to get without blocking */
if (!atomic_cmpxchg(&already_open, 0, 1)) {
/* Success without blocking, allow the access */
return 0;
}
```
若目前值等於 old(0),就把它改為 new(1)並回傳舊值;否則回傳當前值,為回傳的是舊值,所以 `if (!atomic_cmpxchg(...))` 代表如果原值是 0(搶到),就成功,而其他的 process 如果想要開啟檔案,此時 `already_open` 已經被更改為 1,也就是不會立即回傳 0,程式會繼續往下走,此時會有兩種情況
1. 如果 caller 指定 `O_NONBLOCK`,表示它不想被 block,此時回傳 `-EAGAIN
2. 如果 caller 沒指定 `O_NONBLOCK`(預設 blocking),kernel 應該把它放到 wait queue,直到資源可用再醒來繼續。
實際情況為,當 `cmpxchg` 失敗,表示已被其它 process 佔用,此時檢查 `file->f_flags & O_NONBLOCK`,若有 `O_NONBLOCK`,立刻 return `-EAGAIN`,否則呼叫 `try_module_get()` 保持 module 引用,然後進入 sleep 迴圈:
```c
while (atomic_cmpxchg(&already_open, 0, 1)) {
...
...
wait_event_interruptible(waitq, !atomic_read(&already_open));
...
...
```
這裡用 `wait_event_interruptible` 把 process 放到 wait queue,讓 CPU 去處理工作,直到 `wake_up(&waitq)` 被呼叫(在 `module_close()`內)。
在 `close` 內:
```c
static int module_close(struct inode *inode, struct file *file)
{
atomic_set(&already_open, 0);
wake_up(&waitq);
return 0; /* success */
}
```
`atomic_set(&already_open, 0)` 將 `already_open` 設為 0,然後 `wake_up(&waitq)` 喚醒等待的 process,讓它們再次競爭 `atomic_cmpxchg`。
[完整程式碼](https://gist.github.com/leonnig/9ee58d6120bf40acc6ec1f463f55cd5b)
---
## completion
還有一種方式能保證 process 的執行順序,與其用 `/bin/sleep` 指令等待,kernel 還提供了一種方式,叫做 **completions**。
```c
#include <linux/completion.h>
struct completion {
unsigned int done; // Tracks completion state
wait_queue_head_t wait; // Queue of waiting threads
};
```
completions 物件主要包含 3 個部分:
1. 對 `struct completions` 做初始化
2. 使用 `wait_for_completion()` 做等待或是 barrier。
3. 用 `complete()` 做通知。
在範例程式碼中,啟動兩個執行緒: `crank` & `flywheel`,而我們的目的是要讓 `crank` 執行緒總是優先於 `flywheel` 執行,為此,需要為兩個執行緒分別建立完成狀態,而我們用 completions 結構體來表示狀態,在每個執行緒結束時會分別更新對應的 completion ,而 `flywheel` 執行緒就會透過呼叫 `wait_for_completion` 來確保自己不會過早開始,`crank` 執行緒使用 `complete_all()` 更新完成狀態,讓 `flywheel` 執行緒能夠繼續。
相關資料 : [Completions - “wait for completion” barrier APIs](https://docs.kernel.org/scheduler/completion.html)
```c
void wait_for_completion(struct completion *done
void complete_all(struct completion *);
```
使用方式:
```c
CPU#1 CPU#2
struct completion setup_done;
init_completion(&setup_done);
initialize_work(...,&setup_done,...);
/* run non-dependent code */ /* do setup */
wait_for_completion(&setup_done); complete(&setup_done)
```
即便 `flywheel` 執行緒先被啟動,當載入此模組並執行 `dmesg` 時,會看到「turn the crank」總是先被印出,接著才是 `flywheel` 的動作,因為 `flywheel` 在等待 `crank` 完成。
[完整程式碼](https://gist.github.com/leonnig/1d0fc2c2834af5d77d9fd00afd4ec96e)
:::warning
為何不用 Pthraed ?
因為在 kernel space 中,使用的是 linux kernel 的 API,而非 POSIX Threads,pthread_t 是 POSIX thread library(libpthread)的一部分,只能在 user space 使用。
在 Linux kernel 中,每一個 process(或 kernel thread)都由一個 struct task_struct 來描述,這是 Linux process descriptor。
:::
# Synchronization
如果運行在不同 CPU 或是 Thread 的 process 想要存取同一塊記憶體,可能會產生 race condtion,為了避免這種情況,Kernel 提供了各種類型的互斥函式,這些函式用來標示某段程式碼是 **鎖定** 還是 **解鎖** 的,以避免同時嘗試執行該程式碼的情況發生。
## Mutex