参考《Linux设备驱动程序第3版》
Scull字符型驱动程序的设备描述
字符设备驱动程序是分配一段内存,然后提供对这个设备的管理方式,我们通过它提供的操作来操纵字符驱动设备。本质上是一个我们分配的模拟内存来模拟驱动设备。
###设备编号
字符型驱动设备可以通过命令ls -l
查看文件类型为c的设备。
- major主设备编号标识设备对应的驱动程序
- minor次设备号由内核使用,用于正确确定设备文件所指的设备。
- 在内核中,dev_t(定义在<linux/types.h>中)用来保存设备编号,包括major和minor。
- 在建立一个字符设备之前,驱动程序首先要获取一个或多个设备编号。函数为
register_chrdev_region
在<linux/fs.h中声明>。清除函数为unregister_chrdev_region
。这种方式属于静态分配一个当前没有使用的设备号为主设备号。 - 动态分配
alloc_chrdev_region
。 - 一旦分配了设备号,就可以从/proc/devices中得到。
- 安装scull设备,使用内核命令insmod(/sbin/insmod)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38#!/bin/sh
module="scull"
device="scull"
mode="664"
#install scull,invoke insmod with all arguments we got
#use a pathname, as insmod doesn't look in . by default
/sbin/insmod ./$module.ko $* || exit 1
#retrieve the major number
major=$( awk "\$2==\"$module" {print \$1}" /proc/devices)
#which is same as
major=$( awk "\$2==\"scull\" {print \$1}" \proc\devices)
#remove stale nodes
rm -f /dev/${device}[0-3]
echo ${device}0 ${major}0
#make nodes,创建了四个设备
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
ln -sf ${device}0 /dev/${device}
# grep -q表示quiet不显示任何结果,正则表达式匹配^表示匹配字
符串起始位置
# wheel组包含特殊权限,可以执行root权限但并不需要知道root管理员密码。
if grep -q '^staff:' /etc/group; then
group='staff'
else
group='wheel'
fi
chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3] #配置权限664 - 卸载设备
1
2
3
4
5
6
7
8
9#!/bin/sh
module="scull"
device="scull"
#remove module from the kernel
/sbin/rmmod $module $* || exit 1
#remove the device file
rm -f /dev/${device} /dev/${device}[0-3]
可以看出在/dev下,scull这个模块包含四个设备,保存在一个文件夹中,并且文件夹指向scull0设备。
- 除了使用load和unload脚本之外,还可以编写一个init脚本,并保存在发行版使用的init脚本目录中(scull.init)。接收约定的参数(start、stop、restart)也可以完成scull_load和scull_unload双重任务。
- 分配设备号的最佳方式。默认采用动态方式,同时保留加载或编译是指定的主设备编号。
1
2
3
4
5
6
7
8
9
10
11
12
13dev_t dev = 0;
int result;
if(scull_major){ //存在驱动程序
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull"); //在dev设备上注册设备
} else{
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); //分配major驱动程序编号
scull_major = MAJOR(dev); //获取设备驱动
}
if(result < 0){
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
驱动程序需要的数据结构
大部分基本的驱动程序操作涉及到三个重要的内核数据结构,分别是file_operations、file和inode。
file_operations 文件操作
- file_operations是建立驱动程序操作与设备编号之间的连接。该结构定义在<linux/fs.h>,其中包含一组函数指针。
- 每个打开的文件(file)和一组函数关联。这些操作主要用来实现系统调用。我们可以认为文件是一个”对象“,而操作他的函数是”方法“。
- 在查看file_operations方法清单时,我们会注意到许多参数包含__user字符串,表明指针是一个用户空间地址,不能被直接饮用。
基本的file_operations数据结构:
- struct module *owner :指向”拥有“该结构的木偶快的指针。内核使用这个字段来避免在模块的操作正在被使用时卸载该模块。该成员都会被初始化为
THIS_MODULE
, which is defined in <linux/module.h> - loff_t (**llseek) (struct file *, loff_t, int); 该方法用来修改文件当前读写位置,并将新的位置(正的)作为返回值返回。loff_t是一个长偏移量。如果这个函数指针是NULL,那么对seek的调用将以某种不可预期的方式修改file结构中的位置计数器。
- ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 从设备中读取数据,该函数指针为NULL时,将导致read系统调用出错并返回-EINVAL(invalid argument)。函数返回成功读取的字节数。对应的异步读取函数为aio_read。
- ssize_t (*write) (struct file *, const char __user *, size_t, loff_t); 向设备发送数据。对应的异步写入操作 (aio_write)
- int (*open) (struct inode *, struct file *); 这是对设备文件执行的第一个操作,但并不是一定要声明,如果入口为NULL,设备的打开操作会永远成功,但系统不会通知驱动程序。
- int (*lock) (struct file *, int, struct file_lock *); lock方法用于实现文件锁定,锁定是常规文件不可缺少的特性,但设备驱动程序基本不会实现这个方法。
1 | struct file_operations scull_fops={ |
file文件结构
- file结构与用户空间程序中的FILE没有任何联系,(FILE是在C库中定义的,所以不会出现在内核中)struct file是一个内核结构,不会出现在用户结构中。
- struct file表示一个打开的文件(不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间中都有一个对应的file结构)。由内核在open操作时创建,并传递给该文件上进行操作的所有函数,直到最后close函数。
- mode_t f_mode: 文件模式,通过FMODE_READ和FMODE_WRITE来标识文件是否可读或可写。内核在调用驱动程序的read和write之前已经检查了访问权限,没有访问权限的情况下内核将拒绝对该文件进行读写操作。
- loff_t f_pos: 当前read/write位置 (long offsite)。如果驱动程序需要知道文件中的当前位置,可以读取这个值。
- unsigned int f_flags: 文件标志,检查用户请求的是否是非阻塞式操作。
- struct file_operations *f_op 文件相关操作。
- void *pricate_data: open调用前将这个指针置为NULL。驱动可以将这个字段用于任何目的,是跨系统调用时保存状态信息非常有用的资源。
- struct dentry *f_dentry: 文件对应的目录项结构。
inode结构
- 内核用inode结构在内部表示文件,和file结构不同,file表示打开的文件描述符。对单个文件,可能会有许多个表示打开的file结构,但他们都指向单个inode结构。
- inode结构中包含了大量的有关文件信息。常规只有以下两个字段对驱动有用:
- dev_t i_rdev 表示设备文件的inode结构,包含了真正的设备编号
- struct cdev *i_cdev 表示私服设备的内核的内部结构。
Scull设备的注册
内核内部使用struct cdev来表示字符设备。在内核调用设备之前,必须分配并注册一个或多个cdev。(定义在<linux/cdev.h>)
分配和初始化cdev的方法:
- 在运行时获取一个独立的cdev结构:
1
2struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = *my_fops; - 也可以初始化的时候进行分配
1
void cdev_init(struct cdev *cdev, struct file_operations *fops);
定义Scull设备, 并初始化Scull设备
1 | struct scull_dev{ |
Scull设备初始化
1 | int scull_major = SCULL_MAJOR; //defined in scull.h |
定义scull函数接口
1 | struct file_operations scull_fops = { |
Scull设备操作接口
open和release
open方法
open方法应该完成:
- 检查设备特定的错误(如设备未就绪或类似的硬件问题)
- 如果设备首次打开,则初始化
- 如果有必要,更新f_op指针
- 分配并填写置于filp->private_data里的数据结构
int (*open) (struct inode *inode, struct file *filp);
经过简化的scull_open代码,没有进行设备初始化工作。
1 | int scull_open(struct inode *inode, struct file *filp){ |
release方法
与open方法相反。release完成以下任务:
- 释放由open分配的,保存在filp->private_data中的所有内容
- 在最后一个关闭操作时关闭设备。
1 | int scull_release(struct inode *inode, struct file *filp){ |
并不是每个close系统调用都会引起release方法的调用。只有那些真正释放设备数据结构的close调用才会调用这个方法。内核对每个file结构维护其被使用多少次的计数器。无论是fork还是dup,都不会创建新的数据结构(数据结构只能由open创建),它只是增加已有结构的记数。只有在file结构的计数归0的时候,close系统调用才会执行release方法。
Scull内存使用
scull驱动程序引入了内存管理的两个核心函数kmalloc和kfree,定义在<linux/slab.h>。
void *kmalloc(size_t size, int flags);
- 试图分配 size个自己的大小,返回值为内存指针。flags为描述内存的分配方法。目前始终使用GFP_KERNEL。
void kfree(void *ptr);
在scull中,每个设备都使用一个指针链表,每个指针都只想一个scull_qset结构。如下图:
我们把每一个内存区成为一个量子(4000字节),指针数组成为量子集(1000个地址)。
这样scull写入一个字节就会消耗8000或者12000个字节的内存。(每个量子占4000字节,一个量子集占1000*4或1000*8个字节)那么为量子和量子集选择合适的数值就可以配置如何使用该设备。
- 在编译时可以修改scull.h中的宏SCULL_QUANTUM和SCULL_QSET。
- 在加载模块时,可以设置scull_quantum和scull_qset的整数值。
- 或者在运行时,使用ioctl修改当前值或默认值(可能不使用与linux 4.0)
read和write
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
count是请求传输的数据长度,buff是指向用户空间的缓冲间,最后offp是一个指向长偏移量对象的指针,指明用户在文件中进行存取操作的位置。
显然,驱动程序必须访问用户空间的缓冲区以便完成自己的工作。这个过程由内核专用函数提供 <linux/uaccess.h> (在linux4.0版)
unsigned long copy_to_user(void __user *to, const void* from, unsigned long count);
unsigned long copy_from_user(void* to, const void __user *from, unsigned long count);
这两个函数很像memcpy,但内核访问用户空间时,被寻址的用户空间页面可能并不在内存中,这时候该驱动进程就会转为睡眠状态直到页面加载进内存。带来的结果是访问用户空间的任何函数都必须是可重入的,并且能和其他驱动程序函数并发执行,而且必须处于能够合法休眠的状态。
read代码:
1 | ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_ops){ |
write代码:
1 | ssize_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_ops){ |