## 3.2.?主次編號
字符設備通過文件系統中的名子來存取. 那些名子稱為文件系統的特殊文件, 或者設備文件, 或者文件系統的簡單結點; 慣例上它們位于 /dev 目錄. 字符驅動的特殊文件由使用 ls -l 的輸出的第一列的"c"標識. 塊設備也出現在 /dev 中, 但是它們由"b"標識. 本章集中在字符設備, 但是下面的很多信息也適用于塊設備.
如果你發出 ls -l 命令, 你會看到在設備文件項中有 2 個數(由一個逗號分隔)在最后修改日期前面, 這里通常是文件長度出現的地方. 這些數字是給特殊設備的主次設備編號. 下面的列表顯示了一個典型系統上出現的幾個設備. 它們的主編號是 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
~~~
傳統上, 主編號標識設備相連的驅動. 例如, /dev/null 和 /dev/zero 都由驅動 1 來管理, 而虛擬控制臺和串口終端都由驅動 4 管理; 同樣, vcs1 和 vcsa1 設備都由驅動 7 管理. 現代 Linux 內核允許多個驅動共享主編號, 但是你看到的大部分設備仍然按照一個主編號一個驅動的原則來組織.
次編號被內核用來決定引用哪個設備. 依據你的驅動是如何編寫的(如同我們下面見到的), 你可以從內核得到一個你的設備的直接指針, 或者可以自己使用次編號作為本地設備數組的索引. 不論哪個方法, 內核自己幾乎不知道次編號的任何事情, 除了它們指向你的驅動實現的設備.
### 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.?分配和釋放設備編號
在建立一個字符驅動時你的驅動需要做的第一件事是獲取一個或多個設備編號來使用. 為此目的的必要的函數是 register_chrdev_region, 在 <linux/fs.h>中聲明:
~~~
int register_chrdev_region(dev_t first, unsigned int count, char *name);
~~~
這里, first 是你要分配的起始設備編號. first 的次編號部分常常是 0, 但是沒有要求是那個效果. count 是你請求的連續設備編號的總數. 注意, 如果 count 太大, 你要求的范圍可能溢出到下一個次編號; 但是只要你要求的編號范圍可用, 一切都仍然會正確工作. 最后, name 是應當連接到這個編號范圍的設備的名子; 它會出現在 /proc/devices 和 sysfs 中.
如同大部分內核函數, 如果分配成功進行, register_chrdev_region 的返回值是 0. 出錯的情況下, 返回一個負的錯誤碼, 你不能存取請求的區域.
如果你確實事先知道你需要哪個設備編號, register_chrdev_region 工作得好. 然而, 你常常不會知道你的設備使用哪個主編號; 在 Linux 內核開發社團中一直努力使用動態分配設備編號. 內核會樂于動態為你分配一個主編號, 但是你必須使用一個不同的函數來請求這個分配.
~~~
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
~~~
使用這個函數, dev 是一個只輸出的參數, 它在函數成功完成時持有你的分配范圍的第一個數. fisetminor 應當是請求的第一個要用的次編號; 它常常是 0. count 和 name 參數如同給 request_chrdev_region 的一樣.
不管你任何分配你的設備編號, 你應當在不再使用它們時釋放它. 設備編號的釋放使用:
~~~
void unregister_chrdev_region(dev_t first, unsigned int count);
~~~
調用 unregister_chrdev_region 的地方常常是你的模塊的 cleanup 函數.
上面的函數分配設備編號給你的驅動使用, 但是它們不告訴內核你實際上會對這些編號做什么. 在用戶空間程序能夠存取這些設備號中一個之前, 你的驅動需要連接它們到它的實現設備操作的內部函數上. 我們將描述如何簡短完成這個連接, 但首先顧及一些必要的枝節問題.
### 3.2.3.?主編號的動態分配
一些主設備編號是靜態分派給最普通的設備的. 一個這些設備的列表在內核源碼樹的 Documentation/devices.txt 中. 分配給你的新驅動使用一個已經分配的靜態編號的機會很小, 但是, 并且新編號沒在分配. 因此, 作為一個驅動編寫者, 你有一個選擇: 你可以簡單地撿一個看來沒有用的編號, 或者你以動態方式分配主編號. 只要你是你的驅動的唯一用戶就可以撿一個編號用; 一旦你的驅動更廣泛的被使用了, 一個隨機撿來的主編號將導致沖突和麻煩.
因此, 對于新驅動, 我們強烈建議你使用動態分配來獲取你的主設備編號, 而不是隨機選取一個當前空閑的編號. 換句話說, 你的驅動應當幾乎肯定地使用 alloc_chrdev_region, 不是 register_chrdev_region.
動態分配的缺點是你無法提前創建設備節點, 因為分配給你的模塊的主編號會變化. 對于驅動的正常使用, 這不是問題, 因為一旦編號分配了, 你可從 /proc/devices 中讀取它.[[6](#)]
為使用動態主編號來加載一個驅動, 因此, 可使用一個簡單的腳本來代替調用 insmod, 在調用 insmod 后, 讀取 /proc/devices 來創建特殊文件.
一個典型的 /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
~~~
因此加載一個已經安排了一個動態編號的模塊的腳本, 可以使用一個工具來編寫, 如 awk , 來從 /proc/devices 獲取信息以創建 /dev 中的文件.
下面的腳本, snull_load, 是 scull 發布的一部分. 以模塊發布的驅動的用戶可以從系統的 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 行來適用于另外的驅動. 這個腳本僅僅展示了創建 4 個設備, 因為 4 是 scull 源碼中缺省的.
腳本的最后幾行可能有些模糊:為什么改變設備的組和模式? 理由是這個腳本必須由超級用戶運行, 因此新建的特殊文件由 root 擁有. 許可位缺省的是只有 root 有寫權限, 而任何人可以讀. 通常, 一個設備節點需要一個不同的存取策略, 因此在某些方面別人的存取權限必須改變. 我們的腳本缺省是給一個用戶組存取, 但是你的需求可能不同. 在第 6 章的"設備文件的存取控制"一節中, sculluid 的代碼演示了驅動如何能夠強制它自己的對設備存取的授權.
還有一個 scull_unload 腳本來清理 /dev 目錄并去除模塊.
作為對使用一對腳本來加載和卸載的另外選擇, 你可以編寫一個 init 腳本, 準備好放在你的發布使用這些腳本的目錄中. [[7](#)]作為 scull 源碼的一部分, 我們提供了一個相當完整和可配置的 init 腳本例子, 稱為 scull.init; 它接受傳統的參數 -- start, stop, 和 restart -- 并且完成 scull_load 和 scull_unload 的角色.
如果反復創建和銷毀 /dev 節點, 聽來過分了, 有一個有用的辦法. 如果你在加載和卸載單個驅動, 你可以在你第一次使用你的腳本創建特殊文件之后, 只使用 rmmod 和 insmod: 這樣動態編號不是隨機的. [[8](#)]并且你每次都可以使用所選的同一個編號, 如果你不加載任何別的動態模塊. 在開發中避免長腳本是有用的. 但是這個技巧, 顯然不能擴展到一次多于一個驅動.
安排主編號最好的方式, 我們認為, 是缺省使用動態分配, 而留給自己在加載時指定主編號的選項權, 或者甚至在編譯時. scull 實現以這種方式工作; 它使用一個全局變量, scull_major, 來持有選定的編號(還有一個 scull_minor 給次編號). 這個變量初始化為 SCULL_MAJOR, 定義在 scull.h. 發布的源碼中的 SCULL_MAJOR 的缺省值是 0, 意思是"使用動態分配". 用戶可以接受缺省值或者選擇一個特殊主編號, 或者在編譯前修改宏定義或者在 insmod 命令行指定一個值給 scull_major. 最后, 通過使用 scull_load 腳本, 用戶可以在 scull_load 的命令行傳遞參數給 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 的系統通常加載于 /sys. 但是使 scull 通過 sysfs 輸出信息超出了本章的范圍; 我們在 14 章中回到這個主題.
[[7](#)] Linux Standard Base 指出 init 腳本應當放在 /etc/init.d, 但是一些發布仍然放在別處. 另外, 如果你的腳本在啟動時運行, 你需要從合適的運行級別目錄做一個連接給它(也就是, .../rc3.d).
[[8](#)] 盡管某些內核開發者已警告說將來就會這樣做.
[[9](#)] init 腳本 scull.init 不在命令行中接受驅動選項, 但是它支持一個配置文件, 因為它被設計來在啟動和關機時自動使用.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考