LINUX下的裝置驅動程式 (轉)

worldblog發表於2007-12-03
LINUX下的裝置驅動程式 (轉)[@more@]

三、下的裝置
3.1、UNIX下裝置驅動程式的基本結構
在UNIX系統裡,對程式而言,裝置驅動程式隱藏了裝置的具體細節,
對各種不同裝置提供了一致的介面,一般來說是把裝置對映為一個特殊的裝置文
件,使用者程式可以象對其它一樣對此裝置檔案進行操作。UNIX對裝置
支援兩個標準介面:塊特別裝置檔案和字元特別裝置檔案,透過塊(字元)特別
裝置檔案存取的裝置稱為塊(字元)裝置或具有塊(字元)裝置介面。
塊裝置介面僅支援面向塊的I/O操作,所有I/O操作都透過在核心地址空間
中的I/O緩衝區進行,它可以支援幾乎任意長度和任意位置上的I/O請求,即提
供隨機存取的功能。
字元裝置介面支援面向字元的I/O操作,它不經過系統的快取,所以它
們負責管理自己的緩衝區結構。字元裝置介面只支援順序存取的功能,一般不能
進行任意長度的I/O請求,而是限制I/O請求的長度必須是裝置要求的基本塊長
的倍數。顯然,本程式所驅動的卡只能提供順序存取的功能,屬於是字元設
備,因此後面的討論在兩種裝置有所區別時都只涉及字元型裝置介面。
裝置由一個主裝置號和一個次裝置號標識。主裝置號唯一標識了裝置型別,
即裝置驅動程式型別,它是塊裝置表或字元裝置表中裝置表項的。次裝置號
僅由裝置驅動程式解釋,一般用於識別在若干可能的硬體裝置中,I/O請求所涉
及到的那個裝置。
裝置驅動程式可以分為三個主要組成部分:
(1) 自動和初始化子程式,負責檢測所要驅動的硬體裝置是否存在和是否
能正常工作。如果該裝置正常,則對這個裝置及其相關的、裝置驅動程式
需要的狀態進行初始化。這部分驅動程式僅在初始化的時候被一
次。
(2) 服務於I/O請求的子程式,又稱為驅動程式的上半部分。呼叫這部分是由
於系統呼叫的結果。這部分程式在的時候,系統仍認為是和進行呼叫
的程式屬於同一個程式,只是由使用者態變成了核心態,具有進行此係統調
用的使用者程式的執行環境,因此可以在其中呼叫sleep()等與程式執行環
境有關的。
(3) 中斷服務子程式,又稱為驅動程式的下半部分。在UNIX系統中,並不是
直接從中斷向量表中呼叫裝置驅動程式的中斷服務子程式,而是由UNIX
系統來接收硬體中斷,再由系統呼叫中斷服務子程式。中斷可以產生在任
何一個程式執行的時候,因此在中斷服務程式被呼叫的時候,不能依賴於
任何程式的狀態,也就不能呼叫任何與程式執行環境有關的函式。因為設
備驅動程式一般支援同一型別的若干裝置,所以一般在系統呼叫中斷服務
子程式的時候,都帶有一個或多個引數,以唯一標識請求服務的裝置。
在系統內部,I/O裝置的存取透過一組固定的入口點來進行,這組入口點是
由每個裝置的裝置驅動程式提供的。一般來說,字元型裝置驅動程式能夠提供如
下幾個入口點:
(1) open入口點。開啟裝置準備I/O操作。對字元特別裝置檔案進行開啟操
作,都會呼叫裝置的open入口點。open子程式必須對將要進行的I/O
操作做好必要的準備工作,如清除緩衝區等。如果裝置是獨佔的,即同一
時刻只能有一個程式訪問此裝置,則open子程式必須設定一些標誌以表
示裝置處於忙狀態。
(2) close入口點。關閉一個裝置。當最後一次使用裝置終結後,呼叫close
子程式。獨佔裝置必須標記裝置可再次使用。
(3) read入口點。從裝置上讀資料。對於有緩衝區的I/O操作,一般是從緩
衝區裡讀資料。對字元特別裝置檔案進行讀操作將呼叫read子程式。
(4) write入口點。往裝置上寫資料。對於有緩衝區的I/O操作,一般是把數
據寫入緩衝區裡。對字元特別裝置檔案進行寫操作將呼叫write子程式。
(5) ioctl入口點。執行讀、寫之外的操作。
(6) 入口點。檢查裝置,看資料是否可讀或裝置是否可用於寫資料。
select系統呼叫在檢查與裝置特別檔案相關的檔案描述符時使用select入口點。
如果裝置驅動程式沒有提供上述入口點中的某一個,系統會用預設的子程式
來代替。對於不同的系統,也還有一些其它的入口點。

