字元設備是Linux三大設備之一(另外兩種是塊設備,網路設備),字元設備就是位元組流形式通訊的I/O設備,絕大部分設備都是字元設備,常見的字元設備包括滑鼠、鍵盤、顯示器、串口等等,當我們執行 ls l /dev 的時候,就能看到大量的設備文件, c 就是字元設備, b 就是塊設備,網路設備沒有對應的設 ...
字元設備是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_chrdev_region(dev_t from, unsigned count, const char* name);
/* 為字元設備動態申請主設備號 */
int alloc_chrdev_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 36
struct 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.c
static 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 rootfs
OR
製作根文件系統的啟動腳本寫入
mount -t sysfs none sysfs /sys
mdev -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
完成了這些工作,一個簡單的字元設備驅動就搭建完成了,現在就可以寫一個用戶程式進行測試了^ - ^