3.2. 主次編號

2018-02-24 15:49 更新

3.2.?主次編號

字符設備通過文件系統(tǒng)中的名子來存取. 那些名子稱為文件系統(tǒng)的特殊文件, 或者設備文件, 或者文件系統(tǒng)的簡單結點; 慣例上它們位于 /dev 目錄. 字符驅動的特殊文件由使用 ls -l 的輸出的第一列的"c"標識. 塊設備也出現(xiàn)在 /dev 中, 但是它們由"b"標識. 本章集中在字符設備, 但是下面的很多信息也適用于塊設備.

如果你發(fā)出 ls -l 命令, 你會看到在設備文件項中有 2 個數(shù)(由一個逗號分隔)在最后修改日期前面, 這里通常是文件長度出現(xiàn)的地方. 這些數(shù)字是給特殊設備的主次設備編號. 下面的列表顯示了一個典型系統(tǒng)上出現(xiàn)的幾個設備. 它們的主編號是 1, 4, 7, 和 10, 而次編號是 1, 3, 5, 64, 65, 和 129.


 crw-rw-rw- 1 root  root  1,  3 Apr 11  2002 null 
 crw------- 1 root  root  10, 1 Apr 11  2002 psaux 
 crw------- 1 root  root  4,  1 Oct 28 03:04 tty1 
 crw-rw-rw- 1 root  tty   4, 64 Apr 11  2002 ttys0 
 crw-rw---- 1 root  uucp  4, 65 Apr 11  2002 ttyS1 
 crw--w---- 1 vcsa  tty   7,  1 Apr 11  2002 vcs1 
 crw--w---- 1 vcsa  tty   7,129 Apr 11  2002 vcsa1 
 crw-rw-rw- 1 root  root  1,  5 Apr 11  2002 zero  

傳統(tǒng)上, 主編號標識設備相連的驅動. 例如, /dev/null 和 /dev/zero 都由驅動 1 來管理, 而虛擬控制臺和串口終端都由驅動 4 管理; 同樣, vcs1 和 vcsa1 設備都由驅動 7 管理. 現(xiàn)代 Linux 內核允許多個驅動共享主編號, 但是你看到的大部分設備仍然按照一個主編號一個驅動的原則來組織.

次編號被內核用來決定引用哪個設備. 依據(jù)你的驅動是如何編寫的(如同我們下面見到的), 你可以從內核得到一個你的設備的直接指針, 或者可以自己使用次編號作為本地設備數(shù)組的索引. 不論哪個方法, 內核自己幾乎不知道次編號的任何事情, 除了它們指向你的驅動實現(xiàn)的設備.

3.2.1.?設備編號的內部表示

在內核中, dev_t 類型(在 <linux/types.h>中定義)用來持有設備編號 -- 主次部分都包括. 對于 2.6.0 內核, dev_t 是 32 位的量, 12 位用作主編號, 20 位用作次編號. 你的代碼應當, 當然, 對于設備編號的內部組織從不做任何假設; 相反, 應當利用在 <linux/kdev_t.h>中的一套宏定義. 為獲得一個 dev_t 的主或者次編號, 使用:


MAJOR(dev_t dev); 
MINOR(dev_t dev);

相反, 如果你有主次編號, 需要將其轉換為一個 dev_t, 使用:


MKDEV(int major, int minor); 

注意, 2.6 內核能容納有大量設備, 而以前的內核版本限制在 255 個主編號和 255 個次編號. 有人認為這么寬的范圍在很長時間內是足夠的, 但是計算領域被這個特性的錯誤假設搞亂了. 因此你應當希望 dev_t 的格式將來可能再次改變; 但是, 如果你仔細編寫你的驅動, 這些變化不會是一個問題.

3.2.2.?分配和釋放設備編號

在建立一個字符驅動時你的驅動需要做的第一件事是獲取一個或多個設備編號來使用. 為此目的的必要的函數(shù)是 register_chrdev_region, 在 <linux/fs.h>中聲明:


int register_chrdev_region(dev_t first, unsigned int count, char *name);