3.2、系統下的裝置驅動程式
具體到LINUX系統裡,裝置驅動程式所提供的這組入口點由一個結構來向系
統進行說明,此結構定義為:
#include
struct file_operations {
  int (*lseek)(struct inode *inode,struct file *filp,
  off_t off,int pos);
  int (*read)(struct inode *inode,struct file *filp,
  char *buf, int count);
  int (*write)(struct inode *inode,struct file *filp,
  char *buf,int count);
  int (*readdir)(struct inode *inode,struct file *filp,
  struct dirent *dirent,int count);
  int (*select)(struct inode *inode,struct file *filp,
  int sel_type,select_table *wait);
  int (*ioctl) (struct inode *inode,struct file *filp,
  unsigned int cmd,unsigned int arg);
  int (*mmap) (void);

  int (*open) (struct inode *inode, struct file *filp);
  void (*release) (struct inode *inode, struct file *filp);
  int (*fsync) (struct inode *inode, struct file *filp);
};
其中,struct inode提供了關於特別裝置檔案/dev/(假設此裝置名
為driver)的資訊,它的定義為:
#include
struct inode {
  dev_t  i_dev;
  unsigned long  i_ino;  /* Inode number */
  umode_t  i_mode; /* Mode of the file */
  nlink_t  i_nlink;
  uid_t  i_uid;
  gid_t  i_gid;
  dev_t  i_rdev;  /* Device major and minor numbers*/
  off_t  i_size;
  time_t  i_atime;
  time_t  i_mtime;
  time_t  i_ctime;
  unsigned long  i_blksize;
  unsigned long  i_blocks;
  struct inode_operations * i_op;
  struct super_block * i_sb;
  struct wait_queue * i_wait;
  struct file_lock * i_flock;
  struct vm_area_struct * i_mmap;
  struct inode * i_next, * i_prev;
  struct inode * i_hash_next, * i_hash_prev;
  struct inode * i_bound_to, * i_bound_by;
  unsigned short i_count;
  unsigned short i_flags;  /* Mount flags (see fs.h) */
  unsigned char i_lock;
  unsigned char i_dirt;
  unsigned char i_pipe;
  unsigned char i_mount;
  unsigned char i_seek;
  unsigned char i_update;
  union {
  struct pipe_inode_info pipe_i;
  struct minix_inode_info minix_i;
  struct ext_inode_info ext_i;
  struct msdos_inode_info msdos_i;
  struct iso_inode_info isofs_i;
  struct nfs_inode_info nfs_i;
  } u;
};

struct file主要用於與檔案系統對應的裝置驅動程式使用。當然,其它設
備驅動程式也可以使用它。它提供關於被開啟的檔案的資訊,定義為:
#include
struct file {
  mode_t f_mode;
  dev_t f_rdev;  /* needed for /dev/tty */
  off_t f_pos;  /* Curr. posn in file */
  unsigned short f_flags;  /* The flags arg passed to open */
  unsigned short f_count;  /* Number of opens on this file */
  unsigned short f_reada;
  struct inode *f_inode;  /* pointer to the inode struct */
  struct file_operations *f_op;/* pointer to the fops struct*/
};

在結構file_operations裡,指出了裝置驅動程式所提供的入口點位置,分
別是:
(1) lseek,移動檔案指標的位置,顯然只能用於可以隨機存取的裝置。
(2) read,進行讀操作,引數buf為存放讀取結果的緩衝區,count為所要
讀取的資料長度。返回值為負表示讀取操作發生錯誤,否則返回實際讀取
的位元組數。對於字元型,要求讀取的位元組數和返回的實際讀取位元組數都必
須是inode->i_blksize的的倍數。
(3) write,進行寫操作,與read類似。
(4) readdir,取得下一個目錄入口點,只有與檔案系統相關的裝置驅動程式
才使用。
(5) selec,進行選擇操作,如果驅動程式沒有提供select入口,select操
作將會認為裝置已經準備好進行任何的I/O操作。
(6) ioctl,進行讀、寫以外的其它操作,引數cmd為自定義的的命令。
(7) mmap,用於把裝置的內容對映到地址空間,一般只有塊裝置驅動程式使
用。
(8) open,開啟裝置準備進行I/O操作。返回0表示開啟成功,返回負數表
示失敗。如果驅動程式沒有提供open入口,則只要/dev/driver檔案存
在就認為開啟成功。
(9) release,即close操作。
裝置驅動程式所提供的入口點,在裝置驅動程式初始化的時候向系統進行登
記,以便系統在適當的時候呼叫。LINUX系統裡,透過呼叫register_chrdev
向系統註冊字元型裝置驅動程式。register_chrdev定義為:
 #include
 #include
 int register_chrdev(unsigned int major, const char *name,
  struct file_operations *fops);
