扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

Linux驱动-inode,file,file_operations关系

1小时前
145
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

本文目标:

文件描述符和设备节点之间关系;

三大结构体inode,file,file_operations之间的关系;

驱动如何区分出次设备号。

read/write接口函数要能够获得字符设备结构体变量地址。

什么是文件描述符?

Linux 中一切都可以看作文件,包括普通文件、链接文件、Socket 以及设备驱动等,对其进行相关操作时,都可能会创建对应的文件描述符。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符。

Linux启动后,会默认打开3个文件描述符,分别是:

0:标准输入standard input

1:正确输出standard output

2:错误输出:error output

以后打开文件后。新增文件绑定描述符 可以依次增加(从3开始累加)。每一条shell命令执行,都会继承父进程的文件描述符。因此,所有运行的shell命令,都会有默认3个文件描述符。

l  进程级别的文件描述符表:内核为每个进程维护一个文件描述符表,该表记录了文件描述符的相关信息,包括文件描述符、指向打开文件表中记录的指针。

l  系统级别的打开文件表:内核对所有打开文件维护的一个进程共享的打开文件描述表,表中存储了处于打开状态文件的相关信息,包括文件类型、访问权限、文件操作函数(file_operations)等。

l  系统级别的 i-node 表:i-node 结构体记录了文件相关的信息,包括文件长度,文件所在设备,文件物理位置,创建、修改和更新时间等,"ls -i" 命令可以查看文件 i-node 节点。

进程在Linux内核中是由结构体task_struct维护,进程打开的所有文件描述符都在进程维护的结构体task_struct的files变量中维护:

includelinuxsched.h

该结构体定义如下:

该进程所有打开的文件对应的file指针均由fd_array维护,文件描述符和数组下标一一对应。

文件描述符是一种系统资源,可以通过以下命令来查看文件描述符的上限。

打开文件相关操作

查看所有进程允许打开的最大 fd 数量

查看所有进程已经打开的 fd 数量以及允许的最大数量

查看单个进程允许打开的最大 fd 数量.

查看某个文件被哪些进程打开?

可以借助lsof命令

编写调试代码如下:

该代码只打开文件123.c。

&是程序放在后台运行。

3042:程序进程。

查看某个进程打开了哪些文件?

接着上述的例子,ls -l /proc/{PID}/fd 可以查看某个进程打开了哪些文件。

可以看到该进程打开了出了123.c之外,还打开了前面所述的3个默认文件。

 

实际开发中,可能会遇到 fd 资源超过上限导致的 "Too many open files" 之类的问题,一般都是因为没有及时释放掉 fd,若循环执行超过单个进程允许打开的最大 fd 数量,程序就会出现异常。

Linux设备文件三大结构:inode,file,file_operations

驱动程序就是向下控制硬件,向上提供接口,这里的向上提供的接口最终对应到应用层有三种方式:设备文件,/proc,/sys,其中最常用的就是使用设备文件,而Linux设备中用的最多的就是字符设备,本文就以字符设备为例来分析创建并打开一个字符设备的文件内部机制。

struct inode

Linux中一切皆文件,当我们在Linux中创建一个文件时,就会在相应的文件系统创建一个inode与之对应.

对于不同的文件类型,inode被填充的成员内容也会有所不同,以创建字符设备为例,我们知道,add_chrdev_region其实是把一个驱动对象和一个(一组)设备号联系到一起。而创建设备文件,其实是把设备文件设备号联系到一起。至此,这三者就被绑定在一起了。这样,内核就有能力创建一个struct inode实例了,下面是Linux 3.14内核中的inode。这个inode是VFS的inode,是最具体文件系统的inode的进一步封装,也是驱动开发中关心的inode,针对具体的文件系统,还有struct ext2_inode_info 等结构。

//include/linux/fs.h 596

 

/*

* Keep mostly read-only and often accessed (especially for

* the RCU path lookup and 'stat' data) fields at the beginning

* of the 'struct inode'

*/

struct inode {

umode_t            i_mode;

unsigned short        i_opflags;

kuid_t            i_uid;

kgid_t            i_gid;

unsigned int        i_flags;

dev_t            i_rdev;

#ifdef CONFIG_FS_POSIX_ACL

struct posix_acl    *i_acl;

struct posix_acl    *i_default_acl;

#endif

 

const struct inode_operations    *i_op;

struct super_block    *i_sb;

struct address_space    *i_mapping;

 

#ifdef CONFIG_SECURITY

void            *i_security;

#endif

 

/* Stat data, not accessed from path walking */

unsigned long        i_ino;

/*

* Filesystems may only read i_nlink directly.  They shall use the

* following functions for modification:

*

*    (set|clear|inc|drop)_nlink

*    inode_(inc|dec)_link_count

*/

union {

const unsigned int i_nlink;

unsigned int __i_nlink;

};

