Linux 字元裝置驅動架構
字元裝置是Linux三大裝置之一(另外兩種是塊裝置,網路裝置),字元裝置就是位元組流形式通訊的I/O裝置,絕大部分裝置都是字元裝置,常見的字元裝置包括滑鼠、鍵盤、顯示器、串口等等,當我們執行ls -l /dev的時候,就能看到大量的裝置檔案,c就是字元裝置,b就是塊裝置,網路裝置沒有對應的裝置檔案。編寫一個外部模組的字元裝置驅動,除了要實現編寫一個模組所需要的代碼之外,還需要編寫作為一個字元裝置的代碼。
驅動模型
Linux一切皆檔案,那麼作為一個裝置檔案,它的操作方法介面封裝在struct file_operations,當我們寫一個驅動的時候,一定要實現相應的介面,這樣才能使這個驅動可用,Linux的核心中大量使用"註冊+回調"機制進行驅動程式的編寫,所謂註冊回調,簡單的理解,就是當我們open一個裝置檔案的時候,其實是通過VFS找到相應的inode,並執行此前建立這個裝置檔案時註冊在inode中的open函數,其他函數也是如此,所以,為了讓我們寫的驅動能夠正常的被應用程式操作,首先要做的就是實現相應的方法,然後再建立相應的裝置檔案。
#include <linux/cdev.h> //for struct cdev#include <linux/fs.h> //for struct file#include <asm-generic/uaccess.h> //for copy_to_user#include <linux/errno.h> //for error number/* 準備操作方法集 *//* struct file_operations { struct module *owner; //THIS_MODULE //讀裝置 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //寫裝置 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //映射核心空間到使用者空間 int (*mmap) (struct file *, struct vm_area_struct *); //讀寫裝置參數、讀裝置狀態、控制裝置 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //開啟裝置 int (*open) (struct inode *, struct file *); //關閉裝置 int (*release) (struct inode *, struct file *); //重新整理裝置 int (*flush) (struct file *, fl_owner_t id); //檔案定位 loff_t (*llseek) (struct file *, loff_t, int); //非同步通知 int (*fasync) (int, struct file *, int); //POLL機制 unsigned int (*poll) (struct file *, struct poll_table_struct *); 。。。};*/ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset){ return 0;}struct file fops = { .owner = THIS_MODULE, .read = myread, ...};/* 字元裝置物件類型 */struct cdev { //public struct module *owner; //模組所有者(THIS_MODULE),用於模組計數 const struct file_operations *ops; //操作方法集(分工:開啟、關閉、讀/寫、...) dev_t dev; //裝置號(第一個) unsigned int count; //裝置數量 //private ...};static int __init chrdev_init(void){ ... /* 構造cdev裝置對象 */ struct cdev *cdev_alloc(void); /* 初始化cdev裝置對象 */ void cdev_init(struct cdev*, const struct file_opeartions*); /* 為字元裝置靜態申請裝置號 */ int register_chedev_region(dev_t from, unsigned count, const char* name); /* 為字元裝置動態申請主裝置號 */ int alloc_chedev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name); MKDEV(ma,mi) //將主裝置號和次裝置號組合成裝置號 MAJOR(dev) //從dev_t資料中得到主裝置號 MINOR(dev) //從dev_t資料中得到次裝置號 /* 註冊字元裝置對象cdev到核心 */ int cdev_add(struct cdev* , dev_t, unsigned); ...}static void __exit chrdev_exit(void){ ... /* 從核心登出cdev裝置對象 */ void cdev_del(struct cdev* ); /* 從核心登出cdev裝置對象 */ void cdev_put(stuct cdev *); /* 回收裝置號 */ void unregister_chrdev_region(dev_t from, unsigned count); ...}
實現read,write
Linux下各個進程都有自己獨立的進程空間,即使是將核心的資料對應到使用者進程,該資料的PID也會自動轉變為該使用者進程的PID,由於這種機制的存在,我們不能直接將資料從核心空間和使用者空間進行拷貝,而需要專門的拷貝資料函數/宏:
long copy_from_user(void *to, const void __user * from, unsigned long n)long copy_to_user(void __user *to, const void *from, unsigned long n)
這兩個函數可以將核心空間的資料拷貝到回調該函數的使用者進程的使用者進程空間,有了這兩個函數,核心中的read,write就可以實現核心空間和使用者空間的資料拷貝。
ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset){ long ret = 0; size = size > MAX_KBUF?MAX_KBUF:size; if(copy_to_user(user_buf, kbuf,size) return -EAGAIN; } return 0;}
實現ioctl
ioctl是Linux專門為使用者層控制裝置設計的系統調用介面,這個介面具有極大的靈活性,我們的裝置打算讓使用者通過哪些命令實現哪些功能,都可以通過它來實現,ioctl在操作方法集中對應的函數指標是long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);,其中的命令和參數完全由驅動指定,Linux建議如圖所示的方式定義ioctl()命令
裝置類型 序號 方向 資料尺寸
8bit 8bit 2bit 13/14bit
這裡,裝置類型欄位為一個幻數,可以是0~0xff之間的數,核心中的"ioctl-number.txt"給出了一個推薦的和已經被使用的幻數(但是已經好久沒人維護了),新裝置驅動定義幻數的時候要避免與其衝突。命令碼的方向欄位為2bit,表示資料的傳輸方向,可能的值是:_IOC_NONE,_IOC_READ,_IOC_WRITE和_IOC_READ|_IOC_WRITE。命令碼的資料欄位表示涉及的使用者資料的大小,這個成員的寬度依賴於體繫結構,通常是13或14位。核心還定義了_IO(),_IOR(),_IOW(),_IOWR()這4個宏來輔助產生這種格式的命令。這幾個宏的作用是根據傳入的type(裝置類型欄位),nr(序號欄位)和size(資料長度欄位)和宏名銀行的方向欄位移位組合產生命令碼。核心中還預定義了一些I/O控制命令,如果某裝置驅動中包含了與預定義命令一樣的命令碼,這些命令會被當做預定義命令被核心處理而不是被裝置驅動處理,有如下4種:
- FIOCLEX:即file ioctl close on exec 對檔案設定專用的標誌,通知核心當exec()系統帶哦用發生時自動關閉開啟的檔案
- FIONCLEX:即file ioctl not close on exec,清除由FIOCLEX設定的標誌
- FIOQSIZE:獲得一個檔案或目錄的大小,當用於裝置檔案時,返回一個ENOTTY錯誤
- FIONBIO:即file ioctl non-blocking I/O 這個調用修改flip->f_flags中的O_NONBLOCK標誌
我們可以將驅動設計的命令包含在一個標頭檔中,記錄使用者程式和驅動程式的命令約定,下面是一個簡單的例子
//mycmd.h...#include <asm/ioctl.h>#define CMDT 'A'#define KARG_SIZE 36struct karg{ int kval; char kbuf[KARG_SIZE];};#define CMD_OFF _IO(CMDT,0)#define CMD_ON _IO(CMDT,1)#define CMD_R _IOR(CMDT,2,struct karg)#define CMD_W _IOW(CMDT,3,struct karg)...
//chrdev.cstatic long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){ static struct karg karg = { .kval = 0, .kbuf = {0}, }; struct karg *usr_arg; switch(cmd){ case CMD_ON: /* 開燈 */ break; case CMD_OFF: /* 關燈 */ break; case CMD_R: if(_IOC_SIZE(cmd) != sizeof(karg)){ return -EINVAL; } usr_arg = (struct karg *)arg; if(copy_to_user(usr_arg, &karg, sizeof(karg))){ return -EAGAIN; } break; case CMD_W: if(_IOC_SIZE(cmd) != sizeof(karg)){ return -EINVAL; } usr_arg = (struct karg *)arg; if(copy_from_user(&karg, usr_arg, sizeof(karg))){ return -EAGAIN; } break; default: ; }; return 0;}
建立裝置檔案
插入的裝置模組,我們就可以使用cat /proc/devices命令查看當前系統註冊的裝置,但是我們還沒有建立相應的裝置檔案,使用者也就不能通過檔案訪問這個裝置。裝置檔案的inode應該是包含了這個裝置的裝置號,操作方法集指標等資訊,這樣我們就可以通過裝置檔案找到相應的inode進而訪問裝置。建立裝置檔案的方法有兩種,手動建立或自動建立,手動建立裝置檔案就是使用mknod /dev/xxx 裝置類型 主裝置號 次裝置號的命令建立,所以首先需要使用cat /proc/devices查看裝置的主裝置號並通過源碼找到裝置的次裝置號,需要注意的是,理論上裝置檔案可以放置在任何檔案加夾,但是放到"/dev"才符合Linux的裝置管理機制,這裡面的devtmpfs是專門設計用來管理裝置檔案的檔案系統。裝置檔案建立好之後就會和建立時指定的裝置綁定,即使裝置已經被卸載了,如要刪除裝置檔案,只需要像刪除普通檔案一樣rm即可。理論上模組名(lsmod),裝置名稱(/proc/devices),裝置檔案名稱(/dev)並沒有什麼關係,完全可以不一樣,但是原則上還是建議將三者進行統一,便於管理。
除了使用蹩腳的手動建立裝置節點的方式,我們還可以在裝置源碼中使用相應的措施使裝置一旦被載入就自動建立裝置檔案,自動建立裝置檔案需要我們在編譯核心的時候或製作根檔案系統的時候就好相應的配置:
Device Drivers ---> Generic Driver Options ---> [*]Maintain a devtmpfs filesystem to mount at /dev [*] Automount devtmpfs at /dev,after the kernel mounted the rootfsOR
製作根檔案系統的啟動指令碼寫入
mount -t sysfs none sysfs /sysmdev -s //udev也行
有了這些準備,只需要匯出相應的裝置資訊到"/sys"就可以按照我們的要求自動建立裝置檔案。核心給我們提供了相關的API
class_create(owner,name);struct device *device_create_vargs(struct class *cls, struct device *parent,dev_t devt, void *drvdata,const char *fmt, va_list vargs);void class_destroy(struct class *cls); void device_destroy(struct class *cls, dev_t devt);
有了這幾個函數,我們就可以在裝置的xxx_init()和xxx_exit()中分別填寫以下的代碼就可以實現自動的建立刪除裝置檔案
/* 在/sys中匯出裝置類資訊 */ cls = class_create(THIS_MODULE,DEV_NAME); /* 在cls指向的類中建立一組(個)裝置檔案 */ for(i= minor;i<(minor+cnt);i++){ devp = device_create(cls,NULL,MKDEV(major,i),NULL,"%s%d",DEV_NAME,i); }
/* 在cls指向的類中刪除一組(個)裝置檔案 */ for(i= minor;i<(minor+cnt);i++){ device_destroy(cls,MKDEV(major,i)); } /* 在/sys中刪除裝置類資訊 */ class_destroy(cls); //一定要先卸載device再卸載class
完成了這些工作,一個簡單的字元裝置驅動就搭建完成了,現在就可以寫一個使用者程式進行測試了^ - ^
感謝閱讀,希望能協助到大家,謝謝大家對本站的支援!