今天是這個系列的第二個番外篇。在研究 VFS 的過程中,剛好查到了 character device 這種特殊的驅動形式,於是花了一些時間了解其運作方式。雖然這與 network namespace ID 一樣,不在主要的系列文章介紹脈絡中,但仍然值得在這個系列中與大家分享,並且可以讓大家對於 VFS 的運作方式有更深刻的了解。
在 Linux 的「萬物皆檔案」理念下,外圍設備通常以 /dev/xxxx
的檔案形式出現。user space 的應用程式可以使用標準的檔案操作 API(如 open
、read
、write
等 system call)與這些設備進行通訊。例如,對 /dev/sda1
進行讀寫,可以跳過檔案系統,直接對硬碟的原始資料進行操作。
每個外圍設備在 Linux 系統中都會被分配一個 設備編號(Device Number),該編號由 主設備號(Major Number)和 次設備號(Minor Number)組成。主設備號通常對應某一類設備,而次設備號標識具體的設備。
透過 cat /proc/devices
指令,我們可以檢視當前系統中的主設備號和設備類型名稱:
> cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
21 sg
29 fb
89 i2c
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
Block devices:
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
如上所示,Linux 將設備分為兩類:Block devices 和 Character devices。它們的區別在於底層的讀寫單位:Block devices 以區塊為單位進行操作,像硬碟等設備;而 Character devices 則是以字元流為單位進行讀寫,像序列埠(serial port)等設備。
儘管這兩類設備在讀寫單位上有所不同,但這主要發生於驅動層,對於應用程式來說,操作方式幾乎一致,都是透過 open
、read
、write
等 system call 進行。
當設備驅動被系統偵測到時,驅動會向 kernel 註冊設備號,並在 /dev
目錄中建立對應的 設備檔案(Device File)。我們可以使用以下指令檢視系統中的設備檔案:
ls /dev -l
total 0
crw-r--r-- 1 root root 10, 235 九 17 01:20 autofs
drwxr-xr-x 2 root root 680 九 17 01:20 block
drwxr-xr-x 2 root root 80 九 17 01:20 bsg
crw------- 1 root root 10, 234 九 17 01:20 btrfs-control
drwxr-xr-x 3 root root 60 九 17 01:20 bus
lrwxrwxrwx 1 root root 3 九 17 01:20 cdrom -> sr0
drwxr-xr-x 2 root root 3600 九 17 01:20 char
crw------- 1 root root 5, 1 九 17 01:20 console
lrwxrwxrwx 1 root root 11 九 17 01:20 core -> /proc/kcore
crw------- 1 root root 10, 60 九 17 01:20 cpu_dma_latency
crw------- 1 root root 10, 203 九 17 01:20 cuse
drwxr-xr-x 7 root root 140 九 17 01:20 disk
...
brw-rw---- 1 root disk 8, 0 九 17 01:20 sda
brw-rw---- 1 root disk 8, 1 九 17 01:20 sda1
brw-rw---- 1 root disk 8, 2 九 17 01:20 sda2
crw-rw-rw- 1 root tty 5, 0 九 17 01:20 tty
crw--w---- 1 root tty 4, 0 九 17 01:20 tty0
crw--w---- 1 root tty 4, 1 九 17 01:20 tty1
...
指令輸出的第五和第六欄顯示了設備檔案的 主設備號 和 次設備號。
通常,這些檔案由驅動程式自動建立,但我們也可以使用以下指令手動建立:
mknod /dev/hello c 240 0
這裡,mknod
創建了一個 character device(c
) /dev/hello
,其主設備號為 240,次設備號為 0。透過這個檔案,我們可以與實際設備進行通訊。但前提是這個設備號必須對應到驅動認可的可操作設備。
接下來,我們將介紹一個簡單的 Character Device 驅動範例。此範例將展示如何定義一個簡單的 Hello World Character Device 驅動程式,參考自 meetonfriday。
依循我們前面介紹的 VFS 概念,當透過 cat
指令讀取檔案的時候,會依序
因此,在撰寫 Character Device 驅動時,我們需要定義 file_operations
結構,來實現對設備檔案的操作。
// chrdevbase.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define CHRDEVBASE_MAJOR 240
#define DEVICE_NAME "helloworld"
#define HELLO_MSG "Hello World\n"
#define HELLO_MSG_LEN (sizeof(HELLO_MSG))
static int major;
static char msg_buffer[HELLO_MSG_LEN];
static int open_count = 0;
// 打開字元設備
static int dev_open(struct inode *inodep, struct file *filep) {
open_count++;
printk(KERN_INFO "helloworld: device opened %d time(s)\n", open_count);
return 0;
}
// 讀取字元設備
static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
int bytes_read = HELLO_MSG_LEN - *offset;
if (bytes_read == 0) {
return 0; // 已經讀取完畢
}
if (len < bytes_read) {
bytes_read = len;
}
if (copy_to_user(buffer, msg_buffer + *offset, bytes_read)) {
return -EFAULT;
}
*offset += bytes_read;
return bytes_read;
}
// 關閉字元設備
static int dev_release(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "helloworld: device closed\n");
return 0;
}
static struct file_operations fops = {
.open = dev_open,
.read = dev_read,
.release = dev_release,
};
static int __init hello_init(void) {
strcpy(msg_buffer, HELLO_MSG);
// 動態註冊一個主設備號
major = register_chrdev(CHRDEVBASE_MAJOR, DEVICE_NAME, &fops);
if (major < 0) {
printk(KERN_ALERT "helloworld: failed to register a major number\n");
return major;
}
printk(KERN_INFO "helloworld: registered with major number %d\n", major);
return 0;
}
static void __exit hello_exit(void) {
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_INFO "helloworld: unregistered device\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World char device driver");
MODULE_VERSION("0.1");
在這段程式碼中,我們透過 register_chrdev
註冊了主設備號為 CHRDEVBASE_MAJOR
(240
) 的設備類型,表示該主設備號之設備由我們寫的驅動來處理。
並且定義了 dev_read
函數,表示當使用 read
讀取這個設備檔案時,它會回傳字串 Hello World
。
我們需要一個簡單的 Makefile
來編譯這段程式碼:
KERNELDIR := /lib/modules/$(shell uname -r)/build
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
make -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
make -C $(KERNELDIR) M=$(CURRENT_PATH) clean
透過以下指令,編譯並安裝模組後,掛載 Character Device:
make
insmod chrdevbase.ko
mknod /dev/hello c 240 0
當我們透過 cat /dev/hello
讀取這個設備時,會顯示 Hello World
。
在今天的番外篇中,我們介紹了 Character Device 及其基本的驅動程式概念,並展示了如何撰寫一個簡單的 Hello World Character Device 驅動範例。下一篇文章,我們將進一步探討 mknod
與 open
如何連結 VFS 和 Character Device Driver。