字符設備是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);
...
}
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是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種:
我們可以將驅動設計的命令包含在一個頭文件中,記錄用戶程序和驅動程序的命令約定,下面是一個簡單的例子
//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
完成了這些工作,一個簡單的字符設備驅動就搭建完成了,現在就可以寫一個用戶程序進行測試了^ - ^