談這些概念之前,首先不得不說下devfs。devfs(裝置檔案系統)時由Linux2.4核心引入的,它的出現可以使得程式在裝置初始化時在/dev目錄下建立裝置檔案,解除安裝時将它删除。雖然它在2.6核心版本後已被udev取代,這裡還是簡要列出它的範例,友善後面的分析。
static devfs_handle_t devfs_handle
static int __init xxx_init(void){
int ret;
/* 核心中注冊裝置 */
ret = register_chrdev(XXX_MAJOR, DEVICE_NAME, &xxx_fops);
if(ret < ){...}
/* 建立裝置檔案 */
devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT,
XXX_MAJOR, , S_IFCHR | S_IRUSR | S_IWUSR, &xxx_fops, NULL);
return ;
}
static void __exit xxx_exit(void){
devfs_unregister(devfs_handle); /* 撤銷裝置檔案 */
unregister_chrdev(XXX_MAJOR, DEVICE_NAME); /* 登出裝置 */
}
devfs_register()和devfs_unregister()這些API已經被删除了,但register_chrdev()和unregister_chrdev()在Linux2.6以後的核心中仍被采用,在程式中可以直接給register_chrdev()傳遞0主裝置号以獲得可用的主裝置号,這個功能還是很好用的,但是,卻不被推薦使用,因為在udev中出現了新的申請裝置号的方式。
udev中注冊裝置的方法如下。
/* 主裝置号和從裝置号 */
static int dev_major = ;
static int dev_minor = ;
/* 裝置結構體定義 */
struct dev_struct{
struct cdev cdev; /* cdev 結構體 */
int val; /* 裝置的一個屬性,這裡定義它為裝置的一個寄存器 */
struct semaphore sem; /* 信号量,用于裝置驅動并發控制 */
... /* 自定義的其它資料 */
};
dev_struct *struct_devp; /* 裝置結構體指針 */
static ini __init xxx_init(void){
int err;
dev_t devno = ;
/* 動态配置設定主裝置号和從裝置号 */
err = alloc_chrdev_region(&devno, , , DEVICE_NAME);
if(err < ){ ... }
dev_major = MAJOR(devno);
/* 動态申請裝置結構體的記憶體 */
struct_devp = kmalloc(sizeof(struct dev_struct), GFP_KERNEL);
if(!struct_devp){ ... }
memset(struct_devp, , sizeof(struct dev_struct));
/* 初始化裝置 */
devno = MKDEV(dev_major, dev_minor);
cdev_init(&struct_devp->cdev, &xxx_fops);
struct_devp->cdev.owner = THIS_MODULE;
struct_devp->cdev.ops = &xxx_fops;
err = cdev_add(&struct_devp->cdev, devno, );
if(err){ ... }
/* 初始化信号量和寄存器val的值 */
init_MUTE(struct_devp->sem);
struct_devp->val = ;
}
這裡僅僅是udev方式下利用cdev結構體注冊裝置的過程,也就是devfs下register_chrdev()所做的東西,可以看到,代碼多了幾行,其驅動使用的便利性也是要産生額外的步驟的。其中,udev下申請裝置号有兩種方法,動态申請裝置号如上面利用
alloc_chrdev_region(&devno, 0, 1, DEVICE_MODE_NAME)
函數,如果已知主裝置号,可以通過
register_chrdev_region(devno, 1, DEVICE_MODE_NAME)
函數來申請。
那麼,在udev方式下怎麼生成裝置檔案呢,這就回到udev的概念上了,udev是作業系統的一個守護程序,下面是它的說明。
udev是硬體平台無關的,屬于user space的程序,它脫離驅動層的關聯而建立在作業系統之上,基于這種設計實作,我們可以随時修改及删除/dev下的裝置檔案名稱和指向,随心所欲地按照我們的願望安排和管理裝置檔案系統,而完成如此靈活的功能隻需要簡單地修改udev的配置檔案即可,無需重新啟動作業系統。udev已經使得我們對裝置的管理如探囊取物般輕松自如。
udev建立裝置檔案是通過讀取核心從netlink套接字裡發出的資訊來工作,比如,當硬體裝置加入或移除時,核心會發送熱插拔事件。繼續分析,就設計到udev的規則檔案了,我們這裡隻說說概念就行了。在嵌入式系統中,也可以使用udev的輕量級版本mdev,mdev內建與busybox中。Android中采用vold,它和udev是一樣的,同樣是監聽基于netlink的套接字,并解析收到的消息。
說了這麼多,下面給出在程式中利用udev機制建立裝置檔案的方法。接着上面xxx_init()函數裡的程式繼續。
static struct class* dev_class = NULL;
struct device* dev_device = NULL;
/* 在/sys/class/目錄下建立裝置類别目錄DEVICE_CLASS */
dev_class = class_create(THIS_MODULE, DEVICE_CLASS);
if(IS_ERR(dev_class)){ ... }
/* 在/dev/目錄和/sys/class/DEVICE_CLASS目錄下分别建立裝置檔案DEVICE_NAME */
dev_device = device_create(dev_class, NULL, devno, "%s", DEVICE_NAME);
if(IS_ERR(dev_device)){ ... }
其中class_create()函數建立了一個struct class結構體,作為device_create()函數的參數。device_create()函數建立一個裝置并将其注冊到sysfs中,同時在系統的sys/class和sys/device目錄下會生成相應的類和裝置入口。此外device_create()函數還會發出使用者空間udev的動作,udev會根據sysfs下的class在/dev/目錄下建立裝置節點,這為自動建立裝置節點提供了一種途徑。通過該裝置節點(/dev/DEVICE_NAME),可以像通路檔案的方式來通路裝置,其中檔案的各個通路函數由初始化裝置程式
cdev_init(&struct_devp->cdev, &xxx_fops);
中xxx_fops結構體來指定。
通過device_create函數,我們就可以不用mknod指令手動的建立裝置節點了。由此看來,udev與sysfs息息相關,下面說說sys檔案系統。sysfs檔案系統能直覺地反應加入計算機的總線、裝置的資訊(通過kobject對象實作)。但是,它僅僅是一種展現和反應,并不能如devfs一樣,在生成裝置檔案時(不管裝置在不在),就直接把裝置驅動加載進來(其實,這是不必要的)。如果想智能地控制外部裝置的管理,還需要一個任務,一個專業的工具。在devfs裡,是由一個核心線程完成的。而udev正好相反,它通過等待核心注冊過的裝置熱插拔引起的事件,激發相應的功能。
sysfs的作用與proc有些類似,但除了與proc相同的具有檢視和設定核心參數功能之外,還有為Linux統一裝置驅動模型作為管理之用。sysfs相比proc,設計上比較清晰。一個proc虛拟檔案可能有内部格式,如/proc/scsi/scsi,它是可讀可寫的,并且讀寫格式不一樣,代表不同的操作,應用程式讀到了這個檔案的内容一般還需要進行字元串解析,而在寫入時需要先用字元串格式化按指定的格式寫入字元串進行操作;相比而言,sysfs的設計原則是一個屬性檔案隻做一件事情,sysfs屬性檔案一般隻有一個值,直接讀取或寫入。真個/proc/scsi目錄在2.6核心中已被标記為過時(LEGACY),它的功能已經被相應的/sys屬性檔案所完全取代。新設計的核心機制應該盡量使用sysfs機制,而将proc保留給純淨的“程序檔案系統”。
下面對比下通過proc檔案系統控制裝置以及通過sysfs通路裝置屬性檔案的方法。首先看在sysfs下建立裝置屬性檔案。
/* 在/sys/class/DEVICE_CLASS/DEVICE_CLASS目錄下建立屬性檔案val */
err = device_create_file(dev_device, &dev_attr_val);
if(err < ){ ... }
dev_set_drvdata(dev_device, struct_devp);
/* 定義裝置屬性 */
static DEVICE_ATTR(val, S_IRUGO|S_IWUSR, dev_val_show, dev_val_store);
/* 通路設定屬性方法 */
static ssize_t dev_val_show(struct device* dev, struct device_attribute* attr, char* buf);
static ssize_t dev_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count);
/* 讀取裝置屬性 */
static ssize_t dev_val_show(struct device* dev, struct device_attribute* attr, char* buf){
struct dev_struct* hdev = (struct dev_struct*)dev_get_drvdata(dev);
return __dev_get_val(hdev, buf);
}
/* 寫裝置屬性 */
static ssize_t dev_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count){
struct dev_struct* hdev = (struct dev_struct*)dev_get_drvdata(dev);
return __dev_set_val(hdev, buf, count);
}
/* 讀取寄存器val的值到緩沖區buf中,内部使用 */
static ssize_t __dev_get_val(struct dev_struct* dev, char* buf){
int val = ;
/* 同步通路 */
if(down_interruptible(&(dev->sem))){
return -ERESTAETSYS;
}
val = dev->val;
up(&(dev->sem));
return snprintf(buf, PAGE_SIZE, "%d\n", val);
}
/* 把緩沖區buf的值寫到裝置寄存器val中去,内部使用 */
static ssize_t __dev_set_val(struct dev_struct* dev, const char* buf, size_t count){
int val = ;
/* 将字元串轉成數字 */
val = simple_strtol(buf, NULL, );
/* 同步通路 */
if(down_interruptible(&(dev->sem))){
return -ERESTAETSYS;
}
dev->val = val;
up(&(dev->sem));
return count;
}
在sysfs下建立裝置屬性檔案很簡單,也就兩個函數,
device_create_file(dev_device, &dev_attr_val)
函數将dev_device這個裝置與val屬性綁定,val屬性通過
static DEVICE_ATTR(val, S_IRUGO|S_IWUSR, dev_val_show, dev_val_store);
定義,其中
S_IRUGO|S_IWUSR
表示val屬性檔案可讀可寫,其讀寫函數分别為
dev_val_show()
和
dev_val_store()
。
dev_set_drvdata(dev_device, struct_devp)
函數将dev_device裝置與struct_devp裝置結構體指針綁定,後面讀寫函數中通過
struct dev_struct* hdev = (struct dev_struct*)dev_get_drvdata(dev);
方式将裝置結構體指針取出,進而通路裝置結構體中的屬性。
接着看在/proc目錄下建立裝置屬性節點的方法,程式如下。
/* 建立/proc/hello檔案 */
struct proc_dir_entry* entry;
entry = create_proc_entry(DEVICE_PROC_NAME, , NULL);
if(entry){
entry->read_proc = dev_proc_read;
entry->write_proc = dev_proc_write;
}
/* 讀取裝置寄存器val的值,儲存在page緩沖區中 */
static ssize_t dev_proc_read(char* page, char** start, off_t off, int count, int* eof, void* data){
if(off > ){
*eof = ;
return ;
}
return __dev_get_val(struct_devp, page);
}
/* 把緩沖區的值buff儲存到裝置寄存器val中 */
static ssize_t dev_proc_write(struct file* filp, const char __user* buff, unsigned long len, void* data){
int err = ;
char* page = NULL;
if(len > PAGE_SIZE){
printk(KERN_ALERT"The buff is too large:%lu.\n", len);
return -EFAULT;
}
page = (char*)__get_free_page(GFP_KERNEL);
if(!page){
printk(KERN_ALERT"Failed to alloc page.\n");
return -ENOMEM;
}
/* 先把使用者提供的緩沖區值複制到核心緩沖區中 */
if(copy_from_user(page, buff, len)){
printk(KERN_ALERT"Failed to copy buff from user.\n");
err = -EFAULT;
goto out;
}
err = __dev_set_val(struct_devp, page, len);
out:
free_page((unsigned long)page);
return err;
}
可以看到,通過proc檔案系統通路裝置屬性與通過sys檔案系統通路裝置屬性極為類似,在proc檔案系統中,通過
entry = create_proc_entry(DEVICE_PROC_NAME, 0, NULL);
建立/proc/DEVICE_PROC_NAME裝置節點,通過
entry->read_proc = dev_proc_read; entry->write_proc = dev_proc_write;
來指定通路裝置屬性的方法。
相對而言,sys檔案系統中将裝置進行了分類管理,通過
class_create(THIS_MODULE, DEVICE_CLASS);
的方式将裝置進行分類到/sys/class目錄下,class目錄包含系統中的裝置類型(如網卡裝置、聲霸卡裝置、輸入裝置等),/sys下還有block目錄包含所有的塊裝置,devices目錄包含系統所有的裝置,并根據裝置挂接的總線類型組織成層次結構,bus目錄包含系統中的所有總線類型。sysfs下将linux驅動的拓撲結構整理為總線、裝置和類的關系,将裝置、驅動挂在總線上,由總線來比對裝置和驅動,達到系統探測到裝置,然後再加載驅動的效果。這一篇并沒有用到總線,直接就将驅動加載上了,後面再單獨一篇寫linux的裝置驅動模型。