   dev_t            i_rdev;

loff_t            i_size;

struct timespec        i_atime;

struct timespec        i_mtime;

struct timespec        i_ctime;

spinlock_t        i_lock;    /* i_blocks, i_bytes, maybe i_size */

unsigned short          i_bytes;

unsigned int        i_blkbits;

blkcnt_t        i_blocks;

 

#ifdef __NEED_I_SIZE_ORDERED

seqcount_t        i_size_seqcount;

#endif

 

/* Misc */

unsigned long        i_state;

struct mutex        i_mutex;

 

unsigned long        dirtied_when;    /* jiffies of first dirtying */

 

struct hlist_node    i_hash;

struct list_head    i_wb_list;    /* backing dev IO list */

struct list_head    i_lru;        /* inode LRU list */

struct list_head    i_sb_list;

union {

struct hlist_head    i_dentry;

struct rcu_head        i_rcu;

};

u64            i_version;

atomic_t        i_count;

atomic_t        i_dio_count;

atomic_t        i_writecount;

const struct file_operations    *i_fop;    /* former ->i_op->default_file_ops */

struct file_lock    *i_flock;

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;

};

 

__u32            i_generation;

 

#ifdef CONFIG_FSNOTIFY

__u32            i_fsnotify_mask; /* all events this inode cares about */

struct hlist_head    i_fsnotify_marks;

#endif

 

#ifdef CONFIG_IMA

atomic_t        i_readcount; /* struct files open RO */

#endif

void            *i_private; /* fs or device private pointer */

};

 

这里面与本文相关的成员主要有:

struct inode

--602-->i_mode表示访问权限控制

--604-->UID

--605-->GID

--606-->i_flags文件系统标志

--630-->硬链接数计数

--635-->i_size以字节为单位的文件大小

--636-->最后access时间

--637-->最后modify时间

--638-->最后change时间

--669-->i_dentry; //目录项链表

--673-->i_count引用计数,当引用计数变为0时,会释放inode实例

--675-->i_writecount写者计数

--679-->创建设备文件的时候i_fops填充的是def_chr_fops,blk_blk_fops,def_fifo_fops,bad_sock_fops之一,参见创建过程中调用的init_special_inode()

--683-->特殊文件类型的union,pipe,cdev,blk.link etc,i_cdev表示这个inode属于一个字符设备文件,本文中创建设备文件的时候会把与之相关的设备号的驱动对象cdev拿来填充

--702-->inode的私有数据

 

struct file

Linux内核会为每一个进程维护一个文件描述符表,这个表其实就是struct file[]的索引。open()的过程其实就是根据传入的路径填充好一个file结构并将其赋值到数组中并返回其索引。下面是file的主要内容

struct file {

union {

struct llist_node    fu_llist;

struct rcu_head     fu_rcuhead;

} f_u;

struct path        f_path;

#define f_dentry    f_path.dentry

  struct inode        *f_inode;    /* cached value */

    const struct file_operations    *f_op;

 

/*

* Protects f_ep_links, f_flags.

* Must not be taken from IRQ context.

*/

spinlock_t        f_lock;

atomic_long_t        f_count;

    unsigned int         f_flags;

fmode_t            f_mode;

struct mutex        f_pos_lock;

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;

struct list_head    f_tfile_llink;

#endif /* #ifdef CONFIG_EPOLL */

struct address_space    *f_mapping;

#ifdef CONFIG_DEBUG_WRITECOUNT

unsigned long f_mnt_write_state;

#endif

} __attribute__((aligned(4)));    /* lest something weird decides that 2 is OK */

 

struct file

--882-->f_path里存储的是open传入的路径,VFS就是根据这个路径逐层找到相应的inode

--883-->f_inode里存储的是找到的inode

--884-->f_op里存储的就是驱动提供的file_operations对象,这个对象应该在第一次open()的时候被填充,具体地,应用层的open通过层层搜索会调用inode.i_fops->open(),即我们注册的open接口函数chrdev_open()

