UNIX下裝置驅動程式的基本結構(轉)

ba發表於2007-08-12
UNIX下裝置驅動程式的基本結構(轉)[@more@]  在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系統呼叫在檢查與裝置特別檔案相關的檔案描述符時使用select入口點。如果裝置驅動程式沒有提供上述入口點中的某一個,系統會用預設的子程式來代替。對於不同的系統,也還有一些其它的入口點。

3.2、LINUX系統下的裝置驅動程式

  具體到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(假設此裝置名
為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,用於把裝置的內容對映到地址空間,一般只有塊裝置驅動程式使 用。
( 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埠不會使CPU產生異常,也 就不會導致諸如“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_timerjiffies=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。由於在不連結時, gcc只允許一個輸入檔案,因此一個模組的所有部分都必須在一個檔案裡實現。
編譯好的模組*.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命令來把裝置對映為一個特別檔案,其它程式使用這個裝置的時 候,只要對此特別檔案進行操作就行了。

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

相關文章