linux驱动开发学习笔记之字符设备的个人理解
设备驱动分为三种:字符设备驱动、块设备驱动和网络设备驱动
(1) 字符设备驱动:设备对数据的处理是按照字节流的形式进行的,可以支持随机访问,也可以不支持随机访问,典型的字符设备有串口,键盘、帧缓存设备等。
(2) 块设备驱动:设备对数据的处理是按照若干个块进行的,一个块有其固定的大小,比如4096个字节。典型的块设备有硬盘、光盘、SD卡等。
(3) 网络设备驱动:专门针对网络设备的一类驱动,其主要作用是进行网络数据的收发。
字符设备
设备文件一般都会位于/dev目录下,设备文件和普通文件有很多相似之处,都有对应的权限,所属的用户和组等,当比普通文件多了两个数字,分别为主设备号和次设备号。
我们如果手动创建可以通过mknod这个命令手动创建一个设备文件。
mknod 就是make node的缩写,就是创建节点的意思,在linux系统中,一个节点代表一个文件,创建一个文件最重要的就是创建分配一个新的节点(注意:这里是存在于磁盘上的节点,不是内存中的inode节点)。它会被分配一个节点号,节点号是唯一的,所以可以用来判断不同的文件。
mknod命令会将文件名、文件类型和主、从设备号等信息保存到磁盘上。
手动:mknod +文件名 +类型 +主设备号 +次设备号
如: mknod /dev/hello c 250 0
自动:需要在函数里面用到两个函数,一个是class_create创建类,另一个是device_create创建设备节点。
主设备号占12位,此设备号占20位,一共32位

上面函数获取设备号,第一个参数为主设备号,第二个参数为次设备号,操作主要是通过移位操作。
设备会挂载在/dev下,查找主设备号可以用 cat /proc/devices
以下是打开一个文件时一层一层所调用的函数,也就是应用层如何和驱动层联系
其实简单的讲就是应用层使用open的时候是调用到驱动层里面的file_operations里面的open函数指针,而里面的open函数指针则根据不同的设备文件做出不同的open接口出来。具体步骤则是下面的步骤
一. Linux中cdev,inode,file之间的关系
struct cdev---描述一个字符设备对象信息的(设备号+文件操作对象),任何一个字符设备驱动都有该对象
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; //file_operations文件操作接口集合
struct list_head list;
dev_t dev; //设备号
unsigned int count;
};
struct inode---描述文件系统中的某个文件的属性(文件的权限,类型,uid,gid,修改时间)
struct inode {
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct cdev *i_cdev; //里面有设备号
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
};
struct file---描述进程中打开的一个文件的信息:文件名,标志(可读可写),文件偏移
struct file {
struct path f_path;
void *private_data;//私有数据
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
const struct file_operations *f_op;
}
他们之间就是这样的一种关系:
struct task_struct
|
struct files_struct *files;
|
struct file __rcu * fd_array[NR_OPEN_DEFAULT]; //fd就是该数组的下标
|
struct path f_path;
|
struct dentry *dentry;
|
struct inode * d_inode
|
struct cdev *i_cdev; //里面有设备号
在内核中,我们一个进程用一个task_struct结构体对象表示,

。。。。。。。。。。。。。中间还有很多东西,这个进程结构体很庞大

而我们结构体里面包含叫files_struct的结构体

这个结构体里面有个叫fd_array的指针数组,它里面的每一个元素都是一个指向file结构的指针,





。。。。。。。。。也很庞大这个结构体

里面有cdev对象

这个结构体里面的ops里存放这我们所谓的f_op方法,也就是open、read、write啊之类的函数。
kobj就是父类,lilt就是链表,dev是设备号
而我们应用层就像我们之前学过的函数申请fd文件描述符,那这个文件描述符究竟时如何获取到呢?
这时候其实是
1.应用层调用open函数,
2.然后应用层通过系统调用内核中对应的函数sys_open,
3.sys_open调用do_sys_open,
4.在do_sys_open中首先调用了getname函数将文件名从用户空间复制到内核空间,
5.接着调用get_unused_fd_flags来获取一个未使用的文件描述符,其实也就是搜索我们上面的file_struct结构体中的fd_array数组,看看究竟哪个元素还没有使用,并把下标返回回去。
6.接着就是知道下标了,然后调用do_filp_open创建一个file结构,并初始化里面所有的成员,最重要的还是上面所说结构体里面的f_op成员

file_operations结构体存的绝大多数都是函数指针

7.执行do_filp_open成功后,调用fd_install函数,这个函数将刚刚得到的文件描述符fd作为访问fd_array数组的下标,让下标对应的元素指向新构造的file结构中。
8.最后返回到应用层,把刚刚的数组下标作为打开文件的文件描述符fd返回。
以上红色字体就是在应用层中如何获得fd的步骤。下面是概括的流程图