--891-->f_count的作用是记录对文件对象的引用计数,也即当前有多少个使用CLONE_FILES标志克隆的进程在使用该文件。典型的应用是在POSIX线程中。就像在内核中普通的引用计数模块一样,最后一个进程调用put_files_struct()来释放文件描述符。

--892-->f_flags当打开文件时指定的标志,对应系统调用open的int flags,比如驱动程序为了支持非阻塞型操作需要检查这个标志是否有O_NONBLOCK。

--893-->f_mode;对文件的读写模式,对应系统调用open的mod_t mode参数,比如O_RDWR。如果驱动程序需要这个值,可以直接读取这个字段。

--905-->private_data表示file结构的私有数据

 mknod 做了什么事?

本例假定我们创建两个串口com0、com1,他们公用同一个主设备号250,次设备号分别为0、1,他们公用同一个字符设备驱动,那么我们的驱动要能够根据应用进程打开的是设备com0还是com1来操作不同的串口。

首先创建两个设备节点:

mknod /dev/com0 250 0

mknod /dev/com1 250 1

执行结果如下:

内核为了维护这两个文件节点,内核需要创建结构体维护这两个文件,具体如下:

从以上结构图可以得知,当我们通过命令mknod 创建一个字符设备文件,那么内核就会创建好一个inode会存在存储器中,创建和该文件实体一一对应的inode。这个inode和其他的inode一样,用来存储关于这个文件的静态信息(不变的信息),包括这个设备文件对应的设备号,文件的路径以及对应的驱动对象等。

inode作为VFS四大对象之一,在驱动开发中很少需要自己进行填充,更多的是在open()方法中进行查看并根据需要填充我们的file结构。

由上图所示,创建字符设备 /dev/com0、 /dev/com1,只是增加了对应的inode节点,此时VFS层并没有并没有创建file结构体,而且inode和驱动也并没有产生联系。

如果获得次设备号

对于同种类型设备,比如多个串口、网口等,这些驱动比较类似,仅仅是一些寄存器基地址不一样,所以我们没有必须要为每一个设备单独写一个驱动,这些设备的驱动完全可以共用同一个驱动,我们只需要在驱动中区分出设备的次设备号,然后根据次设备号的访问不同的内存地址空间即可。

如果应用程序执行以下代码

fd0 = open("/dev/com0",O_RDWR);

fd1 = open("/dev/com1",O_RDWR);

当应用程序执行open函数,该函数会调用到内核的sys_open(),该函数会根据该设备节点inode保存的信息,i_flags:文件类型, i_rdev:设备号,初始化结构体inode其他信息,比如inode->i_cdev,此时已经指向我们注册的cdev结构体,。通过设备号,可以很容易找到该设备在设备号全局管理数组chedevs[]的下标,进而找到我们注册的驱动cdev以及file_operations。

同时内核会在VFS层为创建结构体file,该函数调用成功之后,应用层会返回整型值用来和该file对应,就是上图的文件描述符fd0、fd1。

其中:

file->f_dentry->d_inode->i_rdev  保存对应的设备节点的设备号,

file-> f_op保存我们注册的file_operations 字符设备接口函数集合。

由此可得在read和write等其他接口函数中,我们可以通过file来得到次设备号。

【注意】同一个文件如果打开了两次,那么第二次linux内核仍然会重新分配1个新的file结构体。

实现代码如下:

这样我们就可以根据次设备号来操作不同的硬件。

如何获得注册的设备结构体私有地址?

 在大多情况下,我们会创建一个自定义的设备信息维护结构体,同时创建一个指针数组用来管理不同的设备。

然后通过成员cdev注册字符设备,

想一个问题:如果我们为每一个同类型设备分配独立的设备结构体,分别注册对应的cdev,假如我打开/dev/com0 进行操作的时候,我怎么知道com0对应我们自己定义的设备管理结构体变量的地址呢?

有问题是好的,我们带着问题出发,看看大牛们是怎么做的。

该函数功能:

字符设备架构调用我们注册的接口函数open会传递参数inode和file,inode->i_cdev指向了我们注册的pmydev[i]->cdev,在open中通过inode->cdev来识别具体的设备,通过container_of来找到对应的pmycdev结构体变量,并将其私有数据隐藏到file结构的private_data中,进而识别同一个驱动操作一类设备

 

而read,write接口函数可以直接通过file的 private_data获取对应的pmycdev结构体变量。

【补充1】

再来看下contianer_of 接口功能参数如下:

