linux字符设备文件的打开操作
2.7 字符设备文件的打开操作(1)
作为例子,这里假定前面对应于/dev/demodev设备节点的驱动程序在自己的代码里实现了如下的struct file_operations对象fops:
- static struct file_operations fops = {
- .open = demoopen,
- .read = demoread,
- .write = demowrite,
- .ioctl = demoioctl,
- };
用户空间open函数的原型为:
- int open(const char *filename, int flags, mode_t mode);
这个函数如果成功,将返回一个文件描述符,否则返回-1。函数的第一个参数filename表示要打开的文件名,第二个参数flags用于指定文件的打开或者创建模式,本书在后续"字符设备的高级操作"一章中会讨论其中一些常见取值对驱动程序的影响,最后一个参数mode只在创建一个新文件时才使用,用于指定新建文件的访问权限,比如可读、可写及可执行等权限。
位于内核空间的驱动程序中open函数的原型为:
- <include/linux/fs.h>
- struct file_operations {
- …
- int (*open) (struct inode *, struct file *);
- …
- };
两者相比差异很大。接下来我们将描述从用户态的open是如何一步一步调用到驱动程序提供的open函数(在我们的例子中,它的具体实现是demoopen)的。如同设备文件节点的生成一样,透彻了解这里的每一个步骤也需要掌握全面的Linux下文件系统的技术细节。从设备驱动程序员的角度,我们依然将重点放在两者如何建立联系的关键点上。
用户程序调用open函数返回的文件描述符,本文用fd表示,这是个int型的变量,会被用户程序后续的read、write和ioctl等函数所使用。同时可以看到,在驱动程序中的demodev_read、demodev_write和demodev_ioctl等函数其第一个参数都是struct file *filp。显然内核需要在打开设备文件时为fd与filp建立某种联系,其次是为filp与驱动程序中的fops建立关联。
用户空间程序调用open函数,将发起一个系统调用,通过sys_open函数进入内核空间,其中一系列关键的函数调用关系如图2-8所示:
图2-8 sys_open到chrdev_open调用流程 |
do_sys_open函数首先通过get_unused_fd_flags为本次的open操作分配一个未使用过的文件描述符fd :
- <fs/open.c>
- long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
- {
- …
- fd = get_unused_fd_flags(flags);
- …
- }
get_unused_fd_flags实际上是封装了alloc_fd的一个宏,真正分配fd的操作发生在alloc_fd函数中,后者会涉及大量文件系统方面的细节,这不是本书的主题。读者这里只需知道alloc_fd将会为本次的open操作分配一个新的fd。
do_sys_open随后调用do_filp_open函数,后者会首先查找"/dev/demodev"设备文件所对应的inode。在Linux文件系统中,每个文件都有一个inode与之对应。从文件名查找对应的inode这一过程,同样会涉及大量文件系统方面的细节。
do_filp_open在成功查找到"/dev/demodev"设备文件对应的inode之后,接着会调用函数get_empty_filp,后者会为每个打开的文件分配一个新的struct file类型的内存空间(本书将把指向该结构体对象的内存指针简写为filp):
- <fs/namei.c>
- struct file *do_filp_open(int dfd, const char *pathname,
- const struct open_flags *op, int flags)
- {
- struct nameidata nd;
- struct file *filp;
- filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
- …
- return filp;
- }
内核用struct file对象来描述进程打开的每一个文件的视图,即使是打开同一文件,内核也会为之生成一个新的struct file对象,用来表示当前操作的文件的相关信息,其定义为:
- <include/linux/fs.h>
- struct file {
- union {
- struct list_head fu_list;
- struct rcu_head fu_rcuhead;
- } f_u;
- struct path f_path;
- #define f_dentry f_path.dentry
- #define f_vfsmnt f_path.mnt
- const struct file_operations *f_op;
- spinlock_t f_lock;
- atomic_long_t f_count;
- unsigned int f_flags;
- fmode_t f_mode;
- loff_t f_pos;
- struct fown_struct f_owner;
- const struct cred *f_cred;
- struct file_ra_state f_ra;
- u64 f_version;
- #ifdef CONFIG_SECURITY
- void *f_security;
- #endif
- /* needed for tty driver, and maybe others */
- void *private_data;
- #ifdef CONFIG_EPOLL
- /* Used by fs/eventpoll.c to link all the hooks to this file */
- struct list_head f_ep_links;
- #endif /* #ifdef CONFIG_EPOLL */
- struct address_space *f_mapping;
- };
2.7 字符设备文件的打开操作(2)
这个结构中与设备驱动程序关系最密切的是f_op、f_flags、f_count和private_data成员。f_op指针的类型是struct file_operations,恰好我们的字符设备驱动程序中也需要实现一个该类型的对象,马上我们将看到这两者之间是如何建立联系的。f_flags用于记录当前文件被open时所指定的打开模式,这个成员将会影响后续的read/write等函数的行为模式。成员f_count用于对struct file对象的使用计数,当close一个文件时,只有struct file对象中f_count成员为0才真正执行关闭操作。private_data常被用来记录设备驱动程序自身定义的数据,因为filp指针会在驱动程序实现的file_operations对象其他成员函数之间传递,所以可以通过filp中的private_data成员在某一个特定文件视图的基础上共享数据。
进程为文件操作维护一个文件描述符表(current->files->fdt),正如在本节开始部分看到的那样,对设备文件的打开,最终会得到一个文件描述符fd,然后用该描述符fd作为进程维护的文件描述符表(指向struct file *类型数组)的索引值,将之前新分配的struct file空间地址赋值给它:
- current->files->fdt->pfd[fd] = filp;
这样,用户空间程序在后续的read、write、ioctl等函数调用中利用fd就可以找到对应的filp,如图2-9所示:
(点击查看大图)图2-9 fd与filp的关联 |
在do_sys_open的后半部分,会调用__dentry_open函数将"/dev/demodev"对应节点的inode中的i_fop赋值给filp->f_op,然后调用i_fop中的open函数:
- <fs/open.c>
- static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,
- struct file *f,
- int (*open)(struct inode *, struct file *),
- const struct cred *cred)
- {
- struct inode *inode;
- …
- f->f_op = fops_get(inode->i_fop);
- …
- if (!open && f->f_op)
- open = f->f_op->open;
- if (open) {
- error = open(inode, f);
- …
- }
- …
- }
__dentry_open函数当初在nameidata_to_filp中被调用时,第四个实参是NULL,所以在__dentry_open中,open = f->f_op->open。在上节设备文件节点的生成中,我们知道inode->i_fop = &def_chr_fops,这样filp->f_op = &def_chr_fops。接下来会利用filp中的这个新的f_op作调用:filp->f_op->open(inode, filp),于是chrdev_open函数将被调用到。该函数非常重要,为了突出其主线,下面先将它改写成以下简单几行:
- <fs/char_dev.c>
- static int chrdev_open(struct inode *inode, struct file *filp)
- {
- int ret = 0, idx;
- struct kobject *kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
- struct cdev *new = container_of(kobj, struct cdev, kobj);
- inode->i_cdev = new;
- list_add(&inode->i_devices, &new->list);
- filp->f_op = new->ops;
- if (filp->f_op->open) {
- ret = filp->f_op->open(inode,filp);
- }
- return ret;
- }
函数首先通过kobj_lookup在cdev_map中用inode->i_rdev来查找设备号所对应的设备new,这里展示了设备号的作用。成功查找到设备后,通过filp->f_op = new->ops这行代码将设备对象new中的ops指针(前面曾讨论过,驱动程序通过调用cdev_init将其实现的file_operations对象的指针赋值给设备对象cdev的ops成员)赋值给filp对象中的f_op成员,此处展示了如何将驱动程序中实现的struct file_operations与filp关联起来,从此图2-9中的filp->f_op将指向驱动程序中实现的struct file_operations对象。
接下来函数会检查驱动程序中是否实现了open函数(if (filp->f_op->open)),如果实现了,就调用设备驱动程序中实现的open函数。打开一个字符设备节点的大体流程如图2-10所示:
图2-10 开一个字符设备节点的功能流程 |
2.7 字符设备文件的打开操作(3)
图中,当应用程序打开一个设备文件时,将通过系统调用sys_open进入内核空间。在内核空间将主要由do_sys_open函数负责发起整个设备文件打开操作,它首先要获得该设备文件所对应的inode,然后调用其中的i_fop函数,对字符设备节点的inode而言,i_fop函数就是chrdev_open(图中标号1的线段),后者通过inode中的i_rdev成员在cdev_map中查找该设备文件所对应的设备对象cdev(图中标号2的线段),在成功找到了该设备对象之后,将inode的i_cdev成员指向该字符设备对象(图中标号3的线段),这样下次再对该设备文件节点进行打开操作时,就可以直接通过i_cdev成员得到设备节点所对应的字符设备对象,而无须再通过cdev_map进行查找。内核在每次打开一个设备文件时,都会产生一个整型的文件描述符fd和一个新的struct file对象filp来跟踪对该文件的这一次操作,在打开设备文件时,内核会将filp和fd关联起来,同时会将cdev中的ops赋值给filp->f_op(图中标号4的线段)。最后,sys_open系统调用将设备文件描述符fd返回到用户空间,如此在用户空间对后续的文件操作read、write和ioctl等函数的调用,将会通过该fd获得文件所对应的filp,根据filp中的f_op就可以调用到该文件所对应的设备驱动上实现的函数。
通过以上过程,我们看到了设备号在其中的重要作用。当设备驱动程序通过cdev_add把一个字符设备对象加入到系统时,需要一个设备号来标记该对象在cdev_map中的位置信息。当我们在用户空间通过mknod来生成一个设备文件节点时,也需要在命令行中提供设备号的信息,内核会将该设备号信息记录到设备文件节点所对应inode的i_rdev成员中。当我们的应用程序打开一个设备文件时,系统将会根据设备文件对应的inode->i_rdev信息在cdev_map中寻找设备。所以在这个过程中务必要保证设备文件节点的inode->i_rdev数据和设备驱动程序使用的设备号完全一致,否则就会发生严重问题。对应到现实世界的操作,那就是在用mknod生成设备节点时所提供的设备号信息一定要与设备驱动程序中分配使用的设备号一致。
在上述open一个设备文件的基础上,接下来不妨看看它的相反操作close。有了前面对open操作技术细节讨论所打下的良好基础,现在理解起close并不困难,在此读者也正好可以看看用户空间open函数返回的文件描述符fd如何被close等函数使用。
用户空间close函数的原型为:
- int close(unsigned int fd);
针对close的系统调用函数为sys_close,这里将其核心代码重新整理如下:
- <fs/open.c>
- int sys_close(unsigned int fd)
- {
- struct file * filp;
- struct files_struct *files = current->files;
- struct fdtable *fdt;
- int retval;
- …
- fdt = files_fdtable(files);
- …
- filp = fdt->fd[fd];
- …
- retval = filp_close(filp, files);
- …
- return retval;
- }
从fd得到filp这段代码,请读者参考本章2-9。接下来调用filp_close函数,close函数的大部分秘密都隐藏在其中,有必要看看其主要代码片段:
- <fs/open.c>
- int filp_close(struct file *filp, fl_owner_t id)
- {
- int retval = 0;
- if (!file_count(filp)) {
- printk(KERN_ERR "VFS: Close: file count is 0\n");
- return 0;
- }
- if (filp->f_op && filp->f_op->flush)
- retval = filp->f_op->flush(filp, id);
- …
- fput(filp);
- return retval;
- }
if (!file_count(filp))用来判断filp中的f_count成员是否为0,如果针对同一个设备文件close的次数多于open次数,就会出现这种情况,此时函数直接返回0,因为实质性的工作都被前面的close做完了。接下来的情况有点意思,如果设备驱动程序定义了flush函数,那么在release函数被调用前,会首先调用flush,这是为了确保在把文件关闭前缓存在系统中的数据被真正写回到硬件中。字符设备很少会出现这种情况,因为这种设备的慢速I/O特性决定了它无须使用这种缓冲机制来提升系统性能,但是块设备就不一样了,比如SCSI硬盘会和系统进行大量数据的传输,为此内核为块设备驱动程序设计了高速缓存机制,这种情况下为了保证文件数据的完整性,必须在文件关闭前将高速缓存中的数据写回到磁盘中。不过这是后话了,块设备驱动程序的这种机制将在"块设备驱动程序"一章中讨论。
函数的最后调用fput,貌似很简单的一个函数,其实内涵却很丰富:
- <fs/file_table.c>
- void fput(struct file *file)
- {
- if (atomic_long_dec_and_test(&file->f_count))
- __fput(file);
- }
函数中的那个atomic_long_dec_and_test是个体系架构相关的原子测试操作,就是说,如果file->f_count的值为1,那么它将返回true,这意味着可以真正关闭当前的文件了,所以__fput将被调用,并最终完成文件关闭的任务,它的一些关键调用节点如下所示:
- <fs/file_table.c>
- static void __fput(struct file *file)
- {
- …
- if (unlikely(file->f_flags & FASYNC)) {
- if (file->f_op && file->f_op->fasync)
- file->f_op->fasync(-1, file, 0);
- }
- if (file->f_op && file->f_op->release)
- file->f_op->release(inode, file);
- …
- fops_put(file->f_op);
- file_free(file);
- }
注意上面的FASYNC标志位,在本书后面的章节会讨论到file_operations中的一些常用的函数实现。然后函数调用到了设备驱动程序中提供的release函数,接下来是一些系统资源的释放。可见,对于应用程序的一个close调用,并非必然对应着release函数的调用,只有在当前文件的所有副本都关闭之后,release函数才会被调用。
2.8 本章小结
本章描述了字符设备驱动程序内核框架的技术细节。基本上可以看到,字符设备驱动内核框架的展开是按照两条线进行的:一条是设备与系统的关系,一个字符设备对象cdev通过cdev_add加入到系统中(由cdev_map所管理的哈希链表),此时设备号作为哈希索引值;另一条是设备与文件系统的关系,设备通过设备号以设备文件的形式向用户空间宣示其存在。这两条线间的联系通过文件系统接口去打开一个字符设备文件而建立:
mknod命令将为字符设备创建一个设备节点,mknod的系统调用将会为此设备节点产生一个inode,mknod命令行中给出的设备号将被记录到inode->i_rdev中,同时inode的i_fop会将open成员指向chrdev_open函数。
当用户空间open一个设备文件时,open函数通过系统进入内核空间。在内核空间,首先找到该设备节点所对应的inode,然后调用inode->i_fop->open(),我们知道这将导致chrdev_open函数被调用。同时,open的系统调用还将产生一个(fd, filp)二元组来标识本次的文件打开操作,这个二元组是一一对应的关系。
chrdev_open通过inode->i_rdev在cdev_map中查找inode对应的字符设备,cdev_map中记录着所有通过cdev_add加入系统的字符设备。
当在cdev_map中成功查找到该字符设备时,chrdev_open将inode->i_cdev指向找到的字符设备对象,同时将cdev->ops赋值给filp->f_op。
字符设备驱动程序负责实现struct file_operations对象,在字符设备对象初始化时cdev_init函数负责将字符设备对象cdev->ops指向该file_operations对象。
用户空间对字符设备的后续操作,比如read、write和ioctl等,将通过open函数返回的fd找到对应的filp,然后调用filp->f_op中实现的各类字符设备操作函数。
以上就是内核为字符设备驱动程序设计的大体框架,从中可以看到设备号在沟通用户空间的设备文件与内核中的设备对象之间所起的重要作用。
另外,对于字符设备驱动程序本身而言,核心的工作是实现struct file_operations对象中的各类函数,file_operations结构中虽然定义了众多的函数指针,但是现实中设备驱动程序并不需要为它的每一个函数指针都提供相应的实现。本书后面的"字符设备的高级操作"一章会详细讨论其中一些重要函数的作用和实现原理。
linux字符设备文件的打开操作的更多相关文章
- 浅析Linux字符设备驱动程序内核机制
前段时间在学习linux设备驱动的时候,看了陈学松著的<深入Linux设备驱动程序内核机制>一书. 说实话.这是一本非常好的书,作者不但给出了在设备驱动程序开发过程中的所须要的知识点(如对 ...
- LINUX中块设备文件和字符设备文件的本质区别
在LINUX系统文件类型分类的文章中我们提到了 块设备 和 字符设备文件,那么什么是块设备 字符设备文件呢?他们之间有什么本质上的区别呢? 设备文件分为 Block(块) Device Driver ...
- linux中c表示字符设备文件符号
linux中c表示字符设备文件,b表示块设备文件,l表示符号链接文件,r表示可读权限,w表示可写权限.linux文件属性解读:文件类型:-:普通文件 (f)d:目录文件b:块设备文件 (block)c ...
- Linux字符设备驱动基本结构
1.Linux字符设备驱动的基本结构 Linux系统下具有三种设备,分别是字符设备.块设备和网络设备,Linux下的字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中某一数据,读取数据 ...
- 深入理解Linux字符设备驱动
文章从上层应用访问字符设备驱动开始,一步步地深入分析Linux字符设备的软件层次.组成框架和交互.如何编写驱动.设备文件的创建和mdev原理,对Linux字符设备驱动有全面的讲解.本文整合之前发表的& ...
- Linux字符设备驱动结构(一)--cdev结构体、设备号相关知识机械【转】
本文转载自:http://blog.csdn.net/zqixiao_09/article/details/50839042 一.字符设备基础知识 1.设备驱动分类 linux系统将设备分为3类:字符 ...
- Linux 字符设备驱动模型
一.使用字符设备驱动程序 1. 编译/安装驱动 在Linux系统中,驱动程序通常采用内核模块的程序结构来进行编码.因此,编译/安装一个驱动程序,其实质就是编译/安装一个内核模块 2. 创建设备文件 通 ...
- ARM Linux字符设备驱动程序
1.主设备号和次设备号(二者一起为设备号): 一个字符设备或块设备都有一个主设备号和一个次设备号.主设备号用来标识与设备文件相连的驱动程序,用来反 映设备类型.次设备号被驱动程序用来辨别操作的是哪个 ...
- Linux字符设备中的两个重要结构体(file、inode)
对于Linux系统中,一般字符设备和驱动之间的函数调用关系如下图所示 上图描述了用户空间应用程序通过系统调用来调用程序的过程.一般而言在驱动程序的设计中,会关系 struct file 和 struc ...
随机推荐
- [洛谷P4346][CERC2015]ASCII Addition
题目大意:给一个像素的$a+b$,每个数字为$7\times5$的像素,每两个数字之间有间隔 题解:乱搞读入 卡点:无 C++ Code: #include <cstdio> #inclu ...
- [hdu6435]Problem J. CSGO
题目大意:给定$n$个$A$类元素和$m$个$B$类元素,每类元素有值$S$和$k$个值$x_0,x_1,\dots,x_{k-1}(k\leqslant 5)$. 要求选出一个$A$类元素$a$和$ ...
- [学习笔记]Senparc.CO2NET 缓存使用笔记
>笔记1:如果需要调用远程的Redis,那么您需要2步 步骤1: 在项目的web.config文件中配置 <!-- Cache.Redis连接配置 --> <add key=& ...
- Tomcat学习笔记(三)
Tomcat连接器 tomcat连接器是tomcat的一个核心组件,在tomcat4中的实现原理如下 1.实现Connector接口 2.创建Request对象 3.创建Response对象 tomc ...
- Access-Control-Allow-Origin设置多个域名
Access-Control-Allow-Origin只能返回一个. 所以用以下方法实现多个白名单域名:创建一个数据,获取请求中origin,如果在数组里,就返回该origin,如果不在,就返回一个默 ...
- webapp的favicon应该怎样组织代码
处理过那么多index 页面了那么关于特别针对于此页的favicon是时候详细的总结一下了 它是网站的头像,它出现在浏览器的收藏夹中(标题的旁边) ,浏览器标签页的左上角,微信公众号的logo,保存网 ...
- Sqlite 修改字段的名称。
Sqlite 不支持直接修改字段的名称. 我们可以使用别的方法来实现修改字段名. 1.修改原表的名称 ALTER TABLE table RENAME TO tableOld; 2.新建修改字段后的表 ...
- PHP-MYSQL时间
Unix 时间戳 Unix timestamp ('1970-01-01 00:00:00' GMT 之后的秒数) MySQL: FROM_UNIXTIME() 给定一个Unix 时间戳 (可以是 ...
- 【linux高级程序设计】(第十四章)TCP高级应用 2
socket多路复用应用 int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct time ...
- error while loading shared libraries:libmysqlclient.so.18 错误
error while loading shared libraries:libmysqlclient.so.18错误 新手安装php的时候如果出现这种问题,解决办法很简单,就是查看你的mysql安装 ...