這里, first 是你要分配的起始設備編號. first 的次編號部分常常是 0, 但是沒有要求是那個效果. count 是你請求的連續(xù)設備編號的總數(shù). 注意, 如果 count 太大, 你要求的范圍可能溢出到下一個次編號; 但是只要你要求的編號范圍可用, 一切都仍然會正確工作. 最后, name 是應當連接到這個編號范圍的設備的名子; 它會出現(xiàn)在 /proc/devices 和 sysfs 中.

如同大部分內核函數(shù), 如果分配成功進行, register_chrdev_region 的返回值是 0. 出錯的情況下, 返回一個負的錯誤碼, 你不能存取請求的區(qū)域.

如果你確實事先知道你需要哪個設備編號, register_chrdev_region 工作得好. 然而, 你常常不會知道你的設備使用哪個主編號; 在 Linux 內核開發(fā)社團中一直努力使用動態(tài)分配設備編號. 內核會樂于動態(tài)為你分配一個主編號, 但是你必須使用一個不同的函數(shù)來請求這個分配.


int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

使用這個函數(shù), dev 是一個只輸出的參數(shù), 它在函數(shù)成功完成時持有你的分配范圍的第一個數(shù). fisetminor 應當是請求的第一個要用的次編號; 它常常是 0. count 和 name 參數(shù)如同給 request_chrdev_region 的一樣.

不管你任何分配你的設備編號, 你應當在不再使用它們時釋放它. 設備編號的釋放使用:


void unregister_chrdev_region(dev_t first, unsigned int count); 

調用 unregister_chrdev_region 的地方常常是你的模塊的 cleanup 函數(shù).

上面的函數(shù)分配設備編號給你的驅動使用, 但是它們不告訴內核你實際上會對這些編號做什么. 在用戶空間程序能夠存取這些設備號中一個之前, 你的驅動需要連接它們到它的實現(xiàn)設備操作的內部函數(shù)上. 我們將描述如何簡短完成這個連接, 但首先顧及一些必要的枝節(jié)問題.

3.2.3.?主編號的動態(tài)分配

一些主設備編號是靜態(tài)分派給最普通的設備的. 一個這些設備的列表在內核源碼樹的 Documentation/devices.txt 中. 分配給你的新驅動使用一個已經(jīng)分配的靜態(tài)編號的機會很小, 但是, 并且新編號沒在分配. 因此, 作為一個驅動編寫者, 你有一個選擇: 你可以簡單地撿一個看來沒有用的編號, 或者你以動態(tài)方式分配主編號. 只要你是你的驅動的唯一用戶就可以撿一個編號用; 一旦你的驅動更廣泛的被使用了, 一個隨機撿來的主編號將導致沖突和麻煩.

因此, 對于新驅動, 我們強烈建議你使用動態(tài)分配來獲取你的主設備編號, 而不是隨機選取一個當前空閑的編號. 換句話說, 你的驅動應當幾乎肯定地使用 alloc_chrdev_region, 不是 register_chrdev_region.

動態(tài)分配的缺點是你無法提前創(chuàng)建設備節(jié)點, 因為分配給你的模塊的主編號會變化. 對于驅動的正常使用, 這不是問題, 因為一旦編號分配了, 你可從 /proc/devices 中讀取它.[6]

為使用動態(tài)主編號來加載一個驅動, 因此, 可使用一個簡單的腳本來代替調用 insmod, 在調用 insmod 后, 讀取 /proc/devices 來創(chuàng)建特殊文件.

一個典型的 /proc/devices 文件看來如下:


Character devices:
 1 mem
 2 pty
 3 ttyp
 4 ttyS
 6 lp
 7 vcs
 10 misc
 13 input
 14 sound 
 21 sg
 180 usb

Block devices:
 2 fd
 8 sd
 11 sr
 65 sd
 66 sd 

因此加載一個已經(jīng)安排了一個動態(tài)編號的模塊的腳本, 可以使用一個工具來編寫, 如 awk , 來從 /proc/devices 獲取信息以創(chuàng)建 /dev 中的文件.

下面的腳本, snull_load, 是 scull 發(fā)布的一部分. 以模塊發(fā)布的驅動的用戶可以從系統(tǒng)的 rc.local 文件中調用這樣一個腳本, 或者在需要模塊時手工調用它.


#!/bin/sh
module="scull"
device="scull"
mode="664"

