前置設定
Kernel module package
核心中已載入哪些模組
模組檔案存在在 /proc/modules
搜尋特定模組 ex : fat
4.5 Passing Command Line Arguments to a Module
模組在命令可以接受參數輸入,但不能使用我們熟悉的 argv,argc
在這舉講義中的範例
問題:for exposing parameters in sysfs (if non-zero) at a later stage.
這段想表達的意思是? sysfs 又是什麼?
範例中,將參數初始化
透過引入 kernel module 來使命令接受參數輸入
下方透過引入 #include <linux/init.h>
#include <linux/module.h>
用做初始化函式?
用作卸載模組
在 cmd 執行範例
4.6 Modules Spanning Multiple Files
start.c
stop.c
在 Makefile 將 start.c 與 stop.c 包裝成 startstop
obj-m
把 startstop.o
作為模組進行編譯,不會編譯到內核中,但會生成一個獨立的 startstop.ko
檔
5.6 Device Drivers
第一個數字為主要編號,告訴你由哪個驅動程式來存取硬體,上面 3 個皆是由同一個驅動程式來控制。
第二個號碼用作區分控制的各種硬體,上面三個有不同的次編號,因此被驅動程式識別為不同的硬體。
Devices are divided into two types:
使用 ls -l
查看,若為 c 開頭表示為 character devices ,若為 b 開頭表示為 block devices 。
5.2 Functions available to modules
在5.2節中舉例
當中 printf
很熟悉,當我們使用 gcc -Wall -o hello hello.c
進行編譯再輸入 strace ./hello
可以查看系統調用的詳細信息。
在最後一行我們看到 write(1, "hello", 5hello)
為 printf
背後的本尊。
6.3 Registering A Device
將驅動程序添加到系統 (即將其註冊到內核中)
unsigned int major
是要請求的主要編號
const char *name
設備名稱,顯示在 /proc/devices 中
struct file_operations *fops
指向驅動程序的 file_operations 表的指針
若返回值為負,表示註冊失敗。注意:不須將次要編號傳給 register_chrdev()
,這是因為核心並不關心次要編號,只有驅動程序使用到他。
如何獲得一個未被使用的主要編號,而不是挪用已經被使用的編號?
我們可以要求核心分配一個動態的主要編號給我們。
如果將主要編號設置為 0 傳遞給 register_chrdev()
,則返回值將是動態分配的主要編號。缺點是您無法預先創建設備文件,因為您不知道主要編號將是什麼。有幾種方法可以做到這一點。首先,驅動程序本身可以打印新分配的號碼,然後我們可以手動創建設備文件。其次,新註冊的設備將在 /proc/devices 中有一個條目,我們可以手動創建設備文件,或者編寫一個 shell 腳本來讀取文件並創建設備文件,第三種方法是,我們的驅動程序在成功註冊後使用 device_create
函數創建設備文件,並在 cleanup_module
調用期間使用 device_destroy
。
這段話中的 device_create
cleanup_module
device_destroy
分別代表什麼?
然而,register_chrdev()
將佔用與給定主要編號相關聯的一系列次要編號。減少 char 設備註冊的浪費的推薦方式是使用 cdev
接口。
表示說 register_chrdev()
一旦註冊將隨附一系列的次要編號?
初始化 char 設備的數據結構,並將其與設備編號關聯起來。
將自己的設備特定結構嵌入到 struct cdev
中
使用以下方式初始化
一旦完成初始化,我們就可以使用 cdev_add 將 char 設備添加到系統中。
6.4 Unregistering A Device
當 root 將核心載卸使用 rmmod 移除時,是不能被隨便允許的。
通常,當您不希望允許某件事時,您會從應該執行該操作的函數中返回一個錯誤碼(一個負數)。對於 cleanup_module
來說,這是不可能的,因為它是一個 void 函數。然而,有一個計數器用於跟踪有多少進程正在使用您的模塊。您可以使用命令 cat /proc/modules
或 sudo lsmod
查看此數字的值。如果這個數字不為零,rmmod
將失敗。請注意,您不需要在 cleanup_module
中檢查計數器,因為系統調用 sys_delete_module
將為您執行檢查,該系統調用定義在 include/linux/syscalls.h
中。您不應該直接使用此計數器,但是在 include/linux/module.h
中定義了一些函數,讓您可以增加、減少和顯示此計數器:
try_module_get(THIS_MODULE)
: 增加當前模塊的引用計數module_put(THIS_MODULE)
: 減少當前模塊的引用計數module_refcount(THIS_MODULE)
: 返回當前模塊的引用計數值保持計數器的準確性非常重要;若曾經失去了正確的使用計數,則將永遠無法卸載模塊。
伴隨而來的就是 reboot
!
6.5 chardev.c
TODO : 程式碼目前對我來說還太難了解,先看下去
6.6 Writing Modules for Multiple Kernel Versions
為了確保模塊在不同內核版本中的正常運行,需要注意內核版本之間的差異,並相應地進行編碼和調試。
比較常見的 macro LINUX_VERSION_CODE
、 KERNEL_VERSION
7 The /proc File System
kernel 和 kernel modules 向行程發送訊息的一種額外機制是 /proc
file system 。最初設計用於存取有關行程的資訊。舉例 :
/proc/modules
提供 module 列表/proc/meminfo
蒐集記憶體使用訊息7.2 Read and Write a /proc File
在 /proc 文件中進行讀取和寫入操作與讀取操作類似,但有一點不同之處在於數據來自 user space,因此您需要將數據從 user space 空間導入到 kernel space(使用 copy_from_user
或 get_user
)。
使用 copy_from_user
或 get_user
的原因是,Linux 內存是分段的。這意味著指針本身並不引用內存中的唯一位置,而只是內存段中的位置,您需要知道它屬於哪個內存段才能使用它。每個進程都有一個自己的內存段,而行程唯一可以訪問的內存段就是自己的內存段。
當編寫運行為行程的常規程序時,通常不需要擔心內存段。但是,當您編寫一個核心模組時,通常希望存取內核內存段,這由系統自動處理。然而,當需要在當前運行的行程和內核之間傳遞內存緩衝區的內容時,內核函數接收的是位於行程段中的內存緩衝區的指針。巨集 put_user
和 get_user
允許您訪問該內存。這些函數僅處理一個字符,您可以使用 copy_to_user
和 copy_from_user
處理多個字符。由於緩衝區(在讀取或寫入函數中)位於 kernel space 中,因此對於寫入函數,您需要導入數據,因為數據來自 user space ,但對於讀取函數,則不需要,因為數據已經位於內核空間中。
Every time the file /proc/helloworld is read, the function procfile_read is called. Two parameters of this function are very important: the buffer (the second parameter) and the offset (the fourth one). The content of the buffer will be returned to the application which read it (for example the cat command). The offset is the current position in the file. If the return value of the function is not null, then this function is called again. So be careful with this function, if it never returns zero, the read function is called endlessly.
/proc
文件的名稱procfile_read()
,每次讀取 /proc/helloworld
都會呼叫此函式總結為將 /proc
文件的內容提供給 user space 的應用程序,以實現相應的讀取操作
procfile_write()
,將 user space 的數據使用 copy_from_user
導入到 kernel space 的 buffer 中,並更新偏移量。7.3 Manage /proc file with standard filesystem
從範例中無法理解如何使用 inode 來管理 /proc file ,待釐清
buffer 在 user space 中?
7.4 Manage /proc file with seq_file
使用 seq_file
管理 /proc
檔案
為了方便撰寫 /proc
,定義一個 API 名為 seq_file
。此 API 基於 3 個函式組成 : start()
、next()
、stop()
,當用戶讀取 /proc 檔案時, seq_file API 開始一個序列。
下方圖說明序列的流程 :
seq_file
為 proc_ops
提供基本功能,如 :seq_read
、seq_lseek
等,但不提供 write
至 /proc file 的功能。
8 sysfs: Interacting with your module
sysfs允許從用戶空間與運行中的 kernel 進行交互,透過讀取或設置 module 內的變量。
輸入 ls -l /sys
可以在系統的 /sys 目錄下找到 sysfs的目錄及檔案。
kobjects 是?
An attribute definition in simply:
For example, the driver model defines struct device_attribute like:
要讀取或寫入屬性,必須在聲明屬性時指定 show()
或 store()
方法。對於常見情況,include/linux/sysfs.h 提供了方便的 macro(如 __ATTR,__ATTR_RO,__ATTR_WO 等),以使定義屬性更加輕鬆,同時使代碼更加簡潔和易讀。
在講義中舉例透過 sysfs 可存取的變量創建的 HelloWorld module 示範:
當我在執行 echo "32" > /sys/kernel/mymodule/myvariable
這段時出現 bash: /sys/kernel/mymodule/myvariable: 拒絕不符權限的操作
盡管我已加上 sudo
9 Talking To Device Files
大多數實體裝置既用於輸入也用於輸出,因此內核中的裝置驅動程序需要一些機制來從進程獲取要發送到裝置的輸出。這通過打開裝置文件以進行輸出並將數據寫入其中來實現,就像寫入文件一樣。在下面的示例中,這是通過 device_write
函數來實現的。
在 Unix 中有意特殊函式 ioctl (Input Output Control) ,每個裝置都可以有自己的 ioctl command ,可以是 read ioctl (將訊息從行程發送至核心),也可以是 write ioctl (將訊息返回給行程),也可以兩個都有或都沒有。注意!這裡再次翻轉了 read 和 write 的角色,在 ioctl 中, read 是將訊息發送核心, write 是從核心接收訊息
在 ioctl()
函式中有三個參數:1.對應設備檔案的描述子 2. ioctl number 3.參數及它的型別,舉例: ret_val = ioctl(file_desc, IOCTL_SET_MSG, message);
其中 ioctl number ,通常由標頭檔的巨集所呼叫(_IO , _IOR , _IOW or _IOWR — depending on the type),從講義範例中的 header file 來看怎麼定義
_IO an ioctl with no parameters
_IOW an ioctl with write parameters (copy_from_user)
_IOR an ioctl with read parameters (copy_to_user)
_IOWR an ioctl with both write and read parameters.
11 Blocking Processes and threads
11.1 Sleep
當 kernel module 被行程打擾時,可以讓行程進入睡眠狀態,直到模組能再次為它提供服務。行程被 kernel 置於睡眠狀態並不斷的被叫醒,這就是多個行程同時在單一 CPU 運行的情況。
核心模組會調用 wait_event_interruptible
將行程置於睡眠狀態,直到該檔案可用為止。這個機制確保了對檔案的獨占性存取。
當行程完成對檔案的操作並將其關閉時,會調用 module_close
函式。這個函式會喚醒等待訪問文件的所有行程,並允許它們繼續運行。這樣,等待存取檔案的行程將能夠依次存取檔案。
重要的是要記住,除了 module_close
函式外,訊號(例如Ctrl + c)也可以喚醒等待存取檔案的行程。這是因為我們使用了 wait_event_interruptible
函式。如果採用了 wait_event
函式,則會導致當Ctrl+c被忽略時用戶會非常生氣。
對於不想進入睡眠狀態的進程,它們可以使用 O_NONBLOCK
標誌來打開文件。在這種情況下,內核會立即返回錯誤碼 -EAGAIN
,而不會阻塞進程的操作。
11.2 Completions
有時在多個執行緒的模組,一件事應該在另一件事之前發生。與使用 /bin/sleep
命令不同,核心有另一種方法可以做到這一點,允許超時或中斷也能發生。
Completion 作為代碼同步機制有三個主要部分:初始化結構體 completion 同步對象,通過 wait_for_completion()
進行等待,以及通過調用 complete()
進行信號發送部分。
例子中啟用兩個行程: crank 和 flywheel ,在 flywheel 行程之前必須首先啟動 crank 行程。
為每個執行緒建立了一個完成(completion)狀態,為 crank 和 flywheel 執行緒分別定義了不同的完成(completion)。在每個執行緒的退出點上更新了相應的完成(completion)狀態,flywheel 執行緒使用 wait_for_completion
函數確保它不會過早開始。crank 執行緒使用 complete_all()
函數更新完成(completion),這讓 flywheel 執行緒繼續執行。
如果在不同的 CPU 上運行的進程或不同的線程嘗試存取同一塊記憶體,那麼可能會發生奇怪的事情,或者您的系統可能會 lock up 。為了避免這種情況,核心提供了各種類型的互斥核心函數。這些函數指示代碼段是否“鎖定”或“解鎖”,從而防止同時運行它的嘗試。
mutex_is_locked
來檢查互斥鎖的鎖定狀態。如果互斥鎖被鎖定,則返回非零值;否則返回零。
最後,無論鎖定是否成功,都應該在臨界區域訪問完畢後使用 mutex_unlock
函數來釋放互斥鎖,以允許其他行程或執行緒群取被保護的資源。
在範例中使用 irq safe
,意味著在 lock 期間發生中斷,不會丟失中斷,而是在解鎖時激活,使用 flag 變數來保留原來狀態。
The example here is "irq safe" in that if interrupts happen during the lock then they will not be forgotten and will activate when the unlock happens, using the flags variable to retain their state.
範例中定義兩種不同的自旋鎖 :
sl_static
sl_dynamic
但為何要分成動態與靜態?
ChatGPT 解釋:
在示例中,動態自旋鎖(sl_dynamic
)和靜態自旋鎖(sl_static
)是兩種不同類型的自旋鎖,它們之間有幾個重要的區別:
初始化方式:
DEFINE_SPINLOCK
宏在全局範圍內靜態定義的。這意味著它在模塊加載時就已經存在,並且佔用了固定的內存空間。spin_lock_init
函數初始化的。這意味著它在運行時動態創建,並且可以根據需要釋放內存。作用域:
佔用的內存:
初始化方式:
spin_lock_init
函數進行初始化。總的來說,靜態自旋鎖是在編譯時創建並佔用固定內存的自旋鎖,而動態自旋鎖是在運行時動態創建並且可能需要動態分配內存的自旋鎖。选择使用哪種自旋鎖取決於具體的應用場景和性能要求。
Taking 100% of a CPU’s resources comes with greater responsibility. Situations where the kernel code monopolizes a CPU are called atomic contexts. Holding a spinlock is one of those situations. Sleeping in atomic contexts may leave the system hanging, as the occupied CPU devotes 100% of its resources doing nothing but sleeping. In some worse cases the system may crash. Thus, sleeping in atomic contexts is considered a bug in the kernel. They are sometimes called “sleep-in-atomic-context” in some materials.
RWLock 的規則
利用“current”指針訪問活動任務的tty結構。在這個結構中,有一個指向字符串寫入函數的指針,方便將字符串傳輸到tty。
範例程式碼中有這段 (ttyops->write)(my_tty, "\015\012", 2);
看了註解但不能完全理解,於是找到這篇 \r\n和\n的差異
運行任務有兩種主要方式:tasklet 和 work queue。Tasklet 是一種快速簡便的方式,用於安排單個函數的運行,例如當從中斷觸發時。而 work queue 則更複雜,但也更適合按順序運行多個任務。
問題:
根據講義描述結果應為:
但我實作結果卻是:
問題還在釐清中
CPU 與電腦硬件之間有兩種類型的交互。第一種類型是 CPU 向硬體發出命令,另一種是硬體需要告訴CPU某些信息。後者被稱為中斷,實現起來更難,因為它必須在對硬件方便而不是 CPU 方便時進行處理。硬體設備通常只有很少的RAM,如果在信息可用時不讀取它們,則將丟失該信息。
在 Linux 硬體中斷稱為 IRQ's (Interrupt ReQuests) ,分成兩種:
盡可能設置為 long IRQ 。
Here is an example where buttons are connected to GPIO numbers 17 and 18 and an LED is connected to GPIO 4. You can change those numbers to whatever is appropriate for your board.
定義 LED 在 GPIO 4,開關分別在 GPIO 17 and 18 :
中斷處理函式,當按鈕被按下時觸發。根據中斷號(IRQ)的不同,決定要執行的操作。如果第一個按鈕被按下且LED為關閉狀態,則將LED打開;如果第二個按鈕被按下且LED為打開狀態,則將LED關閉。
若想在中斷中執行一堆操作,一種常見的方法式將其與 tasklet 結合,這樣可以將大部分的工作推遲到排程器中進行。
在中斷程式中安插 tasklet_schedule