先看下面这张图,这是Linux 中虚拟文件系统、一般的设备文件与设备驱动程序之间的函数调用关系;
上面这张图展现了一个应用程序调用字符设备驱动的过程, 在设备驱动程序的设计中,一般而言,会关心 file
和 inode
这两个结构体。
用户空间使用 open()
函数打开一个字符设备 fd = open("/dev/hello",O_RDWR)
, 这一函数会调用两个数据结构 struct inode{...}
与struct file{...}
,二者均在虚拟文件系统VFS
处,下面对两个数据结构进行解析。
在设备驱动中,这也是个非常重要的数据结构,必须要注意一点,这里的file与用户空间程序中的FILE指针是不同的,用户空间FILE是定义在C库中,从来不会出现在内核中。而struct file,却是内核当中的数据结构,因此,它也不会出现在用户层程序中。
file
结构体指示一个已经打开的文件(设备对应于设备文件),其实系统中的每个打开的文件在内核空间都有一个相应的struct file
结构体,它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数,直至文件被关闭。如果文件被关闭,内核就会释放相应的数据结构。
在内核源码中,struct file
要么表示为file
,或者为filp
(意指“file pointer”), 注意区分一点,file
指的是struct file
本身,而filp
是指向这个结构体的指针。
struct file
结构体成员如下图:
使用open
打开文件时,传入的flags
、mode
等参数会被记录在内核中对应的struct file
结构体里(f_flags
、f_mode
):
下面是几个重要成员:
(1)fmode_t f_mode;
此文件模式通过FMODE_READ
, FMODE_WRITE
识别了文件为可读的,可写的,或者是二者。在open或ioctl函数中可能需要检查此域以确认文件的读/写权限,你不必直接去检测读或写权限,因为在进行ioctl
等操作时内核本身就需要对其权限进行检测。
(2)loff_t f_pos;
当前读写文件的位置。为64位。如果想知道当前文件当前位置在哪,驱动可以读取这个值而不会改变其位置。对read
,write
来说,当其接收到一个loff_t
型指针作为其最后一个参数时,他们的读写操作便作更新文件的位置,而不需要直接执行filp ->f_pos
操作。而lseek
方法的目的就是用于改变文件的位置。
(3)unsigned int f_flags;
文件标志,如O_RDONLY
, O_NONBLOCK
以及O_SYNC
。在驱动中还可以检查O_NONBLOCK
标志查看是否有非阻塞请求。其它的标志较少使用。特别地注意的是,读写权限的检查是使用f_mode
而不是f_flog
。所有的标量定义在头文件中
(4)struct file_operations *f_op;
与文件相关的各种操作。当文件需要迅速进行各种操作时,内核分配这个指针作为它实现文件打开,读,写等功能的一部分。filp->f_op
其值从未被内核保存作为下次的引用,即你可以改变与文件相关的各种操作,这种方式效率非常高。
file_operation 结构体解析如下:file_operations 结构体知识解析
(5)void *private_data;
在驱动调用open
方法之前,open
系统调用设置此指针为NULL
值。你可以很自由的将其做为你自己需要的一些数据域或者不管它,如,你可以将其指向一个分配好的数据,但是你必须记得在file struct
被内核销毁之前在release
方法中释放这些数据的内存空间。private_data
用于在系统调用期间保存各种状态信息是非常有用的。
VFS inode
包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。
内核使用inode
结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file
文件结构)是不同的,我们可以使用多个file
文件结构表示同一个文件的多个文件描述符,但此时,所有的这些file
文件结构全部都必须只能指向一个inode
结构体。
inode
结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可:
(1)dev_t i_rdev;
表示设备文件的结点,这个域实际上包含了设备号。
(2)struct cdev *i_cdev;
struct cdev
是内核的一个内部结构,它是用来表示字符设备的。当inode
结点指向一个字符设备文件时,此域为一个指向inode
结构的指针。
inode
结构体如下:
struct inode {
struct hlist_node i_hash;
struct list_head i_list;
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
unsigned int i_nlink;
uid_t i_uid;//inode拥有者id
gid_t i_gid;//inode所属群组id
dev_t i_rdev;//若是设备文件,表示记录设备的设备号
u64 i_version;
loff_t i_size;//inode所代表大少
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
struct timespec i_atime;//inode最近一次的存取时间
struct timespec i_mtime;//inode最近一次修改时间
struct timespec i_ctime;//inode的生成时间
unsigned int i_blkbits;
blkcnt_t i_blocks;
unsigned short i_bytes;
umode_t i_mode;
spinlock_t i_lock;
struct mutex i_mutex;
struct rw_semaphore i_alloc_sem;
const struct inode_operations *i_op;
const struct file_operations *i_fop;
struct super_block *i_sb;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;//若是字符设备,对应的为cdev结构体
};
从第一幅图中可以看出,通过数据结构 struct inode{...}
中的 i_cdev
成员可以找到cdev
,而所有的字符设备都在 chrdevs
数组中。
下面先看一下 chrdevs
的定义:
#define CHRDEV_MAJOR_HASH_SIZE 255
static DEFINE_MUTEX(chrdevs_lock);
static struct char_device_struct {
struct char_device_struct *next; // 结构体指针
unsigned int major; // 主设备号
unsigned int baseminor; // 次设备起始号
int minorct; // 次备号个数
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; // 只能挂255个字符主设备
可以看到全局数组 chrdevs
包含了255(CHRDEV_MAJOR_HASH_SIZE
的值)个 struct char_device_struct
的元素,每一个对应一个相应的主设备号。
如果分配了一个设备号,就会创建一个 struct char_device_struct
的对象,并将其添加到 chrdevs
中;这样,通过chrdevs
数组,我们就可以知道分配了哪些设备号。
相关函数,(这些函数在上篇已经介绍过,现在回顾一下:
register_chrdev_region( ) //分配指定的设备号范围
alloc_chrdev_region( ) //动态分配设备范围
他们都主要是通过调用函数 __register_chrdev_region()
来实现的;要注意,这两个函数仅仅是注册设备号!如果要和cdev
关联起来,还要调用cdev_add()
。
register_chrdev( )//申请指定的设备号,并且将其注册到字符设备驱动模型中.
register_chrdev
所做的事情为:
(1)注册设备号,通过调用 __register_chrdev_region()
来实现
(2)分配一个cdev
, 通过调用cdev_alloc()
来实现
(3)将cdev
添加到驱动模型中, 这一步将设备号和驱动关联了起来. 通过调用 cdev_add()
来实现
(4)将第一步中创建的 struct char_device_struct
对象的 cdev
指向第二步中分配的cdev
. 由于register_chrdev()
是老的接口,这一步在新的接口中并不需要。
在Linux 字符设备驱动结构:cdev 结构体、设备号相关知识解析有解析
下面看一下上层应用open()
调用系统调用函数的过程
对于一个字符设备文件, 其inode->i_cdev
指向字符驱动对象cdev
, 如果i_cdev
为NULL
,则说明该设备文件没有被打开。
由于多个设备可以共用同一个驱动程序,所以,通过字符设备的inode
中的i_devices
和 cdev
中的list
组成一个链表
首先,系统调用open
打开一个字符设备的时候,通过一系列调用,最终会执行到 chrdev_open
int chrdev_open(struct inode * inode, struct file * filp)
chrdev_open()
所做的事情可以概括如下:
(1)根据设备号(inode->i_rdev
),在字符设备驱动模型中查找对应的驱动程序,这通过kobj_lookup()
来实现,kobj_lookup()
会返回对应驱动程序cdev
的kobject
.
(2)设置inode->i_cdev
,指向找到的cdev
.
(3)将inode
添加到cdev->list
的链表中.
(4)使用cdev
的ops
设置file
对象的f_op
(5)如果ops
中定义了open
方法,则调用该open
方法
(6)返回
执行完chrdev_open()
之后,file
对象的f_op
指向cdev
的ops
,因而之后对设备进行的read, write
等操作,就会执行cdev
的相应操作。