# invoke insmod with all arguments we got
# and use a pathname, as newer modutils don't look in . by default
/sbin/insmod ./$module.ko $* || exit 1

# remove stale nodes
rm -f /dev/${device}[0-3]

major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices) 
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3

# give appropriate group/permissions, and change the group.
# Not all distributions have staff, some have "wheel" instead.
group="staff"
grep -q '^staff:' /etc/group || group="wheel"

chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]

這個腳本可以通過重定義變量和調整 mknod 行來適用于另外的驅動. 這個腳本僅僅展示了創(chuàng)建 4 個設備, 因為 4 是 scull 源碼中缺省的.

腳本的最后幾行可能有些模糊:為什么改變設備的組和模式? 理由是這個腳本必須由超級用戶運行, 因此新建的特殊文件由 root 擁有. 許可位缺省的是只有 root 有寫權限, 而任何人可以讀. 通常, 一個設備節(jié)點需要一個不同的存取策略, 因此在某些方面別人的存取權限必須改變. 我們的腳本缺省是給一個用戶組存取, 但是你的需求可能不同. 在第 6 章的"設備文件的存取控制"一節(jié)中, sculluid 的代碼演示了驅動如何能夠強制它自己的對設備存取的授權.

還有一個 scull_unload 腳本來清理 /dev 目錄并去除模塊.

作為對使用一對腳本來加載和卸載的另外選擇, 你可以編寫一個 init 腳本, 準備好放在你的發(fā)布使用這些腳本的目錄中. [7]作為 scull 源碼的一部分, 我們提供了一個相當完整和可配置的 init 腳本例子, 稱為 scull.init; 它接受傳統(tǒng)的參數(shù) -- start, stop, 和 restart -- 并且完成 scull_load 和 scull_unload 的角色.

如果反復創(chuàng)建和銷毀 /dev 節(jié)點, 聽來過分了, 有一個有用的辦法. 如果你在加載和卸載單個驅動, 你可以在你第一次使用你的腳本創(chuàng)建特殊文件之后, 只使用 rmmod 和 insmod: 這樣動態(tài)編號不是隨機的. [8]并且你每次都可以使用所選的同一個編號, 如果你不加載任何別的動態(tài)模塊. 在開發(fā)中避免長腳本是有用的. 但是這個技巧, 顯然不能擴展到一次多于一個驅動.

安排主編號最好的方式, 我們認為, 是缺省使用動態(tài)分配, 而留給自己在加載時指定主編號的選項權, 或者甚至在編譯時. scull 實現(xiàn)以這種方式工作; 它使用一個全局變量, scull_major, 來持有選定的編號(還有一個 scull_minor 給次編號). 這個變量初始化為 SCULL_MAJOR, 定義在 scull.h. 發(fā)布的源碼中的 SCULL_MAJOR 的缺省值是 0, 意思是"使用動態(tài)分配". 用戶可以接受缺省值或者選擇一個特殊主編號, 或者在編譯前修改宏定義或者在 insmod 命令行指定一個值給 scull_major. 最后, 通過使用 scull_load 腳本, 用戶可以在 scull_load 的命令行傳遞參數(shù)給 insmod.[9]

這是我們用在 scull 的源碼中獲取主編號的代碼:


if (scull_major) {
 dev = MKDEV(scull_major, scull_minor);
 result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
 result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
 scull_major = MAJOR(dev);
}
if (result < 0) {
 printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
 return result;
}

本書使用的幾乎所有例子驅動使用類似的代碼來分配它們的主編號.

[6] 從 sysfs 中能獲取更好的設備信息, 在基于 2.6 的系統(tǒng)通常加載于 /sys. 但是使 scull 通過 sysfs 輸出信息超出了本章的范圍; 我們在 14 章中回到這個主題.

[7] Linux Standard Base 指出 init 腳本應當放在 /etc/init.d, 但是一些發(fā)布仍然放在別處. 另外, 如果你的腳本在啟動時運行, 你需要從合適的運行級別目錄做一個連接給它(也就是, .../rc3.d).

[8] 盡管某些內核開發(fā)者已警告說將來就會這樣做.

[9] init 腳本 scull.init 不在命令行中接受驅動選項, 但是它支持一個配置文件, 因為它被設計來在啟動和關機時自動使用.

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號