文章目录
Linux 设备驱动学习笔记 - 字符设备驱动篇 - Note.1
- LINUX DEVICE DRIVERS,3RD EDITION
一、字符驱动
【1】主次设备号
字符设备通过文件系统中的名字来存取. 那些名字称为文件系统的设备文件
规定它们位于 /dev 目录,通过ls -l 命令可以查看他们的详细信息,c 代表这是一个 char 字符设备,b 代表这是一个块设备,图片中的 1、10 、251代表了他们的主设备号,而后面的7、229 代表了该设备的次设备号,Linux 内核允许多个驱动共享主编号。
【2】设备编号的内部表示
在内核中, dev_t 类型 (在 <linux/types.h>中定义) 用来持有设备编号 – 主次部分都包括. 对于 2.6.0 内核, dev_t 是 32 位的量, 12 位用作主编号, 20 位用作次编号.
利用在<linux/kdev_t.h>中的一套宏定义. 为获得一个 dev_t 的主或者次编号, 使用:
MAJOR(dev_t dev);
MINOR(dev_t dev);
如果你有主次编号, 需要将其转换为一个 dev_t, 可以使用:
MKDEV(int major, int minor);
【3】设备号的申请与注销
不算可控的申请方式
- 第一种申请设备方法,但是内核会一次性帮你申请256个次设备号,这又显得很多余,数量并不算特别可控,所以我们有了下面的动态分配设备号。
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
可控的申请设备号
- 手动申请设备号,你需要提前知道你空闲的设备号,并填入该函数,这很明显不是特别友好,当你填入0时,他会默认帮你找到一个暂时没有用的设备号给你。
int register_chrdev_region(dev_t first, unsigned int count, char *name);
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
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);
}
- 动态为你分配一个主编号
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
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;
}
- 使用这个函数, dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数. fisetminor 应当是请求的第一个要用的次编号; 它常常是 0. count 和 name 参数如同给 request_chrdev_region 的一样. 设备编号的释放使用:
void unregister_chrdev_region(dev_t first, unsigned int count);
/**
* unregister_chrdev_region() - return a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count)
{
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;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
}
实际上我们发现,申请结构体中最终都是跳转到了 __register_chrdev_region 中
/*
* Register a single major with a specified minor range.
*
* If major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If major > 0 this function will attempt to reserve the passed range of
* minors and will return zero on success.
*
* Returns a -ve errno on failure.
*/
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name);
【4】申请设备号的一般形式
if (module_major) {
dev = MKDEV(module_major, module_minor);
result = register_chrdev_region(dev, module_nr_devs, "module");
}
else {
result = alloc_chrdev_region(&dev, module_minor, module_nr_devs, modulel");
module_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "module: can't get major %d\n", module_major);
return result;
}
二、 三个关键数据结构
大部分的基础性的驱动操作包括 3 个重要的内核数据结构, 称为 file_operations, file, 和 inode.
【1】file_operations 文件操作
- 进入内核代码后会发现,平时再用户空间使用的那些接口,在内核空间中都能够找到对应的解释,这也是很有意思的一件事情
- 该结构体中常用到的一些操作函数
struct module *owner
第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针.
它被初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.
loff_t (*llseek) (struct file *, loff_t, int);
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32 位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败.
一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);
初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);
初始化设备上的一个异步写.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的ioctl"), 系统调用返回一个错误.
int (*open) (struct inode *, struct file *);
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如
果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
//对应 close(),相对于 open() 操作
int (*release) (struct inode *, struct file *);
在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.
int (*lock) (struct file *, int, struct file_lock *);
lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几
乎从不实现它.
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).
- 一般最简单的module 设备驱动只实现最重要的设备方法. 它的 file_operations 结构是如下初始化的:
struct file_operations my_module_fops = {
.owner = THIS_MODULE,
.read = module_read,
.write = modulel_write,
.ioctl = module_ioctl,
.open = modulel_open,
.release modulell_release,
};
【2】file文件结构
- 结构体部分截取
struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构.
注意 file 与用户空间程序的 FILE 指针没有任何关系.
-
一个 FILE 定义在 C 库中, 从不出现在内核代码中.
-
一个 struct file, 另一方面, 是一个内核结构, 从不出现在用户程序中.
-
文件结构代表一个打开的文件. (它不特定给设备驱动; 系统中每个打开的文件有一个关联的 struct file 在内核空间). 它由内核在 open 时创建, 并传递给在文件上操作的任何函数, 直到最后的关闭. 在文件的所有实例都关闭后, 内核释放这个数据结构. 在内核源码中, struct file 的指针常常称为 file 或者 filp(“file pointer”). 这个指针称为 filp 以避免和结构自身混淆. 因此, file 指的是结构, 而 filp 是结构指针.
-
注意, release 不是每次进程调用 close 时都被调用. 无论何时共享一个文件结构 (例如, 在一个 fork 或 dup 之后), release 不会调用直到所有的拷贝都关闭了. 在本结构体中也可以看到记录文件操作次数的成员变量。
-
该结构体中常用到的一些操作函数
mode_t f_mode;
文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.
loff_t f_pos;
当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ). 驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它;读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.
unsigned int f_flags;
这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作( 我们在第一章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O ); 其他标志很少使用. 特别地, 应当检查读/写许可, 使用f_mode 而不是 f_flags. 所有的标志在头文件 <linux/fcntl.h> 中定义.
struct file_operations *f_op;
和文件关联的操作. 内核安排指针作为它的 open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替代 filp->f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销. 替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.
void *private_data;
open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.
struct dentry *f_dentry;
关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关心 dentry
结构, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.
【3】inode 结构
- 结构体部分截取
- Linux系统中万物皆文件的思想,每个文件都对应有自己唯一的一个的文件编号,inode 结构由内核在内部用来表示文件。 因此,它和代表打开文件描述符的文件结构是不同的。可能有代表单个文件的多个打开描述符的许多文件结构,但是它们都指向一个单个 inode 结构。通过使用ls -i 指令我们可以查看,这也就是文件的 inode 号。
inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2 个成员对于编写驱动代码有用:
dev_t i_rdev; 对于代表设备文件的节点, 这个成员包含实际的设备编号.
struct cdev *i_cdev;
|-->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 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.
内核开发者已经增加了 2 个宏, 可用来从一个 inode 中获取主次编号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
|-->static inline unsigned iminor(const struct inode *inode)
{
return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
return MAJOR(inode->i_rdev);
}