Linux系統的硬體驅動程式編寫原理

來源:互聯網
上載者:User

本文詳細地介紹如何Linux系統的硬體驅動程式的編寫原理,指出哪些核心常式將會被調用、如何初始化驅動程式及如何分配記憶體等等。大家一定對Linux作業系統有所瞭解了,在此本人也不再贅述了。好吧,下面簡單地介紹一下裝置驅動程式。顧名思義,驅動程式是用來控制電腦外圍裝置的,Linux系統將所有的外圍裝置都高度地抽象成一些位元組的序列,並且以檔案的形式來表示這些裝置。我們可以來看一下Linux的I/O子系統(圖1)。


    圖1 Linux的I/O子系統

  從圖上我們可以看出,核心緊緊地包圍在硬體周圍,核心是一些軟體包的組合,它們可以直接存取系統的硬體,包括處理器、記憶體和I/O裝置。而使用者進程則通過核心提供的使用者服務來和核心通訊,從而間接地控制系統硬體。

我們可以通過圖2來瞭解這些動作的具體情況。


       圖2 使用者級、核心級和硬體級三者之間的通訊

  圖上顯示了使用者級的程式使用核心提供的標準系統調用來與核心通訊,這些系統調用有:open(), read(), write(), ioctl(), close() 等等。

  Linux的核心是一個有機的整體。每一個使用者進程運行時都好像有一份核心的拷貝,每當使用者進程使用系統調用時,都自動地將運行模式從使用者級轉為核心級,此時進程在核心的地址空間中運行。


          圖3 Linux的I/O子系統

  Linux核心使用"裝置無關"的I/O子系統來為所有的裝置服務。每個裝置都提供標準介面給核心,從而儘可能地隱藏了自己的特性。圖3展示了使用者程式使用一些基本的系統調用從裝置讀取資料並且將它們存入緩衝的例子。我們可以看到,每當一個系統調用被使用時,核心就轉到相應的裝置驅動常式來操縱硬體。

  每個裝置在Linux系統上看起來都像一個檔案,它們存放在/dev目錄中並被稱為"特殊檔案"或是"裝置節點"。大家可以使用ls -l /dev/lp* 來得到以下的輸出:

  crw-rw-rw 1 root root 6, 0 April 23 1994 /dev/lp0

  這行輸出表示lp0是一個字元裝置(屬性欄位的第一個字元是'c'),主裝置號是6,次裝置號是0。主裝置號用來向核心表明這一裝置節點所代表的驅動程式的類型(比如:主裝置號是3的塊裝置是IDE磁碟驅動程式,而主裝置號為8的塊裝置是SCSI磁碟驅動程式);每個驅動程式負責管理它所驅動的幾個硬體執行個體,這些硬體執行個體則由次裝置號來表示(例如:次裝置號為0的SCSI磁碟代表整個也可以說是"第一個"SCSI磁碟,而次裝置號為1到15的磁碟代表此SCSI磁碟上的15個分區)。

  到此大家應該對Linux的裝置有所瞭解了吧,下面就可以開始我們的正題"裝置驅動程式"。

  裝置驅動程式是一組由核心中的相關子常式和資料群組成的I/O裝置軟體介面。每當核心意識到要對某個裝置進行特殊的操作時,它就調用相應的驅動常式。這就使得控制從使用者進程轉移到了驅動常式,當驅動常式完成後,控制又被返回至使用者進程。圖5就顯示了以上的過程。

圖5 裝置驅動程式的作用

  每個裝置驅動程式都具有以下幾個特性:

  l 具有一整套的和硬體裝置通訊的常式,並且提供給作業系統一套標準的軟體介面;

  l 具有一個可以被作業系統動態地調用和移除的自包含組件;

  l 可以控制和系統管理使用者程式和物理裝置之間的資料流。

  接下來我們來瞭解一下字元裝置和塊裝置,它們是Linux系統中兩種主要的外圍裝置。我們常見的磁碟是塊裝置,而終端和印表機是字元裝置。塊裝置被使用者程式通過系統緩衝來訪問。特別是系統記憶體配置和管理進程就沒有必要來充當從外設讀寫的資料轉送者了。正好與之相反的是,字元裝置直接與使用者程式進行通訊,而且兩者似乎沒有緩衝區。Linux的傳輸控制機制會根據使用者程式的需要來正確地操縱記憶體和磁碟等外設來取得資料。在Linux系統中字元裝置磁碟機被儲存為/usr/src/linux/drivers/char目錄中。下面我們重點介紹字元裝置驅動程式的開發方法。

  首先瞭解一下Linux的核心編程環境。我們知道每個Linux使用者進程都在一個獨立的系統空間中運行著,與系統區和其他使用者進程相隔離。這樣就保護了一個使用者進程的運行環境,以免被其他使用者進程所破壞。與這種情況正相反的是,裝置驅動程式運行在核心模式,它們具有很大的自由度。這些裝置驅動程式都是被假設為正確和可靠的,它們是核心的一部分,可以處理系統插斷要求和訪問外圍裝置,同時它們有效地處理插斷要求以便系統發送器保持系統需求的平衡。所以裝置驅動程式可以脫離系統的限制來使用系統區,比如系統的緩衝區等等。

  一個裝置驅動程式同時包括中斷和同步地區。其中中斷地區處理即時事件並且被裝置的中斷所驅動;而同步地區則組成了裝置的剩餘部分,處理進程的同步事件。所以,當一個裝置需要一些軟體服務時,就發出一個"中斷",然後中斷處理器得到產生中斷的原因同時進行相應的動作。

  一個Linux進程可能會在事件發生之前一直等待下去。例如,一個進程可能會在運行中等待一些寫入硬體裝置的資訊的到來。其中一種方式是進程可以使用sleep()和wakeup()這兩個系統調用,進程先使自己處於睡眠狀態,等待事件的到來,一旦事件發生,進程即可被喚醒。舉個例子來說:interruptible_sleep_on(&dev_wait_queue)函數使進程睡眠並且將此進程的進程號加到進程睡眠列表dev_wait_queue中,一旦裝置準備好後,裝置發出一個中斷,從而導致裝置驅動程式中相應的常式被調用,這個驅動程式常式處理完一些裝置要求的事宜後會發出一個喚醒進程的訊號,通常使用wake_up_interruptible(&dev_wait_queue)函數,它可以喚醒dev_wait_queue所示列表中的所有進程。

  特別要注意的是,如果兩個和兩個以上的進程共用一些公用資料區時,我們必須將之視為臨界區,臨界區保證了進程間互斥地訪問公用資料。在Linux系統中我們可以使用cli()和sti()兩個核心常式來處理這種互斥,當一個進程在訪問臨界區時可以使用cli()來關閉中斷,離開時則使用sti()再將中斷開啟,就像下面的寫法:

  cli()

   臨界區

  sti()

除了以上這些,我們還得瞭解一下虛擬檔案系統交換(VFS)的概念。


          圖6 虛擬檔案系統交換

圖6中的"檔案操作結構"在/usr/include/linux/fs.h檔案中定義,此結構包含了驅動程式中的函數列表。圖上的初始化常式xxx_init()根據VFS和裝置的主裝置號來註冊"檔案操作結構"。
下面是一些裝置驅動程式的支撐函數(具體使用方法詳見Linux編程手冊,使用man命令):

  add_timer()

  定時間一過,可以引發函數的執行;

  cli()

  關閉中斷,阻止中斷的捕獲;

  end_request()

  當一個請求被完成或被撤銷時被執行;

  free_irq()

  釋放一個先前被request_irq()和irqaction()捕獲的的插斷要求;

  get_fs*()

  允許一個裝置驅動程式訪問使用者區資料(一塊不屬於核心的記憶體區);

  inb(), inb_p()

  從一個連接埠讀取一個位元組,其中inb_p() 會一直阻塞直到從連接埠得到位元組為止;

  irqaction()

  註冊一個中斷;

  IS_*(inode)

  測試inode是否在一個被mount了的檔案系統上;

  kfree*()

  放先前被kmalloc()分配的記憶體區;

  kmalloc()

  分配大於4096個位元組的大塊記憶體區;

  MAJOR()

  返回裝置的主裝置號;

  MINOR()

  返回裝置的次裝置號;

  memcpy_*fs()

  在使用者區和核心區之間複製大塊的記憶體;

  outb(), outb_p()

  向一個連接埠寫一個位元組,其中outb_p()一直阻塞直到寫位元組成功為止;

  printk()

  核心使用的printf()版本;

  put_fs*()

  允許裝置驅動程式將資料寫入使用者區;

  register_*dev()

  在核心中註冊一個裝置;

  request_irq()

  向核心申請一個插斷要求IRQ,如果成功則安裝一個插斷要求處理器;

  select_wait()

  將一個進程加到相應select_wait隊列中;

  *sleep_on()

  使進程睡眠以等待事件的到來,並且將wait_queue 進入點加到列表中以便事件到來時將進程喚醒;

  sti()

  和cti()相對應,恢複中斷捕獲;

  sys_get*()

  系統調用,得到進程的有關資訊;

  wake_up*()

  喚醒先前被*sleep_on() 睡眠的進程;

  Linux的使用者進程不能直接存取系統實體記憶體。每個使用者進程都有自己的記憶體空間(使用者虛擬位址空間,開始於虛擬0地址)。同樣核心也具有自己特定的記憶體空間--系統虛擬位址空間。每當使用者使用系統調用read()或write()時,裝置驅動程式就在核心地址空間和使用者程式地址空間之間拷貝資料。許多Linux常式,比如memcpy_*fs() 和 put_fs*()可以使裝置驅動程式穿越"使用者-系統"邊界來傳輸資料。而且資料可以是位元組、字或任意長度的資料區塊。例如,memcpy_fromfs()可以從使用者記憶體空間傳輸任意長度的資料區塊到裝置,而get_fs_byte()則只從使用者記憶體空間傳輸一個位元組;相同的memcpy_tofs()和 put_fs_byte()也是如此,只不過它們是寫資料到使用者記憶體空間。

  然而,在核心可訪問記憶體空間和裝置本身之間傳輸資料則要視不同的電腦而定。一些電腦需要使用一些特殊CPU輸入輸出指令來完成這項工作,這通常被稱為DMA(直接記憶體存取)。而另一種方案則是使用記憶體對應 I/O來解決,通常使用系統提供的I/O函數,比如inb()和outb()來分別地從I/O地址(即連接埠)讀取和向I/O地址輸出一單位元組,可以使用以下的語句:

  unsigned char inb(int port)

  outb(char data, int port)

  好,下面就可以來看看字元裝置驅動程式的基本結構。6所示xxx_write()常式輪詢裝置是否已經準備好接收資料,如果準備好了,則將指定長度的字串從使用者記憶體空間發送到字元裝置。另外還可以使用中斷來通知裝置是否準備好,這樣就不需要程式為了輪詢而等待,從而提高CPU的利用率。xxx_table[]是一個結構的數組,它包含很多成員變數,包括xxx_wait_queue和bytes_xfered(兩者都被用於讀寫操作)。xxx_open()使用request_irq()或irqaction()來調用xxx_interrupt()常式。

  為了使裝置驅動程式被正確地初始化,每當系統啟動時,xxx_init() 常式必須被調用。為了確保這一操作,需要將語句mem_start = xxx_init(mem_start); 加到/usr/src/linux/driver/char/mem.c檔案的chr_drv_init()函數末。接下來的工作就是將驅動裝置安裝到核心中去了(注意:字元裝置驅動程式只能被安裝在/usr/src/linux/drivers/char/char.a庫檔案中)。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.