# 2025q1 Homework3 (kxo) contributed by < `Nsly0204` > ## 先備知識摘要 [作業要求](https://hackmd.io/@sysprog/linux2025-kxo/%2F%40sysprog%2Flinux2025-kxo-f) ### [2025 年 Linux 核心設計課程作業 —— kxo (B)](/UxosDHbcRJOClsyQ4btHGA) - [x] [一切皆為檔案描述子](https://hackmd.io/@sysprog/io-universality#一切皆為檔案描述子) - [x] [Linux 核心模組運作原理](/pfRzP9sxRYqYbXS6Tm0Ynw) - [ ] LKMPG until chapter 15 [Device Driver 重點整理](https://hackmd.io/@chenging/driver) Linux device driver 可分成 3 種類型: 1. character device driver 2. block device driver 3. network device driver Linux device driver(以下或稱為驅動程式)扮演的角色: userspace > system call > device driver > device file >參考: [一切皆為檔案描述子](https://hackmd.io/@sysprog/io-universality#一切皆為檔案描述子) >這種統一抽象層使不同類型的 I/O (如終端輸出與檔案讀寫) 皆可視為檔案操作,裝置驅動程式的核心目的即在於實作這樣的抽象層,使作業系統透過一致介面存取各類裝置。 >這些操作皆基於檔案描述子(file descriptor),如 open()、read()、write() 和 lseek()。在 UNIX 中,檔案被視為位元組序列,而所有 I/O 操作均透過這些標準介面進行。 由此觀念,可以預期一個驅動程式包含以下部份,以 `ksort` 為例: * 註冊驅動程式: 將 driver 自己「註冊」到 kernel 的 vVirtual File System(VFS) 層,通常直接實作在 `__init` 裡。 * 結構體 file_operations: 提供核心收到 system call 與裝置對應的函式呼叫,其中 `fops` 就是指向該結構體的指標。 * 實作函式對應 system call 的操作,如範例的`main_sort` 。 ### [2025 年 Linux 核心設計課程作業 —— kxo ( C )](/C7w6V41gRdyM0E5ixqAZGQ) - [x] [Linux 核心設計: 中斷處理和現代架構考量](/DaSMrw3dRfqwLFq36fAwww) - [ ] [並行程式設計: Atomics 操作](/OVPTyhEPTwSHumO28EpJnQ) > 問題 > * what does slave PIC do? [Slides (L07-LinuxEvents).pdf](https://github.com/tpn/pdfs/blob/master/Interrupts%20in%20Linux%20-%20Slides%20(L07-LinuxEvents).pdf) **Programmable Interrupt Controller** Programmable Interrupt Controller (PIC) 是控制 Interrupt 的硬體架構,用於把不同的 Interrupt Request Lines (IRQs) 映射成 Interrupt Vectors 送給 CPU ,PIC 在產生 Interrupt Vectors 時就會進行優先程度的分級。 **Interrupt Vectors** 當外部裝置發出中斷訊號,PIC 接受的訊號會被轉換成一組 Interrupt Vectors ,用於讓 CPU 對照 Interrupt Descriptor Table (IDT) ,找到對應的 ISR / Interrupt handler 起始位址進行處理。 現代架構中,不會同時發生的 I/O devices 可以共享一個 IRQ ,也就是共享 vector ,每一個 device 的 handler / IRS 會一起被呼叫,至於最後是哪個 device 有中斷是依照各個 device 自己決定。 > 詳細說明可以參考先前同學的整理 [Linux 核心設計: Interrupt](/WdjSFwexS4qG8cYqCSW96w),部份內容也是引述本篇描述。 **Interrupt Handler / Interrupt Service Routine(IRS)** 在現代的作業系統中,ISR 會被切成 top half 和 botton half 兩個部份,目的是為了減少任務的延遲。Top half 和 botton half 的區分使得系統可以把 interrupt 的處理推遲。 `Top Half` 主旨為確認中斷(先上車)且只執行不可延遲的部份,過程中屏蔽其他中斷(執行在 interrupt context 不可排程),時間須越短越好: * 確認中斷來源:在共享 interrupt line 的情況下,檢查硬體狀態以判斷是否為本裝置觸發的中斷。 * Acknowledgement:向硬體發送確認訊號,表示中斷已被接收,防止重複觸發。 * 排程後續處理:標記需延後處理的任務。 * 其他不可延遲的關鍵任務。 `Bottom Half` 實作中斷後續處理,更準確的說是去完成 `Top Half` 也就是 Interupt Handler / IRS 不處理的剩餘事項,主要有三種實作方式: * softirqs * tasklets:所有 softirq 都由 tasklet(queue) 實現,同時只能有一個 tasklet 被處理器執行(多 CPUs也不行),tasklet 中的 irq 可以有優先級之分。 * workqueues:必定被視為 kernel thread 執行,由於運作於 process context,workqueue允許睡眠。 注意 `Top Half` 和 `Bottom Half` 的分別並沒有硬性規定,給予 driver 撰寫者自行分配的空間。 **Softirqs** 每個 CPU 都會維護自己的 softirq daemon 也就是 `ksoftirqd` kernel thread ,用於處理 softirq,其流程如下: * 在 softirq context 執行,屏蔽大部分睡眠與中斷,但可以被更高優先級的 softirq 中斷。 * 利用 `void open_softirq(softirq_id, handler)` 註冊要處理的 softirq 的 handler 。 * `raise_softirq` 觸發 softirq 的處理。 * `__do_softirq` 讓 handler 處理,結束後呼叫 `exiting_irq()` ` **Workqueue** * `producer` 生產數據,寫入 buffer。 * `consumer` 消費數據,讀取 buffer。 ## 使對奕畫面在使用者層級呈現 > commit [2efc57d](https://github.com/sysprog21/kxo/commit/2efc57dfa0fefc09c0e35d0fdec1cd1b3c189449) 掛載 kxo 對弈模組,並嘗試執行。 ```shell $ make $ sudo insmod kxo.ko $ sudo ./xo-user ``` 按下 `ctrl + Q` 後停止電腦 vs. 電腦的對弈。 觀察 `xo-user.c` 發現 userspace 在 `printboard()` 函式,透過讀取 kfifo buffer `display_buf`,與 kernel space 互動,為了縮減成本把 kernel space 傳送棋盤的方式改成以 16個 2 bit 表示分別對應 16 格棋盤的三種狀態 `'O'` / `'x'` / `' '` 。 ## 透過 sysfs 紀錄並顯示過往棋局 ```c /* Character device stuff */ static int major; static struct class *kxo_class; static struct cdev kxo_cdev; ``` 為了了解 character device (以下稱 `cdev`) ,和 sysfs 如何建立,觀察 `kxo_init` : **character device** 1. 在不知道 major number 的情況下,呼叫 `alloc_chrdev_region()` ,向核心註冊 `cdev` 取得 major 和 minor number,在透過宏 `MAJOR()` 把註冊到的 major number 讀出來以供未來 sysfs 註冊使用。 ```c #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; } int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) ``` ```c /* Register major/minor numbers */ ret = alloc_chrdev_region(&dev_id, 0, NR_KMLDRV, DEV_NAME); if (ret) goto error_alloc; major = MAJOR(dev_id); ``` 2. 初始化並呼叫 `cdev_add()` 讓 `cdev` 加入系統,加入後系統就可以使用此裝置,參考原文註解中 cdev is live。 ```c /* Add the character device to the system */ cdev_init(&kxo_cdev, &kxo_fops); ret = cdev_add(&kxo_cdev, dev_id, NR_KMLDRV); ``` 3. 可以使用以下命令觀察掛載成功的 majpt number 和 device name。 ```shell $ cat /proc/devices ``` **sysfs** device 可以註冊成為 sysfs ,提供 userspace 讀寫核心模組變數的界面,其理所當然的需要一個與 `cdev` 相似的結構體 `device_attribute`。 ```c struct attribute { char *name; struct module *owner; umode_t mode; }; 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); }; ``` 1. 建立作為管理 sysfs 註冊的 class struct ,需要被傳入 `device_create()`。 ```c /** * ... * * A class is a higher-level view of a device that abstracts out low-level * implementation details. Drivers may see a SCSI disk or an ATA disk, but, * at the class level, they are all simply disks. Classes allow user space * to work with devices based on what they do, rather than how they are * connected or how they work. */ struct class { ``` ```c kxo_class = class_create(DEV_NAME); ``` 2. 建立一個新的 device 並註冊成 sysfs。 ```c /** * device_create - creates a device and registers it with sysfs * @class: pointer to the struct class that this device should be registered to * @parent: pointer to the parent struct device of this new device, if any * @devt: the dev_t for the char device to be added * @drvdata: the data to be added to the device for callbacks * @fmt: string for the device's name * * This function can be used by char device classes. A struct device * will be created in sysfs, registered to the specified class. * * A "dev" file will be created, showing the dev_t for the device, if * the dev_t is not 0,0. * If a pointer to a parent struct device is passed in, the newly created * struct device will be a child of that device in sysfs. * The pointer to the struct device will be returned from the call. * Any further sysfs files that might be required can be created using this * pointer. * * Returns &struct device pointer on success, or ERR_PTR() on error. */ struct device *device_create(const struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) ``` ```c /* Register the device with sysfs */ struct device *kxo_dev = device_create(kxo_class, NULL, MKDEV(major, 0), NULL, DEV_NAME); ret = device_create_file(kxo_dev, &dev_attr_kxo_state); ``` ```c static ssize_t kxo_state_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { write_lock(&attr_obj.lock); sscanf(buf, "%c %c %c\n", &(attr_obj.display), &(attr_obj.resume), &(attr_obj.end)); for (int i = 0; i < N_BOARDS; i++) sscanf(buf, "%" "l" "u" , attr_obj.board_record[i++]); write_unlock(&attr_obj.lock); return count; } ``` ```c static ssize_t kxo_state_show(struct device *dev, struct device_attribute *attr, char *buf) { read_lock(&attr_obj.lock); int ret = snprintf(buf, 7, "%c %c %c\n", attr_obj.display, attr_obj.resume, attr_obj.end); int record_size = record_get_size(); ret += snprintf(buf, 4,"%d\n", record_size); for (int i = 0; i < record_size; i++) ret += snprintf(buf, 9,"%llu\n", attr_obj.board_record[i++]); read_unlock(&attr_obj.lock); return ret; } ``` ```c Stopping the kernel space tic-tac-toe game... Moves: C0 -> B3 -> A0 -> C3 -> B0 Moves: A3 -> B2 -> C3 -> B3 -> B0 -> C1 -> A1 -> C2 Moves: C2 -> A3 -> B2 -> A0 -> B1 -> A1 -> A2 ... ```