字元設備和文件系統節點
我一直在讀這本書:https ://lwn.net/Kernel/LDD3/ 。在這裡,作者區分了 3 種類型的設備文件,即字元、塊和網路設備。在第一章的第 6 頁,我發現:
字元設備
字元(char)設備是可以作為字節流(如文件)訪問的設備;char 驅動程序負責實現此行為。這樣的驅動程序通常至少實現
open
、close
、read
和write
系統呼叫。文本控制台(/dev/console
)和串列埠(/dev/ttyS0
和朋友)是字元設備的範例,因為它們很好地由流抽象表示。字元設備通過文件系統節點訪問,例如/dev/tty1
和/dev/lp0
。那麼 char 設備和這個文件系統節點之間到底有什麼區別呢?更讓我困惑的是,
ls -la /dev/
這些文件系統節點也顯示為字元設備(描述以 ac 開頭)。我的猜測是,這本書將 char 設備稱為硬體之間的一一對應,而這些文件系統節點則稱為軟體抽象。任何有關此的良好資源表示讚賞。
那麼 char 設備和這個文件系統節點之間到底有什麼區別呢?
我將此問題解釋為“字元設備驅動程序和字元設備文件有什麼區別?”
字元設備驅動程序是對字節流進行操作的核心軟體,通常用於與也對字節流進行操作的某些硬體進行通信。
字元設備文件是文件系統上的文件。設備文件具有與之關聯的元數據,核心使用這些元數據來了解與文件關聯的字元設備驅動程序。字元設備文件(實際上是所有設備文件)有兩個元數據:主要設備號和次要設備號。當您查看 的輸出時,您可以看到主要/次要數字
ls -l
。例如,考慮字元設備文件/dev/null
:$ ls -l /dev/null crw-rw-rw- 1 root root 1, 3 Jun 6 14:30 /dev/null
請注意,
1, 3
在第二個根之後——即主要設備號 (1) 和次要設備號 (3)。當程序與設備文件互動時,核心使用主設備號來了解哪個核心設備驅動程序處理針對該文件的 I/O。主設備號為 1 的字元設備與儲存設備相關聯;見major.h
:./include/uapi/linux/major.h:#define MEM_MAJOR 1
單個設備驅動程序通常可以“驅動”多個設備;次要設備號告訴核心使用者正在操作的特定設備。例如,以下字元設備文件都具有相同的主設備號,但不同的次設備號:
# ls -l /dev/zero /dev/mem /dev/null /dev/full /dev/random /dev/urandom /dev/kmsg crw-rw-rw- 1 root root 1, 7 Jun 6 14:30 /dev/full crw-r--r-- 1 root root 1, 11 Jun 6 14:30 /dev/kmsg crw-r----- 1 root kmem 1, 1 Jun 6 14:30 /dev/mem crw-rw-rw- 1 root root 1, 3 Jun 6 14:30 /dev/null crw-rw-rw- 1 root root 1, 8 Jun 6 14:30 /dev/random crw-rw-rw- 1 root root 1, 9 Jun 6 14:30 /dev/urandom crw-rw-rw- 1 root root 1, 5 Jun 6 14:30 /dev/zero
以下原始碼片段來自 Linux 5.4.32,文件
drivers/char/mem.c
.從上面的
ls
輸出中,我們觀察到所有這些文件的主設備號為 1。由此我們知道,同一個核心設備驅動程序響應任何程序打開/讀取/寫入這些文件的 I/O 請求。從核心原始碼中,我們看到記憶體設備驅動程序負責處理所有這些文件的 I/O:static const struct memdev { const char *name; umode_t mode; const struct file_operations *fops; fmode_t fmode; } devlist[] = { #ifdef CONFIG_DEVMEM [1] = { "mem", 0, &mem_fops, FMODE_UNSIGNED_OFFSET }, #endif #ifdef CONFIG_DEVKMEM [2] = { "kmem", 0, &kmem_fops, FMODE_UNSIGNED_OFFSET }, #endif [3] = { "null", 0666, &null_fops, 0 }, #ifdef CONFIG_DEVPORT [4] = { "port", 0, &port_fops, 0 }, #endif [5] = { "zero", 0666, &zero_fops, 0 }, [7] = { "full", 0666, &full_fops, 0 }, [8] = { "random", 0666, &random_fops, 0 }, [9] = { "urandom", 0666, &urandom_fops, 0 }, #ifdef CONFIG_PRINTK [11] = { "kmsg", 0644, &kmsg_fops, 0 }, #endif };
請注意,數組索引 — 括號中的數字 — 與相關文件上的次要設備編號匹配。
現在,讓我們考慮一個程序使用其中一個字元設備文件的範例。如果我們有一個包含以下內容的 shell 腳本:
echo "hello" > /dev/null
然後腳本
open()
成為字元設備文件/dev/null
。核心知道這/dev/null
是一個字元設備,並檢查與文件關聯的主要和次要設備號。它看到主設備號 1,因此它將open()
請求路由到處理主設備號 1(記憶體設備)上的操作的字元設備驅動程序。這最終出現在處理打開呼叫的記憶體設備驅動程序中的函式中:static int memory_open(struct inode *inode, struct file *filp) { int minor; const struct memdev *dev; minor = iminor(inode); if (minor >= ARRAY_SIZE(devlist)) return -ENXIO; dev = &devlist[minor]; if (!dev->fops) return -ENXIO; filp->f_op = dev->fops; filp->f_mode |= dev->fmode; if (dev->fops->open) return dev->fops->open(inode, filp); return 0; }
然後該
memory_open()
函式使用次設備號來索引devlist
我們之前看到的數組。如果該設備具有特殊open()
功能,則呼叫該功能,否則僅返回 0;該null
設備沒有特殊open()
功能。最終,該程序將呼叫
write()
將“hello”寫入與打開文件關聯的文件描述符。同樣,核心知道打開的文件與主設備號 1 和次設備號 3 的字元設備相關聯,因此它會將文件路由write()
到主設備類型 1(記憶體設備)的驅動程序。次要編號為 3 的設備具有一組註冊的函式來處理 I/O(此處為null_fops
):[3] = { "null", 0666, &null_fops, 0 },
該
null_fops
結構包含以下指向函式的指針:static const struct file_operations null_fops = { ... .write = write_null, ... };
因此
write()
,對於主編號為 1、次編號為 3 的字元設備文件將導致呼叫write_null()
. 該函式的實現是:static ssize_t write_null(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { return count; }
該
write_null()
函式什麼都不做,並返回count
以指示count
字節已成功寫入(我們期望寫入的行為/dev/null
)。總而言之,字元設備文件包含元數據:主要和次要設備號。當程序對字元設備文件執行 I/O 時,核心使用該元數據在核心中找到正確的字元設備驅動程序,以處理針對文件的 I/O 請求。