Linux设备驱动–(五)字符设备驱动(上)
一、字符设备驱动基础
-
基本概念
在前面几篇博客里,已经基本介绍了Linux的驱动的的分类情况。所谓的字符设备就是指其数据的处理是以字节流的形式。
Linux系统中有一句经典的话"linux下一切皆文件",这句话的意思是什么呢?Linux将其所管理的资源都当做文件来处理。这就意味着,我们的设备最终会以文件的形式被上层(应用层)去调用。
这样做的好处是什么呢?其实就是为了统一上层接口。设想一下,字符设备多种多样,如果我们能够将其统一以文件的形式呈现给应用程序,那么这岂不是很方便,而且也很便于统一管理。设备文件通常位于/dev/目录下,我们可以使用一下指令去到该目录下面去查看相关的设备文件。
ls -l /dev
虽说设备也被当做文件来看待,但是呢。设备文件比普通文件(如文本文件)多了两个数字。这两个数字就是主设备号和次设备号。这两个号是设备在内核中的身份标志,是内核区分不同设备的唯一信息。
主设备号:用来区别一类设备
次设备号:用于区分同一类设备的不同个体
路径名:用户层用来区别设备信息的
既然知道了设备号的作用了,那么我们该怎么来为我们的设备来创建一个属于它的设备号呢?
-
创建设备
设备文件可以自动创建,有可以手动创建。
-
手动创建设备文件
手动创建设备文件需要使用命令mknod,其含义是make node,即创建一个设备结点,也说创建一个设备文件。(这里的节点是存在于磁盘上面的,不过也有存在于内存上面的节点)我们可以在创建这个设备文件的同时给它创建一个设备文件的同时给它指定一个设备号(当然,这个设备号没有被其他设备文件使用过)。示例如下:
# mknode /dev/hello c 256 0 @/dev/hello :设备文件的路径 @c : 代表字符设备,另外b代表块设备。你会发现没有网络设备的没有代表符啊,仔细看上面,网络设备是不能当做文件来看的,关于网络设备的驱动后面会介绍到,不着急。 @256 :主设备号 @0:次设备号 //mknod命令将文件名,文件类型和主次设备号都保存到磁盘上。
- 自动创建
/*Linux中提到了一个宏,MKDEV(主设备号,次设备号),最后主设备号和次设备号会合成一个设备号,其包含了本设备的主次设备号 设备号(32bit--dev_t)==主设备号(12bit) + 次设备号(20bit) 其在内核中的相关宏定义*/ #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //从dev中获取到主设备号 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) /从dev中获取到次设备号 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //合成dev设备号 //ma --- 主设备号 //mi --- 次设备号 //注册一个设备号 1.静态注册设备号 int register_chrdev_region(dev_t from,unsigned count,const char *name); 说明:将构造后的设备号注册到内核中,该函数一次可以连续注册多个设备号,有count形参指定个数,form指定其实的设备号,name用于标记主设备号的名字 成功:返回0 失败:返回负数 缺点:无法动态分贝设备号,如果两个驱动的设备号相同那么就会存在冲突,即后加载的驱动无法被成功加载 2.动态注册设备号 int alloc_register_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name); 功能:动态分配count个设备号并注册到内核中 参数1:合成后的设备号 参数2:起始的次设备号 其他的和静态注册函数式一样的 //创建一个设备文件 struct device *device_create(struct class * class, struct device * parent, dev_t devt, void * drvdata, const char * fmt,...) 参数1: class结构体,class_create调用之后到返回值 参数2:表示父亲,一般直接填NULL 参数3: 设备号类型 dev_t 参数4:私有数据,一般直接填NULL 参数5和6:表示可变参数,字符串,表示设备节点名字 销毁动作: void device_destroy(devcls, MKDEV(dev_major, 0)); 参数1: class结构体,class_create调用之后到返回值 参数2: 设备号类型 dev_t void class_destroy(devcls); 参数1: class结构体,class_create调用之后到返回值
这里我们仅实现自动创建设备文件的功能,实例:
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/device.h> #define MAJOR 256 #define MINOR 0 #define DEV_CNT 1 #define DEV_NAME "hello" static int __init chr_drv_init(void) { int ret; dev_t dev; /*宏__FUNCTION__为当前函数*/ printk("---%s---\n",__FUNCTION__); dev = MKDEV(MAJOR,MINOR); ret = register_chrdev_region(dev,DEV_CNT,DEV_NAME); if(ret) //静态注册未注册成功,改为动态注册 { ret = alloc_chrdev_region(&dev,0,ndevices,"DEV_NAME"); if(ret) { goto err; } } err: printk("分配失败\n"); return ret; } static void __exit chr_drv_exit(void) { dev_t dev; dev = MKDEV(MAJOR,MINOR); printk("--%s---\n",__FUNCTION__); unregister_chrdev_region(dev,DEV_CNT); } module_init(chr_drv_init); module_exit(chr_drv_exit); MODULE_LICENSE("GPL");
-
二、字符设备驱动框架
-
应用程序与驱动程序间联系
我们知道,我们的设备文件最终是要被应用程序调用的。那么应用程序最终有时怎样访问到我们的设备的呢?
前面讲到过,在Linux中,每个文件都有一个struct inode结构体来描述,该结构体中记录了文件的所有信息,比如文件名,文件类型,访问权限等。
每当我们打开一个文件,Linux操作系统就会在VFS层为其分配一个struct file结构体来描述打开的的这个文件。
-
当我们打开设备文件时,会根据相应的设备文件相对应的struct inode结构体的描述信息,获取该设备的相关信息。如该设备是一个字符设备还是一个块设备以及其文件权限。
-
struct inode结构体还存放着描述字符设备的结构体struct cdev。struct cdev函数中有一个struct file_operations结构体记载着设备操作函数的接口,也就是应用程序操作字符设备的函数在驱动中响应的函数。
-
而我们struct file结构体中也有一个,struct file_operations结构体,当然该结构体也是用来存放相关操作函数的入口(其实就是相关操作函数的入口地址)。那么这个地址怎么来的呢?
-
我们通过open打开这个设备之后,Linux系统就会将struct inode结构体中记录的设备的操作函数的入口地址给拷贝到struct file结构体中。如此一来上层设备就获得的相关操作函数的地址了,就可以去操作相关的设备文件了。当然啦,实际上open函数系统调用底层还是很复杂的,这里将其做了非常大的简化,只是为了讲清楚其大体的框架。
看图肯定会清晰很多,所以我在网上找了图:
2. 字符设备驱动实例- 前面我们简单分析了一下字符设备驱动的框架,以及它是如何跟应用程序发生联系的,下面给出实现简单字符设备驱动的代码。
驱动代码:
- 前面我们简单分析了一下字符设备驱动的框架,以及它是如何跟应用程序发生联系的,下面给出实现简单字符设备驱动的代码。
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
/*主设备号,次设备号,注册个数,主设备号的名称*/
#define MAJOR 256
#define MINOR 0
#define DEV_CNT 1
#define DEV_NAME "hello"
//定义字符设备的数据结构
static struct cdev vsdev;
static struct file_operations vser_ops = {
.owner = THIS_MODULE, //使owner指向当前moudule
}
static int __init vser__init(void)
{
int ret;
dev_t dev;
dev = MKDEV(MAJOR,MINOR); //将主设备号核次设备号一起合成一个设备号
ret = register_chrdev_region(dev,DEV_CNT,DEV_NAME);
if(ret) //静态注册未注册成功,改为动态注册
{
ret = alloc_chrdev_region(&dev,0,ndevices,"DEV_NAME");
if(ret)
{
goto err;
}
}
cdev_init(&vsdev,&vser_ops);
vsdev.owner = THIS_MODULE;
ret = cdev_add(&vsdev,dev,VSER_DEV_CNT);
if(ret)
goto add_err;
return 0;
add_err:
unregister_chrdev_region(dev,DEV_CNT);
err:
printk("分配失败\n");
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev;
dev = MKDEV(MAJOR,MINOR);
CDEV_DEL(&vsdev);
unregister_chrdev_region(dev,DEV_MINOR);
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");