天天看點

手把手教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個檔案描述符。

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解
  • 程序級别的檔案描述符表files_struct:核心為每個程序維護一個檔案描述符表,該表記錄了檔案描述符的相關資訊,包括檔案描述符、指向打開檔案表中記錄的指針。
  • 系統級别的打開檔案表file:核心對所有打開檔案維護的一個程序共享的打開檔案描述表,表中存儲了處于打開狀态檔案的相關資訊,包括檔案類型、通路權限、檔案操作函數(file_operations)等。
  • 系統級别的 i-node 表:i-node 結構體記錄了檔案相關的資訊,包括檔案長度,檔案所在裝置,檔案實體位置,建立、修改和更新時間等,"ls -i" 指令可以檢視檔案 i-node 節點。

程序在Linux核心中是由結構體task_struct維護,程序打開的所有檔案描述符都在程序維護的結構體task_struct的files變量中維護:

//include\linux\sched.h

struct 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 數量

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

檢視所有程序已經打開的 fd 數量以及允許的最大數量

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

檢視單個程序允許打開的最大 fd 數量.

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

檢視某個檔案被哪些程序打開?

可以借助lsof指令

編寫調試代碼如下:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

該代碼功能是打開檔案test,然後休眠100秒,我們需要在這100秒内執行lsof操作。

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解
手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

&是程式放在背景運作,為了釋放終端,友善輸入下一個指令;

7284:程式程序ID;

lsof功能:檢視某個檔案被程序打開的詳細資訊。

檢視某個程序打開了哪些檔案?

接着上述的例子,ls -l /proc/{PID}/fd 可以檢視某個程序打開了哪些檔案。

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

可以看到該程序打開了除了test之外,還打開了前面所述的3個預設檔案,結構體對應關系如下:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

實際開發中,可能會遇到 fd 資源超過上限導緻的 "Too many open files" 之類的問題,一般都是因為沒有及時釋放掉 fd,若循環執行超過單個程序允許打開的最大 fd 數量,程式就會出現異常。

驅動程式就是向下控制硬體,向上提供接口,驅動向上提供的接口最終對應到應用層有三種方式:裝置檔案,/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;             //使用者ID
  kgid_t      i_gid;            //使用者組ID
  unsigned int    i_flags;      //檔案系統标志

#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; //最後access時間
  struct timespec    i_mtime; //最後modify時間
  struct timespec    i_ctime; //最後change時間
  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;//引用計數,當引用計數變為0時,會釋放inode執行個體
  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  
//建立裝置檔案的時候i_fops填充的是def_chr_fops,
//blk_blk_fops,def_fifo_fops,bad_sock_fops之一,
//參見建立過程中調用的init_special_inode()
  struct dquot    *i_dquot[MAXQUOTAS];
#endif
  struct list_head  i_devices;
  union {
  //特殊檔案類型的union,pipe,cdev,blk.link etc,
  //i_cdev表示這個inode屬于一個字元裝置檔案,
  //本文中建立裝置檔案的時候會把與之相關的裝置号的驅動對象cdev拿來填充
    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
 //inode的私有資料
  void      *i_private; /* fs or device private pointer */
};
           

重要的成員已經添加注釋。

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

關鍵成員定義如下:

-->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 0
mknod /dev/com1 c 250 1
           

執行結果如下:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

核心為了維護這兩個檔案節點,核心需要建立結構體維護這兩個檔案,具體如下圖所示:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

當我們通過指令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);
           

各個結構體之間關系入下圖所示:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

當應用程式執行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)=%d\n",minor);

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

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

當驅動可以提取次裝置号之後,我們就可以實作一份驅動支援多個同種類型的裝置。

在大多情況下,我們會建立一個自定義的裝置資訊維護結構體,同時建立一個指針數組用來管理不同的裝置。

#define MAX_COM_NUM 2

struct 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】

再來看下contianer_of 接口功能參數如下:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

該宏是如何實作的,留給讀者自己思考。

【補充2】

我們也可以在回調cdev.fops->open()階段重新填充file結構的fop,進而實作同一個驅動操作不同的裝置,這種思想就是核心驅動中常用的分層!

執行結果如下:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

由結果可知,應用程式正确讀取了minor的值。

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

從核心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)=%d\n",minor);

  cd = (struct mydev *)file->private_data;
  printk("read()       file->private_data         cd->test=%d\n",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 OK\n");

  read(fd0,&minor,sizeof(minor));
  printf("minor of /dev/com0 =%d\n",minor);
  close(fd0);
  
  fd1 = open("/dev/com1",O_RDWR);
  if(fd1<0)
  {
    perror("open fail \n");
    return;
  }
  printf("open /dev/com1 OK\n");

  read(fd1,&minor,sizeof(minor));
  printf("minor of /dev/com1 =%d\n",minor);
  close(fd1);
}
           

最後 附送一幅我總結的字元裝置的架構圖:

手把手教Linux驅動4-程式、檔案描述符、file、inode關系詳解

擷取更多關于Linux的資料,請關注公衆号「

一口Linux

歡迎關注公衆号:一口Linux

繼續閱讀