linux核心驅動重要的資料結構
阿新 • • 發佈:2018-12-22
檔案操作
迄今為止, 我們已經保留了一些裝置編號給我們使用, 但是我們還沒有連線任何我們裝置操作到這些編號上. file_operation 結構是一個字元驅動如何建立這個連線. 這個結構, 定義在 , 是一個函式指標的集合. 每個開啟檔案(內部用一個 file 結構來代表, 稍後我們會檢視)與它自身的函式集合相關連( 通過包含一個稱為 f_op 的成員, 它指向一個 file_operations 結構). 這些操作大部分負責實現系統呼叫, 因此, 命名為 open, read, 等等. 我們可以認為檔案是一個"物件"並且其上的函式操作稱為它的"方法", 使用面向物件程式設計的術語來表示一個物件宣告的用來操作物件的動作. 這是我們在 Linux 核心中看到的第一個面向物件程式設計的現象,在後面章節會看到更多。
按照慣例,file_operations結構或者指向這類結構的指標稱為fops。
struct module *owner 第一個 file_operations 成員根本不是一個操作; 它是一個指向擁有這個結構的模組的指標. 這個成員用來在它的操作還在被使用時阻止模組被解除安裝. 幾乎所有時間中, 它被簡單初始化為 THIS_MODULE, 一個在 中定義的巨集. loff_t (*llseek) (struct file *, loff_t, int); llseek 方法用作改變檔案中的當前讀/寫位置, 並且新位置作為(正的)返回值. loff_t 引數是一個"long offset", 並且就算在 32位平臺上也至少 64 位寬. 錯誤由一個負返回值指示. 如果這個函式指標是 NULL, seek 呼叫會以潛在地無法預知的方式修改 file 結構中的位置計數器( 在"file 結構" 一節中描述). ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 用來從裝置中獲取資料. 在這個位置的一個空指標導致 read 系統呼叫以 -EINVAL("Invalid argument") 失敗. 一個非負返回值代表了成功讀取的位元組數( 返回值是一個 "signed size" 型別, 常常是目標平臺本地的整數型別). ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t); 初始化一個非同步讀 -- 可能在函式返回前不結束的讀操作. 如果這個方法是 NULL, 所有的操作會由 read 代替進行(同步地). ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 傳送資料給裝置. 如果 NULL, -EINVAL 返回給呼叫 write 系統呼叫的程式. 如果非負, 返回值代表成功寫的位元組數. ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *); 初始化裝置上的一個非同步寫. int (*readdir) (struct file *, void *, filldir_t); 對於裝置檔案這個成員應當為 NULL; 它用來讀取目錄, 並且僅對檔案系統有用. unsigned int (*poll) (struct file *, struct poll_table_struct *); poll 方法是 3 個系統呼叫的後端: poll, epoll, 和 select, 都用作查詢對一個或多個檔案描述符的讀或寫是否會阻塞. poll 方法應當返回一個位掩碼指示是否非阻塞的讀或寫是可能的, 並且, 可能地, 提供給核心資訊用來使呼叫程序睡眠直到 I/O 變為可能. 如果一個驅動的 poll 方法為 NULL, 裝置假定為不阻塞地可讀可寫. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); ioctl 系統呼叫提供了發出裝置特定命令的方法(例如格式化軟盤的一個磁軌, 這不是讀也不是寫). 另外, 幾個 ioctl 命令被核心識別而不必引用 fops 表. 如果裝置不提供 ioctl 方法, 對於任何未事先定義的請求(-ENOTTY, "裝置無這樣的 ioctl"), 系統呼叫返回一個錯誤. int (*mmap) (struct file *, struct vm_area_struct *); mmap 用來請求將裝置記憶體對映到程序的地址空間. 如果這個方法是 NULL, mmap 系統呼叫返回 -ENODEV. int (*open) (struct inode *, struct file *); 儘管這常常是對裝置檔案進行的第一個操作, 不要求驅動宣告一個對應的方法. 如果這個項是 NULL, 裝置開啟一直成功, 但是你的驅動不會得到通知. int (*flush) (struct file *); flush 操作在程序關閉它的裝置檔案描述符的拷貝時呼叫; 它應當執行(並且等待)裝置的任何未完成的操作. 這個必須不要和使用者查詢請求的 fsync 操作混淆了. 當前, flush 在很少驅動中使用; SCSI 磁帶驅動使用它, 例如, 為確保所有寫的資料在裝置關閉前寫到磁帶上. 如果 flush 為 NULL, 核心簡單地忽略使用者應用程式的請求. int (*release) (struct inode *, struct file *); 在檔案結構被釋放時引用這個操作. 如同 open, release 可以為 NULL. int (*fsync) (struct file *, struct dentry *, int); 這個方法是 fsync 系統呼叫的後端, 使用者呼叫來重新整理任何掛著的資料. 如果這個指標是 NULL, 系統呼叫返回 -EINVAL. int (*aio_fsync)(struct kiocb *, int); 這是 fsync 方法的非同步版本. int (*fasync) (int, struct file *, int); 這個操作用來通知裝置它的 FASYNC 標誌的改變. 非同步通知是一個高階的主題, 在第 6 章中描述. 這個成員可以是NULL 如果驅動不支援非同步通知. int (*lock) (struct file *, int, struct file_lock *); lock 方法用來實現檔案加鎖; 加鎖對常規檔案是必不可少的特性, 但是裝置驅動幾乎從不實現它. ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); 這些方法實現發散/匯聚讀和寫操作. 應用程式偶爾需要做一個包含多個記憶體區的單個讀或寫操作; 這些系統呼叫允許它們這樣做而不必對資料進行額外拷貝. 如果這些函式指標為 NULL, read 和 write 方法被呼叫( 可能多於一次 ). ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *); 這個方法實現 sendfile 系統呼叫的讀, 使用最少的拷貝從一個檔案描述符搬移資料到另一個. 例如, 它被一個需要傳送檔案內容到一個網路連線的 web 伺服器使用. 裝置驅動常常使 sendfile 為 NULL. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); sendpage 是 sendfile 的另一半; 它由核心呼叫來發送資料, 一次一頁, 到對應的檔案. 裝置驅動實際上不實現 sendpage. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 這個方法的目的是在程序的地址空間找一個合適的位置來對映在底層裝置上的記憶體段中. 這個任務通常由記憶體管理程式碼進行; 這個方法存在為了使驅動能強制特殊裝置可能有的任何的對齊請求. 大部分驅動可以置這個方法為 NULL.[10] int (*check_flags)(int) 這個方法允許模組檢查傳遞給 fnctl(F_SETFL...) 呼叫的標誌. int (*dir_notify)(struct file *, unsigned long); 這個方法在應用程式使用 fcntl 來請求目錄改變通知時呼叫. 只對檔案系統有用; 驅動不需要實現 dir_notify. scull 裝置驅動只實現最重要的裝置方法. 它的 file_operations 結構是如下初始化的: struct file_operations scull_fops = { .owner = THIS_MODULE, .llseek = scull_llseek, .read = scull_read, .write = scull_write, .ioctl = scull_ioctl, .open = scull_open, .release = scull_release, }; 這個宣告使用標準的 C 標記式結構初始化語法. 這個語法是首選的, 因為它使驅動在結構定義的改變之間更加可移植, 並且, 有爭議地, 使程式碼更加緊湊和可讀. 標記式初始化允許結構成員重新排序; 在某種情況下, 真實的效能提高已經實現, 通過安放經常使用的成員的指標在相同硬體高速儲存行中.
file結構
struct file, 定義於 , 是裝置驅動中第二個最重要的資料結構. 檔案結構代表一個開啟的檔案. (它不特定給裝置驅動; 系統中每個開啟的檔案有一個關聯的 struct file 在核心空間). 它由核心在 open 時建立, 並傳遞給在檔案上操作的任何函式, 直到最後的關閉. 在檔案的所有例項都關閉後, 核心釋放這個資料結構.
mode_t f_mode; 檔案模式確定檔案是可讀的或者是可寫的(或者都是), 通過位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函式中檢查這個成員的讀寫許可, 但是你不需要檢查讀寫許可, 因為核心在呼叫你的方法之前檢查. 當檔案還沒有為那種存取而開啟時讀或寫的企圖被拒絕, 驅動甚至不知道這個情況. loff_t f_pos; 當前讀寫位置. loff_t 在所有平臺都是 64 位( 在 gcc 術語裡是 long long ). 驅動可以讀這個值, 如果它需要知道檔案中的當前位置, 但是正常地不應該改變它; 讀和寫應當使用它們作為最後引數而收到的指標來更新一個位置, 代替直接作用於 filp->f_pos. 這個規則的一個例外是在 llseek 方法中, 它的目的就是改變檔案位置. unsigned int f_flags; 這些是檔案標誌, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驅動應當檢查 O_NONBLOCK 標誌來看是否是請求非阻塞操作( 我們在第一章的"阻塞和非阻塞操作"一節中討論非阻塞 I/O ); 其他標誌很少使用. 特別地, 應當檢查讀/寫許可, 使用 f_mode 而不是 f_flags. 所有的標誌在標頭檔案 中定義. struct file_operations *f_op; 和檔案關聯的操作. 核心安排指標作為它的 open 實現的一部分, 接著讀取它當它需要分派任何的操作時. filp->f_op 中的值從不由核心儲存為後面的引用; 這意味著你可改變你的檔案關聯的檔案操作, 在你返回呼叫者之後新方法會起作用. 例如, 關聯到主編號 1 (/dev/null, /dev/zero, 等等)的 open 程式碼根據開啟的次編號來替代 filp->f_op 中的操作. 這個做法允許實現幾種行為, 在同一個主編號下而不必在每個系統呼叫中引入開銷. 替換檔案操作的能力是面向物件程式設計的"方法過載"的核心對等體. void *private_data; open 系統呼叫設定這個指標為 NULL, 在為驅動呼叫 open 方法之前. 你可自由使用這個成員或者忽略它; 你可以使用這個成員來指向分配的資料, 但是接著你必須記住在核心銷燬檔案結構之前, 在 release 方法中釋放那個記憶體. private_data 是一個有用的資源, 在系統呼叫間保留狀態資訊, 我們大部分例子模組都使用它. struct dentry *f_dentry; 關聯到檔案的目錄入口( dentry )結構. 裝置驅動編寫者正常地不需要關心 dentry 結構, 除了作為 filp->f_dentry->d_inode 存取 inode 結構.
inode結構
inode 結構由核心在內部用來表示檔案. 因此, 它和代表開啟檔案描述符的檔案結構是不同的. 可能有代表單個檔案的多個開啟描述符的許多檔案結構, 但是它們都指向一個單個 inode 結構.
inode 結構包含大量關於檔案的資訊. 作為一個通用的規則, 這個結構只有 2 個成員對於編寫驅動程式碼有用:
dev_t i_rdev;
對於代表裝置檔案的節點, 這個成員包含實際的裝置編號.
struct cdev *i_cdev;
struct cdev 是核心的內部結構, 代表字元裝置; 這個成員包含一個指標, 指向這個結構, 當節點指的是一個字元裝置檔案時.