一、设备分类
按设备访问方式(特点):
1、字符设备:鼠标,键盘,串口,帧缓存
特点:以字节为单位访问,通常只支持顺序访问,无缓冲存
2、块设备:磁盘,光驱,flash
特点:以固定大小为单位访问,支持随机访问,有缓存,不直接和VFS交互
3、网络设备:
特点:无设备节点,通过套接字访问设备
设备号概念:
在linux中,每个设备对应一个或者多个设备号
设备号为dev_t类型,一般是一个32位的整数
主设备号:区分不同类型的设备
次设备号:区分相同类型的不同设备
对设备号操作的有三个宏函数:
MAJOR(dev_t dev) 获得主设备号
MINOR(dev_t dev) 获得次设备号
MKDEV(int major,int minjor) 将主设备和次设备号转换成dev_t类型的设备号
240—254是实验用的设备号,内核不会使用
1、分配设备号和释放设备号
分为静态和动态分配两种
第一种,静态分配:
#include<linux/fs.h>
int register_chrdev_region(dev_t from, unsigned count, const char *name);
功能:静态分配设备号,即分配我们指定的设备号,可以连续分配多个
参数:
from:是你要分配的起始设备号,first的次设备号经常被设置为0。
count:请求的连续设备号的个数
name:设备名称,这个名称是与设备号范围相关联的。它会出现在 /proc/devices 和 sysfs 中
返回值:成功返回0,错误返回负的错误码
第二种,动态分配:
我们经常不知道设备将要使用那些设备号,很有可能我们指定的设备号已经被别的设备使用,这样就会导致设备号分配失败。这时我们应当动态分配设备号
#include<linux/fs.h>
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count
,
const char *name)
功能:动态分配设备号
参数:
dev: 当成功分配设备号后,此变量用于保存已分配设备号范围的第一个编号
baseminor:
请求的第一个要用的次设备号,它常常是 0
count: 请求的连续设备号的个数
name: 设备名称,这个名称是与设备号范围相关联的。它会出现在 /proc/devices 和 sysfs 中
返回值:成功返回0,错误返回负的错误码
通常我们会查看/proc/devices来得知我们新分配的主设备号
不论你用哪种方法分配设备号,都应该在不使用它们时释放这些设备编号:
#include<linux/fs.h>
void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
from: 要释放的起始设备号
count:要连续释放的设备号的个数
返回值:成功返回0,错误返回负的错误码
2、一些重要的数据结构
1、struct file_operations 定义在
#include<linux/fs.h>中,这个结构包含了一组函数指针,可以通过给这些函数指针赋值,来指定对设备的一系列操作,如open,close,read等
2、struct 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;
};
3、注册字符设备
#include <include/linux/cdev.h>
void cdev_init(struct cdev *cdev, struct file_operations *fops);
经过上述的字符设备注册后,
便将相应的操作函数与设备号关联到了一起。并将这个cdev结构插入到链表中,
struct cdev 有一个拥有者成员, 应当设置为
THIS_MODULE
一旦 cdev 结构建立, 最后的步骤是把它告诉内核, 调用:
#include
<include/linux/cdev.h>
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
num:起始设备号
count:要连续关联的个数
至此,才将设备号和相应的操作关联起来。
如果要移除一个字符设备,调用:
#include
<include/linux/cdev.h>
void cdev_del(struct cdev *dev);
4、建立一个设备节点
mknod /dev/设备节点名 c major minor
5、之后再应用程序中便可以调用相应的open,read函数来操作这个设备节点实现对设备的驱动。
一般在加载函数中实现1-—3步。在卸载函数中实现
cdev_del
二、file和inode结构
1
、
struct file定义在
<linux/fs.h>文件中
,系统中每个打开的文件有一个关联的 struct file 在内核空间,它由内核在 open 时创建。
file结构的一些成员如下所示:
mode_t f_mode;
文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE.
你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可,但由于内核在调用驱动程序的read和write前已经检查了访问权限,所以在驱动程序的read和write不必检查权限。在没有获得对应访问权限而打开文件的情况下,对文件的读写操作将被内核拒绝,驱动无需为此而做额外的判断。
注:此权限检查对应的是文件本身的权限,也就是ls -l是的权限位,而不是open时指定的那个权限。
loff_t f_pos;
当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long )。
unsigned int f_flags;
驱动可以读这个值,如果它需要知道文件中的当前位置,,但是正常地不应该改变它;
读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于
filp->f_pos。 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位
置。
这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作( 我们在第一章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O ); 其他标志很少使用.。
struct file_operations *f_op;
和文件关联的操作. 内核安排指针作为它的 open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用.
void *private_data;
open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使
struct dentry *f_dentry;
用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须
记住在内核销毁
file之前,,在 release 方法中释放那个内存.。private_data
是一个非常有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.
文件对应的目录项(
dentry )结构., 除了用
filp->f_dentry->d_inode 访问索引节点结构外,设备驱动编写者正常地不需要关心
dentry
结构, 实际的
结构里还有其他一些成员, 但是它们对设备驱动没有多大用处。
因为驱动从不创建文件结构,它们只是对别处创建的
file结构进行访问,因此
我们可以安全地忽略这些成员,
2、
struct indoe定义在
<linux/fs.h>文件中
,inode结构表示文件的静态属性,也就是文件本身的真正的属性。而
file结构则是动态属性,
比如多个进程打开同一个文件,会有多个
file结构,但只有一个
indoe结构。
indoe结构中包含了大量的有关文件的信息。作为常规,一般只有下面两个成员对编写驱动代码有用:
dev_t i_rdev;
对于表示设备文件的indoe结构, 这个成员包含了真正的设备编号.
struct cdev *i_cdev;
struct cdev 是对应打开的字符设备的结构,当indoe指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
为了增加代码的可移植性,内核开发者增加了两个宏,可用来从一个indoe中获得主设备号和次设备号
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);