字元裝置驅動核心框架小結(一)
最近一直在學習驅動相關知識,但是如果說會用或者簡單的會寫的話我還不是很滿足,我還是想知道它底層是如何實現的。那閒話少說,最近也看了很多大牛的經驗總結,然後也就談談自己的看法吧~
首先還是先列個大綱:
1、VFS如何實現;
2、字元裝置驅動框架;
一、VFS(虛擬檔案系統)
在學習這方面的知識之前先介紹下幾個關鍵的結構體,inode、file、dentry、file_struct。
1)inode
inode 是 UNIX/Linux 作業系統中的一種資料結構,其本質是結構體,它包含了與檔案系統中各個檔案相關的一些重要資訊,例如檔案及目錄的基本資訊,包含時間、檔名、使用者及群組等
我們可以將inode簡單理解成一個指標,它永遠指向本檔案的具體儲存位置。檔案屬性儲存在索引結點裡,在訪問檔案時,索引結點被複制到記憶體中,從而實現檔案的快速訪問。系統是通過索引節點(而不是檔名)來定位每一個檔案。
所以說建立一個檔案就會有一個inode結構體。比如mknod建立一個裝置節點的時候。
inode內容:
Block 是記錄檔案內容資料的區域,至於 inode 則是記錄"該檔案的相關屬性,以及檔案內容放置在哪一個 Block 之內
- 該檔案的擁有者與群組(owner/group);
- 該檔案的存取模式(read/write/excute);
- 該檔案的型別(type);
- 該檔案建立或狀態改變的時間(ctime)、最近一次的讀取時間(atime)、最近修改的時間(mtime);
- 該檔案的容量;
- 定義檔案特性的旗標(flag),如 SetUID...;
- 該檔案真正內容的指向 (pointer);
inode結構體很大這裡僅給出幾個重要成員
struct inode
{
//........
unsigned long i_ino; //節點號
atomic_t i_count; //引用計數
//........
uid_t i_uid; //使用者id
gid_t i_gid; //使用者id組
//........
struct timespec i_atime; //最後訪問時間
struct timespec i_mtime; //最後修改(modify)時間
struct timespec i_ctime; //最後改變(change)時間
unsigned long i_blocks; //檔案的塊數
unsigned short i_bytes; //使用的位元組數
unsigned long i_state; //狀態標誌
struct list_head i_devices; //塊裝置連結串列
unsigned char i_sock; //是否套接字
atomic_t i_writecount; //寫者計數
//........
};
關於inode與data area:
(inode也只是個抽象,data area才是表示磁碟上的真實資料。)
a. linux FS 可以簡單分成 inode table與data area兩部份。inode table上有許多的inode, 每個inode分別記錄一個檔案的屬性與這個檔案分佈在哪些data block上
b. inode table中紅色區域即inode size,是128Byte,在liunx系統上通過命令我們可以看到,系統就是這麼定義的,Inode size是指分配給一個inode來記錄文件屬性的磁碟塊的大小。
c. data ares中紫色的區域block size,就是我們一般概念上的磁碟塊。這塊區域是我們用來存放資料的地方。
d. 還有一個邏輯上的概念:FS中每分配2048 byte給data area, 就分配一個inode。但一個inode就並不一定就用掉2048 byte, 也不是說files allocation的最小單位是2048 byte, 它僅僅是代表filesystem中inode table/data area分配空間的比例是128/2048,也就是1/16。
e. inode引數是可以通過mkfs.ext3命令改變
注意:
i-node 是標記檔案中資料塊儲存位置的指標,一個I-NODE中能標記的資料塊的個數是固定的,當檔案很小時,一個檔案對應一個I-node ,當檔案比較大時一個檔案對應多個I-NODE,一個inode對應一個檔案,但檔案可以有多個inode。是1:n的關係。
2)dentry結構
dentry是Linux檔案系統中某個索引節點(inode)的連結,每個dentry代表路徑中的一個特定部分。這個索引節點可以是檔案,也可以是目錄。inode(可理解為ext2 inode)對應於物理磁碟上的具體物件,dentry是一個記憶體實體,沒有對應的磁碟資料結構,VFS根據字串形式的路徑名現場建立它,在dentry中,d_inode成員指向對應的inode。也就是說,一個inode可以在執行的時候連結多個dentry,而d_count記錄了這個連結的數量。另外dentry物件有三種狀態:被使用,未被使用和負狀態。
內容:
struct dentry {
atomic_t d_count; //目錄項物件使用計數器,可以有未使用態,使用態和負狀態
unsigned int d_flags; //目錄項標誌
struct inode *d_inode; //與檔名關聯的索引節點
struct dentry *d_parent; //父目錄的目錄項物件
struct list_head d_hash; //散列表表項的指標
struct list_head d_lru; //未使用連結串列的指標
struct list_head d_child; //父目錄中目錄項物件的連結串列的指標
struct list_head d_subdirs; //對目錄而言,表示子目錄目錄項物件的連結串列
struct list_head d_alias; //相關索引節點(別名)的連結串列
int d_mounted; //對於安裝點而言,表示被安裝檔案系統根項
struct qstr d_name; //檔名
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op; //目錄項方法
struct super_block *d_sb; //檔案的超級塊物件
vunsigned long d_vfs_flags;
void *d_fsdata; //與檔案系統相關的資料
unsigned char d_iname [DNAME_INLINE_LEN]; //存放短檔名
};
下面給出進一步的解釋
一個有效的dentry結構必定有一個inode結構,這是因為一個目錄項要麼代表著一個檔案,要麼代表著一個目錄,而目錄實際上也是檔案。所以,只要dentry結構是有效的,則其指標d_inode必定指向一個inode結構。可是,反過來則不然,一個inode卻可能對應著不止一個dentry結構;也就是說,一個檔案可以有不止一個檔名或路徑名。這是因為一個已經建立的檔案可以被連線(link)到其他檔名。所以在inode結構中有一個佇列i_dentry,凡是代表著同一個檔案的所有目錄項都通過其dentry結構中的d_alias域掛入相應inode結構中的i_dentry佇列。
在核心中有一個雜湊表dentry_hashtable ,是一個list_head的指標陣列。一旦在記憶體中建立起一個目錄節點的dentry 結構,該dentry結構就通過其d_hash域鏈入雜湊表中的某個佇列中。
核心中還有一個佇列dentry_unused,凡是已經沒有使用者(count域為0)使用的dentry結構就通過其d_lru域掛入這個佇列。
Dentry結構中除了d_alias 、d_hash、d_lru三個佇列外,還有d_vfsmnt、d_child及d_subdir三個佇列。其中d_vfsmnt僅在該dentry為一個安裝點時才使用。另外,當該目錄節點有父目錄時,則其dentry結構就通過d_child掛入其父節點的d_subdirs佇列中,同時又通過指標d_parent指向其父目錄的dentry結構,而它自己各個子目錄的dentry結構則掛在其d_subdirs域指向的佇列中。
從上面的敘述可以看出,一個檔案系統中所有目錄項結構或組織為一個雜湊表,或組織為一顆樹,或按照某種需要組織為一個連結串列,這將為檔案訪問和檔案路徑搜尋奠定下良好的基礎。
3)file結構體
檔案物件表示程序已開啟的檔案,該物件file(不是物理檔案)由相應的open()系統呼叫建立,有close()系統呼叫銷燬,因為多個程序可以同時開啟和操作一個檔案,所以同一個檔案也可能存在多個對應的檔案物件。檔案物件僅僅在程序觀點上代表已開啟檔案,它反過來指向目錄項物件(反過來指向索引節點),其實只有目錄項物件才表示已開啟的實際檔案,雖然一個檔案對應的檔案物件不是是唯一的,但對應的索引節點和目錄項物件無疑是唯一的,另外類似於目錄項物件,檔案物件實際上沒有對應的磁碟資料。
一個檔案物件是由一個檔案結構體表示的,檔案結構體代表一個開啟的檔案,系統中的每個開啟的檔案在核心空間都有一個關聯的 struct file。它由核心在開啟檔案時建立,並傳遞給在檔案上進行操作的任何函式。在檔案的所有例項都關閉後,核心釋放這個資料結構。
內容:
struct file {
union {
struct list_head fu_list; //檔案物件連結串列指標linux/include/linux/list.h
struct rcu_head fu_rcuhead; //RCU(Read-Copy Update)是Linux 2.6核心中新的鎖機制
} f_u;
struct path f_path; //包含dentry和mnt兩個成員,用於確定檔案路徑
#define f_dentry f_path.dentry //f_path的成員之一,當前檔案的dentry結構
#define f_vfsmnt f_path.mnt //表示當前檔案所在檔案系統的掛載根目錄
const struct file_operations *f_op; //與該檔案相關聯的操作函式
atomic_t f_count; //檔案的引用計數(有多少程序開啟該檔案)
unsigned int f_flags; //對應於open時指定的flag
mode_t f_mode; //讀寫模式:open的mod_t mode引數
off_t f_pos; //該檔案在當前程序中的檔案偏移量
struct fown_struct f_owner; //該結構的作用是通過訊號進行I/O時間通知的資料。
unsigned int f_uid, f_gid; //檔案所有者id,所有者組id
struct file_ra_state f_ra; //在linux/include/linux/fs.h中定義,檔案預讀相關
unsigned long 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; 檔案的事件輪詢等待者連結串列的頭,
spinlock_t f_ep_lock; f_ep_lock是保護f_ep_links連結串列的自旋鎖。
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping; 檔案地址空間的指標
};
4)file——struct
每個程序用一個files_struct結構來記錄檔案描述符的使用情況,這個files_struct結構稱為使用者開啟檔案表,它是程序的私有資料,
struct files_struct {
atomic_t count; /* 共享該表的程序數 */
rwlock_t file_lock; /* 保護該結構體的鎖*/
int max_fds; /*當前檔案物件的最大數*/
int max_fdset; /*當前檔案描述符的最大數*/
int next_fd; /*已分配的檔案描述符加1*/
struct file ** fd; /* 指向檔案物件指標陣列的指標 */
fd_set *close_on_exec; /*指向執行exec()時需要關閉的檔案描述符*/
fd_set *open_fds; /*指向開啟檔案描述符的指標*/
fd_set close_on_exec_init; /* 執行exec()時關閉的初始檔案*/
fd_set open_fds_init; /*檔案描述符的初值集合*/
struct file * fd_array[32]; /* 檔案物件指標的初始化陣列*/
};
下面給出一部分解釋
fd域指向檔案物件的指標陣列。該陣列的長度存放在max_fds域中。通常,fd域指向files_struct結構的fd_array域,該域包括32個檔案物件指標。如果程序開啟的檔案數目多於32,核心就分配一個新的、更大的檔案指標陣列,並將其地址存放在fd域中;核心同時也更新max_fds域的值。
對於在fd陣列中有入口地址的每個檔案來說,陣列的索引就是檔案描述符(file descriptor)。通常,陣列的第一個元素(索引為0)是程序的標準輸入檔案,陣列的第二個元素(索引為1)是程序的標準輸出檔案,陣列的第三個元素(索引為2)是程序的標準錯誤檔案。請注意,藉助於dup()、dup2()和 fcntl ) 系統呼叫,兩個檔案描述符就可以指向同一個開啟的檔案,也就是說,陣列的兩個元素可能指向同一個檔案物件。當用戶使用shell結構(如2>&1)將標準錯誤檔案重定向到標準輸出檔案上時,使用者總能看到這一點。
open_fds域包含open_fds_init域的地址,open_fds_init域表示當前已開啟檔案的檔案描述符的點陣圖。max_fdset域存放點陣圖中的位數。由於資料結構fd_set有1024位,通常不需要擴大點陣圖的大小。不過,如果確實必須的話,核心仍能動態增加點陣圖的大小,這非常類似檔案物件的陣列的情形。
當開始使用一個檔案物件時呼叫核心提供的fget()函式。這個函式接收檔案描述符fd作為引數,返回在current->files->fd[fd]中的地址,即對應檔案物件的地址,如果沒有任何檔案與fd對應,則返回NULL。在第一種情況下,fget()使檔案物件引用計數器f_count的值增1。
-----------------------------
使用者開啟檔案表files_structs是由程序描述符task_struct中的file域指向,所有與程序相關的資訊如開啟的檔案及檔案描述符都包含其中,又從前面可以看出,files_struct通過**file保持對檔案物件file的訪問,於此相似檔案物件file的結構體內成員中包含目錄項dentry,目錄項dentry將檔名與inode相連,最終通過inode中的指標可以訪問儲存實際的資料的地方Data Area.
so他們管理層次關係關係如下:程序->task_struct->files_struct->file->dentry->inode->Data Area
如下圖(圖片太大,可以儲存了,然後放大檢視):
5.硬連結與軟連結
1)硬連結
由於linux下的檔案是通過索引節點(inode)來識別檔案,硬連結可以認為是一個指標,指向原檔案inode的指標,系統並不為它重新分配inode和建立檔案;即硬連結檔案和原檔案其實是同一個檔案,只是名字不同。每新增一個硬連結,檔案inode的連結數就加1;刪除一個硬連結,inode的連結數減1,檔案內容依然存在,直到inode的連結數為0,才刪除inode對應的檔案。
特點:硬連結只能引用同一檔案系統中的檔案。它引用的是檔案在檔案系統中的物理索引(也稱為inode)。當移動或刪除原始檔案時,硬連結不會被破壞,因為它所引用的是檔案的物理資料而不是檔案在檔案結構中的位置。硬連結的檔案不需要使用者有訪問原始檔案的許可權,也不會顯示原始檔案的位置,這樣有助於檔案的安全。如果刪除的檔案有相應的硬連結,那麼這個檔案依然會保留,直到所有對它的引用都被刪除。
可以用ln命令來建立硬連結。語法:
ln [options] existingfile newfile ln [options] existingfile-list directory
用法:
- 第一種:為 existingfile 建立硬連結,檔名為 newfile 。
- 第二種:在 directory 目錄中,為 existingfile-list 中包含的所有檔案建立一個同名的硬連結。
常用選項[options]
- -f 無論 newfile 存在與否,都建立連結。
- -n 如果 newfile 已存在,就不建立連結。
硬連結的不足:
- 不可以在不同檔案系統的檔案間建立連結
- 只有超級使用者才可以為目錄建立硬連結
2)軟連結
軟連結也叫符號連結,它是指向另一個檔案的特殊檔案,這種檔案的資料部分僅包含它所要連結檔案的路徑名。軟連結是為了克服硬連結的不足而引入的,軟連結不直接使用inode號作為檔案指標,而是使用檔案路徑名作為指標(軟連結:檔名+ 資料部分–>目標檔案的路徑名)。軟連結有自己的inode,並在磁碟上有一小片空間存放路徑名。因此,軟連結能夠跨檔案系統,也可以和目錄連結!其二,軟連結可以對一個不存在的檔名進行連結,但直到這個名字對應的檔案被建立後,才能開啟其連結。
軟連結克服了硬連結的不足,沒有任何檔案系統的限制,任何使用者可以建立指向目錄的符號連結。因而現在更為廣泛使用,它具有更大的靈活性,甚至可以跨越不同機器、不同網路對檔案進行連結,如同Windows下的快捷方式。
可以用:ln -s 命令來建立軟連結:
ln -s existingfile newfile ln -s existingfile-list directory
3).兩者的區別
軟連結與硬連結的區別不僅僅是在概念上,在實現上也是不同的,整理如下:
- 對於硬連結,原檔案和硬連結檔案公用一個inode號,這說明他們是同一個檔案,而對於軟連結,原檔案和軟連結檔案擁有不同的inode號,表明他們是兩個不同的檔案;
- 在檔案屬性上軟連結明確寫出了是連結檔案,而硬連結沒有寫出來,因為在本質上硬連結檔案和原檔案是完全平等關係;
- 連結數目是不一樣的,軟連結的連結數目不會增加;
- 檔案大小是不一樣的,硬連結檔案顯示的大小是跟原檔案是一樣的,因為是等同的,而這裡軟連結顯示的大小與原檔案就不同了,file1大小是48B,而file1soft是5B,這裡面的5實際上就是“file1”的大小。
- 在同一檔案系統下,可以建立軟連結或硬連結(同文件系統不同目錄下也可以)。
總之,建立軟連結就是建立了一個新檔案。當訪問連結檔案時,系統就會發現他是個連結檔案,它讀取連結檔案找到真正要訪問的檔案。
當然軟連結也有硬連結沒有的缺點,因為連結檔案包含有原檔案的路徑資訊,所以當原檔案從一個目錄下移到其他目錄中,再訪問連結檔案,系統就找不到了,而硬連結就沒有這個缺陷,想怎麼移就怎麼移;還有它要系統分配額外的空間用於建立新的索引節點和儲存原檔案的路徑。
------------------------------------------------------------------------------------------------------
二、字元裝置框架
首先通過上面的介紹我們已經知道,我們寫的驅動程式只是一個程式,然後將底層硬體驅動抽象出來用一個結構體來表示cdev。那麼我們需要做的就是從使用者空間寫的open、write、read等系統呼叫來操作硬體,這其中要經過核心,而核心的實現就是利用了一切皆檔案的思想,就是上層直接操作檔案但實質底層就是在操作硬體,這個轉換就是VFS。大概過程就是說open(“/dev/first_drv”,..)會進入核心->建立一個file結構體->該結構體指向->inode->通過主裝置號查詢到cdev結構(即是驅動)->找到cdev指向的操作函式指標->找到.open->自己的open函式。
下面詳細說明底層如何實現:
相關資料結構:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
struct kobj_map {
struct probe {
struct probe *next;
dev_t dev;
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data;
} *probes[255];
struct mutex *lock;
};
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct file_operations *fops;
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
#define CHRDEV_MAJOR_HASH_SIZE 255
下面本文通過一下三個方面以及他們的關聯來描述字元裝置驅動:
1. 字元驅動模型
2. 字元裝置的裝置號
3. 檔案系統中對字元裝置檔案的訪問
1. 字元驅動模型
每個字元驅動由一個 cdev 結構來表示.
在裝置驅動模型(device driver model)中, 使用 (kobject mapping domain) 來記錄字元裝置驅動.
這是由 struct kobj_map 結構來表示的. 它內嵌了255個struct probe指標陣列
kobj_map由全域性變數 cdev_map 引用: static struct kobj_map *cdev_map;
相關函式說明:
cdev_alloc() 用來建立一個cdev的物件
cdev_add() 用來將cdev物件新增到驅動模型中,其主要是通過kobj_map()來實現的.
kobj_map() 會建立一個probe物件,然後將其插入cdev_map中的某一項中,並關聯probe->data 指向 cdev
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
根據裝置號,在cdev_map中查詢其cdev物件內嵌的kobject. (probe->data->kobj),返回的是cdev的kobject
2. 字元裝置的裝置號
字元裝置的主,次裝置號的分配:
全域性陣列 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( ) 申請指定的裝置號,並且將其註冊到字元裝置驅動模型中.
它所做的事情為:
1. 註冊裝置號, 通過呼叫 __register_chrdev_region() 來實現
2. 分配一個cdev, 通過呼叫 cdev_alloc() 來實現
3. 將cdev新增到驅動模型中, 這一步將裝置號和驅動關聯了起來. 通過呼叫 cdev_add() 來實現
4. 將第一步中建立的 struct char_device_struct 物件的 cdev 指向第二步中分配的cdev. 由於register_chrdev()是老的介面,這一步在新的介面中並不需要.
3. 檔案系統中對字元裝置檔案的訪問
對於一個字元裝置檔案, 其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的相應操作.