contributed by < MathewSu-001 >
遇到問題如下:
參考 Ubuntu 22.04 default GCC version does not match version that built latest default kernel 與 yehsudo同學後,使用 ls /usr/bin/ -l
,我的 gcc-12
是連結 x86_64-linux-gnu-gcc-12
的,所以兩個 gcc
版本是一樣的,所以忽略了這個警告。
掛載編譯出來的模組:
接著查看是否掛載成功:
卸載稍早載入的 hello
核心模組:
fibdrv.ko
核心模組在 Linux 核心掛載後的行為要先透過 insmod
將模組載入核心後才會有下面的裝置檔案 /dev/fibonacci
與預期輸入數字 256
有出入。試著對照 fibdrv.c,找尋彼此的關聯。
發現到與 register_chrdev有關
以修訂過的 LKMPG 為主要參照標的。
閱讀 Character Device drivers 該篇,我學到了輸出 511
代表 major number ,表示現在驅動程式是處理哪個 device file;0
代表 minor number ,用在以防驅動程式一次處理多個時,不知是哪個 device file。用來產生 major number 的函式為 register_chrdev
當註冊一個字符設備時,避免使用重複的主要設備號(major number)是至關重要的。register_chrdev 函式提供了一種便利的方法來動態分配主要設備號,確保設備的唯一性,同時減少了衝突的風險。這種動態分配的主要好處在於簡化了裝置驅動程序的開發過程,因為開發者無需手動管理主要設備號的分配。如果註冊失敗,則返回一個負值,表明註冊操作未能成功完成,通常是因為出現了錯誤或者註冊的條件不符合系統的要求。
insmod
fibdrv.ko
不是能在 shell 呼叫並執行的執行檔,它只是 ELF 格式的 object file。因此我們需要透過 insmod
這個程式(可執行檔)來將 fibdrv.ko
植入核心中。kernel module 是執行在 kernel space 中,但是 insmod fibdrv.ko
是一個在 user space 的程序,因此在 insmod
中應該需要呼叫相關管理記憶體的 system call,將在 user space 中 kernel module 的資料複製到 kernel space 中。
我自己有觀查到,可以利用 ls /dev
來檢測 sudo insmod name.ko
有沒有註冊成功,如果有的話 name 會出現在上面。
MODULE_LICENSE
巨集指定的授權條款對核心的影響以 MODULE_LICENSE("Dual MIT/GPL")
為例,被展開後的 __stringify(tag) "=" info
會是 "license = Dual MIT/GPL"
字串。
總結這部份,MODULE_XXX 系列的巨集在最後都會被轉變成
再放到 fibdrv.ko
中 .modinfo 對應區段中。
在這之前,因為我對於 Linux 核心的並行處理還是摸不著頭緒,所以 kkkkk1109 同學推薦給我一個影片有幫助到我更了解到何謂並行處理,會遇到甚麼問題,以及後續會產生 deadlock 的問題該怎麼解決。
4.3 中,有提到
These macros are defined in include/linux/init.h and serve to free up kernel memory. When you boot your kernel and see something like Freeing unused kernel memory: 236k freed, this is precisely what the kernel is freeing.
不過在運行程式碼 hello-3.c 後,
出來的成果
似乎沒有顯示任何相關釋放記憶體的內容。
在這章,我學到如何建立最簡單的核心模組。如果需要運作的話,最少需要用到兩個函式 init_module()
以及 cleanup_module()
,從版本 2.3.13 後可透過兩個巨集module_init()
和 module_exit()
來自定義函式名稱。另外可以使用巨集 module_param()
來宣告變數,不過沒有很懂可以實際應用在何處。
理解 User Space
和 Kernel Space
的差別,再呼叫函式(如 printf()
)是調動到 Kernel Space
裡的 write()
去輸出。
每一個硬體都可以由設備檔來表示,分別為 major
用來告訴使用者是哪個驅動程式來控制硬體; minor
則是用來讓驅動程式在擁有相同 major
時得以區分不同硬體。另外,每個 device
可以被區分為兩種: character devices
和 block devices
,兩種最大差別為 block devices
的大小會隨著 device
而有所限制;但是 character devices
的位元較為自由,因此大部分都是 character devices
。
學習到 character device
如果需要註冊進入 kernel 裡,需要被分配一個 major 編號。如果要確保該編號沒有被使用的話 ,可以利用 register_chedev
來被分配一個動態編號;亦或者使用 register_chrdev_region
以及 alloc_chrdev_region
減少佔用資源。
此外,我們會需要一個 file_operation
來保存由驅動程式定義的指向各種設備操作函數(如 open、read 等)的指針。架構如下所示:
實測 chardev.c
測試如果開啟 /dev/chardev
device_read
跟 device_write
是如何實際應用在 kernel 裡,用途會是什麼本章要在介紹在 Linux 還有一個附加機制提供內核和內核模塊之間傳遞訊息 – /proc 系統
。與前面的核心模組比較不同的是:讀取函式是用來輸出訊息,但寫入函式用於輸入。原因在於如果一個進程從內核讀取某些內容,那麼內核需要輸出它;而如果一個進程向內核寫入某些內容,那麼內核接收它作為輸入。
在實作上,我有分別將 procfs.c 一到三版都裝進核心模組裡,不過似乎沒有達到預期的結果,以 procfs2.c 為例
不知道為何會出現 failed 的情況。
另外還有一個叫 seq_file
的 API 可以幫助編寫 /proc檔案。以下為實作 procfs4.c 結果:
透過 sysfs
能夠從用戶空間與運行中的內核通過讀取或設置模塊內的變量進行交互。
hello-sysfs.c
透過 __ATTR()
來定義 sysfs
的屬性,根據 include/linux/sysfs.h
來找到對應讀取(.show) 和寫入(.store) 的函式,所以在程式碼中 myvariable_show
將 myvariable
的值化為字符並寫入 buf
; myvariable_store
讀取 buf
並存入 myvariable
裡。
再來是創建一個目錄在 sysfs
結構裡,根據 Everything you never wanted to know about kobjects, ksets, and ktypes 可以利用 kobject_create_and_add
來創建一個簡單的目錄。
不過這邊我就沒有很懂程式碼中
kernel_kobj 是從哪裡跑出來的,在整個程式碼中都沒有定義到。Driver Basics 裡有提到
struct kobject *parent
the parent kobject of this kobject, if any.
不過就不是很懂意思。
實做結果
如果要讓 process 與設備檔相互溝通的話,可以使用 device_write
來完成的,不過在 UNIX 裡還有一個特殊函式叫做 ioctl
也可以做到。
如果 process 要向內核所求服務(讀取檔案、請求新的記憶體等)的話,就會需要透過 System Calls
。它是一般規則的例外。System Calls
可以讓使用者不再受限於用戶模式下,而是作為作業系統核心運行。
運行 syscall-steal.c
的結果
與書本上寫的結果不盡相同
不知道 D 跟 R 分別代表的含意,然後在嵌入 syscall-steal.ko
後,得到的結果也不知道什麼意思。
當有多個進程同時呼叫一個核心模組時,內核會會透過 sleep
的方法將其他進程設置為睡眠狀態並加進 WaitQ ,等到目前的進程運行完畢後再喚醒下一個。
module_open
可以判斷有沒有其他進程會打擾,透過 return -EAGAIN 來中斷其他進程。不過 is_sig
的用途及用法我沒有很瞭解。
運行 sleep.c
的結果,透過指令 tail -f [file]
用於實時查看指定文件的末尾部分
可以看到重新查看 /proc/sleep
的話,就會發現被阻擋。查看後台進程
如果要關掉的話,有別於書上寫的,需要改寫成 kill %3
來終止作業號為 3 的後台進程。並且再次查看後台進程可以看到有確實中止掉
如果在擁有多個線程中,想要確保某個事件要在另一個事件前發生的話,可以利用 wait_for_completion()
。
運行 completions.c
的結果
如果有多個進程嘗試存取相同的記憶體,則可能會發生存取錯誤的問題。因此 linux 提供多種 mutex 的方法來進行解鎖與開鎖的動作。
在 linux/mutex.c 中,有定義 mutex_trylock
的功用
Try to acquire the mutex atomically. Returns 1 if the mutex has been acquired successfully, and 0 on contention.
因此可以透過此函式來確定有沒有被鎖上。
透過自旋鎖,會鎖定正在執行程式碼的 CPU,佔用其 100% 的資源。
追蹤 spin_lock_irqsave
的用法,從 linux/spinlock.h 可以得知
再往前追朔到 locking/spinlock.c
繼續追朔到 linux/spinlock_api_smp.h
閱讀 Linux 核心設計: Interrupt 得知 local_irq_save
的用途為將狀態存入一個 Interrupt flag 並且關閉 interrupt; 追蹤 spin_unlock_irqrestore
到 linux/spinlock_api_smp.h 可以找到函式 local_irq_restore
,其功用會回存 flag,回復到 local_irq_save
之前的狀態。
透過 spinlock
可以確保不會有其他進程搶佔的問題,程式碼獨占 CPU 的情況就稱作 atomic contexts,但是需確保沒有含有任何休眠的函數。
spinlock
差不多,不同的是可以獨佔讀取某些內容或寫入某些內容。在本篇段落末尾,有提到說如果不會觸發 irq 的話,可以將 read_lock_irqsave(&myrwlock, flags);
替換為 read_lock(&myrwlock)
。但這邊不太了解 irq 是什麼?
如果想要確保在進行算術時,不會受到其他多現成影響,可以使用 atomic operations 。
執行 example_atomic.c
結果
在整合之前,先實做 作業三 出來。已經完成的步驟有轉換為定點數數算,電腦與電腦對奕的演算法其一為 MCTS,另一者參照作業三後選擇 negamax。
simrupt.c
初始化函式 simrupt_init
首先透過 alloc_chrdev_region
向核心註冊設備號碼。
同一個初始化函式裡,在 13.2 Flashing keyboard LEDs 中提到,可以透過 timer_setup
初始化計時器,並指定處理函式 timer_handler
。
於是,當在終端機輸入 cat /dev/simrupt
時,會觸發 simrupt_open
函式。此時,透過 atomic_inc_return
檢查是否為第一次打開文件,如果條件成立,便會執行 mod_timer
指令。根據 How to use timers in Linux kernel device drivers? 的說明,該函式用於設置計時器 timer,並且根據前述設置,將觸發 timer_handler
函式來執行其內部內容。
timer_handler
函式會禁用本地中斷並呼叫 process_data
,並且利用 mod_timer
在每次觸發後都會重新設置為在下一個 delay time 後觸發,以此達到循環。在將新數據放入快速循環緩衝區(fast buf)後,根據 14.1 Tasklets 的內容,我們可以了解到,利用巨集 DECLARE_TASKLET_OLD
,當執行到 tasklet_schedule
後,會呼叫 simrupt_tasklet_func
函式。
透過巨集 DECLARE_WORK
宣告一個 workqueue 項目,並指派一個處理函式 simrupt_work_func
給它。當工作佇列中的項目被執行時,simrupt_work_func
函數會被呼叫。這個函數會從快速循環緩衝區中提取數據,並透過 produce_data
函數將其放入 kfifo 緩衝區中。
現在有點被搞混,快速循環緩衝區 fast_buf ,workqueue 跟 kfifo 緩衝區的差別是什麼以及各自的用途?
那麼放入 kfifo 的資料是怎麼顯示在 userspace 上的呢?
在 simrupt_read
函數中,會使用 wait_event_interruptible
函式進行等待。當在 simrupt_work_func
函數中使用 wake_up_interruptible
觸發時,將會喚醒所有在等待隊列上的任務。這時,將會把 rx_fifo 中的資料打印到 userspace 上
實際跑過 simrupt.c
程式的結果如下
參閱 vax-r 同學的程式碼後,我發現要將數據存儲在 kfifo 中的 rx_fifo,然後透過 simrupt_read
函數來提取 rx_fifo 中的值。
所以我的初步作法會是:
simrupt_init
裡面初始化一個空的棋盤。timer_handler
觸發時,判斷是否有贏家,就會讓電腦去選擇下在哪一步,透過 fast_buf_put
存放在快速循環緩衝區中。produce_data
函數將其放入 kfifo 緩衝區中。simrupt_read
去提取 rx_fifo 的值,並打印在 userspace 。透過 init_board
函式來初始化一個空的棋盤,然後利用 produce_data
函式依次將 O 放入棋盤格中。接著,使用 kfifo_in
函數將更新後的棋盤格存入 kfifo 的 rx_fifo 中。當 simrupt_read
函數被觸發時,就可以將這些數據呈現在 userspace 上。
步驟一測試成果
commit a418265
如果要引入第三次作業的相關檔案,C 的函式庫在 Linux 核心中無法直接引入,需採用相對應的替代方法。此外,在 mcts.c
中需要使用隨機數來進行模擬,但無法使用 rand()
函式生成隨機數。因此,我引入了linux/random.h 中的 get_random_bytes
函式來取得隨機數。
This returns random bytes in arbitrary quantities. The quality of the random bytes is good as /dev/urandom.
然後在編譯過程中,出現問題如下:
查看了 4.6 Modules Spanning Multiple Files 才發現到說如果要引用多個檔案的話,要先創造一個組合模塊,告訴 make 這個模塊包含哪些目標文件。
重新編譯一次結果如下:
這邊不知道 BTF 的用途為何
commit 5749eac
遇到問題,只要執行電腦的某一個線程就會 100% 被佔用,然後執行 cat /dev/simrupt
就會卡死無法使用,想要移除也沒有辦法,只能重開機。
所以接下來我就先測試都使用 mcts 下棋的成果如何
commit cc7560e
重新測試用兩個 agent 下棋後,程式依然無法執行,但是用 sudo dmesg
觀看報錯原因後,發現是在 zobrist_init
裡面出問題,看起來是在傳遞給 memset 時給了空指針而出現的錯誤。
查閱後才知道,在 Linux 內核代碼和某些其他項目中,通常使用 u64 來表示 64 位無符號整數類型。我推測這個原因導致在使用 kmalloc
分配記憶體時,由於 zobrist_tabl
的定義錯誤而導致指針為空。解決了這個問題後,就可以將相應的程式碼整合到核心中。
以下為一次下棋的結果
commit 84f98fe
重新審視程式後,我認為不需要使用快速循環緩衝區(fast buf)。可以在 simrupt_work_func
函式中讓 agent 判斷棋步,並利用 produce_data
將數據放入 kfifo 中。
commita270386
但是觀看 dmesg
後發現到原本預期的排程結果應該是: work item 1 -> work item 2 -> work item 1
輪流運行,但是會遇到和 vax-r 同學提及過得問題相似: work item 1 -> tasklet enqueue -> tasklet enqueue -> work item 2
,simrupt_tasklet_func
會重複空跑好幾次。
根據 queuework 設計實驗 的描述,透過 schedule_work()
會將 work 放到 workqueue 中,並指定 CPU 來執行 work。下面為執行成果:
commit bd95383
此時遇到的問題:
tasklet_schedule
排程失敗,導致 kfifo 裡面沒有資料可以取用。queue_work_on
,但是會變成無法指定 cpu 。CPU #0
和 CPU #1
)。但是我再查閱後,看到相關的資歷只有在 CMWQ/flag 中,有提到 WQ_CPU_INTENSIVE 在指定 CPU 上有幫助,但是還是不知道要怎麼指定?https://shengyu7697.github.io/cpp-sched_setaffinity/
https://hackmd.io/@sysprog/HJXlHtlB2#設計實驗
simrupt 如何確保每個 processor core 都有對應的任務可以執行?