重要的数据结构
设备号注册只是驱动程序代码必须执行的许多任务中的第一个。 我们将很快查看其他重要的驱动程序组件,但首先需要另一个题外话。 大多数基本驱动程序操作涉及三个重要的内核数据结构,称为 file_operations、file 和 inode。 需要对这些结构有基本的熟悉才能做很多有趣的事情,因此我们现在将快速浏览一下它们中的每一个,然后再深入了解如何实现基本驱动程序操作的细节。
文件操作【File Operations】
到目前为止,我们已经保留了一些设备号供我们使用,但我们还没有将我们的驱动程序的任何操作连接【connect】到这些设备号上。 file_operations 结构是字符驱动程序如何设置此连接的。 该结构定义在 <linux/fs.h> 中,它是函数指针的集合。 每个打开的文件(在内部由一个 file
结构表示,我们将很快检查)都与其自己的一组函数相关联(通过包含一个名为 f_op 的字段,该字段指向 file_operations 结构)。 这些操作主要负责实现系统调用,因此被命名为 open, read, 等。 我们可以将文件【file】视为一个“对象”,将对其进行操作的函数视为其“方法”,使用面向对象的编程术语来表示对象声明的作用于其自身的操作。 这是我们在 Linux 内核中看到的面向对象编程的第一个标志,我们将在后面的章节中看到更多。
按照惯例,一个 file_operations 结构或指向一个 file_operations 结构的指针被称为 fops(或其他变体)。 该结构中的每个字段【field】必须指向实现特定操作的驱动程序中的函数,或者为不支持的操作保留 NULL。 当指定NULL指针时,内核的确切行为(其实就是缺省行为)对于每个函数都是不同的,正如本节后面的列表所示。
以下列表介绍了应用程序【application】可以在设备【device】上调用的所有操作。我们尽量保持列表简短,以便它可以用作参考,仅总结每个操作和使用NULL指针时的默认内核行为。
当您通读 file_operations 方法列表时,您会注意到许多参数包括字符串 _ _user。 该注释是一种文档形式,指出指针是不能直接解引用【dereferenced】的用户空间地址【user-space address】。 对于正常的编译,_ _user没有作用,但是可以被外部检查软件用来发现对用户空间地址的滥用。
本章的其余部分,在描述了其他一些重要的数据结构之后,解释了最重要的操作的作用,并提供了提示、注意事项和真实的代码示例。 我们将更复杂的操作的讨论推迟到后面的章节,因为我们还没有准备好深入探讨内存管理、阻塞操作和异步通知等主题。
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 位宽。 错误【Errors】由负返回值表示。 如果此函数指针为 NULL,则 seek 调用将以潜在不可预测的方式修改 file
结构中的位置计数器【position counter】。
ssize_t (*read) (struct file *, char _ _user *, size_t, loff_t *);
用于从设备中检索数据。 此位置的空指针会导致 read 系统调用失败并显示 -EINVAL(“无效参数”)。 非负返回值表示成功读取的字节数。
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 *);
在设备上启动异步写入。
int (*readdir) (struct file *, void *, filldir_t);
对于设备文件,该字段应该为 NULL; 它用于读取目录,仅对文件系统有用。
unsigned int (*poll) (struct file *, struct poll_table_struct *);
poll 方法是三个系统调用的后端:poll、epoll 和 select,它们都用于查询对一个或多个文件描述符的读取或写入是否会阻塞。 poll 方法应返回一个位掩码,指示是否可以进行非阻塞读取或写入,并且可能向内核提供可用于使调用进程休眠【sleep】直到 I/O 变为可能的信息。 如果驱动程序将其 poll 方法保留为 NULL,则假定设备可读可写且不会阻塞。
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ioctl 系统调用提供了一种方法来发出特定于设备的命令(例如格式化软盘的磁道,既不是读也不是写)。 外,内核可以在不引用 fops
表的情况下识别一些 ioctl 命令。 如果设备不提供 ioctl 方法,则系统调用会为任何未预定义的请求返回错误(-ENOTTY
,“该设备没有这样的 ioctl”)。
int (*mmap) (struct file *, struct vm_area_struct *);
mmap 用于请求将设备内存映射到进程的地址空间。 如果此方法为 NULL,则 mmap 系统调用返回 -ENODEV
。
int (*open) (struct inode *, struct file *);
虽然这总是在设备文件上执行的第一个操作,但驱动程序不需要声明相应的方法。如果此条目为NULL,则打开设备总是成功,但不会通知您的驱动程序。
int (*flush) (struct file *);
当进程关闭设备的文件描述符副本时,将调用 flush 操作; 它应该执行(并等待)设备上任何未完成的操作。 这一定不能与用户程序请求的 fsync 操作混淆。 目前,很少有驱动程序使用 flush ; 例如,SCSI 磁带驱动程序使用它来确保所有写入的数据在设备关闭之前写入磁带。 如果 flush 为 NULL,内核将简单地忽略用户应用程序请求。
int (*release) (struct inode *, struct file *);
释放
file
结构时调用此操作。与 open 一样,release 可以为 NULL
int (*fsync) (struct file *, struct dentry *, int);
This method is the back end of the fsync system call, which a user calls to flush any pending data. If this pointer is NULL
, the system call returns -EINVAL
.
此方法是 fsync 系统调用的后端,用户调用它来刷新任何挂起【Pending】的数据。 如果此指针为 NULL,则系统调用返回 -EINVAL
。
int (*aio_fsync)(struct kiocb *, int);
fsync 方法的异步版本。
int (*fasync) (int, struct file *, int);
此操作用于通知设备其 FASYNC
标志发生变化。异步通知是一个高级主题,在第 6 章中进行了描述。如果驱动程序不支持异步通知,该字段可以为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
*);
这些方法实现分散/聚集【scatter/gather】读写操作。 应用程序偶尔需要执行涉及多个内存区域的单个读取或写入操作; 这些系统调用允许他们这样做而无需对数据进行额外的复制操作。 如果这些函数指针保留为 NULL,则改为调用 read 和 write 方法(可能不止一次)。
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
此方法实现了 sendfile 系统调用的读取端,它通过最少的复制将数据从一个文件描述符移动到另一个文件描述符。 例如,需要通过网络连接发送文件内容的 Web 服务器使用它。 设备驱动程序通常将 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)
此方法允许模块检查传递给 fcntl(F_SETFL...) 调用的标志【flag】。
int (*dir_notify)(struct file *, unsigned long);
当应用程序使用 fcntl 请求目录更改通知时调用此方法。 它仅对文件系统有用; 驱动程序不需要实现 dir_notify。
scull 设备驱动程序只实现最重要的设备方法。 它的file_operati file_operations
ons结构初始化如下:
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 标记结构初始化语法【tagged structure initialization syntax】。 这种语法是首选,因为它使驱动程序在结构定义的变化中更具可移植性,并且可以说,使代码更紧凑和可读。 标记初始化允许重新排序结构成员; 在某些情况下,通过在同一硬件高速缓存行中放置指向经常访问的成员的指针,可以实现实质性的性能改进。
file 结构
在 <linux/fs.h> 中定义的 struct
file
是设备驱动程序中使用的第二重要的数据结构。 请注意,file
,与用户空间程序的 FILE
指针无关。 FILE
在 C 库中定义,从不会出现在内核代码中。 另一方面,struct
file
是一个内核结构,也从不会出现在用户程序中。
file
结构表示一个打开的文件(它不特定于设备驱动程序;系统中每个打开的文件在内核空间中都有一个与之关联的 struct
file
)。它由内核在 open 时创建,并传递给对该文件进行操作的任何函数,直到最后被关闭。 在文件的所有实例关闭后,内核释放数据结构。
在内核源代码中,指向struct
file
的指针通常称为 file
或者 filp
(“文件指针”)。 我们将始终使用 filp
以防止与结构体本身产生歧义。 因此, file
指的是结构体,而 filp
指的是指向该结构体的指针。
struct
file
最重要的字段都列举在这里。 与上一节一样,第一次阅读时可以跳过该列表。 然而,在本章后面,当我们面对一些真正的 C 代码时,我们将更详细地讨论这些字段。
mode_t f_mode;
文件模式通过位 FMODE_READ
和 FMODE_WRITE
将文件标识为可读或可写(或两者)。 您可能想在 open 或 ioctl 函数中检查该字段的读/写权限,但您不需要检查读写权限,因为内核会在调用您的方法之前进行检查。 。当文件没有被打开时,试图读或写这种类型的访问将被拒绝,而驱动程序甚至不知道它。
loff_t f_pos;
当前的读或写位置。 loff_t
在所有平台上都是 64 位(gcc 术语中的 long long)。 如果驱动程序需要知道文件中的当前位置,则可以读取该值,但通常不应更改它; read 和 write 应该使用他们收到的指针作为最后一个参数来更新该位置,而不是直接作用于 filp->f_pos
。 该规则的一个例外是 llseek 方法,该方法的目的是更改文件位置。
unsigned int f_flags;
这些是文件标志【file flags】,例如 O_RDONLY、O_NONBLOCK 和 O_SYNC。 驱动程序应检查 O_NONBLOCK 标志以查看是否已请求非阻塞操作(我们在第 6.2.3 节中讨论非阻塞 I/O); 其他标志很少使用。 特别是,应使用 f_mode
而不是 f_flags
检查读/写权限。 所有标志都在 <linux/fcntl.h> 头文件中定义。
struct file_operations *f_op;
与文件关联的操作。 内核将指针赋值作为其 open 实现的一部分,然后在需要分派【dispacher】任何操作时读取它。 filp->f_op
中的值永远不会被内核保存以备后用; 这意味着您可以更改与您的文件相关联的文件操作,并且新绑定的方法将在您调用返回后立即生效。 例如,与主号 1 (例如/dev/null, /dev/zero等等)关联的 open 代码根据打开的次编号替换 filp->f_op
中的操作。 这种做法允许在同一个主号下实现多个行为,而不会在每次系统调用时引入开销。 替换文件操作的能力在内核中相当于面向对象编程中的“方法覆盖【method overriding】”。
void *private_data;
open 系统调用在调用驱动程序的 open 方法之前将此指针设置为 NULL。 您可以自由地使用或忽略该字段; 您可以使用该字段指向分配的数据,但是您必须记住在文件结构被内核销毁之前在释放方法中释放该内存。 private_data 是用于跨系统调用保存状态信息的有用资源,我们的大多数示例模块都使用它。
struct dentry *f_dentry;
与文件关联的目录条目(dentry)结构。 设备驱动程序编写者通常不需要关心 dentry 结构,除了通过 filp->f_dentry->d_inode
访问 inode 结构。
inode 结构
内核在内部使用 inode 结构来表示文件。 因此,它不同于表示一个打开的文件描述符的 file 结构。 可以有许多 file 结构代表在单个文件上的多个打开的描述符,但它们都指向单个 inode 结构。
inode 结构包含有关文件的大量信息,但是在驱动开发时只关注其中两个属性即可。
- dev_t i_rdev; 对于表示设备文件【device file】的 inode,该字段包含实际的设备编号【device number】
- struct cdev *i_cdev; struct cdev 是表示字符设备的内核内部结构; 当 inode 引用字符设备文件【char device file】时,该字段包含指向该结构的指针。
内核提供了两个宏方法用户获取某个 inode 上的主次版本号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);