其实上面的应用层和glibc层都还只是我们的用户空间,也是进程空间,因为我们自己在应用层写的函数会去调用动态库里面的函数,当我们一旦去调用到如open某一个函数时,一定会切换到内核中去,切换也叫陷入到内核空间,它是通过一个汇编指令swi,叫做软中断指令,它其实是一个专门产生异常的指令。在我们linux当中,如果我们调用open、read、write等函数,我们把这些函数通通成为系统调用,说的简单点就是内核给用户空间提供的一组接口。就是这些函数调用的时候都会产生软中断指令,内核会为这些函数进行编号,假设现在open的编号是1,那么我们调用open的时候,会在软中断指令后加入编号,如swi 1。我们这边一产生异常,内核就会处理异常,他会去区分不同的异常
其实do_filp_open这个函数的作用很大,包含的内容也很多,其实就像我们之前的应用层调用的open函数,里面第一个参数是不是文件路径,第二个参数是权限,第三个参数是创建文件时的权限,如open(“/dev/vers”,O_RDWR|O_CREAT,0666).而do_filp_open这个函数就是对路径、参数、权限等进行判断并执行操作。以下简单跟读该函数代码。
先调用path_openat函数,进行实际打开操作

再调用get_empty_filp函数,得到一个file结构,其实也是对这个file里的成员进行初始化,上面有说过了。


接着调用link_path_walk函数,处理文件路径中除去最后一个分量的前面部分,如:/dev/vser0, link_path_walk需要处理/dev这部分。

接下来调用do_last来处理最后一个分量






这个函数会先根据open_flag中的标志位来判断该文件是否需要被创建,如果不需要,则会进行最后的indoe节点查找,如果此时查找成功,这直接调用path_to_nameidata()函数通过如下语句设置nd->path.dentry的值.
若没有查找成功并且flag中有没有将O_CREAT标志为置位。那么,此时通过将error设置成-ENOTDIR并向用户态返回NODIR的出错提示
但是,如果用户在使用open函数时,使用了create选项的话,那么我们只好进行下面的操作了:创建新的inode节点。这项工作是通过__open_namei_create()函数完成的。在该函数内部,会调用VFS层的vfs_create()函数将控制下发到不同的文件系统处理函数中。它是通过这样的调用过成完成的:
我们在驱动层时,我们会用到一个函数register_chrev,他是一个申请设备号的函数,我们可以使用它进行动态或者静态的申请,他还有一个register_chrdev_region,就是静态的申请设备号,还有一个是alloc_chrdev_region,是动态申请设备号,其实也是调用到上面静态的函数,这个不多说。
register_chrev其实里面也调用了register_chrdev_region,并且帮们把cdev结构对象一条龙服务的申请并初始化好了,我们register_chrdev_region和alloc_chrdev_region都是只是创建设备号并没有创建cdev对象,

cdev对象初始化后,会使用cdev_add函数,把cdev添加到cdev_map散列表中

cdev_add函数的主要工作是将主设备号通过对255取余,将余数作为cdev_map数组的下标索引,然后构造prode对象,并通过data指向要添加的cdev结构地址,然后添加到链表中。


这里的参数p只指向一个cdev对象,但该对象可以同时管理多个设备,由count来决定具体有多少个设备,那么cdev和设备就不是一一对应的关系。
也就是说我们如果想让一个驱动支持多个设备时,我们可以有两种方法:
- 为每个设备分配一个cdev对象,每次调用cdev_add时添加一个对象,直到多个cdev对象全部加载到内核中。
- 只构造一个cdev对象,但在调用cdev_add函数时,指定添加的cdev可以管理多个设备。


上面所讲的都是register_chrdev下创建cdev的过程,并且把file_operation传给cdev对象。。

如果我们使用的只是register_chrdev_region和alloc_chrdev_region的话,就只是单单注册设备号,所以我们自己还得像上面一样创建cdev对象,就得如下操作。

上面的都是静态分配cdev对象,我们也可以用下面cdev_alloc函数获取cdev对象,成功返回动态分配的cdev对象地址,失败则返回NULL。

当然不管哪一种,我们创建cdev对象后,在模块退出时,自然就应该删除该cdev对象,使用的是cdev_del函数。




他其实就是根据cdev找到散列表中的prode,并进行删除
在字符驱动层中,设备号、cdev和file_operations操作方法至关重要,我们应用层在创建一个文件的时候都会在文件系统内生成一个inode节点,inode节点内有主、次设备号,内核通过文件路径找到对应的inode后,和驱动进行连接,首先我们是根据inode中的设备号找到cdev的,然后通过cdev找到关联的file_operations操作方法集合,然后把cdev的file_operations给到file的file_operations中,从而完成调用驱动所提供的操作方法来完成对设备的具体操作。
虽然打开的步骤很繁琐,但是当我们拿到open的操作方法后,是不是意味着我们知道了他的文件描述符fd,然后当我们调用read等函数时就直接用这个文件描述符作为参数传递给内核,内核得到这个文件描述符后直接索引fd_array,找到对应的file结构,然后调用相应的operations方法。
上面说到的通过inode节点获得主、次设备号是通过两个函数可以获得的,如下
iminor函数和imajor函数

所以当我们有inode节点时是不是可以拿到主、次设备号,那是不是可以通过设备号遍历cdev链表,从而找到对应的cdev,然后找到operations,内核就是这么做的。
第二个上面说了,只要通过open,其他的比如read、write等就可以直接通过file找到相对应的operations,我们通过这个file_operations可以知道里面的参数都有一个file结构体,那它如果是相同的我们是不是就可以直接知道open对应的file结构体就可以调用其他的operations函数,我们也可以通过这个file里面的一个私有数据void* private_data;(万能指针)在不同的函数里传送数据啊,比如write函数拿到open函数里的数据,不过注意的一点是,传递的数据不能是临时变量,因为一退出就没有了,除非加上static。

下面的vfs_write刚好验证了我们上面的猜想


浙公网安备 33010602011771号