Linux驱动(字符设备):02—设备号(dev_t、MAJOR、MINOR、MKDEV、register_chrdev_region、alloc_chrdev_region)
一、主设备号、次设备号
对字符设备的访问是通过文件系统内的设备名称进行的。通过ls -l命令查看/dev目录下的字符设备文件,在文件日期的前面有两个数,分别为主设备号和次设备号(注:"c"类型的文件才是字符设备)
主设备号:
主设备号标识设备对应的驱动程序。例如:/dev/null和/dev/zero由驱动程序1管理,而虚拟控制台和串口终端由驱动程序4管理,vcsl和vcsal设备都由驱动程序7管理。现在的Linux内核允许多个驱动程序共享主设备号,但我们看到的大多数设备仍然按照“一个主设备号对应一个驱动程序”的组织原则
次设备号:
由内核使用,用于正确确定设备文件所指的设备。依赖于驱动程序的编写 方式,我们可以通过次设备号获得一个指向内核设备的直接指针,也可将次设备号当做设备本地数组的索引。不管用哪种方式,除了知道次设备号用来指向驱动程序所实现的设备之外,内核本身基本上不关心关于次设备号的任何其他信息
二、设备号类型(dev_t)
在内核中,dev_t用来保存设备编号。头文件:#include <linux/types.h>
dev_t是一个32位数,其中的12位用来保存主设备号,其余20位用来表示次设备号。
注意事项:Linux 2.6内核可以容纳大量的设备,而先前的内核版本缺限于255个主设备号和255个次设备号。我们可以认为更宽的范围在相当长的时间内是足够使用的,但是dev_t格式在将来会发生变化,因此我们需要小心编写自己的驱动程序
三、获取设备号(MAJOR宏、MINOR宏、MKDEV宏)
#include <linux/kdev.h>
MAJOR(dev_t dev);
MINOR(dev_t dev);
MKDEV(int major,in minor);
MAJOR宏:获得主设备号。
MINOR宏:获得次设备号。
MKDEV宏:将主设备号和次设备号转换为dev_t类型,参数1为主设备号,参数2为次设备号。
四、分配和释放设备编号
#include <linux/fs.h>
int register_chrdev_region(dev_t first,unsigned int count,char *name);
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
void unregiser_chrdev_region(dev_t first,unsigned int count);
在建立一个字符设备之前,我们的驱动程序首先要做的事情就是获得一个或者多个设备编号。
register_chrdev_region函数
功能:用来静态分配一个设备号。如果我们提前明确知道我们所需的设备编号,那么使用此函数比较合适。如果是动态分配,则使用下面的alloc_chrdev_region函数比较合适
参数:
first:要分配的设备编号的起始值,是一个dev_t类型(通常first次设备号设置为0,但不强制要求)
count:是请求的连续设备编号的个数(注意:如果count非常大,则所请求的范围可能和下一个主设备号重叠,但只要我们所请求的设备编号范围是可用的,则不会带来任何问题)
name:和该编号范围关联的设备名称(这个名称就是显示在/proc/devices和sysfs中的名称)
返回值:
分配成功:返回0;出错:返回一个负的错误码。
alloc_chrdev_region函数
功能:用来动态分配一个设备号。如果我们不知道该分配什么设备编号,则调用这个函数比较合适,调用此函数内核会为我们恰当分配所需要的主设备号。
参数:
dev:用户自己传进来的值,用于函数执行成功后保存所分配范围的第一个编号
firstminor:要使用的被请求的第一个次设备号(通常为0)
count:是请求的连续设备编号的个数(注意:如果count非常大,则所请求的范围可能和下一个主设备号重叠,但只要我们所请求的设备编号范围是可用的,则不会带来任何问题)
name:和该编号范围关联的设备名称(这个名称就是显示在/proc/devices和sysfs中的名称)
返回值:
分配成功:返回0。出错:返回一个负的错误码
unregiser_chrdev_region函数
功能:用来释放一个设备编号。我们不应该在设备编号正在被使用时释放。通常在模块的清除函数中调用unregister_chrdev_region函数。
参数:
first:设备号的起始值
count:释放的连续设备编号的个数
五、附加知识点:动态分配主设备号
为什么建议使用动态分配主设备号?
一部分主设备号已经静态地分配给了大部分常见设备。在内核源码树的Documentaion/devices.txt中可以找到这些设备的清单
如果我们简单静态的选定一个尚未被使用的编号,如果我们的驱动程序只有我们自己使用,那么这种方法行的通,但是如果驱动程序被广泛使用,那么静态选定的主设备号可能会造成冲突和麻烦
因此,对于一个新的驱动程序,我们强烈建议不要随便选择一个当前未使用的设备号作为主设备号,而应该使用动态分配机制获取主设备号,因此建议使用alloc_chrdev_region函数而不是register_chrdev_region函数
动态分配的缺点是:由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点。对于驱动程序的一般用法,这倒不是问题,因为一旦分配了设备号,就可以从/proc/devices中读取得到(/proc/devices文件示例如下)。因此,为了加载一个使用动态主设备号的设备驱动程序,对insmod的调用可替换为一个简单的脚本,该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号,然后创建对应的设备文件
下面我们以scull字符设备文件为例,演示其设备号的创建以及使用:
scull_load脚本设计
下面我们以scull字符设备的scull_load脚本为例,使用以模块形式发行的驱动程序的用户可以在系统的rc.local文件中调用这个脚本,或者在需要模块时手工调用
下面的脚本是一个模板,可适用于其他驱动程序,只需要重新定义变量并调整mknod那几行就可以了
脚本分析:
利用wak工具从/proc/devices文件中获取动态分配的主设备号
该脚本调用mknod在/dev目录下创建了四个设备
chmod:由于这个脚本必须由超级用户运行,所以新创建的设备文件自然属于root。默认的权限位只允许root对其有写访问权,而其他用户只有读权限。通常,设备节点需要不同的访问策略,因此有时需要修改访问权限
chgrp:我们的脚本默认地把访问权赋于一个用户组(这个选项可以自己选择是否设置)
scull_unload脚本:
scull_load用来加载模块,scull_unload脚本用来清除/dev目录下的相关设备文件并卸载这个模块
scull.init脚本:
除了上面的scull_load和scull_unload脚本之外,我们还可以编写一个init脚本,并将其保存在发型版使用的init脚本目录(/etc/init.d)中。init脚本名为scull.init,用来加载、卸载、重新加载,其可接受的分别参数为:start、stop、restart
如果反复创建和删除/dev节点显得有些繁琐的话,有一个解决办法:如果只是装载和卸载单个驱动程序,则可在第一次创建设备文件之后仅适用rmmod和insmod这两个命令,因为动态设备号并不是随机生成的(不过,某些内核开发人员已经预示在不久的将来将会按随机方式进行处理),如果不受其他(动态)模块影响的话,可以预期获得相同的动态主设备号。在开发过程中避免脚本过长是有益的。但很明显,这个技巧不能适用于有多个驱动程序存在的场合
scull_major全局变量
分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。scull的实现采用了这种方式,其使用了一个全局变量scull_major来保存主设备号,用scull_minor来保存次设备号
scull_major:初始化值是SCULL_MAJOR,这个宏定义在scull.h。其默认值取0,即“选择动态分配”
在指定主设备号时:用户可以使用SCULL_MAJOR宏的默认值或者选择某个特定的主设备号,也可以在编译scull源代码前修改SCULL_MAJOR宏的值,也可以通过insmod命令行指定scull_major的值
下面是scull.c中用来获取主设备号的代码:(我们自己编写模块代码也可以使用这个模板)
Linux驱动(字符设备):03—struct file、struct inode、struct file_operaions结构体,iminor、imajor宏
一、struct file
file结构代表一个打开的文件(它并不仅仅限于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的file结构)
file结构由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到文件的所有实例都被关闭之后(通过close函数),内核才会释放这个结构
注意:file结构与用户空间中的FILE没有任何关联。FILE在C库中定义且不会出现在内核代码中;而file结构是一个内核结构,它不会出现在用户程序中
在内核源码中,指向struct file的指针通常被称为“file”或“filp”(文件指针)。为了不至于和这个结构本身相混淆,我们一致将该指针成为filp。这样,file指的是结构本身,filp则是指向该结构的指针
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations*f_op; /* 和文件关联的操作*/
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags; /*文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC*/
fmode_t f_mode; /*文件读/写模式,FMODE_READ和FMODE_WRITE*/
struct mutex f_pos_lock;
loff_t f_pos; /* 当前读写位置 */
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_statef_ra;
u64 f_version;
#ifdef CONfiG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data; /*文件私有数据*/
#ifdef CONfiG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONfiG_EPOLL */
struct address_space*f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
常见的成员如下:
二、struct inode
内核用inode结构再内部表示文件,因此它和file结构不同。file表示打开的文件描述符,对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构
inode结构包含了大量有关文件的信息。作为常规,只有下面两个字段对编写驱动程序有用:
struct inode {
...
umode_t i_mode; /* inode的权限 */
uid_t i_uid; /* inode拥有者的id */
gid_t i_gid; /* inode所属的群组id */
dev_t i_rdev; /* 若是设备文件,此字段将记录设备的设备号 */
loff_t i_size; /* inode所代表的文件大小 */
struct timespec i_atime; /* inode最近一次的存取时间 */
struct timespec i_mtime; /* inode最近一次的修改时间 */
struct timespec i_ctime; /* inode的产生时间 */
unsigned int i_blkbits;
blkcnt_t i_blocks; /* inode所使用的block数,一个block为512 字节 */
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
/* 若是块设备,为其对应的block_device结构体指针 */
struct cdev *i_cdev; /* 若是字符设备,为其对应的cdev结构体指针 */
}
...
};
常见的成员:
iminor、imajor宏
inode中的i_rdev成员在Linux 2.5开发系列版本发生了变化,这破坏了大量的驱动程序代码的兼容性。为了鼓励编写可移植性更强的代码,内核开发者增加了两个新的宏,可用来从一个inode中获得主设备号和次设备号。
为了防止因为类似的改变而出现为,应该使用上述宏,而不是直接操作i_rdev。
三、struct file_operaions
结构体介绍
与file结构体的关系:每个打开的文件在内核中都是以struct file结构体表示,文件有很多的系统调用,例如open、read等等,这些系统调用的指针就是file_operaions结构体提供的,因此在file结构体中有一个file_operaions结构体的指针(f_op)
如果采用面向对象编程的方式来看:我们可以把文件看成是一个“对象”,而操作它的函数就是“方法”,这些“方法”是由file_operaions结构体提供的。file_operaions结构体还可以将我们申请的设备编号与驱动程序连接到一起
在内核中,通常把指向于file_operaions结构的指针称为“fops”
#include <linux/fs.h>
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};
结构体成员
file_operaions结构体中是一些函数指针
结构体中的每一个字段都必须指向驱动程序中实现特定操作的函数,对于不支持的操作,对应字段可置为NULL。对各个函数而言,如果对应函数指针被赋为NULL指针,那么内核的具体处理行为是不用的,后面介绍这些差异
很多函数的参数包含“__user”字符串,其实这是一种形式的文档而已,表名指针是一个用户空间地址,因此不能被直接引用。对通常的编译来将,“__user”没有任何效果,但是可由外部检查软件使用,用来寻找对用户空间地址的错误使用
下面列出了应用程序可在某个设备上调用的所有操作。为了简洁,下面仅仅总结了每个操作以及使用NULL时的内核默认行为
演示案例
scull设备驱动程序所实现的只是最重要的设备方法,其file_operaions结构被初始化如下形式:
struct file_operations scull_fops={
.owner = THIS_MODULE,
.llseek = scull_llseek;
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release= scull_rlease,
};
这个声明采用了标准C的标记化结构初始化语法。这种语法是值得采用的,因为它使驱动程序在结构的定义发生变化时更具可移植性,并且使得代码更加紧凑和易读。标记化的初始化方法允许对结构成员进行重新排列。在某些场合下,将频繁被访问的成员放在相同的硬件缓存行上,将大大提供性能
Linux驱动(字符设备):04—字符设备的初始化、注册、移除(struct cdev、cdev_init、cdev_add、cdev_del、、register_chrdev)
一、struct cdev
在Linux内核中,使用cdev结构体描述一个字符设备,cdev结构体的定义如下:
#include <linux/cdev.h>
struct cdev {
struct kobject kobj; /* 内嵌的kobject对象 */
struct module *owner; /* 所属模块*/
struct file_operations *ops; /* 文件操作结构体*/
struct list_head list;
dev_t dev; /* 设备号*/
unsigned int count;
};
二、字符设备的申请、初始化、注册、移除
#include <linux/cdev.h>
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *cdev, struct file_operations *fops);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);
cdev_alloc:用于动态申请一个cdev内存
cdev_init:用于初始化cdev的成员,并建立cdev和file_operations之间的连接
cdev_add:通过告诉内核,向系统添加一个cdev,完成字符设备的注册。常发生在字符设备驱动模块加载函数中。参数:
dev:要添加的cdev设备结构体 num:该设备对应的第一个设备编号
count:和该设备关联的设备编号的数量。经常取1,但是在某些情形下,会有多个设备编号对应于一个特定的设备。例如:考虑SCSI磁带驱动程序,它通过每个物理设备的多个次设备号来允许用户空间选择不同的操作模式(比如密度)
注意事项: ①这个调用可能会失败,如果调用失败了会返回一个负的错误编码,则设备不会被添加到系统中 ②函数调用成功:这个调用几乎总会返回成功,此时又面临一个问题,如果此函数返回了,我们的设备就被“激活”了,它的操作就会被内核调用。因此,在驱动程序还没有完全准备好处理设备上的操作时,就不能调用此函数
cdev_del:向系统删除一个cdev,完成字符设备的注销。常发生在字符设备驱动模块卸载函数中。将cdev结构传递给该函数进行注销之后,就不应该再去访问cdev结构了
三、老版本注册、移除设备的方式
int register_chrdev(unsigned int major,const char *name
struct file_operations *fops);
int unregister_chrdev(unsignde int major,const char *name);
这些都是Linux 2.6之前的老代码,不建议再去使用
register_chrdev
unregister_chrdev
四、演示案例:scull中的设备注册
在scull内部,我们通过struct scull_dev的结构来表示每个设备,定义如下:
下面的函数用来初始化设备并将其添加进系统中:
Linux驱动(字符设备):05—字符设备的open与release(container_of)
一、设备的打开(open)
open方法提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。在大部分驱动程序中,open应完成如下的工作:
①检查设备特定的错误(诸如设备未就绪或者类似的硬件问题)
②如果设备是首次打开,则读其进行初始化
③如有必要,更新f_op指针
④分配并填写置于filp->private_data里的数据结构
open方法的原型:
int (*open)(struct inode *inode,struct file *filp);
inode参数:此参数为struct inode类型
获得我们打开的设备:
open方法的inode参数的i_cdev字段(struct cdev类型)中包含了我们所需要的设备信息。有时候我们需要获取这些设备的信息,可以通过下面两种方式获取:
①container_of宏:
但是有一个问题是,我们通常不需要cdev结构本身,而是希望得到包含cdev结构的scull_dev结构。于是C语言提供了下面一个宏来帮我们完成了这个技巧
#include <linux/kernel.h>
container_of(pointer,container_type,container_field);
功能:该宏通过字段名返回一个包含该字段的结构指针 参数: container_type:一种结构类型的名称
container_field:是属于参数2结构类型中的一个字段的指针
备注:使用时container_type参数传入的是一个结构体类型的名称,而不要传入实际的值/变量 返回值:返回参数2所指的结构类型的指针
演示案例:下面cdev是属于struct scull_dev结构体中的一个字段名,container_of通过解析,返回一个struct
scull_dev结构体类型的指针,接着我们将dev指针保存在filp的private_data中,可以方便今后对该指针进行访问
②通过次设备号获取
另一个确定要打开的设备的方法是:检查保存在inode结构中的次设备号。如果读者利用register_chrdev注册自己的设备,则必须使用该技术。
但是一定要使用iminor宏从inode结构中获取次设备号,确保它对应于驱动程序真正准备打开的设备。
演示案例
下面是scull设备open的代码:(经过微简化的)
Linux 字符设备驱动结构(二)—— 自动创建设备节点
上一篇我们介绍到创建设备文件的方法,利用cat /proc/devices查看申请到的设备名,设备号。
第一种是使用mknod
手工创建:mknod filename type major minor
第二种是自动创建设备节点:利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。
具体udev相关知识这里不详细阐述,可以移步Linux 文件系统与设备文件系统 —— udev 设备文件系统,这里主要讲使用方法。
在驱动用加入对udev 的支持主要做的就是:在驱动初始化的代码里调用class_create( )为该设备创建一个class,再为每个设备调用device_create( )创建对应的设备。
内核中定义的struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create( )函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create( )函数来在/dev目录下创建相应的设备节点。
这样,加载模块的时候,用户空间中的udev会自动响应 device_create()函数,去/sysfs下寻找对应的类从而创建设备节点。
下面是两个函数的解析:
1、class_create( ) 函数
功能:创建一个类;
下面是具体定义:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
owner:THIS_MODULE
name : 名字
__class_create(owner, name, &__key)源代码如下:
struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)
{
struct class *cls;
int retval;
cls = kzalloc(sizeof(*cls), GFP_KERNEL);
if (!cls) {
retval = -ENOMEM;
goto error;
}
cls->name = name;
cls->owner = owner;
cls->class_release = class_create_release;
retval = __class_register(cls, key);
if (retval)
goto error;
return cls;
error:
kfree(cls);
return ERR_PTR(retval);
}
EXPORT_SYMBOL_GPL(__class_create);
销毁函数:void class_destroy(struct class *cls)
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}
2、device_create(…) 函数
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
功能:创建一个字符设备文件
参数:
struct class *class :类
struct device *parent:NULL
dev_t devt :设备号
void *drvdata :null、
const char *fmt :名字
返回:
struct device *
下面是源码解析:
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
device_create_vargs(class, parent, devt, drvdata, fmt, vargs)解析如下:
struct device *device_create_vargs(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt,
va_list args)
{
return device_create_groups_vargs(class, parent, devt, drvdata, NULL,
fmt, args);
}
现在就不继续往下跟了,大家可以继续往下跟;
下面是一个实例:
hello.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static int major = 250;
static int minor=0;
static dev_t devno;
static struct class *cls;
static struct device *test_device;
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 \n");
devno = MKDEV(major,minor);
ret = register_chrdev(major,"hello",&hello_ops);
cls = class_create(THIS_MODULE, "myclass");
if(IS_ERR(cls))
{
unregister_chrdev(major,"hello");
return -EBUSY;
}
test_device = device_create(cls,NULL,devno,NULL,"hello");//mknod /dev/hello
if(IS_ERR(test_device))
{
class_destroy(cls);
unregister_chrdev(major,"hello");
return -EBUSY;
}
return 0;
}
static void hello_exit(void)
{
device_destroy(cls,devno);
class_destroy(cls);
unregister_chrdev(major,"hello");
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
下面可以看几个class几个名字的对应关系: