本文介绍字符型设备驱动的基本框架。
一个字符型设备驱动基本框架主要包括:
1. 设备结构体、全局变量的定义
2. 实现设备模块加载、卸载函数
3. 实现 fops 中操作设备相关的函数
1、结构体、全局变量定义:
a. 通常会定义一个设备结构体:
struct xxx_dev{ struct cdev cdev; //字符设备结构体,必须要有 /* 以下均为驱动中可能需要用到的变量,如缓存数组、信号量、等待队列头等 */ unsignedint current_len; //当前fifo中的数据长度 unsignedchar mem[GLOBALFIFO_SIZE]; //全局内存 struct semaphore sem; //信号量,并发访问用到 wait_queue_head_t r_wait; //用于阻塞读 wait_queue_head_t w_wait; //用于阻塞写 struct fasync_struct *async_queue; //异步结构体指针,用于读 }; |
b. 然后实例化一个设备结构体指针:
struct xxx_dev *xxx_devp; |
通常该结构体指针会在 xxx_open() 函数中赋给 filp->private_data,这样,在read、write、ioctl等函数中就可以直接通过filp->private_data获取设备结构体。
c. 实例化文件操作结果体:
static const struct file_operations xxx_fops={ .owner = THIS_MODULE, .read = xxx_read, .write = xxx_write, .open = xxx_open, .release = xxx_release, .poll = xxx_poll, .fasync = xxx_fasync, }; |
该结构体中的成员很多,但通常需要实现的只有read、write、open、release、ioctl,而poll用于非阻塞访问的查询操作,fasync用于异步通知。
这些函数在应用程序调用响应的系统调用时被调用,系统调用相当于应用程序与内核的接口,而驱动中的这些函数即是内核与硬件之间的接口。 xxx_fops 是在驱动模块装载时通过 cdev_init() 函数与 xxx_dev->cdev 建立起联系的。
d. 主设备号:
static int xxx_major = XXX_MAJOR; |
一般来说,XXX_MAJOR定义为0,而变量 xxx_major 会作为模块参数:
module_param(xxx_major,int, S_IRUGO); |
这样做是可以让用户指定主设备号:
insmod xxx.ko xxx_major=249 |
而如果用户没有指定,那么默认为0,在初始化时,xxx_init()函数中会让系统分配一个还没有被使用的主设备号,以避免主设备号冲突的情况。
2、驱动模块装载、卸载函数
a. 模块装载函数 xxx_init()
模块加载函数主要要做以下几件事情:
1) 注册设备号:
32位的设备号由12位的主设备号和20位的次设备号构成。
主设备号可以指定,也可以由系统分配,为了防止主设备号冲突,由系统分配是个不错的办法。
此设备号通常从0开始。
2) 为设备结构体申请内存空间:
xxx_devp= kmalloc(sizeof(struct xxx_dev), GFP_KERNEL);
3) 初始化设备结构体中的字符设备结构体cdev :
首先,调用 cdev_init() 将 xxx_fops 与 xxx_devp->cdev 绑定。(cdev结构体中有个ops成员, 实际上就是将该成员指向xxx_fops)
其次,xxx_devp->cdev.owner= THIS_MODULE;
最后,调用 cdev_add()向系统添加一个cdev,完成字符型设备的注册
4) 初始化一些需要用到的其他变量,如信号量、等待队列头等
5) 增加udev支持,linux2.6之后引入了udev 设备文件系统,如果不加该语句,insmod之后,还需要通过mknod创建设备文件节点,而在驱动程序添加了udev支持后,有udev的linux系统会自动创建设备文件节点。
static int __init xxx_init(void) { int ret; /* 由主设备号和次设备号生成设备号 */ dev_t devno = MKDEV(xxx_major,0);
/* 注册设备号 */ if(xxx_major>0) /* 用户设置好了主设备号,1表示注册1个设备, "xxx" 是设备名称,在cat /proc/devices时可以看到 */ ret = register_chrdev_region(devno,1,"xxx"); else { /* 用户没有设置主设备号,由系统自动分配主设备号, 0表示次设备号从0开始, 1表示注册1个设备, 返回设备号 */ ret = alloc_chrdev_region(&devno,0,1,"xxx"); /* 由返回的设备号计算主设备号 */ xxx_major = MAJOR(devno); } /* 注册失败,返回错误代码*/ if (ret<0) return ret;
/* 为设备结构体申请内存空间 */ xxx_devp = kmalloc(sizeof(struct xxx_dev), GFP_KERNEL); /* 申请失败,返回错误代码,并且需要注销之前注册成功的设备号 */ if (xxx_devp==NULL){ ret = -ENOMEM; goto fail_malloc; } memset(xxx_devp,0,sizeof(struct xxx_dev));
/* 初始化信号量和等待队列头 */ sema_init(&xxx_devp->sem,1);//init_MUTEX(&xxx_devp->sem); init_waitqueue_head(&xxx_devp->r_wait); init_waitqueue_head(&xxx_devp->w_wait); /* 初始化字符型设备结构体cdev */ ret = xxx_setup_cdev(xxx_devp,0); if (ret<0) gotofail_add_cdev; return 0;
fail_add_cdev: kfree(xxx_devp); fail_malloc: unregister_chrdev_region(devno,1); return ret; }
static int xxx_setup_cdev(struct xxx_dev*dev,int index) { int err, devno= MKDEV(xxx_major, index);
/* 初始化cdev */ cdev_init(&dev->cdev,&xxx_fops); dev->cdev.owner= THIS_MODULE;
/* 添加字符型设备驱动 */ err = cdev_add(&dev->cdev, devno,1); if(err) printk(KERN_ALERT"Error %d adding xxx", err); return err; } |
b.模块卸载函数 xxx_exit()
static void __exit xxx_exit(void) { /* udev支持 */ device_destroy(xxx_devp->myclass, MKDEV(xxx_major,0)); class_destroy(xxx_devp->myclass); /* 删除cdev结构 */ cdev_del(&xxx_devp->cdev); /* 释放设备结构体内存 */ kfree(xxx_devp); /* 注销设备区域 */ unregister_chrdev_region(MKDEV(xxx_major,0),1); } |
3. 实现结构体 xxx_fops 中的函数
a. 打开和关闭
首先来看文件打开和关闭所对应的 xxx_open() 和xxx_release()函数。
函数原型如下:
static int xxx_open(struct inode*inode,struct file *filp);
static int xxx_release(struct inode*inode,struct file *file); |
通常在xxx_open()中要做的事情是一些硬件上的初始化,例如配置一些寄存器,申请中断等。通常还会把 xxx_devp放到 filp->private_data中:
file->private_data = key_devp;
以便在读写函数中获取设备结构体指针。
在xxx_release()中则是做和 xxx_open()相反的事情,例如:释放中断等。
b. 读写函数
与文件读写相关的 xxx_read()和 xxx_write()函数原型如下:
static ssize_txxx_read(struct file*filp,char __user *buf, size_t size, loff_t* ppos);
static ssize_t xxx_write(struct file*filp,constchar __user*buf, size_t size, loff_t*ppos) |
其中buf是用户空间用于读写的指针,size是用户需要读写的字节数,ppos是文件读写指针的位置,返回值是实际读写的字节数。
在 xxx_read()中可以调用 copy_to_user()函数将数据拷贝到buf处,而在xxx_write中可以调用copy_from_user()函数从buf中读取数据。
c. 其他函数
ioctl用于io控制命令;
poll用于非阻塞访问的查询操作;
fasync用于异步通知等,
以后用到再细说。