5. 設備驅動程序
-
Linux 內核是一個比較龐大的系統,深入理解內核可以減少在系統移植中的障礙。在系統移植中設備驅動開發是一項很復雜的工作,由于 Linux 內核提供了一部分源代碼,同時還提供了對某些公共部分的支持,例如, USB 驅動對讀寫 U 盤、鍵盤、鼠標等設備提供了通用驅動程序,一般情況可以直接使用內核提供的驅動。但是對于復雜的 USB 設備沒有現成的驅動,就需要讀者對驅動開發過程有一定的認識,必要時參考 Linux 源碼重新開發驅動程序。
-
設備驅動,實際上是硬件功能的一個抽象。針對同一個硬件不同的驅動可以將硬件封裝成不同的功能。設備驅動是硬件層和應用程序(或者操作系統)的媒介,能夠讓應用程序或者操作系統使用硬件。
-
在 Linux 操作系統下有 3 類主要的設備文件類型:塊設備、字符設備和網絡設備。設備驅動程序是指管理某個外圍設備的一段代碼,它負責傳送數據、控制特定類型的物理設備的操作,包括開始和完成 I/O 操作,檢測和處理設備出現的錯誤。
1. 字符設備驅動程序
-
字符設備是一種能像字節流一樣進行串行訪問的設備,對設備的存取只能按順序、按字節存取,不能隨機訪問。字符設備沒有請求緩沖區,必須按順序執行所有的訪問請求。常見的字符設備有鼠標、鍵盤、串口、控制臺等。
-
應用程序對字符設備的訪問是通過字符設備結點來完成的。字符設備是 Linux 中最簡單的設備,可以像文件一樣訪問。應用程序使用標準系統調用打開、讀、寫和關閉字符設備,完全可以把它們當做普通文件一樣進行操作,甚至被 PPP 守護進程使用,用于將一個 Linux系統連接到網上的 modem,也被看做一個普通文件。
-
當字符設備初始化時,它的設備驅動程序向 Linux 內核注冊,向
chrdevs
向量表中增加一個device_struct
數據結構項。 -
通常一種類型設備的主設備標識符是固定的,例如 tty 設備是 4。設備的主設備標識符,用作chrdevs 向量表的索引。
-
向量表中的每一項(即 device_struct 數據結構)包括兩個元素:
- 一個是指向登記的設備驅動程序名字的指針;
- 另一個是指向一組文件操作的指針。這組文件操作本身位于這個設備的字符設備驅動程序中,每一個都處理一個特定的文件操作(如打開、讀、寫和關閉)。
-
用戶進程通過設備文件對硬件進行訪問,對設備文件的操作方式通過一些系統調用來實現,如
open
、read
、write
和close
等。下面通過一個關鍵的數據結構file_operations
,將系統調用和驅動程序關聯起來:struct file_operations {int (*seek) (struct inode * , struct file *, off_t , int);int (*read) (struct inode * , struct file *, char , int);int (*write) (struct inode * , struct file *, off_t , int);int (*readdir) (struct inode * , struct file *, struct dirent * , int);int (*select) (struct inode * , struct file *, int , select_table *);int (*ioctl) (struct inode * , struct file *, unsined int , unsigned long);int (*mmap) (struct inode * , struct file *, struct vm_area_struct *);int (*open) (struct inode * , struct file *);int (*release) (struct inode * , struct file *);int (*fsync) (struct inode * , struct file *);int (*fasync) (struct inode * , struct file *, int);int (*check_media_change) (struct inode * , struct file *);int (*revalidate) (dev_t dev); };
- 該結構中每一個成員的名字都對應著一個系統調用。用戶進程利用系統調用在對設備文件進行諸如 read/write 操作時,系統調用根據設備文件的主設備號找到對應的設備驅動程序,然后讀取這個數據結構相應的函數指針,接著把控制權交給該函數。
-
編寫驅動程序就是針對上面相應的函數編寫具體的實現,然后將它們對應上。編寫完驅動后,把驅動程序嵌入內核。驅動程序可以采用兩種方式進行編譯:一種是編譯進內核,驅動被靜態加載;另一種是編譯成模塊(modules),驅動模塊需要動態加載。
-
在模塊被調入內存時,
init()
函數向系統的字符設備表登記了一個字符設備:int __init chr_dev_init(void) {if (devfs_register_chrdev(CHR_MAJOR,"chr_name",&chr_fops))printk("unable to get major %d for chr devs\n", MEM_MAJOR);...return 0; }
-
當
cleanup_chr_dev()
函數被調用時,它釋放字符設備 chr_name 在系統字符設備表中占有的表項:void cleanup_chr_dev(void) {unregister_chrdev(CHR_MAJOR, "chr_name"); }
2. 塊設備驅動程序
-
塊設備具有請求緩沖區,從塊設備讀取數據時,可以從任意位置讀取任意長度,即塊設備支持隨機訪問而不必按照順序存取數據。例如,可以先存取后面的數據,然后再存取前面的數據,字符設備則不能采用該方式存取數據。 常見的塊設備有各種硬盤、 flash 磁盤、 RAM 磁盤等。Linux 下的磁盤設備均為塊設備,應用程序訪問 Linux 下的塊設備結點是通過文件系統及其高速緩存來訪問塊設備的,并非直接通過設備結點讀寫塊設備上的數據。
-
塊設備既可以用做普通的裸設備存放任意數據,也可以將塊設備按某種文件系統類型的格式進行格式化,然后根據該文件系統類型的格式進行讀取。無論使用哪種方式,訪問設備上的數據都必須通過調用設備本身的方法實現。兩者的區別在于前者直接調用塊設備的操作方法,而后者則間接(通過文件系統)調用塊設備的操作方法。
-
塊設備用與字符設備類似的方法進行設備的注冊與釋放。塊設備使用
register_blkdev()
函數和block_device_operations
結構的指針,其中定義的open
、release
和ioctl
方法和字符設備的對應方法相同,但沒有對 read 和 write 操作定義,因為所有涉及塊設備的 I/O 通常由系統進行緩沖處理。 -
塊驅動程序最終必須提供完成實際塊 I/O 操作的機制,在 Linux 中,用于這些 I/O 操作的方法稱為
request
(請求)。 -
注冊塊設備時,通過
blk_init_queue
來完成對 request 隊列的初始化,blk_init_queue
函數創建隊列,并將該驅動程序的 request 函數關聯到隊列。在模塊的清除階段,調用blk_cleanup_queue
函數。 -
初始化塊設備的時候,將塊設備注冊到內核中,下面為塊設備的注冊函數
mtdblock_release()
的實現:int register_blkdev(unsigned int major, const char *name) {struct blk_major_name **n, *p;int index, ret = 0;mutex_lock(&block_class_lock);/*為塊設備指定主設備號,如果指定為 0 則表示由系統來分配*/if (major == 0) {for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) {if (major_names[index] == NULL)break;}if (index == 0) {printk("register_blkdev: failed to get major for %s\n", name);ret = -EBUSY;goto out;}major = index;ret = major;}/*為塊設備名字分配空間*/p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL);if (p == NULL) {ret = -ENOMEM;goto out;}p->major = major;strlcpy(p->name, name, sizeof(p->name));p->next = NULL;index = major_to_index(major);for (n = &major_names[index]; *n; n = &(*n)->next) {if ((*n)->major == major)break;}if (!*n)*n = p;elseret = -EBUSY;if (ret < 0) {printk("register_blkdev: cannot get major %d for %s\n",major, name);kfree(p);} out:mutex_unlock(&block_class_lock);return ret; }
-
塊設備被注冊到系統后,訪問硬件的操作 open 和 release 等就能夠被對應的系統調用指針所綁定,應用程序使用系統調用就可以對硬件進行訪問了。下面是塊設備主要的操作函數 open()和 release():
-
塊設備 open()操作函數:
static int mtdblock_open(struct mtd_blktrans_dev *mbd) {struct mtdblk_dev *mtdblk;struct mtd_info *mtd = mbd->mtd;int dev = mbd->devnum;DEBUG(MTD_DEBUG_LEVEL1,"mtdblock_open\n");if (mtdblks[dev]) {/*如果設備已經打開,則只需要增加其引用計數*/mtdblks[dev]->count++;return 0;}/*為設備創建 mtdblk_dev 對象保存 mtd 設備的信息*/mtdblk = kzalloc(sizeof(struct mtdblk_dev), GFP_KERNEL);if (!mtdblk)return -ENOMEM;mtdblk->count = 1;mtdblk->mtd = mtd;mutex_init(&mtdblk->cache_mutex);mtdblk->cache_state = STATE_EMPTY;if ( !(mtdblk->mtd->flags & MTD_NO_ERASE) && mtdblk->mtd->erasesize){mtdblk->cache_size = mtdblk->mtd->erasesize;mtdblk->cache_data = NULL;}mtdblks[dev] = mtdblk;DEBUG(MTD_DEBUG_LEVEL1, "ok\n");return 0; }
-
塊設備 release()操作函數:
static int mtdblock_release(struct mtd_blktrans_dev *mbd) {int dev = mbd->devnum;struct mtdblk_dev *mtdblk = mtdblks[dev];DEBUG(MTD_DEBUG_LEVEL1, "mtdblock_release\n");mutex_lock(&mtdblk->cache_mutex);write_cached_data(mtdblk);mutex_unlock(&mtdblk->cache_mutex);if (!--mtdblk->count) { mtdblks[dev] = NULL; /*用戶計數遞減為 0 時釋放設備*/if (mtdblk->mtd->sync)mtdblk->mtd->sync(mtdblk->mtd);vfree(mtdblk->cache_data);kfree(mtdblk);}DEBUG(MTD_DEBUG_LEVEL1, "ok\n");return 0; }
-
3. 網絡設備驅動程序
-
網絡設備是面向數據報文的、不支持隨機訪問, 也沒有請求緩沖區。在 Linux里網絡設備也可以被稱為網絡接口,如 eth0,應用程序是通過 Socket(套接字),而不是設備結點來訪問網絡設備,在系統中不存在網絡設備結點。
-
網絡設備用來與其他設備交換數據,它可以是硬件設備,也可以是純軟件設備,如loopback 接口。網絡設備由內核中的網絡子系統驅動,負責發送和接收數據包,但它不需要了解每項事務如何映射到實際傳送的數據包。 許多網絡連接(如TCP連接)是面向流的,但網絡設備圍繞數據包的傳輸和接收設計。
-
網絡驅動程序不需要知道各個連接的相關信息,它只需處理數據包。字符設備和塊設備都有設備號,而網絡設備沒有設備號,只有一個獨一無二的名字,例如 eth0、 eth1 等,這個名字也無須與設備文件結點對應。
-
內核利用一組數據包傳輸函數與網絡設備驅動程序進行通信,它們不同于字符設備和塊設備的 read()和 write()方法。
-
Linux 網絡設備驅動程序從下到上分為 4 層,依次為”網絡設備與媒介層、設備驅動功能層、網絡設備接口層和網絡協議接口層“。
-
在設計具體的網絡設備驅動程序時,需要完成的主要工作是編寫設備驅動功能層的相關函數以填充
net_device
數據結構的內容,并將net_device
注冊入內核。 -
下面以 DM9000 代碼為例說明網絡設備驅動的注冊、注銷等主要過程。
-
驅動的注冊(在設備初始化時被調用)
static int __init dm9000_init(void) {printk(KERN_INFO "%s Ethernet Driver, V%s\n", CARDNAME,DRV_VERSION);return platform_driver_register(&dm9000_driver); }int platform_driver_register(struct platform_driver *drv) {drv->driver.bus = &platform_bus_type;if (drv->probe)drv->driver.probe = platform_drv_probe;if (drv->remove)drv->driver.remove = platform_drv_remove;if (drv->shutdown)drv->driver.shutdown = platform_drv_shutdown;if (drv->suspend)drv->driver.suspend = platform_drv_suspend;if (drv->resume)drv->driver.resume = platform_drv_resume;return driver_register(&drv->driver); }
-
驅動的注銷(在設備被清除時被調用,其中包括將設備從系統中移除和將驅動從總線上移除。 )
static void __exit dm9000_cleanup(void) {platform_driver_unregister(&dm9000_driver); }void platform_driver_unregister(struct platform_driver *drv) {driver_unregister(&drv->driver); }void driver_unregister(struct device_driver *drv) {driver_remove_groups(drv, drv->groups);bus_remove_driver(drv); }static void driver_remove_groups(struct device_driver *drv, struct attribute_group **groups) {int i;if (groups)for (i = 0; groups[i]; i++)sysfs_remove_group(&drv->p->kobj, groups[i]); }void bus_remove_driver(struct device_driver *drv) {if (!drv->bus)return;remove_bind_files(drv);driver_remove_attrs(drv->bus, drv);driver_remove_file(drv, &driver_attr_uevent);klist_remove(&drv->p->knode_bus);pr_debug("bus: '%s': remove driver %s\n", drv->bus->name,drv->name);driver_detach(drv);module_remove_driver(drv);kobject_put(&drv->p->kobj);bus_put(drv->bus); }
- 有關網絡設備驅動的詳細接口函數解析和驅動移植將在后面的章節中敘述。
-
4. 內存與I/O操作
-
一般來說,在系統運行時,外設的 I/O 內存資源的物理地址是已知的,由硬件的設計決定。但是 CPU 通常并沒有為這些已知的外設 I/O 內存資源的物理地址,預定義虛擬地址范圍,驅動程序并不能直接通過物理地址訪問 I/O 內存資源,只能先將它們映射到內核的虛擬地址空間內(通過頁表),然后才能根據映射的內核虛擬地址范圍訪問這些 I/O 內存資源。
-
Linux 在
io.h
頭文件中聲明了函數ioremap()
和iounmap()
,分別用來將 I/O 內存資源的物理地址映射和解映射到核心虛擬地址空間(3GB~4GB)中,原型如下:void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags); void iounmap(void * addr);
-
在將 I/O 內存資源的物理地址映射成內核的虛擬地址后,就可以像讀寫 RAM 那樣直接讀寫 I/O 內存資源了。但為了保證驅動程序跨平臺的可移植性,應該使用 Linux 中特定的函數訪問 I/O 內存資源,而不是通過指向內核虛擬地址的指針直接訪問。如在 ARM平臺上,讀寫 I/O 的函數如下:
#define __raw_base_writeb(val,base,off) __arch_base_putb(val,base,off) #define __raw_base_writew(val,base,off) __arch_base_putw(val,base,off) #define __raw_base_writel(val,base,off) __arch_base_putl(val,base,off)#define __raw_base_readb(base,off) __arch_base_getb(base,off) #define __raw_base_readw(base,off) __arch_base_getw(base,off) #define __raw_base_readl(base,off) __arch_base_getl(base,off)
-
驅動程序中
mmap()
函數的實現原理是,用 mmap 映射一個設備,表示將用戶空間的一段地址關聯到設備內存上,這樣當程序在分配的地址范圍內進行讀取或者寫入時,實際上就是對設備的訪問。這一映射原理類似于 Linux 下 mount 命令,將一種類型的文件系統或設備掛載到另外一個文件系統或者目錄下時,掛載成功后,對掛載點的任何操作實際上是對被掛載的文件系統和設備的操作。