1. 程式人生 > >Linux 字元裝置驅動結構(三)—— file、inode結構體及chardevs陣列等相關知識解析

Linux 字元裝置驅動結構(三)—— file、inode結構體及chardevs陣列等相關知識解析

       先看下面這張圖,這是Linux 中虛擬檔案系統、一般的裝置檔案與裝置驅動程式值間的函式呼叫關係;

        上面這張圖展現了一個應用程式呼叫字元裝置驅動的過程, 在裝置驅動程式的設計中,一般而言,會關心 file 和 inode 這兩個結構體

        使用者空間使用 open() 函式開啟一個字元裝置 fd = open("/dev/hello",O_RDWR) , 這一函式會呼叫兩個資料結構 struct inode{...}struct file{...} ,二者均在虛擬檔案系統VFS處,下面對兩個資料結構進行解析:

一、file 檔案結構體

在裝置驅動中,這也是個非常重要的資料結構,必須要注意一點,這裡的file與使用者空間程式中的FILE指標是不同的,使用者空間FILE是定義在C庫中,從來不會出現在核心中。而struct file,卻是核心當中的資料結構

,因此,它也不會出現在使用者層程式中。

       file結構體指示一個已經開啟的檔案(裝置對應於裝置檔案),其實系統中的每個開啟的檔案在核心空間都有一個相應的struct file結構體,它由核心在開啟檔案時建立,並傳遞給在檔案上進行操作的任何函式,直至檔案被關閉。如果檔案被關閉,核心就會釋放相應的資料結構。

     在核心原始碼中,struct file要麼表示為file,或者為filp(意指“file pointer”), 注意區分一點,file指的是struct file本身,而filp是指向這個結構體的指標。

下面是幾個重要成員:

a -- fmode_t f_mode;

      此檔案模式通過FMODE_READ, FMODE_WRITE識別了檔案為可讀的,可寫的,或者是二者。在open或ioctl函式中可能需要檢查此域以確認檔案的讀/寫許可權,你不必直接去檢測讀或寫許可權,因為在進行octl等操作時核心本身就需要對其許可權進行檢測。

 b -- loff_t f_pos;

     當前讀寫檔案的位置。為64位。如果想知道當前檔案當前位置在哪,驅動可以讀取這個值而不會改變其位置。對read,write來說,當其接收到一個loff_t型指標作為其最後一個引數時,他們的讀寫操作便作更新檔案的位置,而不需要直接執行filp ->f_pos操作。而llseek方法的目的就是用於改變檔案的位置。

c -- unsigned int f_flags;

     檔案標誌,如O_RDONLY, O_NONBLOCK以及O_SYNC。在驅動中還可以檢查O_NONBLOCK標誌檢視是否有非阻塞請求。其它的標誌較少使用。特別地注意的是,讀寫許可權的檢查是使用f_mode而不是f_flog。所有的標量定義在標頭檔案中

d -- struct file_operations *f_op;

    與檔案相關的各種操作。當檔案需要迅速進行各種操作時,核心分配這個指標作為它實現檔案開啟,讀,寫等功能的一部分。filp->f_op 其值從未被核心儲存作為下次的引用,即你可以改變與檔案相關的各種操作,這種方式效率非常高。

e -- void *private_data;

      在驅動呼叫open方法之前,open系統呼叫設定此指標為NULL值。你可以很自由的將其做為你自己需要的一些資料域或者不管它,如,你可以將其指向一個分配好的資料,但是你必須記得在file struct被核心銷燬之前在release方法中釋放這些資料的記憶體空間。private_data用於在系統呼叫期間儲存各種狀態資訊是非常有用的。

二、 inode結構體

         VFS inode 包含檔案訪問許可權、屬主、組、大小、生成時間、訪問時間、最後修改時間等資訊。它是Linux 管理檔案系統的最基本單位,也是檔案系統連線任何子目錄、檔案的橋樑。

        核心使用inode結構體在核心內部表示一個檔案。因此,它與表示一個已經開啟的檔案描述符的結構體(即file 檔案結構)是不同的,我們可以使用多個file 檔案結構表示同一個檔案的多個檔案描述符,但此時,所有的這些file檔案結構全部都必須只能指向一個inode結構體

      inode結構體包含了一大堆檔案相關的資訊,但是就針對驅動程式碼來說,我們只要關心其中的兩個域即可:

(1) dev_t i_rdev;

      表示裝置檔案的結點,這個域實際上包含了裝置號

(2) struct cdev *i_cdev;

      struct cdev是核心的一個內部結構,它是用來表示字元裝置的,當inode結點指向一個字元裝置檔案時,此域為一個指向inode結構的指標。

下面是原始碼:

