1、什么是字符设备
1.1 基本概念
字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
1.2 数据结构
struct cdev//设备结构体
{
struct kobject kobj; /* 内嵌的kobject 对象 */
struct module *owner; /*所属模块*/
struct file_operations *ops; /*文件操作结构体*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count;
};
2、字符设备驱动开发的基本步骤
2.1 确定主设备号和次设备号
设备号是一个32bit的整数,由主设备号和从设备号组成。主设备号是内核识别一类设备的标识。整数(占12bits),范围从0到4095,通常使用1到255。次设备号由内核使用,用于正确确定设备文件所指的设备。整数(占20bits),范围从0到1048575,一般使用0到255。
设备号需要向内核申请,有静态申请和动态申请两种方式。静态申请就是自己定义一个值,然后调用相关的函数向内核申请,这种方法可能会和别的设备号冲突,一般不使用;动态申请就是直接调用相关的函数,由内核返回一个设备号给你。下面第三节会介绍这些函数的,再此之前先来看看几个宏定义。
将主设备号和次设备号转换成dev_t类型的宏:
MKDEV (int major,int minor);
如dev_t devno = MKDEV(232, 0);
从dev_t获得主设备号和次设备号的宏:
MAJOR(dev_t);
/*以上几个宏定义如下:*/
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2.2 实现字符驱动程序
2.2.1 实现file_operations结构体
首先,我们先来看看3.0内核下../include/linux/fs.h中file_operations结构体的定义:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};
在kernel 3.0中已经完全删除了struct file_operations 中的ioctl 函数指针,剩下unlocked_ioctl和compat_ioctl。主要改进就是不再需要上内核锁 (调用之前不再先调用lock_kernel()然后再unlock_kernel())。
一般我们只需要实现以下四个函数就行了:
(1)ssize_t (read) (struct file , char __user , size_t, loff_t );
应用程序调用read()函数时,驱动的这个函数会被调用。
(2)ssize_t (write) (struct file , const char __user , size_t, loff_t );
应用程序调用write()函数时,驱动的这个函数会被调用。
(3) unsigned int (poll) (struct file , struct poll_table_struct *);
当应用程序需要支持select或者poll机制时,驱动的poll函数需要被实现,具体看我的另一篇博客《select机制的驱动实现及原理》。
(4)long (unlocked_ioctl) (struct file , unsigned int, unsigned long);
当应用程序调用ioctl函数时,驱动的这个函数会被调用。它的原型如下:
long (*ioctl)(struct file *file, unsigned int cmd, unsigned long arg) //kernel 3.0后
int (*ioctl) (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) //kernel 3.0前
这里详细介绍一下cmd参数。
在编写ioctl代码之前,需要选择对应不同命令的编号。为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一,这种错误匹配并不是不会发生,程序可能发现自己正在试图对FIFO和audio等这类非串行设备输入流修改波特率,如果每一个ioctl命令都是唯一的,应用程序进行这种操作时就会得到一个EINVAL错误,而不是无意间成功地完成了意想不到的操作。
尽管ioctl系统调用绝大部分用于操作设备,但是还有一些命令是可以由内核识别的。要注意,当这些命令用于我们的设备时,他们会在我们自己的文件操作被调用之前被解码。所以,如果你为自己的ioctl命令选用了与这些预定义命令相同的编号,就永远不会收到该命令的请求,而且由于ioctl编号冲突,应用程序的行为将无法预测。
以上两段话来自ldd3,说了为什么cmd在系统里要有唯一性。为了做到唯一性,内核约定cmd被分为四个字段,具体含义如下图:
设备类型(幻数) | 序列号 | 方向 | 数据尺寸 |
---|---|---|---|
8 bit | 8 bit | 2 bit | 8~14 bit |
幻数:说得再好听的名字也只不过是个0~0xff的数,占8bit(_IOC_TYPEBITS)。一个设备对应一个幻数。
序列号:用这个数来给自己的命令编号,占8bit(_IOC_NRBITS)
方向:读或者写,方向是以应用层的角度来描述的。
数据尺寸:所涉及的用户数据大小。系统并不强制使用这个位字段,也就是说,内核不会检测这个位字段。
我们开发驱动时要按Linux内核的约定方法为驱动程序选择ioctl编号,应该首先看看include/asm/ioctl.h和Doucumention/ioctl-number.txt这两个文件。头文件定义了要使用的位字段:类型(幻数)、序数、传送方向以及参数大小等。ioctl-number.txt文件中罗列了内核所使用的幻数,选择自己的幻数要避免和内核冲突。
在include/asm/ioctl.h头文件中,定义一些宏用来操作cmd。
//构造无参数的命令编号
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
//构造从驱动程序中读取数据的命令编号
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
//用于向驱动程序写入数据命令
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
//用于双向传输
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
//从命令参数中解析出数据方向,即写进还是读出
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
//从命令参数中解析出幻数type
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
//从命令参数中解析出序数number
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
//从命令参数中解析出用户数据大小
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
例如我们可以这样来来定义一个cmd:
#define HELLO_MAGIC 'k'
#define HELLO_CMD1 _IO(HELLO_MAGIC,0x1a)
#define HELLO_CMD2 _IO(HELLO_MAGIC,0x1b)
/*
对幻数的编号千万不能重复定义,如ioctl-number.txt已经说明‘k'的编号已经被占用的范围为:
'k' 00-0F linux/spi/spidev.h conflict!
'k' 00-05 video/kyro.h conflict!
所以我们在这里分别编号为0x1a和0x1b.
*/
在我看来,内核对cmd的这种约定,只是一种规范而已,最多只能避免我们定义的cmd不和内核定义的冲突,根本无法避免在同一个系统上两个互不相关的驱动程序员不会使用相同的幻数。但是能避免不和内核冲突已经够了,只要应用程序调用ioctl时,不把设备描述符传错应该就不会有什么太大的问题。
2.2.2 实现初始化函数,注册字符设备
首先我们要向内核申请设备号,然后将设备号和设备绑定,还要将设备和文件描述结构体绑定。具体流程看第三节的函数介绍和第四节的例子。
2.2.3 实现销毁函数,释放字符设备
驱动卸载时需要释放资源,否则第二次insmod驱动时会出错。具体流程看第三节的函数介绍和第四节的例子。
2.3 创建设备文件节点
1、手动创建
mknod /dev/node_name c major minor
例如:
mknod /dev/hello c 250 0
2、自动创建
可以在模块注册函数里调用class_create和device_create。
例如:
p_hello_class = class_create(THIS_MODULE, HELLO_NAME);
p_hello_device = device_create(p_hello_class, NULL, devno, NULL, HELLO_NAME);
用class_create函数来创建一个名字为HELLO_NAME的类,这个类存放于sys/class下面。一旦创建好了这个类,再调用 device_create函数。这样,加载模块的时候,用户空间中的udev或mdev会自动响应 device_create函数,去sys/class下寻找对应的类从而在dev目录下创建相应的设备节点。
3、相关函数介绍
(1)静态申请设备号
int register_chrdev_region(dev_t first, unsigned int count, char *name);
头文件:#include <linux/fs.h>
返回值:成功返回0,失败返回一个负的错误码。
参数:
first:分配的起始设备编号。first 的次编号部分常常是 0, 但是没有要求是那个效果。
count:所请求的连续设备编号的个数。
name:是应当连接到这个编号范围的设备的名字。
(2)动态申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
头文件:#include <linux/fs.h>
返回值:成功返回0,失败返回一个负的错误码。
参数:
dev:返回的设备号
firstminor:被请求的第一个次设备号
count:请求的个数
name:是应当连接到这个编号范围的设备的名字。
(3)释放设备号
void unregister_chrdev_region(dev_t first, unsigned int count);
(4)添加设备
int cdev_add(struct cdev *, dev_t, unsigned);
把申请的设备号与设备绑定,然后向系统添加一个cdev,至此,系统通过设备号就能找到这个设备。完成字符设备的注册,通常在模块加载函数中调用
(5)删除设备
void cdev_del(struct cdev *);
分别向系统删除一个cdev,完成字符设备的注销,通常在模块的卸载函数中调用
(6)字符设备初始化
void cdev_init( struct cdev *, struc t file_operations *);
用于初始化cdev的成员,并建立cdev和file_operations之间的连接
注意点:
1、在调用cdv_add()函数向系统注册字符设备之前,应先调用register_chrdev_region()函数或alloc_chrdev_region函数向系统申请设备号。
2、相反地,在调用cdev_del()函数从系统注销字符设备后,unregister_chrdev_region函数应该被调用以释放原先申请的设备号。
4、例子
源文件:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/major.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/uaccess.h>
#define HELLO_NAME "myhello"
static struct cdev hello_cdev;
static int hello_major = 0;
static int hello_minor = 0;
static struct class * p_myhello_class = NULL;
static struct device *p_myhello_device = NULL;
static int hello_open(struct inode *inode,struct file *file)
{
printk("myhello open\n");
return 0;
}
static int hello_release(struct inode *inode,struct file *file)
{
printk("myhell release\n");
return 0;
}
static int hello_read(struct file *filp,char __user *buf,size_t count,loff_t *offp)
{
printk("myhello read\n");
return 0;
}
static int hello_write(struct file *flip,const char __user *buf,size_t count,loff_t *offp)
{
printk("myhello write\n");
return 0;
}
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
//.ioctl = hello_ioctl,
};
static int hello_setup_cdev(struct cdev *cdev,dev_t devno)
{
int ret = 0;
cdev_init(cdev,&hello_fops);
cdev->owner = THIS_MODULE;
ret = cdev_add(cdev,devno,1);
return ret;
}
static int __init hello_init(void)
{
int ret = 0;
dev_t devno;
printk("hello_init...\n");
if(hello_major) {
devno = MKDEV(hello_major,hello_minor);
ret = register_chrdev_region(devno,1,HELLO_NAME);
} else {
ret = alloc_chrdev_region(&devno,hello_minor,1,HELLO_NAME);
hello_major = MAJOR(devno);
}
if(ret < 0) {
printk("register cdev failed! ret = %d\n", ret);
return ret;
}
ret = hello_setup_cdev(&hello_cdev,devno);
if(ret) {
printk("setup cdev failed! ret = %d\n", ret);
goto cdev_add_fail;
}
p_myhello_class = class_create(THIS_MODULE, HELLO_NAME);
ret = IS_ERR(p_myhello_class);
if(ret) {
printk("myhello class_create failed!\n");
goto class_create_fail;
}
//p_myhello_device = class_device_create(p_hello_class, NULL, devno, NULL, HELLO_NAME);
p_myhello_device = device_create(p_myhello_class, NULL, devno, NULL, HELLO_NAME);
ret = IS_ERR(p_myhello_device);
if (ret) {
printk(KERN_WARNING "myhello device_create failed, ret = %ld", PTR_ERR(p_myhello_device));
goto device_create_fail;
}
return 0;
device_create_fail:
class_destroy(p_myhello_class);
class_create_fail:
cdev_del(&hello_cdev);
cdev_add_fail:
unregister_chrdev_region(devno,1);
return ret;
}
static void __exit hello_exit(void)
{
dev_t devno;
printk("hello_exit...\n");
devno = MKDEV(hello_major,hello_minor);
device_destroy(p_myhello_class, devno);
class_destroy(p_myhello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(devno,1);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("Jimmy");
MODULE_DESCRIPTION("hello driver");
MODULE_LICENSE("GPL");
Makefile文件:
ifneq ($(KERNELRELEASE),)
obj-m := myhello.o
else
KERNELDIR ?= /home/share/linux-2.6.32-devkit8500
#KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers *.order