1. 程式人生 > >Linux下PCI裝置驅動程式開發基本框架

Linux下PCI裝置驅動程式開發基本框架

PCI是一種廣泛採用的匯流排標準,它提供了許多優於其它匯流排標準(如EISA)的新特性,目前已經成為計算機系統中應用最為廣泛,並且最為通用的匯流排標準。Linux的核心能較好地支援PCI匯流排,本文以Intel 386體系結構為主,探討了在Linux下開發PCI裝置驅動程式的基本框架。
  
  一、PCI匯流排系統體系結構
  
  PCI是外圍裝置互連(Peripheral Component Interconnect)的簡稱,作為一種通用的匯流排介面標準,它在目前的計算機系統中得到了非常廣泛的應用。PCI提供了一組完整的匯流排介面規範,其目的是描述如何將計算機系統中的外圍裝置以一種結構化和可控化的方式連線在一起,同時它還刻畫了外圍裝置在連線時的電氣特性和行為規約,並且詳細定義了計算機系統中的各個不同部件之間應該如何正確地進行互動。
  
  無論是在基於Intel晶片的PC機中,或是在基於Alpha晶片的工作站上,PCI毫無疑問都是目前使用最廣泛的一種匯流排介面標準。同舊式的ISA匯流排不同,PCI將計算機系統中的匯流排子系統與儲存子系統完全地分開,CPU通過一塊稱為PCI橋(PCI-Bridge)的裝置來完成同匯流排子系統的互動,如圖1所示。
  
   

  
       圖1 PCI子系統的體系結構
  
  由於使用了更高的時鐘頻率,因此PCI匯流排能夠獲得比ISA匯流排更好的整體效能。PCI匯流排的時鐘頻率一般在25MHz到33MHz範圍內,有些甚至能夠達到66MHz或者133MHz,而在64位系統中則最高能達到266MHz。儘管目前PCI裝置大多采用32位資料匯流排,但PCI規範中已經給出了64位的擴充套件實現,從而使PCI匯流排能夠更好地實現平臺無關性,現在PCI匯流排已經能夠用於IA-32、Alpha、PowerPC、SPARC64和IA-64等體系結構中。
  
  PCI匯流排具有三個非常顯著的優點,使得它能夠完成最終取代ISA匯流排這一歷史使命:
  
  在計算機和外設間傳輸資料時具有更好的效能;
  
  能夠儘量獨立於具體的平臺;
  
  可以很方便地實現即插即用。
  
  圖2是一個典型的基於PCI匯流排的計算機系統邏輯示意圖,系統的各個部分通過PCI匯流排和PCI-PCI橋連線在一起。從圖中不難看出,CPU和RAM需要通過PCI橋連線到PCI匯流排0(即主PCI匯流排),而具有PCI介面的顯示卡則可以直接連線到主PCI總線上。PCI-PCI橋是一個特殊的PCI裝置,它負責將PCI匯流排0和PCI匯流排1(即從PCI主線)連線在一起,通常PCI匯流排1稱為PCI-PCI橋的下游(downstream),而PCI匯流排0則稱為PCI-PCI橋的上游(upstream)。圖中連線到從PCI總線上的是SCSI卡和乙太網卡。為了相容舊的ISA匯流排標準,PCI匯流排還可以通過PCI-ISA橋來連線ISA匯流排,從而能夠支援以前的ISA裝置。圖中ISA總線上連線著一個多功能I/O控制器,用於控制鍵盤、滑鼠和軟碟機。
  

  
        圖2 PCI系統示意圖
  
  在此我只對PCI匯流排系統體系結構作了概括性介紹,如果讀者想進一步瞭解,David A Rusling在The Linux Kernel(http://tldp.org/LDP/tlk/dd/pci.html)中對Linux的PCI子系統有比較詳細的介紹。
  
  二、Linux驅動程式框架
  
  Linux將所有外部裝置看成是一類特殊檔案,稱之為“裝置檔案”,如果說系統呼叫是Linux核心和應用程式之間的介面,那麼裝置驅動程式則可以看成是Linux核心與外部裝置之間的介面。裝置驅動程式嚮應用程式遮蔽了硬體在實現上的細節,使得應用程式可以像操作普通檔案一樣來操作外部裝置。
  
  1. 字元裝置和塊裝置
  
  Linux抽象了對硬體的處理,所有的硬體裝置都可以像普通檔案一樣來看待:它們可以使用和操作檔案相同的、標準的系統呼叫介面來完成開啟、關閉、讀寫和I/O控制操作,而驅動程式的主要任務也就是要實現這些系統呼叫函式。Linux系統中的所有硬體裝置都使用一個特殊的裝置檔案來表示,例如,系統中的第一個IDE硬碟使用/dev/hda表示。每個裝置檔案對應有兩個裝置號:一個是主裝置號,標識該裝置的種類,也標識了該裝置所使用的驅動程式;另一個是次裝置號,標識使用同一裝置驅動程式的不同硬體裝置。裝置檔案的主裝置號必須與裝置驅動程式在登入該裝置時申請的主裝置號一致,否則使用者程序將無法訪問到裝置驅動程式。
  
  在Linux作業系統下有兩類主要的裝置檔案:一類是字元裝置,另一類則是塊裝置。字元裝置是以位元組為單位逐個進行I/O操作的裝置,在對字元裝置發出讀寫請求時,實際的硬體I/O緊接著就發生了,一般來說字元裝置中的快取是可有可無的,而且也不支援隨機訪問。塊裝置則是利用一塊系統記憶體作為緩衝區,當用戶程序對裝置進行讀寫請求時,驅動程式先檢視緩衝區中的內容,如果緩衝區中的資料能滿足使用者的要求就返回相應的資料,否則就呼叫相應的請求函式來進行實際的I/O操作。塊裝置主要是針對磁碟等慢速裝置設計的,其目的是避免耗費過多的CPU時間來等待操作的完成。一般說來,PCI卡通常都屬於字元裝置。
  
  所有已經註冊(即已經載入了驅動程式)的硬體裝置的主裝置號可以從/proc/devices檔案中得到。使用mknod命令可以建立指定型別的裝置檔案,同時為其分配相應的主裝置號和次裝置號。例如,下面的命令:
  
  [
[email protected]
root]# mknod /dev/lp0 c 6 0
  
  將建立一個主裝置號為6,次裝置號為0的字元裝置檔案/dev/lp0。當應用程式對某個裝置檔案進行系統呼叫時,Linux核心會根據該裝置檔案的裝置型別和主裝置號呼叫相應的驅動程式,並從使用者態進入到核心態,再由驅動程式判斷該裝置的次裝置號,最終完成對相應硬體的操作。
  
  2. 裝置驅動程式介面
  
  Linux中的I/O子系統向核心中的其他部分提供了一個統一的標準裝置介面,這是通過include/linux/fs.h中的資料結構file_operations來完成的:
  
  
  struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
  };
  
  當應用程式對裝置檔案進行諸如open、close、read、write等操作時,Linux核心將通過file_operations結構訪問驅動程式提供的函式。例如,當應用程式對裝置檔案執行讀操作時,核心將呼叫file_operations結構中的read函式。
  
  2. 裝置驅動程式模組
  
  Linux下的裝置驅動程式可以按照兩種方式進行編譯,一種是直接靜態編譯成核心的一部分,另一種則是編譯成可以動態載入的模組。如果編譯進核心的話,會增加核心的大小,還要改動核心的原始檔,而且不能動態地解除安裝,不利於除錯,所有推薦使用模組方式。
  
  從本質上來講,模組也是核心的一部分,它不同於普通的應用程式,不能呼叫位於使用者態下的C或者C++庫函式,而只能呼叫Linux核心提供的函式,在/proc/ksyms中可以檢視到核心提供的所有函式。
  
  在以模組方式編寫驅動程式時,要實現兩個必不可少的函式init_module( )和cleanup_module( ),而且至少要包含和兩個標頭檔案。在用gcc編譯核心模組時,需要加上-DMODULE -D__KERNEL__ -DLINUX這幾個引數,編譯生成的模組(一般為.o檔案)可以使用命令insmod載入Linux核心,從而成為核心的一個組成部分,此時核心會呼叫模組中的函式init_module( )。當不需要該模組時,可以使用rmmod命令進行解除安裝,此進核心會呼叫模組中的函式cleanup_module( )。任何時候都可以使用命令來lsmod檢視目前已經載入的模組以及正在使用該模組的使用者數。
  
  3. 裝置驅動程式結構
  
  瞭解裝置驅動程式的基本結構(或者稱為框架),對開發人員而言是非常重要的,Linux的裝置驅動程式大致可以分為如下幾個部分:驅動程式的註冊與登出、裝置的開啟與釋放、裝置的讀寫操作、裝置的控制操作、裝置的中斷和輪詢處理。
  
  驅動程式的註冊與登出
  
  向系統增加一個驅動程式意味著要賦予它一個主裝置號,這可以通過在驅動程式的初始化過程中呼叫register_chrdev( )或者register_blkdev( )來完成。而在關閉字元裝置或者塊裝置時,則需要通過呼叫unregister_chrdev( )或unregister_blkdev( )從核心中登出裝置,同時釋放佔用的主裝置號。