Linux 的輸出入系統

Linux 的輸出入系統會透過硬體模組介面,以管理各式各樣的驅動程式。Linux 將硬體裝置分為『區塊、字元、網路』等三種類型,這三種類型的驅動程式都必須支援檔案存取的介面,因為在 Linux 當中裝置是以檔案的方式呈現的。像是 /dev/hda1, /dev/sda1, /dev/tty1 等,程式可以透過開檔 open()、讀檔 read()、寫檔 write() 的方式存取裝置,就像存取一個檔案一樣。

因此,所有的驅動程式必須支援檔案 (file) 的操作 (file_operations),以便將裝置偽裝成檔案,以供作業系統與應用程式進行呼叫。這種將裝置偽裝成檔案的的方式,是從 UNIX 所承襲下來的一種相當成功的模式。

字元類的裝置 (Character Device) 是較為簡單的,常見的字元裝置有鍵盤、滑鼠、印表機等,這些裝置所傳遞的並非一定要是字元訊息,只要可以用串流型式表式即可。因此字元裝置又被稱為串流裝置 (Stream Device)。字元裝置必須支援基本的檔案操作,像是 open(), read(), ioctl() 等。

區塊裝置式形成檔案系統的基礎,除了基本的檔案操作外,區塊裝置還必須支援區塊性的操作 (block_device_operations)。而網路裝置由於必須支援網路定址等特性,因此成為一類獨立的裝置。舉例而言,網路裝置通常必須支援 TCP/IP,以形成網路子系統,因此具有獨特且複雜的操作。像是必須支援封包傳送機制、網路位址的ARP, RARP 協定、MAC Address 等,所以網路裝置的驅動程式也是最複雜的。

由於2.6 版的 Linux 採用模組 (module) 的方式掛載驅動程式,因此必須先透過 module_init(xxx_init_module) 與 module_exit(xxx_cleanup_module) 的方式,將驅動程式掛載到 Linux 中。這種掛載的機制仍然是一種『註冊-反向呼叫機制』,但由於核心程式乃是先編譯好的,因此必須透過一個動態載入器將模組掛載到核心中,原理並不困難,但細節卻很繁瑣。範例 1 顯示了一個最基本的模組 HelloModule.c,該模組透過 module_init(hello_init) 與 module_exit(hello_exit) 兩行巨集,將hello_init() 與 hello_exit() 函數包裝成 linux 可使用的模組介面形式,讓模組載入器得以順利載入該模組。

範例 1. 最基本的模組 (Module) 程式範例 – HelloModule.c

#include <linux/init.h>                     引用檔案
#include <linux/module.h>                   
#include <linux/kernel.h>                   

static int __init hello_init(void) {        模組起始函數
  printk(KERN_ALERT "hello_init()");        印出 hello_init()
  return 0;                                 
}                                           

static void __exit hello_exit(void) {       模組結束函數
  printk(KERN_ALERT "hello_exit()");        印出 hello_exit()
}                                           

module_init(hello_init);                    模組掛載(巨集)
module_exit(hello_exit);                    模組清除(巨集)

當您在 Linux 中撰寫了一個模組織之後,可以利用 gcc 編譯該模組,編譯成功後再利用 insmod指令將該模組掛載到核心中,最後透過 rmmod 指令將該模組從核心中移除。範例 2 顯示了這個操作過程。

範例 2 模組的編譯、掛載與清除過程

gcc -c -O -W -DMODULE -D__KERNEL__ HelloModule.c -o HelloModule.ko
insmod ./helloModule.ko
hello_init()
rmmod ./helloModule
hello_exit()

Linux 的驅動程式是一個具有特定結構的模組,必需將裝置的存取函數包裝成檔案存取的形式,讓裝置的存取就像檔案的存取一般。在驅動程式的內部,仍然必須採用『註冊-反向呼叫』機制,將這些存取函數掛載到檔案系統當中,然後就可以透過檔案操作的方式讀取 (read) 或寫入 (write) 這些裝置,或者透過 ioctl() 函數操控這些裝置。範例 3 顯示了 Linux 當中裝置驅動程式的結構,該範例是一個字元裝置 device1 的驅動程式片段,其中的 Linux 中的cdev 是字元裝置結構 (struct),該驅動程式必需實作出 device1_read(), device1_write(), device1_ioctl(), device1_open(), device1_release() 等檔案操作函數,然後封裝在 file_operations 結構的 device1_fops 變數中,透過指令 cdev_init(&dev->cdev, &device1_fops) 向 Linux 註冊,以將這些實作函數掛載到 Linux 系統中。

範例 3 Linux 中裝置驅動程式的結構範例

int device1_init_module(void) {                                           模組掛載函數
 register_chrdev_region(dev, 1, "device1");                               設定裝置代號
 …                                                                        
 device1_devices = kmalloc(…);                                            分配記憶體(slab)
 …                                                                        
 cdev_init(&dev->cdev, &device1_fops);                                    註冊檔案操作函數群fops
 dev->cdev.owner = THIS_MODULE;                                           
 dev->cdev.ops = &device1_fops;                                           
 err = cdev_add (&dev->cdev, devno, 1);                                   
…                                                                         
fail:                                                                     
  device1_cleanup_module();                                               若掛載失敗則執行清除函數
}                                                                         

int device1_open(struct inode *inode, struct file *filp) {...}            裝置開啟函數
int device1_release(struct inode *inode, struct file *filp) {...}         裝置釋放函數
ssize_t device1_read(struct file *filp, …){...}                           裝置讀取函數
ssize_t device1_write(struct file *filp,…) {...}                          裝置寫入函數
int device1_ioctl(struct inode *inode, struct file *filp…) {…}            裝置控制函數

struct file_operations device1_fops = {                                   裝置的檔案操作函數群,
 .owner = THIS_MODULE,                                                    包含 read(), write(), ioctl(), 
 .read = device1_read,                                                    open(), release() 等。
 .write = device1_write,                                                  
 .ioctl = device1_ioctl,                                                  
 .open = device1_open,                                                    
 .release = device1_release,                                              
};                                                                        

module_init(device1_init_module);                                         模組掛載(巨集)
module_exit(device1_cleanup_module);                                      模組清除(巨集)

走筆至此,我們已經介紹完 Linux 中的行程管理、記憶體管理、輸出入系統與檔案系統,完成了我們對 Linux 作業系統的介紹。但是 Linux 是個龐大的系統,筆者無法進行太詳細與深入的介紹,有興趣的讀者請進一步參考相關書籍。


书籍推荐