Linux主要將設備分爲二類:字符設備和塊設備。字符設備是指設備發送和接收數據以字符的形式進行;而塊設備則以整個數據緩衝區的形式進行。字符設備的驅動相對比較簡單。
下面我們來假設一個非常簡單的虛擬字符設備:這個設備中只有一個4個字節的全局變量int global_var,而這個設備的名字叫做"gobalvar"。對"gobalvar"設備的讀寫等操作即是對其中全局變量global_var的操作。
驅動程序是內核的一部分,因此我們需要給其添加模塊初始化函數,該函數用來完成對所控設備的初始化工作,並調用register_chrdev() 函數註冊字符設備:
static int __init gobalvar_init(void) { if (register_chrdev(MAJOR_NUM, " gobalvar ", &gobalvar_fops)) { //…註冊失敗 } else { //…註冊成功 } } |
其中,register_chrdev函數中的參數MAJOR_NUM爲主設備號,"gobalvar"爲設備名,gobalvar_fops爲包含基本函數入口點的結構體,類型爲file_operations。當gobalvar模塊被加載時,gobalvar_init被執行,它將調用內核函數register_chrdev,把驅動程序的基本入口點指針存放在內核的字符設備地址表中,在用戶進程對該設備執行系統調用時提供入口地址。
與模塊初始化函數對應的就是模塊卸載函數,需要調用register_chrdev()的"反函數" unregister_chrdev():
static void __exit gobalvar_exit(void) { if (unregister_chrdev(MAJOR_NUM, " gobalvar ")) { //…卸載失敗 } else { //…卸載成功 } } |
隨着內核不斷增加新的功能,file_operations結構體已逐漸變得越來越大,但是大多數的驅動程序只是利用了其中的一部分。對於字符設備來說,要提供的主要入口有:open ()、release ()、read ()、write ()、ioctl ()、llseek()、poll()等。
open()函數 對設備特殊文件進行open()系統調用時,將調用驅動程序的open () 函數:
int (*open)(struct inode * ,struct file *); |
其中參數inode爲設備特殊文件的inode (索引結點) 結構的指針,參數file是指向這一設備的文件結構的指針。open()的主要任務是確定硬件處在就緒狀態、驗證次設備號的合法性(次設備號可以用MINOR(inode-> i - rdev) 取得)、控制使用設備的進程數、根據執行情況返回狀態碼(0表示成功,負數表示存在錯誤) 等;
release()函數 當最後一個打開設備的用戶進程執行close ()系統調用時,內核將調用驅動程序的release () 函數:
void (*release) (struct inode * ,struct file *) ; |
release 函數的主要任務是清理未結束的輸入/輸出操作、釋放資源、用戶自定義排他標誌的復位等。
read()函數 當對設備特殊文件進行read() 系統調用時,將調用驅動程序read() 函數:
ssize_t (*read) (struct file *, char *, size_t, loff_t *); |
用來從設備中讀取數據。當該函數指針被賦爲NULL 值時,將導致read 系統調用出錯並返回-EINVAL("Invalid argument,非法參數")。函數返回非負值表示成功讀取的字節數(返回值爲"signed size"數據類型,通常就是目標平臺上的固有整數類型)。
globalvar_read函數中內核空間與用戶空間的內存交互需要藉助第2節所介紹的函數:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off) { … copy_to_user(buf, &global_var, sizeof(int)); … } |
write( ) 函數 當設備特殊文件進行write () 系統調用時,將調用驅動程序的write () 函數:
ssize_t (*write) (struct file *, const char *, size_t, loff_t *); |
向設備發送數據。如果沒有這個函數,write 系統調用會向調用程序返回一個-EINVAL。如果返回值非負,則表示成功寫入的字節數。
globalvar_write函數中內核空間與用戶空間的內存交互需要藉助第2節所介紹的函數:
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off) { … copy_from_user(&global_var, buf, sizeof(int)); … } |
ioctl() 函數 該函數是特殊的控制函數,可以通過它向設備傳遞控制信息或從設備取得狀態信息,函數原型爲:
int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long); |
unsigned int參數爲設備驅動程序要執行的命令的代碼,由用戶自定義,unsigned long參數爲相應的命令提供參數,類型可以是整型、指針等。如果設備不提供ioctl 入口點,則對於任何內核未預先定義的請求,ioctl 系統調用將返回錯誤(-ENOTTY,"No such ioctl fordevice,該設備無此ioctl 命令")。如果該設備方法返回一個非負值,那麼該值會被返回給調用程序以表示調用成功。
llseek()函數 該函數用來修改文件的當前讀寫位置,並將新位置作爲(正的)返回值返回,原型爲:
loff_t (*llseek) (struct file *, loff_t, int); |
poll()函數 poll 方法是poll 和select 這兩個系統調用的後端實現,用來查詢設備是否可讀或可寫,或是否處於某種特殊狀態,原型爲:
unsigned int (*poll) (struct file *, struct poll_table_struct *); |
我們將在"設備的阻塞與非阻塞操作"一節對該函數進行更深入的介紹。
設備"gobalvar"的驅動程序的這些函數應分別命名爲gobalvar_open、gobalvar_ release、gobalvar_read、gobalvar_write、gobalvar_ioctl,因此設備"gobalvar"的基本入口點結構變量gobalvar_fops 賦值如下:
struct file_operations gobalvar_fops = { read: gobalvar_read, write: gobalvar_write, }; |
上述代碼中對gobalvar_fops的初始化方法並不是標準C所支持的,屬於GNU擴展語法。
完整的globalvar.c文件源代碼如下:
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <asm/uaccess.h> MODULE_LICENSE("GPL"); #define MAJOR_NUM 254 //主設備號 static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*); static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*); //初始化字符設備驅動的file_operations結構體 struct file_operations globalvar_fops = { read: globalvar_read, write: globalvar_write, }; static int global_var = 0; //"globalvar"設備的全局變量 static int __init globalvar_init(void) { int ret; //註冊設備驅動 ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops); if (ret) { printk("globalvar register failure"); } else { printk("globalvar register success"); } return ret; } static void __exit globalvar_exit(void) { int ret; //註銷設備驅動 ret = unregister_chrdev(MAJOR_NUM, "globalvar"); if (ret) { printk("globalvar unregister failure"); } else { printk("globalvar unregister success"); } } static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off) { //將global_var從內核空間複製到用戶空間 if (copy_to_user(buf, &global_var, sizeof(int))) { return - EFAULT; } return sizeof(int); } static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off) { //將用戶空間的數據複製到內核空間的global_var if (copy_from_user(&global_var, buf, sizeof(int))) { return - EFAULT; } return sizeof(int); } module_init(globalvar_init); module_exit(globalvar_exit); |
運行:
gcc -D__KERNEL__ -DMODULE -DLINUX -I /usr/local/src/linux2.4/include -c -o globalvar.o globalvar.c |
編譯代碼,運行:
inmod globalvar.o |
加載globalvar模塊,再運行:
cat /proc/devices |
發現其中多出了"254 globalvar"一行,如下圖:
接着我們可以運行:
mknod /dev/globalvar c 254 0 |
創建設備節點,用戶進程通過/dev/globalvar這個路徑就可以訪問到這個全局變量虛擬設備了。我們寫一個用戶態的程序globalvartest.c來驗證上述設備:
#include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <fcntl.h> main() { int fd, num; //打開"/dev/globalvar" fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR); if (fd != -1 ) { //初次讀globalvar read(fd, &num, sizeof(int)); printf("The globalvar is %d\n", num); //寫globalvar printf("Please input the num written to globalvar\n"); scanf("%d", &num); write(fd, &num, sizeof(int)); //再次讀globalvar read(fd, &num, sizeof(int)); printf("The globalvar is %d\n", num); //關閉"/dev/globalvar" close(fd); } else { printf("Device open failure\n"); } } |
編譯上述文件:
gcc -o globalvartest.o globalvartest.c |
運行
./globalvartest.o |
可以發現"globalvar"設備可以正確的讀寫。