该宏是如何实现的,留给读者自己思考。

【补充2】

我们也可以在回调cdev.fops->open()阶段重新填充file结构的fop,进而实现同一个驱动操作不同的设备,这种思想就是内核驱动中常用的分层!

执行结果如下:

由结果可知,应用程序正确读取了minor的值。

从内核log来看,MINOR(file->f_dentry->d_inode->i_rdev)可以成功读取此设备号。而read接口函数也成功通过file->private_data得到了设备结构体变量(初始化的时候为不同设备的test成员附了不同的值)。

 驱动程序:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
static int major = 250;
static int minor = 0;
static dev_t devno;

#define MAX_COM_NUM 2

struct mydev{
struct cdev cdev;
char *reg;
int test;
};
struct mydev *pmydev[MAX_COM_NUM];

ssize_t dev_fifo_read (struct file *file, char __user *buf, size_t size, loff_t *pos)
{
int minor = MINOR(file->f_dentry->d_inode->i_rdev);
struct mydev *cd;

printk("read() MINOR(file->f_dentry->d_inode->i_rdev)=%dn",minor);

cd = (struct mydev *)file->private_data;
printk("read()       file->private_data         cd->test=%dn",cd->test);

if(copy_to_user(buf, &minor, size)){
return -EFAULT;
}

return size;
}
int dev_fifo_close (struct inode *inode, struct file *file)
{
printk("dev_fifo_close()n");
return 0;
}
//打开设备
static int dev_fifo_open (struct inode *inode, struct file *file)
{
struct mydev *cd;

cd = container_of(inode->i_cdev, struct mydev, cdev);
file->private_data = cd;
return 0;
}
static struct file_operations dev_fifo_ops =
{
.open = dev_fifo_open,
.read = dev_fifo_read,
.release = dev_fifo_close,
};
static int dev_fifo_init(void)
{
int result;
int error;
int i = 0;

printk("dev_fifo_init n");
devno = MKDEV(major,minor);
result = register_chrdev_region(devno, MAX_COM_NUM, "test");
if(result<0)
{
printk("register_chrdev_region fail n");
goto ERR1;
}

for(i=0;i<MAX_COM_NUM;i++)
{
pmydev[i] =kmalloc(sizeof(struct mydev), GFP_KERNEL);
}

for(i=0;i<MAX_COM_NUM;i++)
{
pmydev[i]->test = i;

cdev_init(&pmydev[i]->cdev,&dev_fifo_ops);

devno = MKDEV(major,i);

error = cdev_add(&pmydev[i]->cdev,devno,1);
if(error < 0)
{
printk("cdev_add fail n");
goto ERR2;
}
}
return 0;
ERR2:
devno = MKDEV(major,0);
unregister_chrdev_region(devno,MAX_COM_NUM);
for(i=0;i<MAX_COM_NUM;i++)
{
kfree(pmydev[i]);
}
return error;
ERR1:
return result;
}
static void dev_fifo_exit(void)
{
int i;

printk("dev_fifo_exit n");

for(i=0;i<MAX_COM_NUM;i++)
{
cdev_del(&pmydev[i]->cdev);
}
for(i=0;i<MAX_COM_NUM;i++)
{
kfree(pmydev[i]);
}

devno = MKDEV(major,0);
unregister_chrdev_region(devno,MAX_COM_NUM);
return;
}
MODULE_LICENSE("GPL");
MODULE_AUTHOR("daniel.peng");
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);

测试程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
int fd0,fd1;
int minor;

fd0 = open("/dev/com0",O_RDWR);
if(fd0<0)
{
perror("open fail n");
return;
}
printf("open /dev/com0 OKn");

read(fd0,&minor,sizeof(minor));
printf("minor of /dev/com0 =%dn",minor);
close(fd0);

fd1 = open("/dev/com1",O_RDWR);
if(fd1<0)
{
perror("open fail n");
return;
}
printf("open /dev/com1 OKn");

read(fd1,&minor,sizeof(minor));
printf("minor of /dev/com1 =%dn",minor);

close(fd1);

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

公众号『一口Linux』号主彭老师,拥有15年嵌入式开发经验和培训经验。曾任职ZTE,某研究所,华清远见教学总监。拥有多篇网络协议相关专利和软件著作。精通计算机网络、Linux系统编程、ARM、Linux驱动、龙芯、物联网。原创内容基本从实际项目出发,保持原理+实践风格,适合Linux驱动新手入门和技术进阶。