iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0
自我挑戰組

Linux Kernel 網路巡禮系列 第 10

番外篇 (2) Character Device

  • 分享至 

  • xImage
  •  

今天是這個系列的第二個番外篇。在研究 VFS 的過程中,剛好查到了 character device 這種特殊的驅動形式,於是花了一些時間了解其運作方式。雖然這與 network namespace ID 一樣,不在主要的系列文章介紹脈絡中,但仍然值得在這個系列中與大家分享,並且可以讓大家對於 VFS 的運作方式有更深刻的了解。

Linux 的設備檔案與 Character Device

在 Linux 的「萬物皆檔案」理念下,外圍設備通常以 /dev/xxxx 的檔案形式出現。user space 的應用程式可以使用標準的檔案操作 API(如 openreadwrite 等 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 devicesCharacter devices。它們的區別在於底層的讀寫單位:Block devices 以區塊為單位進行操作,像硬碟等設備;而 Character devices 則是以字元流為單位進行讀寫,像序列埠(serial port)等設備。

儘管這兩類設備在讀寫單位上有所不同,但這主要發生於驅動層,對於應用程式來說,操作方式幾乎一致,都是透過 openreadwrite 等 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 devicec/dev/hello,其主設備號為 240,次設備號為 0。透過這個檔案,我們可以與實際設備進行通訊。但前提是這個設備號必須對應到驅動認可的可操作設備。

Character Device 驅動範例

接下來,我們將介紹一個簡單的 Character Device 驅動範例。此範例將展示如何定義一個簡單的 Hello World Character Device 驅動程式,參考自 meetonfriday

依循我們前面介紹的 VFS 概念,當透過 cat 指令讀取檔案的時候,會依序

  1. 呼叫 open system call,實例化 file 結構,然後設置 file.f_ops (file_operation)。
  2. 呼叫 read system call,底下執行 file->f_ops->read。

因此,在撰寫 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 驅動範例。下一篇文章,我們將進一步探討 mknodopen 如何連結 VFS 和 Character Device Driver。


上一篇
虛擬檔案系統 VFS (3)
下一篇
番外篇 (3) mknod 與 device driver
系列文
Linux Kernel 網路巡禮30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言