(点击上方蓝字,快速关注)
大多数基本的驱动操作涉及到内核的 3 个重要数据结构: file_operations, file 和 inode 。
我们已经拥有一些设备号,但是如何将其与驱动操作连在一起呢? file_operations 结构就是这个桥梁,这个结构体定义在 <linux/fs.h> 中,它是一群函数的指针集合,每个所打开的文件都存在一个 f_op 指针指向 file_operations 结构体,里面的操作大部分主要完成系统调用,如 open,read 等。我们可以将 file 看成对象 , 对它操作的操作看成是方法 , 使用面向对象程序设计( object-oriented programming )这个术语表征某一对象的行为声明会作用于它自身。后面将会看到更多这种情况。
一般来说,一个 指向file_operations 结构的指针称为 fops 。这个结构体里面的每个域必须指向驱动中的某些函数以完成一些特定的操作,或者赋予 NULL 值表示没有支持的操作。当被赋予 NULL 时,内核的具体行为对每个函数来说都不尽相同。
1.file_operations 结构
struct file_operations {
/**
* 拥有该结构的模块的指针。避免模块正在被使用时,误卸载模块。
* 几乎在所有情况下,该成员都会被初始化为THIS_MODULE。
*/
struct module *owner;
/**
* 方法llseek用来修改文件的当前读写位置。并将新位置作为返回值返回。
* 参数loff_t是一个长偏移量,即使在32位平台上也至少占用64位的数据宽度。
* 出错时返回一个负的返回值。如果这个函数指针是NULL,对seek的调用将会 以某种不可预期的方式修改file结构中的位置计数。
*/
loff_t (*llseek) (struct file *, loff_t, int);
/**
* 用来从设备中读取数据。该函数指针被赋为NULL时,将导致read系统调用出错并返回-EINVAL。函数返回非负值表示成功读取的字节数。
*/
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
/**
* 初始化一个异步的读取操作。即在函数返回之前可能不会完成的读取操作。如果该方法为NULL,所有的操作都通过read同步完成。
*/
ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
/**
* 向设备发送数据。如果没有这个函数,write系统调用会向程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。
*/
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
/**
* 初始化设备上的异步写入操作。
*/
ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
/**
* 对于设备文件来说,这个字段应该为NULL。它仅用于读取目录,只对文件系统有用。
* filldir_t用于提取目录项的各个字段。
*/
int (*readdir) (struct file *, void *, filldir_t);
/**
* POLL方法是poll、epoll和select这三个系统调用的后端实现。这三个系统调用可用来查询某个或多个文件描述符上的读取或写入是否会被阻塞。
* poll方法应该返回一个位掩码,用来指出非阻塞的读取或写入是否可能。并且也会向内核提供将调用进程置于休眠状态直到IO变为可能时的信息。
* 如果驱动程序将POLL方法定义为NULL,则设备会被认为既可读也可写,并且不会阻塞。
*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/**
* 系统调用ioctl提供了一种执行设备特殊命令的方法(如格式化软盘的某个磁道,这既不是读也不是写操作)。
* 另外,内核还能识别一部分ioctl命令,而不必调用fops表中的ioctl。如果设备不提供ioctl入口点,则对于任何内核未预先定义的请求,ioctl系统调用将返回错误(-ENOTYY)
*/
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
/**
* 与ioctl类似,但是不获取大内核锁。
*/
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
/**
* 64位内核使用该方法实现32位系统调用。
*/
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
/**
* 请求将设备内存映射到进程地址空间。如果设备没有实现这个方法,那么mmap系统调用将返回-ENODEV。
*/
int (*mmap) (struct file *, struct vm_area_struct *);
/**
* 尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明一个相应的方法。
* 如果这个入口为NULL,设备的打开操作永远成功,但系统不会通知驱动程序。
*/
int (*open) (struct inode *, struct file *);
/**
* 对flush操作的调用发生在进程关闭设备文件描述符副本的时候,它应该执行(并等待)设备上尚未完结的操作。
* 请不要将它同用户程序使用的fsync操作相混淆。目前,flush仅仅用于少数几个驱动程序。比如,SCSI磁带驱动程序用它来确保设备被关闭之前所有的数据都被写入磁带中。
* 如果flush被置为NULL,内核将简单地忽略用户应用程序的请求。
*/
int (*flush) (struct file *);
/**
* 当file结构被释放时,将调用这个操作。与open相似,也可以将release设置为NULL。
*/
int (*release) (struct inode *, struct file *);
/**
* 该方法是fsync系统调用的后端实现。用户调用它来刷新待处理的数据。如果驱动程序没有实现这一方法,fsync系统调用将返回-EINVAL。
*/
int (*fsync) (struct file *, struct dentry *, int datasync);
/**
* 这是fsync的异步版本。
*/
int (*aio_fsync) (struct kiocb *, int datasync);
/**
* 这个操作用来通知设备其FASYNC标志发生了变化。异步通知是比较高级的话题,如果设备不支持异步通知,该字段可以是NULL。
*/
int (*fasync) (int, struct file *, int);
/**
* LOCK方法用于实现文件锁定,锁定是常规文件不可缺少的特性。但是设备驱动程序几乎从来不会实现这个方法。
*/
int (*lock) (struct file *, int, struct file_lock *);
/**
* 和writev用来实现分散、聚集型的读写操作。应用程序有时需要进行涉及多个内存区域的单次读或写操作。
* 利用这些系统调用可完成这类工作,而不必强加额外的数据拷贝操作。如果被设置为NULL,就会调用read和write方法(可能是多次)
*/
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
/**
* 这个方法实现sendfile系统调用的读取部分。sendfile系统调用以最小的复制操作将数据从一个文件描述符移动到另一个。
* 例如,WEB服务器可以利用这个方法将鞭个文件的内容发送到网络联接。设备驱动程序通常将sendfile设置为NULL。
*/
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
/**
* sendpage是sendfile系统调用的另一半。它由内核调用以将数据发送到对应的文件。每次一个数据页。
* 设备驱动程序通常也不需要实现sendfile。
*/
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
/**
* 在进程的地址空间中找到一个合适的位置,以便将底层设备中的内存段映射到该位置。
* 该任务通常由内存管理代码完成,但该方法的存在可允许驱动程序强制满足特定设备需要的任何对齐要求。大部分驱动程序可设置该方法为NULL。
*/
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
/**
* 该方法允许模块检查传递给fcntl调用的标志。当前只适用于NFS
*/
int (*check_flags)(int);
/**
* 当应用程序使用fcntl来请求目录改变通知时,该方法将被调用。该方法仅对文件系统有用,驱动程序不必实现dir_notify。
* 当前适用于CIFS。
*/
int (*dir_notify)(struct file *filp, unsigned long arg);
/**
* 用于定制flock系统调用的行为。当进程试图对文件加锁时,回调此函数。
*/
int (*flock) (struct file *, int, struct file_lock *);
};
2. file 文件结构
在设备驱动中,这也是个非常重要的数据结构,必须要注意一点,这里的 file 与用户空间程序中的FILE 指针是不同的,用户空间 FILE 是定义在 C 库中,从来不会出现在内核中。而 struct file ,却是内核当中的数据结构,因此,它也不会出现在用户层程序中。
file 结构体指示一个已经打开的文件,其实系统中的每个打开的文件在内核中都有一个相应的 struct file 结构体,直至文件被关闭。如果文件被关系,内核就会释放相应的数据结构。
在内核源码中, struct file 要么表示为 file ,或者为 filp( 意指“ file pointer”), 注意区分一点, file指的是 struct file 本身,而 filp 是指向这个结构体的指针。
fmode_t f_mode;
此文件模式通过 FMODE_READ, FMODE_WRITE 识别了文件为可读的,可写的,或者是二者。在open 或 ioctl 函数中可能需要检查此域以确认文件的读 / 写权限,你不必直接去检测读或写权限,因为在进行 open/ioctl 等操作时内核本身就需要对其权限进行检测。
loff_t f_pos;
当前读写文件的位置。为 64 位。如果想知道当前文件当前位置在哪,驱动可以读取这个值而不会改变其位置。对 read,write 来说,当其接收到一个 loff_t 型指针作为其最后一个参数时,他们的读写操作便作更新文件的位置,而不需要直接执行 filp ->f_pos 操作。而 llseek 方法的目的就是用于改变文件的位置。
unsigned int f_flags;
文件标志,如 O_RDONLY, O_NONBLOCK 以及 O_SYNC 。在驱动中还可以检查 O_NONBLOCK 标志查看是否有非阻塞请求。其它的标志较少使用。特别地注意的是,读写权限的检查是使用 f_mode 而不是 f_flog 。所有的标量定义在头文件 <linux/fcntl.h> 中
struct file_operations *f_op;
与文件相关的各种操作。当文件需要迅速进行各种操作时,内核分配这个指针作为它实现文件打开,读,写等功能的一部分。 filp->f_op 其值从未被内核保存作为下次的引用,即你可以改变与文件相关的各种操作,这种方式效率非常高。
void *private_data;
在驱动调用 open 方法之前, open 系统调用设置此指针为 NULL 值。你可以很*的将其做为你自己需要的一些数据域或者不管它,如,你可以将其指向一个分配好的数据,但是你必须记得在 file struct 被内核销毁之前在 release 方法中释放这些数据的内存空间。 private_data 用于在系统调用期间保存各种状态信息是非常有用的。
3. inode 结构
内核使用inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file 文件结构)是不同的,我们可以使用多个file 文件结构表示同一个文件的多个文件描述符,但此时,所有的这些file文件结构全部都必须只能指向一个inode结构体。
inode结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可:
(1) dev_t i_rdev;
表示设备文件的结点,这个域实际上包含了设备号。
(2)struct cdev *i_cdev;
struct cdev是内核的一个内部结构,它是用来表示字符设备的,当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。
此外,内核也提供了两个宏可以从inode结点中获取主次设备号,宏的原型如下:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);