手把手教Linux驱动4-进程、文件描述符、file、inode关系详解

  1. 什么是文件描述符?

  2. 进程打开文件相关信息管理

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

  4. mknod 做了什么事?

  5. 进程打开设备文件

  6. 驱动如何支持同类型设备?

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

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

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

0:标准输入 standard input

1:正确输出 standard output

2:错误输出 error output

这就是为什么我们在程序运行时可以直接打印信息和从命令终端获取信息的原因。

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

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

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

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

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

    //includelinuxsched.hstruct task_struct {      ……  /* open file information */      struct files_struct *files;      ……}

    该结构体定义如下:

    /* * Open file table structure */struct files_struct {  /*   * read mostly part   */    atomic_t count;    struct fdtable __rcu *fdt;    struct fdtable fdtab;  /*   * written part on a separate cache line in SMP   */    spinlock_t file_lock ____cacheline_aligned_in_smp;    int next_fd;    unsigned long close_on_exec_init[1];    unsigned long open_fds_init[1];    struct file __rcu * fd_array[NR_OPEN_DEFAULT];};

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

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

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

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

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

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

    可以借助lsof命令编写调试代码如下:

    该代码功能是打开文件test,然后休眠100秒,我们需要在这100秒内执行lsof操作。

    &是程序放在后台运行,为了释放终端,方便输入下一个命令;

    7284:程序进程ID;

    lsof功能:查看某个文件被进程打开的详细信息。

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

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

    可以看到该进程打开了除了test之外,还打开了前面所述的3个默认文件,结构体对应关系如下:

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

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


    重要的成员已经添加注释。

    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 */

    关键成员定义如下:

    -->f_path里存储的是open传入的路径,VFS就是根据这个路径逐层找到相应的inode
    -->f_inode里存储的是找到的inode
    -->f_op里存储的就是驱动提供的file_operations对象,这个对象应该在第一次open()的时候被填充,具体地,应用层的open通过层层搜索会调用inode.i_fops->open(),即我们注册的open接口函数chrdev_open()
    -->f_count的作用是记录对文件对象的引用计数,也即当前有多少个使用CLONE_FILES标志克隆的进程在使用该文件。典型的应用是在POSIX线程中。就像在内核中普通的引用计数模块一样,最后一个进程调用put_files_struct()来释放文件描述符。
    -->f_flags当打开文件时指定的标志,对应系统调用open的int flags,比如驱动程序为了支持非阻塞型操作需要检查这个标志是否有O_NONBLOCK。
    -->f_mode;对文件的读写模式,对应系统调用open的mod_t mode参数,比如O_RDWR。如果驱动程序需要这个值,可以直接读取这个字段。
    -->private_data表示file结构的私有数据


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

    首先创建两个设备节点:

    mknod /dev/com0 c 250 0mknod /dev/com1 c 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结构体和文件描述符。

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

    根据上一届内容,驱动的read、write可以通过以下方式获得设备 :

    file->f_dentry->d_inode->i_rdev

    这样我们就可以通过宏MINOR来提取此设备 。

    实现代码如下:

    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;}

    当驱动可以提取次设备 之后,我们就可以实现一份驱动支持多个同种类型的设备。

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

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

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

    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;    }  }

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

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

    //打开设备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;}

    该函数功能:

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

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

    cd = (struct mydev *)file->private_data;

    【补充1】

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

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

    从内核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 2struct 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);}

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2020年8月10日
    下一篇 2020年8月10日

    相关推荐