一、字符设备基础知识
1.1、设备驱动分类
Linux 系统将设备分为 3 类:字符设备、块设备、网络设备。使用驱动程序:
字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和 LED 设备等。
块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U 盘和 SD 卡等。
每一个字符设备或块设备都在 /dev 目录下对应一个设备文件。Linux 用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。
1.2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
如图,在 Linux 内核中:
a -- 使用 cdev 结构体来描述字符设备;
b -- 通过其成员 dev_t 来定义设备号(分为主、次设备号)以确定字符设备的唯一性;
c -- 通过其成员 file_operations 来定义字符设备驱动提供给 VFS 的接口函数,如常见的 open()、read()、write() 等;
在 Linux 字符设备驱动中:
a -- 模块加载函数通过 register_chrdev_region() 或 alloc_chrdev_region() 来静态或者动态获取设备号;
b -- 通过 cdev_init() 建立 cdev 与 file_operations 之间的连接,通过 cdev_add() 向系统添加一个 cdev 以完成注册;
c -- 模块卸载函数通过 cdev_del() 来注销 cdev,通过 unregister_chrdev_region() 来释放设备号;
用户空间访问该设备的程序:
a -- 通过 Linux 系统调用,如 open()、read()、write(),来 调用 file_operations 中定义的字符设备驱动提供给 VFS 的接口函数;
1.3、字符设备驱动模型
二、cdev 结构体解析
在 Linux 内核中,使用 cdev 结构体来描述一个字符设备,cdev 结构体的定义如下:
<include/linux/cdev.h>
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
内核给出的操作 struct cdev 结构的接口主要有以下几个:
a -- void cdev_init(struct cdev *, const struct file_operations *);
其源代码如代码清单如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
该函数主要对 struct cdev 结构体做初始化,最重要的就是建立 cdev 和 file_operations 之间的连接:
(1)将整个结构体清零;
(2)初始化 list 成员使其指向自身;
(3)初始化 kobj 成员;
(4)初始化 ops 成员;
b -- struct cdev *cdev_alloc(void);
该函数主要分配一个 struct cdev 结构,动态申请一个 cdev 内存,并做了 cdev_init 中所做的前面 3 步初始化工作(第四步初始化工作需要在调用 cdev_alloc 后,显式的做初始化,即:.ops = xxx_ops)。
其源代码清单如下:
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
在上面的两个初始化的函数中,我们没有看到关于 owner 成员、dev 成员、count 成员的初始化;其实,owner 成员的存在体现了驱动程序与内核模块间的亲密关系,struct module 是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显示的初始化:.owner = THIS_MODULE,该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而 dev 成员和 count 成员则在 cdev_add 中才会赋上有效的值。
c -- init cdev_add(struct cdev *p, dev_t dev, unsigned count);
该函数向内核注册一个 struct cdev 结构,即正式通知内核由 struct cdev *p 代表的字符设备已经可以使用了。
当然这里还需要提供两个参数:
(1)第一个设备号 dev;
(2)和该设备关联的设备编号的数量。
这两个参数直接赋值给 struct cdev 的 dev 成员和 count 成员。
d -- void cdev_del(struct cdev *p);
该函数向内核注销一个 struct cdev 结构,即正式通知内核由 struct cdev *p 代表的字符设备已经不可以使用了。
从上数的接口讨论中,我们发现对于 struct cdev 的初始化和注册的过程中,我们需要提供几个东西:
(1)struct file_operations 结构指针;
(2)dev 设备号;
(3)count 次设备号个数。
但是我们依旧不明白这几个值到底代表着什么,而我们又该如何去构造这些值!
三、设备号相应操作
3.1、主设备号和次设备号(二者一起为设备号):
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
Linux 内核中,设备号用 dev_t 来描述,2.6.28 中定义如下:
typedef u_long dev_t;
在 32 位机中是 4 个字节,高 12 位表示主设备号,低 20 位表示次设备号。
内核也为我们提供了几个方便操作的宏实现 dev_t:
1)-- 从设备号中提取 major 和 minor:
MAJOR(dev_t dev);
MINOR(dev_t dev);
2)-- 通过 major 和 minor 构建设备号:
MKDEV(int major, int minor);
注:这只是构建设备号。并未注册,需要调用 register_chrdev_region 静态申请;
//宏定义:
#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))</span>
3.2、分配设备号(两种方法):
a -- 静态申请:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
其源代码清单如下:
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);
}
b -- 动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
其源代码清单如下:
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;
}
可以看到二者都是调用了 __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);
/* temporary */
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;
strlcpy(cd->name, name, sizeof(cd->name));
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和 alloc_chrdev_region 的区别,register_chrdev_region直接将 Major 注册进入,而 alloc_chrdev_region 从 Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;
二者应用可以简单总结如下:
register_chrdev_region
alloc_chrdev_region
devno = MKDEV(major,minor); ret = register_chrdev_region(devno, 1, "hello"); cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1); | alloc_chrdev_region(&devno, minor, 1, "hello"); major = MAJOR(devno); cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1) | register_chrdev(major,"hello",&hello) |
可以看到,除了前面两个函数,还加了一个 register_chrdev 函数,可以发现这个函数的应用非常简单,只要一句就可以搞定签名函数所做之事。
下面分析一下 register_chrdev 函数,其源代码定义如下:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
调用了 __register_chrdev(major, 0, 256, name, fops) 函数:
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, 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);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
可以看到这个函数不只帮我们注册了设备号,还帮我们做了 cdev 的初始化以及 cdev 的注册。
3.3、注销设备号:
void unregister_chrdev_region(dev_t from, unsigned count);
3.4、创建设备文件:
利用 cat /proc/devices 查看申请到的设备名,设备号。
1)使用 mknod 手工创建:mknod filename type major minor
2)自动创建设备节点:
利用 udev(mdev)来实现设备文件的自动创建,首先应保证支持 udev(mdev),由 busybox 配置。在驱动初始化代码里调用 class_create 为该设备创建一个 class,再为每个设备调用 device_create 创建对应的设备。
下面看一个实例,练习一下上面的操作:
hello.c:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static int major = 250;
static int minor = 0;
static dev_t devno;
static struct cdev cdev;
static int hello_open (struct inode *inode, struct file *filep)
{
printk("hello_open \n");
return 0;
}
static struct file_operations hello_ops=
{
.open = hello_open,
};
static int hello_init(void)
{
int ret;
printk("hello_init");
devno = MKDEV(major,minor);
ret = register_chrdev_region(devno, 1, "hello");
if(ret < 0)
{
printk("register_chrdev_region fail \n");
return ret;
}
cdev_init(&cdev,&hello_ops);
ret = cdev_add(&cdev,devno,1);
if(ret < 0)
{
printk("cdev_add fail \n");
return ret;
}
return 0;
}
static void hello_exit(void)
{
cdev_del(&cdev);
unregister_chrdev_region(devno,1);
printk("hello_exit \n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
测试程序 test.c:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
main()
{
int fd;
fd = open("/dev/hello",O_RDWR);
if(fd<0)
{
perror("open fail \n");
return ;
}
close(fd);
}
makefile:
ifneq ($(KERNELRELEASE),)
obj-m:=hello.o
$(info "2nd")
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:
$(info "1st")
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
endif
编译成功后,使用 insmod 命令加载:
然后用 cat /proc/devices 查看,会发现设备号已经申请成功。