其中,major是為裝置驅動程式向系統申請的主裝置號,如果為0則系統為此
驅動程式動態地分配一個主裝置號。name是裝置名。fops就是前面所說的對各個
呼叫的入口點的說明。此函式返回0表示成功。返回-EINVAL表示申請的主裝置號
,一般來說是主裝置號大於系統所允許的最大裝置號。返回-EBUSY表示所申
請的主裝置號正在被其它裝置驅動程式使用。如果是動態分配主裝置號成功,此
函式將返回所分配的主裝置號。如果register_chrdev操作成功,裝置名就會出
現在/proc/devices檔案裡。
初始化部分一般還負責給裝置驅動程式申請系統資源,包括、中斷、時
鍾、I/O埠等,這些資源也可以在open子程式或別的地方申請。在這些資源不
用的時候,應該釋放它們,以利於資源的共享。
在UNIX系統裡,對中斷的處理是屬於系統核心的部分,因此如果裝置與系
統之間以中斷方式進行資料的話,就必須把該裝置的驅動程式作為系統核心
的一部分。裝置驅動程式透過呼叫request_irq函式來申請中斷,透過free_irq
來釋放中斷。它們的定義為:
#include
int request_irq(unsigned int irq,
  void (*handler)(int irq,void dev_id,struct pt_regs *regs),
  unsigned long flags,
  const char *device,
  void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
引數irq表示所要申請的硬體中斷號。handler為向系統登記的中斷處理子
程式,中斷產生時由系統來呼叫,呼叫時所帶引數irq為中斷號,dev_id為申
請時告訴系統的裝置標識,regs為中斷髮生時暫存器內容。device為裝置名,
將會出現在/proc/interrupts檔案裡。flag是申請時的選項,它決定中斷處理
程式的一些特性,其中最重要的是中斷處理程式是快速處理程式(flag裡設定
了SA_INTERRUPT)還是慢速處理程式(不設定SA_INTERRUPT),快速處理程式
執行時,所有中斷都被遮蔽,而慢速處理程式執行時,除了正在處理的中斷外,
其它中斷都沒有被遮蔽。在LINUX系統中,中斷可以被不同的中斷處理程式共享,
這要求每一個共享此中斷的處理程式在申請中斷時在flags裡設定SA_SHIRQ,
這些處理程式之間以dev_id來區分。如果中斷由某個處理程式獨佔,則dev_id
可以為NULL。request_irq返回0表示成功,返回-INVAL表示irq>15或
handler==NULL,返回-EBUSY表示中斷已經被佔用且不能共享。
作為系統核心的一部分,裝置驅動程式在申請和釋放記憶體時不是呼叫malloc
和free,而代之以呼叫kmalloc和kfree,它們被定義為:
#include
void * kmalloc(unsigned int len, int priority);
void kfree(void * obj);
引數len為希望申請的位元組數,obj為要釋放的記憶體指標。priority為分配記憶體操
作的優先順序,即在沒有足夠空閒記憶體時如何操作,一般用GFP_KERNEL。
與中斷和記憶體不同,使用一個沒有申請的I/O埠不會使產生異常,也
就不會導致諸如“segmentation fault"一類的錯誤發生。任何程式都可以訪問
任何一個I/O埠。此時系統無法保證對I/O埠的操作不會發生衝突,甚至會
因此而使系統崩潰。因此,在使用I/O埠前,也應該檢查此I/O埠是否已有
別的程式在使用,若沒有,再把此埠標記為正在使用,在使用完以後釋放它。
這樣需要用到如下幾個函式:
int check_region(unsigned int from, unsigned int extent);
void request_region(unsigned int from, unsigned int extent,
  const char *name);
void release_region(unsigned int from, unsigned int extent);
呼叫這些函式時的引數為:from表示所申請的I/O埠的起始地址;
extent為所要申請的從from開始的埠數;name為裝置名,將會出現在
/proc/ioports檔案裡。check_region返回0表示I/O埠空閒,否則為正在
被使用。
在申請了I/O埠之後,就可以如下幾個函式來訪問I/O埠:
#include
inline unsigned int inb(unsigned short port);
inline unsigned int inb_p(unsigned short port);
inline void outb(char value, unsigned short port);
inline void outb_p(char value, unsigned short port);
其中inb_p和outb_p插入了一定的延時以適應某些慢的I/O埠。
在裝置驅動程式裡,一般都需要用到計時機制。在LINUX系統中,時鐘是由
系統接管,裝置驅動程式可以向系統申請時鐘。與時鐘有關的系統呼叫有:
#include
#include
void add_timer(struct timer_list * timer);
int  del_timer(struct timer_list * timer);
inline void init_timer(struct timer_list * timer);
struct timer_list的定義為:
struct timer_list {
  struct timer_list *next;
  struct timer_list *prev;
  unsigned long expires;
  unsigned long data;
  void (*function)(unsigned long d);
  };
其中expires是要執行function的時間。系統核心有一個全域性變數JIFFIES
表示當前時間,一般在呼叫add_timer時jiffies=JIFFIES+num,表示在num個
系統最小時間間隔後執行function。系統最小時間間隔與所用的硬體平臺有關,
在核心裡定義了常數HZ表示一秒內最小時間間隔的數目,則num*HZ表示num
秒。系統計時到預定時間就呼叫function,並把此子程式從定時佇列裡刪除,
因此如果想要每隔一定時間間隔執行一次的話,就必須在function裡再一次調
用add_timer。function的引數d即為timer裡面的data項。
在裝置驅動程式裡,還可能會用到如下的一些系統函式:
#include
#define cli() __asm__ __volatile__ ("cli"::)
#define sti() __asm__ __volatile__ ("sti"::)
這兩個函式負責開啟和關閉中斷允許。
#include
void memcpy_fromfs(void * to,const void * from,unsigned long n);
void memcpy_tofs(void * to,const void * from,unsigned long n);
在使用者程式呼叫read 、write時,因為程式的執行狀態由使用者態變為核心
態,地址空間也變為核心地址空間。而read、write中引數buf是指向使用者程
序的私有地址空間的,所以不能直接訪問,必須透過上述兩個系統函式來訪問用
戶程式的私有地址空間。memcpy_fromfs由使用者程式地址空間往核心地址空間
複製,memcpy_tofs則反之。引數to為複製的目的指標,from為源指標,n
為要複製的位元組數。
在裝置驅動程式裡,可以呼叫printk來列印一些資訊,用法與printf
類似。printk列印的資訊不僅出現在螢幕上,同時還記錄在檔案syslog裡。

3.3、LINUX系統下的具體實現
在LINUX裡,除了直接修改系統核心的,把裝置驅動程式加進核心裡
以外,還可以把裝置驅動程式作為可載入的模組,由員動態地載入它,
使之成為核心地一部分。也可以由系統管理員把已載入地模組動態地解除安裝下來。
LINUX中,模組可以用C語言編寫,用gcc編譯成目標檔案(不進行連結,作
為*.o檔案存在),為此需要在gcc命令列里加上-c的引數。在編譯時,還應該在
gcc的命令列里加上這樣的引數:-D__KERNEL__ -DMODULE。由於在不連結時,g
cc只允許一個輸入檔案,因此一個模組的所有部分都必須在一個檔案裡實現。
編譯好的模組*.o放在/lib/modules/xxxx/misc下(xxxx表示核心版本,如
在核心版本為2.0.30時應該為/lib/modules/2.0.30/misc),然後用depmod -a
使此模組成為可載入模組。模組用insmod命令載入,用rmmod命令來解除安裝,並可
以用lsmod命令來檢視所有已載入的模組的狀態。
編寫模組程式的時候,必須提供兩個函式,一個是int init_module(void),
供insmod在載入此模組的時候自動呼叫,負責進行裝置驅動程式的初始化工作。
init_module返回0以表示初始化成功,返回負數表示失敗。另一個函式是void
cleanup_module (void),在模組被解除安裝時呼叫,負責進行裝置驅動程式的清除
工作。
在成功的向系統註冊了裝置驅動程式後(呼叫register_chrdev成功後),
就可以用mknod命令來把裝置對映為一個特別檔案,其它程式使用這個裝置的時
候,只要對此特別檔案進行操作就行了。
附錄:參考文獻
1、 《UNIX設計與實現》
 陳華瑛、李建國主編
 電子工業出版社出版
2、  《Linux Kernel er's Gu》
 作者:Michael K. Johnson
3、 《Kernel Jorn》
 作者:Alessandro Rubini & Georg Zezchwitz
 連載於《Linux Journal》1996年36期
4、 Linux核心原始碼(核心版本2.0.30)
5、 Linux-HOWTO


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-987613/,如需轉載,請註明出處,否則將追究法律責任。

相關文章