Linux

字元設備和文件系統節點

  • June 22, 2020

我一直在讀這本書:https ://lwn.net/Kernel/LDD3/ 。在這裡,作者區分了 3 種類型的設備文件,即字元、塊和網路設備。在第一章的第 6 頁,我發現:

字元設備

字元(char)設備是可以作為字節流(如文件)訪問的設備;char 驅動程序負責實現此行為。這樣的驅動程序通常至少實現openclosereadwrite系統呼叫。文本控制台(/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 請求。

引用自:https://unix.stackexchange.com/questions/594200