字符设备驱动模型
<背景>
在linux系统驱动程序中,因为要面临各种各样的硬件,字符设备,快设备,网络接口设备,USB设备,PCI设备,平台设备,混在设备 ,设备不同则所对应的驱动模型不同,这就导致我们要掌握众多的驱动模型,能从这些众多的驱动模型中找到共性,则是学号linux驱动的关键
<linux 驱动程序的编写流程>
创建设备驱动文件:
mknode /dev/文件名 类型(c,b...) 主设备号(当一个设备被挂载就会有相应的设备号) 次设备号(用于区分同类型的设备)
设备号:
静态创建设备号:
mkdev(主设备号,次设备号) //就是将主设备号和次设备号连接起来
分离设备号:
主:major(设备号)
次:minor(设备号)
申请分配设备号:
静态分配:
register_chrdev_region()
动态分配:
alloc_chrdev_region()
注销设备号:
当一个设备使用完就需要将占有的设备号释放
unregister_chrdev_region()
linux 系统通过chardev[] 组织256 个device_struct 结构,主要就是记录设备的名称和函数。
linux 设备描述结构
1)字符cdev结构体
在Linux2.6 内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:
struct cdev {
struct kobject kobj;
struct module *owner; /*通常为THIS_MODULE*/
struct file_operations *ops; /*在cdev_init()这个函数里面与cdev结构 联系起来*/
struct list_head list;
dev_t dev; /*设备号可以动态分配/或静态分配*/
unsigned int count;
};
设备描述结构的分配:
静态分配:
struct cdev 名称 //直接定义一个设备描述结构
动态分配
struct cdev *pdev= cdev_alloc();
初始化设备驱动程序:
1)定义一个设备描述结构
2)实现file_operation以便于应用程序的调用
llseek()
read()
write()
ioctl()/unlocked_ioctl() //该函数用于控制设备
open()
releade()
注意:在Linux系统中每打开一个文件便会在Linux内核中关联一个struct file。
来源于用户空间的指针不能被被内核空间直接使用,这时需要调用函数:
int copy_from_user(void *to,const viod_user* from ,int n)
int copy_to_user(void _user *to ,const void *from ,int n)
3)调用函数操作设备描述结构的函数对对该结构进行操作(放在设备初始化函数中,最后用moudle_init() 函数进行加载)
(1)将设备描述结构和设备操作集关系起来
cdev_init(stuct cdev *,const struct file_operation *)
(2)将设备描述结构注册到Linux系统中,让系统知道有这么个东西
int cdev_add(struct cdev *,dev_t,unsigned);向linux系统注册一个cdev结构。
卸载设备驱动程序:
1)调用设备描述结构函数对该结构进行操作
(1)调用函数void cdev_del(struct cdev *);来将之前注册的设备描述结构注销掉。
实现对设备的控制:
1)用于操作设备描述结构的函数:
linux 2.6.36 以前:
long (*ioctl)(struct inode *node ,struct file* filp ,unsigned int cmd ,unsigned long arg)
Linux2.6.36 以后:
long (*unlocked_iotcl )(struct file *filp,unsigned int cmd ,unsigned long arg)
注意:这两个函数是使用在应用程序中的,和read(),write()一样。
2)定义命令
命令的实质就是一个整数,但是又不是一般的整数,该整数分为几个段:类型(8位),序号,参数传递方向,参数长度
Type (类型/幻术):表明这是属于哪个设备的命令
Number (序号):用来区分同一设备的不同命令
Direction:参数传递的方向(IOC_NONE(没有数据传送,IOC_READ(读取参数),IOC_WRITE(向设备写入参数))
Size:参数长度
《《设备驱动模型》》
struct kobject kobj:
该结构体用于构建Linux设备驱动模型的模型建立
struct list_head
{
struct list_head *prev,*next;
};
该结构体用于建立设备文件和设备结构体建立联系,以找到对应的设备操作函数。
假设应用程序大开设备文件A,那么设备会产生一个inode节点,这样就可以通过高inode节点的i_devices找到设备结构体,进而找到设备操作函数。
《驱动初始化》
<分配设备描述结构>
在linux的任何一种驱动模型中,都会有内核的一种结构来描述,字符设备在内核中使用结构体数组:
struct cdev
{
struct kobject kobj;
struct module *owner;
const struct file operations *ops; //设备操作集
struct list_head list;
dev_t dev; //设备号
unsigned int count; //设备数
}
数据分析:
(1)设备数:
表示某种同类设备总共有几个
(2)设备号:
在目录/dev/ 中会有详细的描述,分为主设备号和次设备号。
主设备号:通过主设备号和设备文件和设备驱动联系起来。
次设备号:用于区分同类设备中的不同设备
在linux系统中,dev_t 这种数据类型是32位,其中高12位主设备号,低20位是次设备号。
使用宏:MKDEV(主设备号,次设备号) 将设备号组合起来
使用宏:MAJOR(设备号) 将主设备号分离出来
使用宏:MINOR(设备号) 将次设备号分离出来
(3)分配设备号:
1)静态分配:
使用函数register_chrdev_region()向内核申请,缺点,可能申请的设备号已经被使用,这样就会导致申请失败。
该函数会到该指针数组里面查找是否已经将该设备号注册进入内核。
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
const char *name;
struct file_operations *fops;
struct cdev *cdev; /* will die */
} *chrdevs[MAX_PROBE_HASH];
搜索方法:
搜索以哈希列表的方式,首先需要有个一个索引值,在这里使用设备号。
i = major_to_index(major);
调用:
i = major%255;
利用i对chrdevs[i]进行扫描,如果i已经在节点上面了,将会在进行对之前的内存分配进行释放后,然后返回错误,反之就会将该设备注册到内核链表中。
2)动态分配:
使用函数alloc_chrdev_region() 由内核分配一个主设备号。优点是内核知道哪些设备号没有被使用,所以不会出现分配已使用的设备号导致分配失败。
该内核函数会调用函数:
95__register_chrdev_region(unsigned int major, unsigned int baseminor,
96 int minorct, const char *name)
97{
98 struct char_device_struct *cd, **cp;
99 int ret = 0;
100 int i;
101
102 cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
103 if (cd == NULL)
104 return ERR_PTR(-ENOMEM);
105
106 mutex_lock(&chrdevs_lock);
107
108 /* temporary */
109 if (major == 0) {
110 for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
111 if (chrdevs[i] == NULL)
112 break;
113 }
114
115 if (i == 0) {
116 ret = -EBUSY;
117 goto out;
118 }
119 major = i;
120 ret = major;
121 }
}
该内核函数会扫描内核数组chrdevs[],知道找到没有被使用的设备号。
将字符设备加入到系统中去调用函数:
int cdev_add(struct cdev *p, dev_t dev,unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
该函数会操作操作一个全局变量cdev_map来将dev加入到哈希链表中(注:这里的cdev_map也是一个指针数组,该结构体如下。)
struct kobj_map {
struct probe {
struct probe *next; /* 这样形成了链表结构 */
dev_t dev; /* 设备号 */
unsigned long range; /* 设备号的范围 */
struct module *owner;
kobj_probe_t *get;
int (*lock) (dev_t, void *);
void *data; /* 指向struct cdev对象 */
} *probes[255];
struct mutex *lock;
}
kobj_map()和注册设备号一样,通过扫描probe[]数组来将字符设备加入到内核链表中。
(4)注销设备号:
无论是用那种方法分配设备的设备号,都应该在驱动退出时,使用unregister_chrdev_region()来释放设备号。
调用函数:
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev,p->count);
kobject_put(&p->kobj);
}
(5)设备操作集:
当应用程序调用系统函数,该系统函数同时是需要访问硬件的,则系统首先会根据函数去找一个表,根据这个表找到相应的驱动函数,然后再根据这些驱动完成相应的操作。——这个表就是设备操作集。
struct file_operations:
struct file_operations是一个函数指针的集合集合,定义能在设备进行的操作,若果设备不支持某项操作,就将其赋值为空指针
struct file_operations dev_fops
{
.llseek = NULL;
.read = dev_read;
.write = dev_write;
.ioctl = dev_ioctl;
.open = dev_open;
.release = dev_release;
}
<初始设备描述结构>
<注册设备描述结构>
<硬件初始化>
例:
《字符设备驱动》
<分配cdev>
首先设备描述结构是一个结构体数组,即是一个变量,凡是变量的分配有两种方式,一种是静态分配,另一种对动态分配。
1)静态分配
struct cdev mdev
2)动态分配
使用函数cdev_alloc()
struct dev *pdev = cdev_alloc();
<初始化cdev>
设备描述结构的初始化使用函数 cdev_init()来完成。
函数原型:
cdev_init(struct cdev *cdev ,const struct file_operations *fops)
参数分析:
cdev :待初始化的设备描述结构
fops:设备对应的操作函数
<注册cdev>
字符设备的注册使用函数cdev_add()来完成。
函数原型:
cdev_add(struct cdev*p,dev_t dev,unsigned count)
参数分析:
p:待添加到内核中的设备描述结构
dev:设备号
count:该类设备的个数
<硬件初始化>
如果是一个实际的硬件,就去完成相应硬件设备的初始化。
《实现设备操作》(针对设备结构描述里面的 struct file_operations )
<打开设备文件>
int (*open)(struct inode *,struct file*)
操作open 是用来为以后的操作完成相应的第一步初始化工作,左右工作有:
1)表明设备号
2)启动设备
<关闭设备>
int (*release)(struct inode*,struct file*)
该操作的作用和open刚好相反,主要作用:
1)关闭设备
<重新定位指针>
loff_t(*llseek)(srtuct file*,loff_t ,int )
<读文设备件>
ssize_t (*read)(struct file*,char __user*,size_t ,loff_t *)
主要完成两件事:
1)从设备中读取数据(属于硬件访问操作)
2)将读取到数据返回给应用数据
注意:
凡是来源于用户空间的指针,不能被内核直接应用,必须使用专门的函数。一下两个函数会在read()调用。
int copy_from_user(void *to ,const void __user *from ,int n)
int copy_to _user(void __user *to ,const ,const void *from ,int n)
<写设备文件>
ssize_t (*write)(struct file *,char __user ,size_t ,loff_t*)
注意:
struct file
在linux系统中,每一打开的 文件都会关联一个struct file,他由内核在打开文件时创建,在关闭文件时释放。
重要成员:
loff_t f_pos /文件读写位置指针/
struct file_operations *f_op /该文件所对应的操作/
struct inode
每一在文件系统中的文件都会关联一个inode 结构,该结构主要用来记录文件物理上的信息,因此他和打开的 文件file 不同,一个文件没有被打开时是不会关联file结构的,但是却会关联一个inode 结构。
重要成员:
dev_t i_rdev /设备号/
例:
struct file_operations dev_fops
{
.llseek = NULL;
.read = dev_read;
.write = dev_write;
.ioctl = dev_ioctl;
.open = dev_open;
.release = dev_release;
}
《驱动注销》
使用函数cdev_del() 来将字符设备从内核中注销掉。