随着linux内核的更新换代和计算机硬件的不断增多,字符设备驱动在不同内核版本下也呈现出了几种不同的写法,本文将具体随着linux发展的脚步详述字符设备驱动写法的更新。
在Linux2.4以前,内核中所有已分配的字符设备编号都记录在一个名为 chrdevs ,元素个数为255的散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,代表主设备号相同的一组设备。它在内核中的定义如下:
static struct char_device_struct {struct char_device_struct *next; // 指向散列表中的下一个指针
unsigned int major; // 主设备号
unsigned int baseminor; // 起始次设备号
int minorct; // 设备编号数
char name[64]; // 设备驱动名
struct file_operations *fops; // 指向该设备对应的文件操作函数结构体指针
struct cdev *cdev; // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev()、register_chrdev_region()和alloc_chrdev_region() 。这三个函数都会调用一个共用的 __register_chrdev_region()函数来注册一组设备编号范围(即一个 char_device_struct 结构)。
1. __register_chrdev_region()
首先来看一下__register_chrdev_region函数的定义:
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name){
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
if (major == 0) {
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--)
if (chrdevs[i] == NULL)
break;
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
ret = major;
}
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strncpy(cd->name,name, 64);
i = major_to_index(major);
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
( ((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)) ))
break;
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
}
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
- 函数 __register_chrdev_region() 主要执行以下步骤:
2. register_chrdev()
在Linux2.4内核以前使用的是这种分配设备编号范围的函数。它每次都是粗粒度的分配一个单独主设备号和 0 ~ 255 的次设备号范围(如果申请的主设备号为 0 则动态分配一个)。该函数内部自动分配了一个新的 cdev 结构,我们另外还需传入一个 file_operations 结构的指针,用来和新建的char_device_struct结构体绑定,以后凡是相同主设备号(即所有256个共享该主设备号的次设备号设备)的设备均使用同一个file_operations,不管你实际使用了几个次设备号,默认都会将相应主设备号下的256个次设备号连续注册,造成了极大的浪费。其定义如下:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops){
struct char_device_struct *cd;
struct cdev *cdev;
char *s;
int err = -ENOMEM;
cd = __register_chrdev_region(major, 0, 256, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
*s = '!';
err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, 0, 256));
return err;
}
- 其对应的设备注销函数为unregister_chrdev(),内部调用了__unregister_chrdev_region()函数.
- 为了避免资源的浪费,在linux2.4以后内核采用了register_chrdev_region或者alloc_chrdev_region函数来分配和注册字符设备。
3. register_chrdev_region()
看到region这个词相比大家应该知道,这是分配一个域,即指定的一个次设备号范围。当我们使用自定义的主设备号和次设备号范围时,使用该函数。
如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,并在每个子范围上调用 __register_chrdev_region() 。如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。具体请看其实现代码:
int register_chrdev_region(dev_t from, unsigned count, const char *name){
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
/*
from: 注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0
count:需要连续注册的次设备编号个数,比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上
*name:字符设备名称
*/
- 其对应的设备注销函数为unregister_chrdev_region(),内部也同样调用了__unregister_chrdev_region()函数.
4. alloc_chrdev_region()
该函数用于动态申请设备编号范围,即我们事先不指定主设备号的情况下使用。它通过dev指针参数返回实际获得的起始设备编号。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name){
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
/*
*dev: 存放起始设备编号的指针,当注册成功, *dev就会等于分配到的起始设备编号,可以通过MAJOR()和MINNOR()函数来提取主次设备号
baseminor:次设备号基地址,也就是起始次设备号
count:需要连续注册的次设备编号个数,比如: 起始次设备号(baseminor)为0,baseminor=2,表示0~1的此设备号都要绑定在同一个file_operations操作方法结构体上
*name:字符设备名称
*/
- 其对应的设备注销函数也为unregister_chrdev_region(),内部也同样调用了__unregister_chrdev_region()函数.
5. 后续工作
static struct cdev hello1_cdev; //定义字符设备cdev_init(&hello1_cdev, &hello1_fops); //为字符设备绑定文件操作符
cdev_add(&hello1_cdev, MKDEV(major,0), 2); //为字符设备绑定设备号
cls=class_create(THIS_MODULE, "hello"); //创建一种类,即创建/proc/class/hello
class_device_create(cls,0, MKDEV(major,0), 0, "hello0"); //创建字符设备节点/dev/hello0
一般字符设备的驱动需要以下几个步骤:
为了更好的说明字符设备的编写步骤,下面举一个具体的程序,即在同一主设备号下创建两个设备域,并分别与不同的文件操作函数绑定。
/**创建两个字符设备,他们公用同一个主设备号;
*但次设备号0~1对应第一个字符设备,使用hello1_fops文件操作符;
* 次设备号2~3对应第二个字符设备,使用hello2_fops文件操作符;
* 次设备号4不对应字符设备,不使用文件操作符;
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/list.h>
#include <linux/cdev.h>
static int hello_fops1_open(struct inode *inode, struct file *file)
{
printk("open_hello1\n");
return 0;
}
static int hello_fops2_open (struct inode *inode, struct file *file)
{
printk("open_hello2\n");
return 0;
}
/* 操作结构体1 */
static struct file_operations hello1_fops={
.owner=THIS_MODULE,
.open =hello_fops1_open,
};
/* 操作结构体2 */
static struct file_operations hello2_fops={
.owner=THIS_MODULE,
.open =hello_fops2_open,
};
static int major; //主设备
static struct cdev hello1_cdev; //保存 hello1_fops操作结构体的字符设备
static struct cdev hello2_cdev; //保存 hello2_fops操作结构体的字符设备
static struct class *cls;
static int chrdev_region_init(void)
{
dev_t devid;
#if 0
major = register_chrdev(0,"hello",&hello_fops); //以前采用这种形式
#else
if(major)
{
devid = MKDEV(major,0);
register_chrdev_region(devid, 2, "hello");
}else
{
alloc_chrdev_region(&devid, 0, 2,"hello"); //动态分配字符设备
major=MAJOR(devid);
}
cdev_init(&hello1_cdev, &hello1_fops); //初始化cdev,绑定fops结构体
cdev_add(&hello1_cdev, MKDEV(major,0), 2); //注册cdev,即绑定(major,0~1)
devid = MKDEV(major,2);
register_chrdev_region(devid, 2, "hello2");
cdev_init(&hello2_cdev, &hello2_fops);
cdev_add(&hello2_cdev,devid, 2); //注册cdev,即绑定(major,2~3)
#endif
cls=class_create(THIS_MODULE, "hello");
/*创建字符设备节点*/
class_device_create(cls,0, MKDEV(major,0), 0, "hello0"); //对应hello_fops1操作结构体
class_device_create(cls,0, MKDEV(major,1), 0, "hello1"); //对应hello_fops1操作结构体
class_device_create(cls,0, MKDEV(major,2), 0, "hello2"); //对应hello_fops2操作结构体
class_device_create(cls,0, MKDEV(major,3), 0, "hello3"); //对应hello_fops2操作结构体
class_device_create(cls,0, MKDEV(major,4), 0, "hello4"); //对应空
return 0;
}
void chrdev_region_exit(void)
{
class_device_destroy(cls, MKDEV(major,4));
class_device_destroy(cls, MKDEV(major,3));
class_device_destroy(cls, MKDEV(major,2));
class_device_destroy(cls, MKDEV(major,1));
class_device_destroy(cls, MKDEV(major,0));
class_destroy(cls);
cdev_del(&hello1_cdev);
unregister_chrdev_region(MKDEV(major,0), 2); //注销(major,0)~(major,1)
cdev_del(&hello2_cdev);
unregister_chrdev_region(MKDEV(major,2), 2); //注销(major,2)~(major,3)
}
module_init(chrdev_region_init);
module_exit(chrdev_region_exit);
MODULE_LICENSE("GPL");
针对以上驱动,编写如下测试程序:
#include <stdio.h>#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void print_useg(char arg[]) //打印使用帮助信息
{
printf("useg: \n");
printf("%s [dev]\n",arg);
}
int main(int argc,char **argv)
{
int fd;
if(argc!=2)
{
print_useg(argv[0]);
return -1;
}
fd=open(argv[1],O_RDWR);
if(fd<0)
printf("can't open %s \n",argv[1]);
else
printf("can open %s \n",argv[1]);
return 0;
}
装载驱动后,进行测试,得到如下结果:
# ls /dev/hello* -lcrw-rw---- 1 0 0 252, 0 Jan 1 00:12 /dev/hello0
crw-rw---- 1 0 0 252, 1 Jan 1 00:12 /dev/hello1
crw-rw---- 1 0 0 252, 2 Jan 1 00:12 /dev/hello2
crw-rw---- 1 0 0 252, 3 Jan 1 00:12 /dev/hello3
crw-rw---- 1 0 0 252, 4 Jan 1 00:12 /dev/hello4
#./a.out /dev/hello0 //打开/dev/hello0时,调用的是hello1_fops里的.open()
open hello0
#
#
#./a.out /dev/hello2 //打开/dev/hello2时,调用的是hello1_fops里的.open()
open hello2
#
#
#./a.out /dev/hello4 //打开无效,因为在驱动代码里没有分配次设备号4的操作结构体
can't open /dev/hello4
#