struct inode {
 struct hlist_node i_hash;
 struct list_head i_list;
 struct list_head i_sb_list;
 struct list_head i_dentry;
 unsigned long  i_ino;
 atomic_t  i_count;
 unsigned int  i_nlink;
 uid_t   i_uid;//inode擁有者id
 gid_t   i_gid;//inode所屬群組id
 dev_t   i_rdev;//若是裝置檔案,表示記錄裝置的裝置號
 u64   i_version;
 loff_t   i_size;//inode所代表大少
#ifdef __NEED_I_SIZE_ORDERED
 seqcount_t  i_size_seqcount;
#endif
 struct timespec  i_atime;//inode最近一次的存取時間
 struct timespec  i_mtime;//inode最近一次修改時間
 struct timespec  i_ctime;//inode的生成時間
 unsigned int  i_blkbits;
 blkcnt_t  i_blocks;
 unsigned short          i_bytes;
 umode_t   i_mode;
 spinlock_t  i_lock; 
 struct mutex  i_mutex;
 struct rw_semaphore i_alloc_sem;
 const struct inode_operations *i_op;
 const struct file_operations *i_fop; 
 struct super_block *i_sb;
 struct file_lock *i_flock;
 struct address_space *i_mapping;
 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;//若是字元裝置,對應的為cdev結構體
 };


三、chardevs 陣列

     從圖中可以看出,通過資料結構 struct inode{...} 中的 i_cdev 成員可以找到cdev,而所有的字元裝置都在 chrdevs 陣列中

下面先看一下 chrdevs 的定義:

#define CHRDEV_MAJOR_HASH_SIZE 255
static DEFINE_MUTEX(chrdevs_lock);

static struct char_device_struct {
	struct char_device_struct *next; // 結構體指標
	unsigned int major;              // 主裝置號
	unsigned int baseminor;          // 次裝置起始號
	int minorct;                     // 次備號個數
	char name[64];
	struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];      // 只能掛255個字元主裝置<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">  </span>

       可以看到全域性陣列 chrdevs 包含了255(CHRDEV_MAJOR_HASH_SIZE 的值)個 struct char_device_struct的元素,每一個對應一個相應的主裝置號。

       如果分配了一個裝置號,就會建立一個 struct char_device_struct 的物件,並將其新增到 chrdevs 中;這樣,通過chrdevs陣列,我們就可以知道分配了哪些裝置號。


相關函式,(這些函式在上篇已經介紹過,現在回顧一下:

register_chrdev_region( ) 分配指定的裝置號範圍

alloc_chrdev_region( ) 動態分配裝置範圍

他們都主要是通過呼叫函式 __register_chrdev_region() 來實現的;要注意,這兩個函式僅僅是註冊裝置號!如果要和cdev關聯起來,還要呼叫cdev_add()。

register_chrdev( )申請指定的裝置號,並且將其註冊到字元裝置驅動模型中.

  它所做的事情為:

a -- 註冊裝置號, 通過呼叫 __register_chrdev_region() 來實現

b -- 分配一個cdev, 通過呼叫 cdev_alloc() 來實現

c -- 將cdev新增到驅動模型中, 這一步將裝置號和驅動關聯了起來. 通過呼叫 cdev_add() 來實現

d -- 將第一步中建立的 struct char_device_struct 物件的 cdev 指向第二步中分配的cdev. 由於register_chrdev()是老的介面,這一步在新的介面中並不需要。

四、cdev 結構體

五、檔案系統中對字元裝置檔案的訪問

        下面看一下上層應用open() 呼叫系統呼叫函式的過程

        對於一個字元裝置檔案, 其inode->i_cdev 指向字元驅動物件cdev, 如果i_cdev為 NULL ,則說明該裝置檔案沒有被開啟.

  由於多個裝置可以共用同一個驅動程式.所以,通過字元裝置的inode 中的i_devices 和 cdev中的list組成一個連結串列


        首先,系統呼叫open開啟一個字元裝置的時候, 通過一系列呼叫,最終會執行到 chrdev_open

  (最終是通過呼叫到def_chr_fops中的.open, 而def_chr_fops.open = chrdev_open. 這一系列的呼叫過程,本文暫不討論)

  int chrdev_open(struct inode * inode, struct file * filp)

chrdev_open()所做的事情可以概括如下:

  1. 根據裝置號(inode->i_rdev), 在字元裝置驅動模型中查詢對應的驅動程式, 這通過kobj_lookup() 來實現, kobj_lookup()會返回對應驅動程式cdev的kobject.

  2. 設定inode->i_cdev , 指向找到的cdev.

  3. 將inode新增到cdev->list 的連結串列中.

  4. 使用cdev的ops 設定file物件的f_op

  5. 如果ops中定義了open方法,則呼叫該open方法

  6. 返回

執行完 chrdev_open()之後,file物件的f_op指向cdev的ops,因而之後對裝置進行的read, write等操作,就會執行cdev的相應操作。