3.1. scull设计
scull0 -- scull3 ,4个设备,每个由一个全局永久的内存区组成.
1 全局意味着如果设备被多次打开, 设备中含有的数据由所有打开它的文件描述符共享.
2 永久意味着如果设备关闭又重新打开,数据不会丢失.
3 它可以用惯常的命令来存取和测试, 例如cp,cat,以及I/O重定向.
scullpipe0 -- scullpipe3
1 4个FIFO(先入先出) 设备类似管道. 一个进程读的内容来自另一个进程所写的.
2 如果多个进程读同一个设备,它们竞争数据.
3 scullpipe 的内部实现将展示阻塞读写和非阻塞读写的实现,而不必采取中断。真实的驱动使用硬件中断来同步设备,
3.2 设备号
3.2.1
在终端输入 ls -l 命令,可以看到类似如下信息:
crw-rw-rw- 1 root root 1, 3 Apr 11 2002 null
第一个'c' 表示字符设备文件。
主设备编号是1 次设备号是3
主编号
传统上,主编号标识设备相连的驱动. 例如, /dev/null和/dev/zero都由驱动1来管理。也可以一个主编号一个驱动。
次设备号
次编号被内核用来决定引用哪个设备. 依据驱动,你可以从内核得到一个你的设备的直接指针,或者作为本地设备数组的索引.
内核自己几乎不知道次编号的任何事情, 除了它们指向你的驱动实现的设备.
设备号的获取
<linux/types.h> 中定义了设备编号,是dev_t类型,12位用作主编号,20位用作次编号.
<linux/kdev_t.h>中定义了取得设备号的宏定义:为了以后的扩展性建议用下面的方法。
MAJOR(dev_t dev); 获得主设备号
MINOR(dev_t dev); 获得次设备号
MKDEV(int major, int minor); 获得dev_t类型的设备号。
3.2.2 分配和释放设备
第一件事是注册一个或多个设备编号,使用如下函数 (我们已经知道设备号)
<linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first 输入参数,是你要分配的起始设备编号. first 的次编号部分常是0,但不是必须的。
count 是你请求的连续设备编号的总数.如果count 太大,会溢出。
name 是应当连接到这个编号范围的设备的名子; 它会出现在/proc/devices 和 sysfs中.
返回 0表示成功,负数是失败原因。失败则不能使用该设备号。
当然可以自动分配设备号,并注册设备:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
dev 输出参数, 它在函数成功时,分配一个编号.
fisetminor 请求的第一个要用次设备编号,它常常是 0.
count 如上。
name 如上。
最后,应当在不使用时释放设备编号。常在 cleanup 函数调用。
void unregister_chrdev_region(dev_t first, unsigned int count);
动态分配设备号
原因:
一些主设备编号是被静态分派了的.可以再内核源码树的Documentation/devices.txt中看到.
若使用了已用的静态主编号,当你的驱动广泛被使用了,主编号将导致冲突和麻烦.
因此,强烈建议使用动态分配主设备编号,即使用alloc_chrdev_region()并非register_chrdev_region().
缺点:
无法提前创建设备节点,动态分配的主编号会变化. 这也不是问题, 分配之后,可从 /proc/devices 中读取它.
方法:
可使用一个简单的脚本来调用insmod,在调用insmod后,读取/proc/devices 来创建特殊文件。
例:下面这个名为 snull_load 的脚本,是scull发布的一部分. 可以从系统的 rc.local 中调用该脚本,或施工调用。
#!/bin/sh
module="scull"
device="scull"
mode="664"
# 使用传入到本脚本的参数调用insmod.
/sbin/insmod ./$module.ko $* || exit 1 #使用路径名指定模块位置,因为新的modutils默认不在当前目录查找模块。
rm -f /dev/${device}[0-3] #删除所有模块
major=$(awk "\\$2==\"$module\" {print\\$1}" /proc/devices)
mknod /dev/${device}0 c $major 0 #注册4个模块
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
# 给定适当的组属性及许可,并修改组。 因为该脚本需要root权限运行。
# Not all distributions have staff, some have "wheel" instead.
group="staff"
grep -q '^staff:' /etc/group || group="wheel"
chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]
还有一个snull_nuload用于卸载。
scull的实现方式:
使用全局变量
scull_major作为主设备号,初始化为 SCULL_MAJOR==0,表示"动态分配", 定义在 scull.h.
scull_minor作为次设备号
或者在使用 insmod 命令时行指定一个值给 scull_major.
scull源码中获取主编号的代码:
if (scull_major)
{ //已经有scull_major号了
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
}else
{ //自动获取
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
}
if (result < 0)
{
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
3.3 一些重要的数据结构
重要的结构有三个:
file_operations <linux/fs.h>
file
inode
3.3.1 file_operations 结构
用途:连接设备操作到设备编号上。
实现机制:
1 打开的文件和一组函数相关联。(内部用一个file结构来代表)
2 这些操作大部分负责实现系统调用,命名为open, read, 等等.
3 我们可以认为文件是一个"对象"并且其上的函数操作称为它的"方法",
4 一个 file_operation 结构的每个成员必须指向驱动中的函数或者留置为NULL.
当为NULL时内核的确切的行为是每个函数不同的,后面介绍。
例 :scull 设备驱动只实现最重要的设备方法. file_operations 结构是如下初始化的:
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_release,
};
此声明使用标准的C标记式结构初始化语法,具有可移植性,他允许结构成员重新排序;
所有操作列表:(可供用户程序使用)
struct module *owner 它指向拥有这个结构的模块的指针.
用途:避免该模块在使用过程中被卸载,被初始化为 THIS_MODULE <linux/module.h>
loff_t (*llseek) (struct file *, loff_t, int)
用途:改变文件中的当前的读/写位置,返回一个新位置(正的).错误时返回负值.
loff_t 是"long offset"在32位平台上也至少64位宽.
如果该函数指针是NULL,调用seek会以无法预的修改file结构中的位置计数器。
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)
用途:从设备中获取数据.
返回: 非负返回值代表了读取成功的字节数。
如果该函数指针是NULL,调用read返回 -EINVAL("Invalid argument")
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.
ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);
用途:初始化设备上的一个异步写。
int (*readdir) (struct file *, void *, filldir_t);
用途:对于设备文件无用,应当为NULL; 它用来读取目录的,仅对文件系统有用.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
用途:poll, epoll,select, 都用作查询 一个或多个文件描述符的读或写是否会阻塞.
返回:一个位掩码指示读写是否非阻塞。
如果该函数指针是NULL, 则假定为可读可写.
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
用途:提供了执行特定命令的方法(例如格式化磁道,不是读也不是写).
如果该函数指针是NULL,则该设备不提供ioctl方法,返回 -ENOTTY。
int (*mmap) (struct file *, struct vm_area_struct *);
用途:请求将设备内存映射到进程的地址空间.
如果该函数指针是NULL,返回-ENODEV.
int (*open) (struct inode *, struct file *);
用途:这常常是对设备文件进行的第一个操作, 不要求一定有该方法。
如果该函数指针是NULL,设备打开一直成功。
int (*flush) (struct file *);
用途:为确保所有写的数据在设备关闭前写到磁带上.
如果该函数指针是NULL,内核简单地忽略用户应用程序的请求.
int (*release) (struct inode *, struct file *);
用途:在文件结构被释放时引用这个操作.release 可以为NULL.
int (*fsync) (struct file *, struct dentry *, int);
用途:用户调用来刷新任何挂着的数据. 如果这个指针是NULL, 系统调用返回 -EINVAL.
int (*aio_fsync)(struct kiocb *, int);
是fsync方法的异步版本.
int (*fasync) (int, struct file *, int);
用途:通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题,在第6章中描述.
int (*lock) (struct file *, int, struct file_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。
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
用途:实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个.
设备驱动常常使 sendfile 为 NULL.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
用途:sendpage 是 sendfile 的另一半;它由内核调用来发送数据,一次一页,到对应的文件.
设备驱动实际上不实现sendpage.
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
用途:在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中.
这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求.
大部分驱动可以置这个方法为 NULL.
int (*check_flags)(int)
用途:允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.
int (*dir_notify)(struct file *, unsigned long);
用途:在应用程序使用fcntl来请求目录改变通知时调用.只对文件系统有用;驱动不需要实现。
3.3.2文件结构
1 struct file 定义于 <linux/fs.h> 完全不同于用户空间程序的 FILE。
2 文件结构代表一个打开的文件. (系统中每个打开的文件有一个关联的struct file在内核空间).
3 它在open时创建, 并传递给在文件上操作的任何函数, 直到最后的关闭. 内核释放这个数据结构.
4 在内核源码中,file指的是结构,而 filp是struct file的结构指针.
5 struct file 最重要成员如下:
mode_t f_mode 文件模式
标定定文件是可读、可写、可读写。通过位 FMODE_READ 和 FMODE_WRITE标志。
loff_t f_pos 当前读写位置
loff_t 通过读此值,可以知道在文件中的位置,不要直接操作它。
有一个例外是在 llseek 方法中,它的目的就是改变文件位置.
unsigned int f_flags; 这些是文件标志,
O_RDONLY 此标志是否是请求非阻塞操作 他们<linux/fcntl.h>中定义
O_NONBLOCK 很少用
O_SYNC 很少用
struct file_operations *f_op; 和文件关联的操作.
内核安排此指针作为 open 实现的一部分。
void *private_data;
open系统调用,在驱动调用open前设置这个指针为 NULL。可以使用这个成员来指向分配的数据。
可以用来在系统调用间保留状态信息,我们大多模块都会使用它.
struct dentry *f_dentry;
关联到文件的目录入口(dentry)结构.
注:由于驱动从不自己填写file结构,而是访问已创建的file。所以可以忽略一些成员。
3.3.3 inode结构
1 inode 结构由内核在内部用来表示文件. 因此,不同于file. inode结构包含大量关于文件的信息.
2 作为一个通用的规则,这个结构的2个成员对编写驱动代码有用:
dev_t i_rdev;
对于代表设备文件的节点, 这个成员包含实际的设备编号.
struct cdev *i_cdev;
struct cdev 是内核的内部结构,代表字符设备;
当 inode指向设备时,i_cdev指向struct cdev结构。
3 为了可移植编程性,以下函数可以获得主次编号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
3.4 字符设备的注册
待续....