高级字符设备驱动是在简单字符设备驱动的基础上添加ioctl方法、阻塞非阻塞读写、poll方法、和自动创建设备文件的功能,首先我们还是来了解一些重要的相关知识点;
一、重要知识点
> ioctl 系统调用
除了读取和写入设备操作之外,大部分驱动程序还需要另外一些能力,即通过设备驱动程序执行各种类型的硬件控制,而这些对硬件设备的控制是通过ioctl方法来实现的,该方法实现了同名的系统调用;
在用户空间,ioctl 系统调用具有如下模型:
int ioctl ( int fd, unsigned long cmd , . . .); /* . . . 表示可变数码的参数表,具体形式依赖于 cmd 控制命令,习惯用char *argp 定义 */
在驱动程序中,ioctl 方法原型和用户空间的形式有些不同:
int ( *ioctl ) ( struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg );
inode、filp: 对应于应用程序传递的文件描述符fd;
cmd: 由用户空间直接传递给驱动程序;
arg: 无论用户空间对应的参数arg使用的是指针还是整数,它都以unsigned long 的形式传给驱动;如果调用的程序没有定义第三个参数,那么驱动程序所接收的arg就处于未定义状态;
正如大多数ioctl方法实现的那样,ioctl的实现都包涵一个switch语句来根据cmd参数选着对应的操作;为了简化代码通常使用符号代替数值,下面重点探讨ioctl命令--cmd参数;
为了方便程序员创建唯一的ioctl命令,每个命令被分为多个位字段;使用4个字段定义一个ioctl命令,这些位字段在头文件中被定义为:类型、序数、传送方向和参数大小等;
type: 幻数。这个字段有8位位宽(_IOC_TYPEBITS),描述了命令属于那些设备;
nr: 序数。也是8位位宽(_IOC_NRBITS),描述了这条命令在命令列表中的序列号,次序位置;
direction:传输数据的方向;可以使用的值包括_IOC_NONE(没有数据传输)、_IOC_READ、_IOC_WRITE以及_IOC_READ|_IOC_WRITE(双向数据传输);数据是从应用程序的角度看的,例如_IOC_READ意味着从设备中读取数据;
size:所涉及用户数据的大小,当不涉及数据传输时,此字段无效;
在Linux内核中还定义了一些构造命令编号的宏:_IO(type,nr)用于构造无参数的命令编号;_IOR(type,nr,datatype)用于构造从驱动程序中读取数据的命令编号;_IOW(type,nr,datatype)用于写入数据的命令;_IOWR(type,nr,datatype)用于双向传输。type和nr位字段通过参数传入,而size位字段通过datatype参数取sizeof获取;
在使用ioctl系统调用之前,还有一点就是如何使用arg这个可变参数,他可以是整数也可以是指针。如果是整数可以直接使用,如果是指针还需确保指向的是用户空间;对未经检验的用户空间的指针直接使用可能会引发一系列系统安全问题,所以因对这个指针进行适当的检查,检查驱动程序接收的arg指针参数是否合法;通常,我们使用函数access_ok验证地址是否合法。函数申明如下:
int access_ok(int type,const void *addr,unsigned long size );
type:VERIFY_READ或VERIFY_WRITE,取决于要执行的动作是读取还是写入用户空间内存区;
addr:用户空间的地址;
size:字节数;
关于access_ok函数有两点需要注意:一、它确保了访问的地址没有指向内核空间的内存区;二、大多数驱动程序不需要调用到access_ok函数;
int put_user(datum, ptr)
int get_user(local, ptr)
int __put_user(datum, ptr)
int __get_user(local, ptr)
用于向(或从)用户空间保存(或获取)单个数据项的宏。传送的字节数目由sizeof(*ptr)决定。前两个要先调用access_ok,后两个(__put_user和__get_user)则假设access_ok已经被调用过了。
>阻塞型IO
当我们在读写设备文件时,有时会出现数据不可以的情况。在这种情况下驱动程序应该(默认)阻塞该进程,将其置入休眠状态直到请求可继续;对Linux设备驱动程序来讲,让一个进程进入休眠状态很容易。但是,为了进程以一种安全的方式进入休眠,我们必须牢记两条规则:一、不要在原子上下文中进入休眠;二、确保我们等待的条件为真。另外还必须确保能够找到休眠的进程,即需要维护一个等待队列。等待队列通过一个”等待队列头“来管理的,队列头是一个类型为wait_queue_head_t的结构体。使用如下方法定义并初始化一个等待队列:
typedef struct
{/*…
..*/} wait_queue_head_t;/* 等待队-列-队列头 */
wait_queue_head_t my_queue; /* 动态定义并初始化 */
init_waitqueue_head(&my_queue) ;
DECLARE_WAIT_QUEUE_HEAD(queue) /* 静态定义并初始化 */
预先定义的Linux内核等待队列类型。wait_queue_head_t类型必须显示地初始化,初始化方法可以在运行时调用init_wait_queue_head_t,或在编译时DECLARE_WAIT_QUEUE_HEAD。
wait_event((wait_queue_head_t q, intcondition)
wait_event_interruptible(wait_queue_head_t q, int condition)
wait_event_timeout(wait_queue_head_t q,int condition, int time)
wait_event_interruptible_timeout(wait_queue_head_t q, int condition, int time)
使进程在指定的队列上休眠,直到给定的condition值为真。
void wake_up(wait_queue_head_t **queue)
void wake_up_interruptible(wait_queue_head_t **queue)
这些函数唤醒休眠在队列queue上的进程。_interruptible形式的函数只能唤醒可中断的进程。在实践中约定做法是在使用wait_event时用
wake_up,而在使用wait_event_interruptible时使用wake_up_interruptible。
>poll方法
使用非阻塞I/O的应用程序时也会经常使用poll、select和epoll系统调用。poll、select和epoll的功能基本一样的:都允许进程决定是否对一个或者多个打开的文件做非阻塞的读取或者写入。这些调用也会阻塞进程,直到给定的文件描述符集合中的任何一个可以写入或者读取;所有三个系统调用均通过驱动程序的poll方法来提供,其原型:
unsigned int (*poll) (struct file *filp,poll_table *wait);
poll方法分两步处理:
第一步调用poll_wait指定等待队列,如果当前没有文件描述符可用来执行I/O,则内核将使进程传递到该系统调用的所有描述符对应的等待队列上;
第二步返回一个用来描述操作是否可以立即无阻阻塞执行的位掩码;
POLLIN表示设备可读的掩码,POLLRDORM表示数据可读的掩码。POLLOUT表示设备可写的掩码,POLLWRNORM表示数据可读的掩码。一般同时返回POLLIN和POLLRDORM或者POLLOUT和POLLWRNORM。
>select系统调用
原型为int select(int mafdp1, fd_set *restrict readfds, fd_set
*restrict writefds, fd_set*restrict exceptfds, struct timeval *restrict
tvptr) 返回值:就绪的描述符数,若超时则返回0,若出错则返回-1;
void FD_ISSET(int fd, fd_set *fdset)
void FD_CLR(int fd, fd_set *fdset)
void FD_SET(int fd, fd_set *fdset)
void FD_ZERO(fd_set *fdset)
调用FD_ZERO将一个指定的fd_set变量的所有位设置为0。调用FD_SET设置一个fd_set变量指定位。调用FD_CLR则将一指定位清除。最后,调用FD_ISSET测试一指定位是否设置。
5.自动创建设备文件
struct class *class_create(struct module*owner, const char *name)
struct device *device_create(struct class*class, struct device *parent, dev_t devt, const char *fmt, ...)
通过这两个函数可以专门用来创建一个字符设备文件节点,class_create
第一个参数指定所有者,第二参数指定类得名字。class_device_create第一个参数指定第一个参数指定所要创建的设备所从属的类,第二个参
数是这个设备的父设备,如果没有就指定为NULL,第三个参数是设备号,第四个参数是设备名称。
二、驱动代码
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <linux/ioctl.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/device.h>
#define MEMDEV_MAJOR 251
#define MEMDEV_NUM 2
#define MEMDEV_SIZE 1024
//定义设备IOCTL命令
#define MEMDEV_IOC_MAGIC 'k'
#define MEMDEV_IOC_NR 2
#define MEMDEV_IOC_PRINT_IO(MEMDEV_IOC_MAGIC, 0)
#define MEMDEV_IOC_RD_IOR(MEMDEV_IOC_MAGIC, 1, int)
#define MEMDEV_IOC_WT_IOW(MEMDEV_IOC_MAGIC, 2, char)
struct mem_dev
{
unsigned int size;
char *data;
struct semaphore sem;
wait_queue_head_t inque;
};
static int mem_major = MEMDEV_MAJOR;
struct cdev mem_cdev;
struct mem_dev *mem_devp;
bool havedata = false;
static int mem_open(struct inode *inode,struct file *filp)
{
struct mem_dev *dev;
unsigned int num;
printk("mem_open.\n");
num= MINOR(inode->i_rdev); //获得次设备号
if(num> (MEMDEV_NUM -1)) //检查次设备号有效性
return-ENODEV;
dev= &mem_devp[num];
filp->private_data= dev; //将设备结构保存为私有数据
return 0;
}
static int mem_release(struct inode *inode,struct file *filp)
{
printk("mem_release.\n");
return 0;
}
static ssize_t mem_read(struct file *filp,char __user *buf, size_t size, loff_t *ppos)
{
int ret = 0;
struct mem_dev *dev;
unsigned long p;
unsigned long count;
printk("mem_read.\n");
dev= filp->private_data; //获得设备结构
count= size;
p= *ppos;
//检查偏移量和数据大小的有效性
if(p> MEMDEV_SIZE)
return 0;
if(count> (MEMDEV_SIZE-p))
count= MEMDEV_SIZE - p;
if(down_interruptible(&dev->sem)) //锁定互斥信号量
return -ERESTARTSYS;
while(!havedata)
{
up(&dev->sem);
if(filp->f_flags& O_NONBLOCK)
return -EAGAIN;
printk("ready to go sleep");
if(wait_event_interruptible(dev->inque,havedata)) //等待数据
return -ERESTARTSYS;
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
//读取数据到用户空间
if(copy_to_user(buf,dev->data+p, count))
{
ret= -EFAULT;
printk("copy from user failed\n");
}
else
{
*ppos+= count;
ret = count;
printk("read %d bytes from dev\n", count);
havedata = false; //数据已经读出
}
up(&dev->sem); //解锁互斥信号量
return ret;
}
static ssize_t mem_write(struct file *filp,const char __user *buf, size_t size, loff_t *ppos) //注意:第二个参数和read方法不同
{
int ret = 0;
struct mem_dev *dev;
unsigned long p;
unsigned long count;
printk("mem_write.\n");
dev= filp->private_data;
count= size;
p= *ppos;
if(p> MEMDEV_SIZE)
return 0;
if(count> (MEMDEV_SIZE-p))
count= MEMDEV_SIZE - p;
if(down_interruptible(&dev->sem)) //锁定互斥信号量
return -ERESTARTSYS;
if(copy_from_user(dev->data+p,buf, count))
{
ret= -EFAULT;
printk("copy from user failed \n");
}
else
{
*ppos+= count;
ret = count;
printk("write %d byte(s) to dev \n", count);
havedata= true;
wake_up_interruptible(&dev->inque); //唤醒等待数据的队列
}
up(&dev->sem); //解锁互斥信号量
return ret;
}
static loff_t mem_llseek(struct file *filp,loff_t offset, int whence)
{
int newpos;
printk("mem_llseek.\n");
switch(whence)
{
case0://从文件头开始
newpos = offset;
break;
case1://从文件当前位置开始
newpos= filp->f_pos + offset;
break;
case2://从文件末尾开始
newpos= MEMDEV_SIZE - 1 + offset;
break;
default:
return -EINVAL;
}
if((newpos<0)|| (newpos>(MEMDEV_SIZE - 1)))
return -EINVAL;
filp->f_pos= newpos;
return newpos;
}
static int mem_ioctl(struct inode *inode,struct file *filp, unsigned int cmd, unsigned long arg)
{
int err = 0, ret = 0;
int ioarg = 0;
char rdarg = '0';
//参数检查
if(_IOC_TYPE(cmd)!= MEMDEV_IOC_MAGIC) //参数类型检查
return -ENOTTY;
if(_IOC_NR(cmd)> MEMDEV_IOC_NR) //参数命令号检查
return -ENOTTY;
//用户空间指针有效性检查
if(_IOC_DIR(cmd)& _IOC_READ)
err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
elseif(_IOC_DIR(cmd) & _IOC_WRITE)
err= !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
if(err)
return -ENOTTY;
//根据命令执行操作
switch(cmd)
{
case MEMDEV_IOC_PRINT:
printk("memdev ioctl print excuting...\n");
break;
case MEMDEV_IOC_RD:
ioarg= 1024;
ret = __put_user(ioarg, (int *)arg); //用户空间向内核空间获得数据
printk("memdev ioctl read excuting... \n");
break;
case MEMDEV_IOC_WT:
ret = __get_user(rdarg, (char *)arg); //用户空间向内核空间传输数据
printk("memdev ioctl write excuting... arg:%c\n", rdarg);
break;
default:
return -ENOTTY;
}
returnret;
}
static unsigned int mem_poll(struct file*filp, poll_table *wait)
{
structmem_dev *dev;
unsignedint mask = 0;
dev= filp->private_data;
if(down_interruptible(&dev->sem))//锁定互斥信号量
return-ERESTARTSYS;
poll_wait(filp,&dev->inque, wait);
if(havedata)
mask|= POLLIN | POLLRDNORM; //返回可读掩码
up(&dev->sem); //释放信号量
return mask;
}
static const struct file_operationsmem_fops = {
.owner= THIS_MODULE,
.open= mem_open,
.write= mem_write,
.read= mem_read,
.release= mem_release,
.llseek= mem_llseek,
.ioctl= mem_ioctl,
.poll= mem_poll,
};
static int __init memdev_init(void)
{
int result;
int err;
int i;
struct class *memdev_class;
//申请设备号
dev_t devno = MKDEV(mem_major, 0);
if(mem_major)
result= register_chrdev_region(devno, MEMDEV_NUM, "memdev"); //注意静态申请的dev_t参数和动态dev_t参数的区别
else
{ //
静态直接传变量,动态传变量指针
result = alloc_chrdev_region(&devno, 0, MEMDEV_NUM, "memdev");
mem_major = MAJOR(devno);
}
if(result< 0)
{
printk("can't get major devno:%d \n", mem_major);
return result;
}
//注册设备驱动
cdev_init(&mem_cdev,&mem_fops);
mem_cdev.owner= THIS_MODULE;
err = cdev_add(&mem_cdev, MKDEV(mem_major, 0), MEMDEV_NUM);//如果有N个设备就要添加N个设备号
if(err)
printk("add cdev faild,err is %d\n", err);
//分配设备内存
mem_devp= kmalloc(MEMDEV_NUM *(sizeof(struct mem_dev)), GFP_KERNEL);
if(!mem_devp)
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp,0, MEMDEV_NUM*(sizeof(struct mem_dev)));
for(i=0;i<MEMDEV_NUM; i++)
{
mem_devp[i].size= MEMDEV_SIZE;
mem_devp[i].data= kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data,0, MEMDEV_SIZE);
init_MUTEX(&mem_devp[i].sem);//初始化互斥锁
//初始化等待队列
init_waitqueue_head(&mem_devp[i].inque);
}
//自动创建设备文件
memdev_class= class_create(THIS_MODULE, "memdev_driver");
device_create(memdev_class,NULL, MKDEV(mem_major, 0), NULL, "memdev0");
return result;
fail_malloc:
unregister_chrdev_region(MKDEV(mem_major,0), MEMDEV_NUM);
return result;
}
static void memdev_exit(void)
{
cdev_del(&mem_cdev);
unregister_chrdev_region(MKDEV(mem_major,0), MEMDEV_NUM);//注意释放的设备号个数一定要和申请的设备号个数保存一致
//
否则会导致设备号资源流失
printk("memdev_exit \n");
}
module_init(memdev_init);
module_exit(memdev_exit);
MODULE_AUTHOR("Y-Kee");
MODULE_LICENSE("GPL");