設計字元設備 文件系統調用系統IO的內核處理過程 在Linux文件系統管理中,當應用程式調用open函數時,內核會根據文件路徑找到文件的索引結點(inode),為文件分配文件描述符和文件對象,並根據打開模式和許可權等參數進行相應的操作和設置。 硬體層原理 思路:把底層寄存器配置操作放在文件操作介面里, ...
設計字元設備
文件系統調用系統IO的內核處理過程
inode索引節點是文件系統中的一種數據結構,用於存儲文件的元數據信息,包括文件的大小、訪問許可權、創建時間、修改時間等。每個文件在文件系統中都對應著一個唯一的inode節點,通過inode節點可以查找到文件的實際數據塊的位置。inode節點通常存儲在磁碟的inode表中,文件系統通過inode號來訪問和管理文件。
file_operation結構體是函數指針表,用於定義文件的操作方法。當應用程式通過文件描述符打開文件時,內核會根據文件描述符找到對應的inode節點,並獲取與inode節點關聯的file_operation表。通過file_operation表中的函數指針,內核可以調用相應的函數來執行文件操作,如open、read、write、close等。不同內核可以有不同的file_operation表,因為不同的內核可能有不同的文件操作方法和特性。
task_struct結構體用於描述和管理進程,內容很多很複雜。裡面有個成員變數是struct files_struct *files,用於存儲與進程相關的文件描述符表的信息(文件描述符表記錄了進程打開的文件以及相應的操作許可權等信息)。要想獲取進程的文件描述符相關信息,需要通過訪問task_struct結構體的files指針來獲取files_struct結構體,進而訪問文件描述符表的信息。
files_struct結構體用於跟蹤和管理進程打開的文件。fd_array[]為指針數組,用於存儲進程打開的文件描述符的信息,即每個文件描述符都對應一個files_struct。通過fd_array數組可以快速訪問和操作這些文件描述符,數組索引值對應著文件描述符的值。
硬體層原理
思路:把底層寄存器配置操作放在文件操作介面里,新建一個文件綁定該文件操作介面,應用程式通過操作指定文件來配置底層寄存器。
基本介面實現:查原理圖,數據手冊,確定底層需要配置的寄存器。類似於裸機開發。實現一個文件的底層操作介面,這是文件的基本特征。
struct file_operations存放在ebf-buster-linux/include/linux/fs.h。
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64); int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t, u64); int (*fadvise)(struct file *, loff_t, loff_t, int); } __randomize_layout;
幾乎所有成員都是函數指針,用來實現文件的具體操作。
驅動層原理
設備號(dev_t)是uint32_t類型,主設備號(高12位)+次設備號(低20位)。主設備號用於標識該驅動所管理的設備類型,次設備號用於標識同一類型設備的具體設備實例。
把file_operations文件操作介面註冊到內核,內核通過主次設備號來記錄它。(cdev_init存放在ebf-buster-linux/fs/char_dev.c)
cdev_init() //把用戶構建的file_operations結構體記錄在內核驅動的基本對象cdev
cdev_init()用於初始化字元設備驅動中的struct cdev結構體。struct cdev結構體是字元設備驅動的核心數據結構,描述了字元設備驅動的屬性和操作(file_operations)。
當開發者編寫字元設備驅動時,需要先調用cdev_init()函數來初始化struct cdev結構體。這個函數會將struct cdev的各個成員初始化為合適的值,並建立與字元設備驅動相關的關聯。
cdev_init()函數通常在字元設備驅動載入時init函數中調用,確保驅動模塊的正確初始化。
cdev_add(存放在ebf-buster-linux/fs/char_dev.c),作用:根據哈希函數保存cdev到probes哈希表中,方便內核查找file_operation使用。
int cdev_add(struct cdev *p, dev_t dev, unsigned int count);
參數:
p:指定的字元設備對象
dev:設備號
count:設備號數量
cdev_add()函數用於向字元設備驅動註冊字元設備,即將一個已初始化的字元設備對象(struct cdev)和一個設備號(dev_t)進行關聯,並將其註冊到內核中,使其能夠被用戶程式訪問和使用。
cdev_add()函數通常在字元設備驅動初始化階段調用來註冊字元設備,在此之前,需要先通過cdev_init()初始化。註意:需要正確初始化之後才調用,否則可能導致註冊失敗或出現意外的行為。
兩個Hash表(幫助找到cdev結構體)
chrdevs:登記設備號。
cdev_map->probe:保存驅動基本對象struct cdev。
文件系統層原理
mknod + 主次設備號
構建一個新的設備文件,通過主次設備號在cdev_map中找到cdev->file_operations,把cdev->file_operations綁定到新的設備文件中。
到這一步,應用程式就可以使用open()、write()、read()等函數來控制設備文件了。
設備號的組成與哈希表
ebf-buster-linux/include/linux/kdev_t.h描述了設備號的具體構成。
/* 截取部分代碼,關於設備號的描述 */ #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) -1) #define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int)((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
理論取值範圍:
主設備號:2^12=4K
次設備號:2^20=1M
cat /proc/devices:查看已註冊的設備號。
內核是希望一個設備驅動(file_operation)可以獨自占有一個主設備號和多個次設備號,而通常一個設備文件綁定一個主設備號和一個次設備號,所以設備驅動與設備文件是一對一或者一對多的關係。
Hash Table(哈希表、散列表,數組和鏈表的混合使用)
以主設備號為編號,使用哈希函數f(major)= major % 255 來計算主設備號的對應數組下標。
主設備號衝突(如0、255,都掛載在數組0下標),則以次設備號為比較值來排序鏈表節點。
哈希函數的設計目標:鏈表節點儘量平均分佈在各個數組元素中,提高查詢效率。
設備號管理
關鍵的數據結構:char_device_struct(存放在ebf-buster-linux/fs/char_dev.c)
static struct char_device_struct { struct char_device_struct *next; //指向下一個鏈表節點 unsigned int major; //主設備號 unsigned int baseminor; //次設備號 int minorct; //次設備號的數量 char name[64]; //設備名稱 struct cdev *cdev; //內核字元對象(已丟棄) } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
關鍵的函數:__register_chrdev_region(存放在ebf-buster-linux/fs/char_dev.c)
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { struct char_device_struct *cd, **cp; int ret = 0; int i;
/* 動態申請記憶體 */ cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); if (cd == NULL) return ERR_PTR(-ENOMEM); /* 加互斥鎖保護資源 */ mutex_lock(&chrdevs_lock); if (major == 0) {
/* 主設備號為0,從chadevs哈希表中查找一個空閑位置 */ ret = find_dynamic_major(); if (ret < 0) { pr_err("CHRDEV \"%s\" dynamic allocation region is full\n", name); goto out; }
/* 返回主設備號 */ major = ret; } if (major >= CHRDEV_MAJOR_MAX) { pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n", name, major, CHRDEV_MAJOR_MAX-1); ret = -EINVAL; goto out; }
/* 保存參數 */ cd->major = major; cd->baseminor = baseminor; cd->minorct = minorct; strlcpy(cd->name, name, sizeof(cd->name));
/* 哈希函數,計算哈希表的位置 */ i = major_to_index(major);
/* 鏈表排序,按主設備號從小到大排序。如果主設備號相等,按次設備號從小到大排序,要考慮次設備號的最大值 */ for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ( (*cp)->major > major || ( (*cp)->major == major && ( ( (*cp)->baseminor >= baseminor ) || ( (*cp)->baseminor + (*cp)->minorct > baseminor) ) ) ) break; /* 如果主設備號相等,檢查次設備號是否存在衝突 */ if (*cp && (*cp)->major == major) {
/* 獲取鏈表節點的次設備號範圍 */ int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
/* 獲取新設備的次設備號範圍 */ int new_min = baseminor; int new_max = baseminor + minorct - 1; /* 判斷新設備的次設備號最大值是否位於鏈表節點的次設備號範圍 */ if (new_max >= old_min && new_max <= old_max) {
/* 確定衝突,返回錯誤 */ ret = -EBUSY; goto out; } /* 判斷新設備的次設備號最小值是否位於鏈表節點的次設備號範圍 */ if (new_min <= old_max && new_min >= old_min) {
/* 確定衝突,返回錯誤 */ ret = -EBUSY; goto out; }
/* 判斷新設備的次設備號是否跨越鏈表節點的次設備號範圍 */ if (new_min < old_min && new_max > old_max) {
/* 確定衝突,返回錯誤 */ ret = -EBUSY; goto out; } }
/* 插入新設備的鏈表節點 */ cd->next = *cp; *cp = cd; mutex_unlock(&chrdevs_lock); return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }
上訴函數主設備號相等,判斷新舊次設備號三種錯誤圖如下。
該函數用於註冊字元設備驅動,保存新註冊的設備號到chrdevs哈希表中,防止設備號衝突。
主設備為0時,需要動態分配設備號(優先使用255~234,其次使用511~384),函數會從字元設備哈希表中找到一個空閑的位置,分配主設備號,並將該主設備號保存到字元設備的數據結構中。主設備號最大為512。
然後函數將傳入的參數保存到字元設備結構體cd中,並計算出在字元設備哈希表中的位置。如果遍歷字元設備哈希表的鏈表,按主設備號從小到大排序。
如果遍歷過程中,主設備號和傳入的主設備號衝突或次設備號範圍與傳入的次設備號範圍有重疊,函數會返回錯誤。否則函數會將新字元設備的節點插入到鏈表中,並返回字元設備結構體cd。
該函數一般在字元設備驅動載入時調用,用於註冊字元設備。通過註冊字元設備,內核可以識別和管理相應的設備,並提供相應的介面供用戶空間程式進行讀寫操作。
保存file_operation結構體
關鍵數據結構:cdev(存放在ebf-buster-linux/include/linux/cdev.h)
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; } __randomize_layout;
關鍵數據結構:kobj_map(與哈希表有關,存放在ebf-buster-linux/drivers/base/map.c)
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; };