Linux 系統透過裝置節點讓使用者空間程式與核心空間的硬體驅動程式互動。字元裝置驅動程式負責處理字元裝置的輸入輸出操作,開發者需要理解裝置註冊、初始化、file_operations 結構的定義及錯誤處理機制。早期 Linux 系統需要手動使用 mknod 建立裝置節點,現在則可利用 devtmpfs 自動建立,簡化開發流程並提升效率。透過 class_create 和 device_create 函式,驅動程式可以更方便地管理裝置節點,並在 /dev 目錄下自動生成對應的裝置檔案。
字元裝置驅動程式
一般來說,作業系統的設計目的是隱藏底層硬體的細節,讓使用者應用程式無需直接與硬體互動。然而,應用程式仍需要存取由硬體週邊擷取的資料,以及驅動週邊輸出資料。由於週邊暫存器只能由Linux核心存取,因此只有核心能夠收集這些週邊擷取的資料流。
Linux需要一種機制來將資料從核心傳輸到使用者空間。這種資料傳輸是透過裝置節點(device nodes)來處理的,也被稱為虛擬檔案。裝置節點存在於根檔案系統中,但它們並不是真正的檔案。當使用者從裝置節點讀取資料時,核心會將底層驅動程式擷取的資料流複製到應用程式的記憶體空間中。當使用者寫入裝置節點時,核心會將應用程式提供的資料流複製到驅動程式的資料緩衝區中,並最終透過底層硬體輸出。這些虛擬檔案可以使用標準的系統呼叫來「開啟」和「讀取」或「寫入」。
每個裝置都有一個唯一的驅動程式,用於處理最終傳遞給核心的使用者應用程式請求。Linux支援三種型別的裝置:字元裝置、區塊裝置和網路裝置。雖然概念相同,但每種裝置的驅動程式之間的差異在於檔案被「開啟」和「讀取」或「寫入」的方式。字元裝置是最常見的裝置,它們直接讀寫而不進行緩衝,例如鍵盤、監視器、印表機和序列埠。區塊裝置只能以區塊大小的倍數進行寫入和讀取,通常是512或1024位元組。它們可以被隨機存取,也就是說任何區塊都可以被讀取或寫入,無論它在裝置上的哪個位置。一個典型的區塊裝置例子是硬碟驅動器。網路裝置則是透過BSD socket介面和網路子系統來存取的。
字元裝置在列表的第一列中用c標識,而區塊裝置則用b標識。每個裝置都提供了存取許可權、所有者和組別。
字元驅動程式的操作
從應用程式的角度來看,字元裝置本質上是一個檔案。一個行程只知道/dev檔案路徑。該行程使用open()系統呼叫開啟檔案,並執行標準的檔案操作,如read()和write()。
為了實作這一點,字元驅動程式必須實作在file_operations結構中描述的操作(在核心原始碼樹中的include/linux/fs.h中宣告)並註冊它們。在下面顯示的file_operations結構中,您可以看到字元驅動程式的一些最常見的操作:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
};
Linux檔案系統層將確保當使用者空間應用程式發出相應的系統呼叫時,這些操作將被呼叫(在核心端,驅動程式實作並註冊回呼操作)。
資料交換與錯誤處理
核心驅動程式將使用特定的函式copy_from_user()和copy_to_user()與使用者空間交換資料,如前圖所示。
read()和write()方法在發生錯誤時傳回負值。傳回值大於或等於0時,則告訴呼叫程式已經成功傳輸了多少位元組。如果某些資料正確傳輸後發生錯誤,則傳回值必須是成功傳輸的位元組數,並且錯誤不會被報告,直到下一次函式被呼叫。實作這一慣例當然需要您的驅動程式記住錯誤已經發生,以便將來傳回錯誤狀態。
對於read(),其傳回值被應用程式解釋如下:
- 如果該值等於傳遞給
read系統呼叫的count引數,則表示已傳輸了請求的位元組數。這是最佳情況。 - 如果該值為正數,但小於
count,則表示只有部分資料被傳輸。這可能由於多種原因而發生,具體取決於裝置。大多數情況下,應用程式會重試讀取。例如,如果您使用fread()函式讀取,函式庫函式將重新發出系統呼叫,直到完成請求的資料傳輸。 - 如果該值為0,表示已到達檔案末尾。
- 負值表示發生了錯誤。該值根據
<linux/errno.h>指定了錯誤型別。錯誤傳回的典型值包括-EINTR(系統呼叫被中斷)或-EFAULT(位址錯誤)。
裝置識別
在Linux中,每個裝置都由兩個數字標識:主編號和次編號。透過在主機PC上執行ls -l /dev可以檢視這些編號。每個裝置驅動程式向核心註冊其主編號,並完全負責管理其次編號。當存取裝置檔案時,主編號選擇要呼叫哪個裝置驅動程式來執行輸入/輸出操作。主編號由核心用來識別要存取的正確裝置驅動程式。次編號的作用取決於裝置,並在驅動程式內部處理。例如,樹莓派有多個硬體UART埠。同一個驅動程式可以用來控制所有的UART,但每個物理UART需要其自己的裝置節點,因此這些UART的裝置節點將具有相同的主編號,但具有唯一的次編號。
實驗4.1:「helloworld字元」模組
傳統上,Linux系統通常使用靜態裝置建立方法,在/dev下建立大量裝置節點(有時甚至數千個節點),無論對應的硬體裝置是否實際存在。這通常透過一個包含多個對mknod程式呼叫的MAKEDEV指令碼來完成。
內容解密:
file_operations結構是用來定義字元驅動程式操作的核心結構。read()和write()方法用於在使用者空間和核心空間之間交換資料。- 主編號和次編號用於唯一標識一個裝置,並由核心用來選擇正確的裝置驅動程式。
- 字元驅動程式需要實作並註冊在
file_operations結構中定義的操作,以支援標準的檔案操作。 - 錯誤處理和資料傳輸狀態透過這些方法的傳回值來傳達給應用程式。
字元裝置驅動程式開發
在 Linux 核心中,字元裝置是一種重要的裝置型別,用於處理使用者空間與核心空間之間的資料傳輸。本文將介紹如何開發字元裝置驅動程式,包括裝置註冊、初始化和檔案操作等內容。
裝置註冊與登出
字元裝置的註冊和登出是透過指定主裝置號和次裝置號來完成的。dev_t 型別用於儲存裝置的識別符號(主裝置號和次裝置號),可以使用 MKDEV 巨集來獲得。
靜態分配裝置號
使用 register_chrdev_region() 函式可以靜態分配裝置號,該函式需要指定第一個裝置號、count 表示連續的裝置號數量,以及裝置名稱。
int register_chrdev_region(dev_t first, unsigned int count, char *name);
動態分配裝置號
建議使用 alloc_chrdev_region() 函式動態分配裝置號,該函式會自動選擇一個可用的主裝置號,並傳回第一個次裝置號。
int alloc_chrdev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name);
初始化和註冊字元裝置
在分配裝置號之後,需要使用 cdev_init() 函式初始化字元裝置,並使用 cdev_add() 函式將其註冊到核心中。
struct my_device_data {
struct cdev cdev;
/* my data starts here */
[...]
};
struct my_device_data devs[MY_MAX_MINORS];
const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
.unlocked_ioctl = my_ioctl
};
int init_module(void)
{
int i, err;
register_chrdev_region(MKDEV(MY_MAJOR, 0), MY_MAX_MINORS, "my_device_driver");
for(i = 0; i < MY_MAX_MINORS; i++) {
/* initialize devs[i] fields and register character devices */
cdev_init(&devs[i].cdev, &my_fops);
cdev_add(&devs[i].cdev, MKDEV(MY_MAJOR, i), 1);
}
return 0;
}
內容解密:
register_chrdev_region()用於靜態註冊一個範圍內的字元裝置號。cdev_init()初始化一個字元裝置,並將其與特定的檔案操作結構 (my_fops) 相關聯。cdev_add()將初始化後的字元裝置註冊到核心中,使其可供使用者空間存取。- 在
init_module()中,迴圈初始化和註冊多個字元裝置。
銷毀和登出字元裝置
當模組解除安裝時,需要使用 cdev_del() 函式刪除字元裝置,並使用 unregister_chrdev_region() 函式登出裝置號。
void cleanup_module(void)
{
int i;
for(i = 0; i < MY_MAX_MINORS; i++) {
/* release devs[i] fields */
cdev_del(&devs[i].cdev);
}
unregister_chrdev_region(MKDEV(MY_MAJOR, 0), MY_MAX_MINORS);
}
內容解密:
cdev_del()用於刪除之前註冊的字元裝置。unregister_chrdev_region()用於登出之前靜態分配的裝置號範圍。- 在
cleanup_module()中,迴圈刪除多個字元裝置,並登出相關的裝置號。
字元裝置驅動程式開發
簡介
字元裝置驅動程式是Linux核心模組的重要組成部分,用於實作使用者空間與硬體裝置之間的互動。本文將探討字元裝置驅動程式的開發流程、關鍵技術及實務應用。
開發流程
1. 定義主裝置號
首先,需要定義一個主裝置號(Major Number)用於標識字元裝置。範例程式碼中定義了MY_MAJOR_NUM為202。
2. 註冊字元裝置區域
使用register_chrdev_region函式註冊字元裝置區域,該函式接受三個引數:裝置識別碼、裝置數量和裝置名稱。
dev_t dev = MKDEV(MY_MAJOR_NUM, 0);
register_chrdev_region(dev, 1, "my_char_device");
3. 初始化cdev結構
使用cdev_init函式初始化cdev結構,該結構代表字元裝置內部表示。然後,使用cdev_add函式將cdev結構新增到核心空間。
static struct cdev my_dev;
cdev_init(&my_dev, &my_dev_fops);
cdev_add(&my_dev, dev, 1);
4. 定義file_operations結構
定義file_operations結構,該結構包含了對字元裝置的操作函式指標,例如open、release和unlocked_ioctl。
static const struct file_operations my_dev_fops = {
.owner = THIS_MODULE,
.open = my_dev_open,
.release = my_dev_close,
.unlocked_ioctl = my_dev_ioctl,
};
5. 實作操作函式
實作file_operations結構中定義的操作函式,例如my_dev_open、my_dev_close和my_dev_ioctl。
static int my_dev_open(struct inode *inode, struct file *file)
{
pr_info("my_dev_open() is called.\n");
return 0;
}
static int my_dev_close(struct inode *inode, struct file *file)
{
pr_info("my_dev_close() is called.\n");
return 0;
}
static long my_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
pr_info("my_dev_ioctl() is called. cmd = %d, arg = %ld\n", cmd, arg);
return 0;
}
實務應用
1. 編譯和佈署驅動程式
編譯字元裝置驅動程式,並將其佈署到目標系統。
2. 建立裝置節點
使用mknod命令建立字元裝置節點,命令格式為:mknod <device_name> c <major_number> <minor_number>。
3. 編譯和佈署測試應用程式
編譯測試應用程式,並將其佈署到目標系統。測試應用程式使用open、ioctl和close系統呼叫與字元裝置驅動程式互動。
int main(void)
{
int my_dev = open("/dev/mydev", 0);
if (my_dev < 0) {
perror("Fail to open device file: /dev/mydev.");
} else {
ioctl(my_dev, 100, 110); /* cmd = 100, arg = 110. */
close(my_dev);
}
return 0;
}
字元驅動程式開發:從手動建立裝置檔案到使用 devtmpfs
在 Linux 驅動程式開發中,字元驅動程式是一種常見的驅動程式型別,用於處理字元裝置的輸入輸出操作。本篇文章將介紹字元驅動程式的基本概念,並透過例項講解如何從手動建立裝置檔案轉變為使用 devtmpfs 自動建立裝置檔案。
手動建立裝置檔案
在早期的 Linux 系統中,裝置檔案需要手動使用 mknod 命令建立。這種方法需要系統開發者維護裝置檔案與核心處理的裝置之間的協調性。
例項:helloworld_rpi3_char_driver.ko
首先,我們來看看 helloworld_rpi3_char_driver.ko 模組的載入和測試過程:
- 載入模組:
insmod helloworld_rpi3_char_driver.ko - 檢視分配的裝置號:
cat /proc/devices - 手動建立裝置檔案:
mknod /dev/mydev c 202 0 - 執行測試程式:
./ioctl_test - 移除模組:
rmmod helloworld_rpi3_char_driver.ko
程式碼解析
在 helloworld_rpi3_char_driver.c 中,主要涉及以下步驟:
- 初始化字元裝置結構
cdev,並註冊到核心。 - 使用
register_chrdev_region或alloc_chrdev_region註冊裝置號。
使用 devtmpfs 自動建立裝置檔案
從 Linux 2.6.32 版本開始,核心引入了 devtmpfs,一種根據 RAM 的檔案系統,用於自動建立裝置檔案。驅動程式可以使用 devtmpfs 建立裝置節點。
LAB 4.2: “class character” 模組
本實驗將在前一個驅動程式的基礎上,使用 devtmpfs 自動建立裝置檔案,並在 /sys/class/ 目錄下新增一個類別入口。
主要變更
- 包含必要的標頭檔案:
#include <linux/device.h>用於class_create()和device_create()。 - 定義類別名稱和裝置名稱:分別為
"hello_class"和"mydev"。 - 修改
hello_init()函式:- 使用
alloc_chrdev_region()動態分配裝置號。 - 使用
class_create()建立類別。 - 使用
device_create()建立裝置節點。
- 使用
程式碼範例
static int __init hello_init(void)
{
dev_t dev_no;
int Major;
struct device* helloDevice;
// 動態分配裝置號
ret = alloc_chrdev_region(&dev_no, 0, 1, DEVICE_NAME);
Major = MAJOR(dev_no);
dev = MKDEV(Major, 0);
// 初始化 cdev 結構並新增到核心
cdev_init(&my_dev, &my_dev_fops);
ret = cdev_add(&my_dev, dev, 1);
// 建立類別和裝置節點
helloClass = class_create(THIS_MODULE, CLASS_NAME);
helloDevice = device_create(helloClass, NULL, dev, NULL, DEVICE_NAME);
return 0;
}
#### 內容解密:
此段程式碼展示瞭如何初始化字元裝置並使用 devtmpfs 自動建立裝置檔案。關鍵步驟包括:
- 使用
alloc_chrdev_region()取得裝置號,避免了手動指定的麻煩。 class_create()和device_create()分別用於建立類別和裝置節點,使得裝置檔案能夠自動出現在/dev目錄下。- 這種方法提高了驅動程式的自動化和可維護性。