以上所舉的例子僅僅是一些常見的 sysfs 屬性用法,實際的系統中還常常有很多其它的從未見過的 sysfs 屬性,因此只有舉例是不夠的,即使維護了一份 sysfs 屬性用法參考大全也不夠,未來的內核版本還會出現新的 sysfs 屬性,因此還必須了解 Linux 內核代碼以找到實現這些屬性的代碼位置,以學會在沒有相應屬性文檔的情況從內核源代碼來分析其 sysfs 屬性功能。
Sysfs 源碼分析和編程實踐
從源代碼中理解 sysfs 屬性的用途
更多的 sysfs 屬性的功能只能靠閱讀源代碼來理解。還是以上文提到的 scsi_host 的 scan 屬性來理解,這個功能沒有任何文檔上有描述,因此只能去讀源代碼。
在內核中, sysfs 屬性一般是由 __ATTR 系列的宏來聲明的,如對設備的使用 DEVICE_ATTR ,對總線使用 BUS_ATTR ,對驅動使用 DRIVER_ATTR ,對類別(class)使用 CLASS_ATTR, 這四個高級的宏來自於 <include/linux/device.h>, 都是以更低層的來自 <include/linux/sysfs.h> 中的 __ATTR/__ATRR_RO 宏實現; 因此我們在內核源碼樹中相應位置 drivers/scsi/ 找到這幾個宏的使用情況,可以得到在 drivers/scsi/scsi_sysfs.c 中:
|
DEVICE_ATTR 宏聲明有四個參數,分別是名稱、權限位、讀函數、寫函數。這里對應的,名稱是 scan, 權限是只有屬主可寫(S_IWUSR)、沒有讀函數、只有寫函數。因此讀寫功能與權限位是對應的,因為 DEVICE_ATTR 把權限位聲明與真正的讀寫是否實現放在了一起,減少了出現不一致的可能。(上文提到 /proc/scsi/scsi 接口的權限位聲明與其功能不對應,這與注冊 proc 接口的函數設計中的不一致是有關系的,權限位聲明與功能實現不在代碼中同一個位置,因此易出錯。雖然修復 /proc/scsi/scsi 的權限位錯誤很容易,但內核團隊中多年來一直沒有人發現或未有人去修正這個 BUG,應該是與 /proc/scsi/ 接口的過時有關,過時的功能會在未來某個內核版本中去除。)
上面的 scan 屬性寫入功能是在 store_scan 函數中實現的,這個接口的四個參數中, buf/count 代表用戶寫入過來的字符串,它把 buf 進一步傳給了 scsi_scan 函數;如果進一步分析 scsi_scan 函數實現可以知道,它期望從 buf 中接受三個或四個整型值(也接受"-"作為通配符),分別代表 host, channel, id 三個值,(第四個整數在早期內核中曾代表 lun 號碼,但在較新內核中第四個數字被忽略,僅作為向后兼容保留接受四個整數),然后對具體的 (host, channel, id) 進行重新掃描以發現這個 SCSI 控制器上的設備變動。
添加 sysfs 支持
如果你正在開發的設備驅動程序中需要與用戶層的接口,一般可選的方法有:
注冊虛擬的字符設備文件,以這個虛擬設備上的 read/write/ioctl 等接口與用戶交互;但 read/write 一般只能做一件事情, ioctl 可以根據 cmd 參數做多個功能,但其缺點是很明顯的: ioctl 接口無法直接在 Shell 腳本中使用,為了使用 ioctl 的功能,還必須編寫配套的 C語言的虛擬設備操作程序, ioctl 的二進制數據接口也是造成大小端問題 (big endian與little endian)、32位/64位不可移植問題的根源;
注冊 proc 接口,接受用戶的 read/write/ioctl 操作;同樣的,一個 proc 項通常使用其 read/write/ioctl 接口,它所存在的問題與上面的虛擬字符設備的的問題相似;
注冊 sysfs 屬性;
最重要的是,添加虛擬字符設備支持和注冊 proc 接口支持這兩者所需要增加的代碼量都並不少,最好的方法還是使用 sysfs 屬性支持,一切在用戶層是可見的透明,且增加的代碼量是最少的,可維護性也最好;方法就是使用 <include/linux/device.h> 頭文件提供的這四個宏,分別應用於總線/類別/驅動/設備四種內核數據結構對象上:
#define BUS_ATTR(_name, _mode, _show, _store)
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define CLASS_ATTR(_name, _mode, _show, _store)
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define DRIVER_ATTR(_name, _mode, _show, _store)
struct driver_attribute driver_attr_##_name =
__ATTR(_name, _mode, _show, _store)
#define DEVICE_ATTR(_name, _mode, _show, _store)
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
|
總線(BUS)和類別(CLASS)屬性一般用於新設計的總線和新設計的類別,這兩者一般是不用的;因為你的設備一般是以PCI等成熟的常規方式連接到主機,而不會去新發明一種類型;使用驅動屬性和設備屬性的區別就在於:看你的 sysfs 屬性設計是針對整個驅動有效的還是針對這份驅動所可能支持的每個設備分別有效。
從頭文件中還可以找到 show/store 函數的原型,注意到它和虛擬字符設備或 proc 項的 read/write 的作用很類似,但有一點不同是 show/store 函數上的 buf/count 參數是在 sysfs 層已作了用戶區/內核區的內存復制,虛擬字符設備上常見的 __user 屬性在這里並不需要,因而也不需要多一次 copy_from_user/copy_to_user, 在 show/store 函數參數上的 buf/count 參數已經是內核區的地址,可以直接操作。
上面四種都是 Linux 統一設備模型所添加的高級接口,如果使用 sysfs 所提供的底層接口的話,則還有下面兩個,定義來自 <include/linux/sysfs.h> :(上面的總線/類別/驅動/設備四個接口都是以這里的__ATTR實現的)
#define __ATTR(_name,_mode,_show,_store) {
.attr = {.name = __stringify(_name), .mode = _mode },
.show = _show,
.store = _store,
}
#define __ATTR_RO(_name) {
.attr = { .name = __stringify(_name), .mode = 0444 },
.show = _name##_show,
} |
上面這些宏都是在注冊總線/類別/驅動/設備時作為缺省屬性而使用的,在實際應用中還有一種情況是根據條件動態添加屬性,如 PCI 設備上的 resource{0,1,2,...} 屬性文件,因為一個 PCI 設備上的可映射資源究竟有多少無法預知,也只能以條件判斷的方式動態添加上。
int __must_check sysfs_create_file(struct kobject *kobj,
const struct attribute *attr);
int __must_check sysfs_create_bin_file(struct kobject *kobj,
struct bin_attribute *attr); |
這兩個函數可以對一個 kobject 動態添加上文本屬性或二進制屬性,這也是唯一可以添加二進制屬性的方法。
二進制屬性與普通文本屬性的區別在於:
二進制屬性 struct bin_attribute 中內嵌一個 struct attribute 結構體對象,因此具有普通屬性的所有功能特征;
二進制屬性上多一個 size 用來描述此二進制文件的大小,而普通屬性文件的大小總是 4096, 准確地說,應該是一個內存頁的大小,因為從當前 sysfs 內核實現來說,它分配一個內存頁面來作為 (buf/count) 的緩沖區;
二進制屬性比普通屬性多內存映射(mmap)接口的支持;
編程示例,對 LDD3 一書中的 lddbus 驅動程序的 sysfs 改進
首先,這個程序本身是針對當時作者寫書的年代的內核(2.6.11)而編寫的,在當前的 Fedora10 系統 (2.6.27.5-117.fc10.i686) 上甚至無法編譯編譯通過;因此首先需要將它移植過來至少達到可運行狀態;
附件的壓縮包中含有修改過的 lddbus, sculld 的源代碼和修改過程的四個patch:
第一個 0001-ldd3-examples-build-on-fedora-10-2.6.27.5-117.fc10.i.patch 是將 lddbus 和 sculld 移植到 Fedora10 內核上可運行,這其中主要是一此內核 API 的變化;
第二個 0002-port-dmem-proc-entry-to-use-sysfs-entry.patch 演示了怎樣將原有的 proc 接口改進成為 sysfs 屬性接口的,從這個 patch 中可以看到刪除的代碼多而新增加的代碼少,這說明對於相同的功能,使用 sysfs 編程接口的代碼量更少,而且 sysfs 代碼看起來也比 proc 更為整潔:打印每個設備的調試信息可以做成每個設備上分別有自己的接口,而不是統一的一個 proc 接口;設備屬性文件最終出現的位置如 "/sys/devices/ldd0/sculld0/dmem";
static ssize_t sculld_show_dmem(struct device *ddev,
struct device_attribute *attr, char *buf)
{
/* 其中打印每個設備調試信息的代碼復制自原proc接口 */
}
static DEVICE_ATTR(dmem, S_IRUGO, sculld_show_dmem, NULL);
static int __init sculld_register_dev(struct sculld_dev *dev, int index)
{
/* 創建此device屬性文件 */
ret |= device_create_file(&dev->ldev.dev, &dev_attr_dmem);
} |
第三個 0003-add-.gitignore.patch 是增加了 .gitignore 文件,屏蔽一些編譯生成的臨時文件;
第四個 0004-port-qset-get-set-ioctl-to-use-sysfs-entry.patch 演示了怎樣把基於 ioctl 的操作接口改進成為基於 sysfs 接口,由於原來的 ioctl 接口設置和獲取 qset 信息是表示整個驅動模塊級的變量,它用來控制整個驅動程序而非驅動所支持的單個的設備,因此這個 qset 屬性使用 DRIVER_ATTR 來添加更為合適;
ssize_t sculld_show_qset(struct device_driver *driver, char *buf)
{
return snprintf(buf, PAGE_SIZE, "%dn", sculld_qset);
}
ssize_t sculld_store_qset(struct device_driver *driver, const char *buf,
size_t count)
{
sculld_qset = simple_strtol(buf, NULL, 0);
return count;
}
/* 聲明一個權限為0644的可同時讀寫的driver屬性 */
static DRIVER_ATTR(qset, S_IRUGO | S_IWUSR, sculld_show_qset, sculld_store_qset);
/* 創建此driver屬性文件 */
result = driver_create_file(&sculld_driver.driver, &driver_attr_qset);
|
驅動屬性最終出現如 "/sys/bus/ldd/drivers/sculld/qset" ,這里聲明的是同時可讀寫的,權限位 0644 與其保持一致。
6446 0 -rw-r--r-- 1 root root 4096 12月 14 07:44 /sys/bus/ldd/drivers/sculld/qset
小結
sysfs 給應用程序提供了統一訪問設備的接口,但可以看到, sysfs 僅僅是提供了一個可以統一訪問設備的框架,但究竟是否支持 sysfs 還需要各設備驅動程序的編程支持;在 2.6 內核誕生 5年以來的發展中,很多子系統、設備驅動程序逐漸轉向了 sysfs 作為與用戶空間友好的接口,但仍然也存在大量的代碼還在使用舊的 proc 或虛擬字符設備的 ioctl 方式;如果僅從最終用戶的角度來說, sysfs 與 proc 都是在提供相同或類似的功能,對於舊的 proc 代碼,沒有絕對的必要去做 proc 至 sysfs 的升級;因此在可預見的將來, sysfs 會與 proc, debugfs, configfs 等共存很長一段時間。