設計字元設備 文件系統調用系統IO的內核處理過程 inode索引節點是文件系統中的一種數據結構,用於存儲文件的元數據信息,包括文件的大小、訪問許可權、創建時間、修改時間等。每個文件在文件系統中都對應著一個唯一的inode節點,通過inode節點可以查找到文件的實際數據塊的位置。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; //表示32位設備號 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; };
創建一個設備文件
mknod命令:創建指定類型的特殊文件
用法:mknod [選項]... 名稱 類型 [主設備號 次設備號]
以指定的 <名稱> 創建指定 <類型> 的特殊文件。
當 <類型> 為 b、c 或 u 時必須指定 <主設備號> 和 <次設備號>,當 <類型>為 p 時不得指定 <主設備號> 和 <次設備號>。
如果 <主設備號> 或 <次設備號>以 0x 或 0X 開頭,則將它們視為十六進位數進行解析;如果以 0 開頭,則視為八進位數;其餘情況下,視為十進位數。<類型> 可以是:
b 創建一個(帶緩衝的)塊特殊文件
c, u 創建一個(不帶緩衝的)字元特殊文件
p 創建一個 FIFO 文件舉例:mkmod /dev/test c 2 0
原理分析
init_special_inode函數定義在ebf-buster-linux/fs/inode.c。主要內容是判斷文件的inode類型,如果是字元設備類型,則把def_chr_fops作為該文件的操作介面,並把設備號記錄在inode->i_rdev。
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) { inode->i_mode = mode; if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = rdev; } else if (S_ISFIFO(mode)) inode->i_fop = &pipefifo_fops; else if (S_ISSOCK(mode)) ; /* leave it no_open_fops */ else printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for inode %s:%lu\n", mode, inode->i_sb->s_id, inode->i_ino); }
inode上的file_operation並不是自己構造的file_operation(保存在內核的字元設備驅動哈希表裡),而是字元設備通用的def_chr_fops。
自己構造的file_operation會在chrdev_open函數那裡才會綁定到我們的文件上。所以自己構建的file_operation等在應用程式調用open函數後才會綁定在文件上。
LED字元設備驅動實驗
驅動模塊 = 內核模塊(.ko)+ 驅動介面(file_operations)
實驗步驟
- 在內核模塊入口函數里獲取GPIO相關寄存器並初始化。
- 構造file_operations介面,並註冊到內核。
- 創建設備文件,綁定自定義file_operations介面。
- 應用程式echo通過寫設備文件控制硬體led。
驅動模塊初始化
虛擬地址映射ioremap函數
GPIO寄存器物理地址和虛擬地址映射。函數ioremap存放於ebf-buster-linux/arch/arm/include/asm/io.h。
void __iomem *ioremap(resource_size_t res_cookie, size_t size); 參數: res_cookie:物理地址 size:映射長度 返回值: void *類型的指針,指向被映射的虛擬地址 __iomem 主要是用於編譯器的檢查地址在內核空間的有效性
虛擬地址讀寫
舊機制:
readl() / writel()
新機制:
unsigned int ioread32(void __iomem *addr); //讀取一個雙字
void iowrite32(u32 b, void __iomem *addr); //寫入一個雙字
檢查CPU大小端,調整位元組序,以提高驅動的可移植性。
自定義led的file_operation介面
static struct file_operation led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release };
owner:設置驅動介面關聯的內核模塊,防止驅動程式運行時內核模塊被卸載。
release:文件引用數為0時調用。
拷貝數據函數copy_from_user(存放於ebf-buster-linux/include/linux/uaccess.h)
static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) 參數: *to:將數據拷貝到內核的地址 *from:需要拷貝數據的用戶空間地址 n:拷貝數據的長度(位元組) 返回值: 成功:0 失敗:沒有被拷貝的位元組數
註冊字元設備驅動
層次由高到低:register_chadev -> __register_chadev -> __register_chadev_region(該功能實現了cdev_init函數和cdev_add函數功能,但是在__register_chadev函數中規定了次設備號從0開始,有256個),即一個file_operations對應256個設備文件。
chrdev.c文件
#include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <asm/io.h> #define DEV_MAJOR 0 /* 動態申請主設備號 */ #define DEV_NAME "red_led" /* led設備名字 */ /* GPIO虛擬地址指針 */ static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_GPIO1_IO04; static void __iomem *SW_PAD_GPIO1_IO04; static void __iomem *GPIO1_GDIR; static void __iomem *GPIO1_DR; static int led_open(struct inode *inode, struct file *filp) { return 0; } static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return -EFAULT; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { unsigned char databuf[10]; if(cnt > 10) cnt = 10; /* 從用戶空間拷貝數據到內核空間 */ if(copy_from_user(databuf, buf, cnt)){ return -EIO; } if(!memcmp(databuf, "on", 2)){ iowrite32(0<<4, GPIO1_DR); }else if(!memcmp(databuf, "off", 3)){ iowrite32(1<<4, GPIO1_DR); } /* 寫成功後,返回寫入的位元組數 */ return cnt; } static int led_release(struct inode *inode, struct file *filp) { return 0; } /* 自定義led的file_operation介面 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release }; int major = 0; static int __init led_init(void) { /* GPIO相關寄存器操作 */ IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4); SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4); SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4); GPIO1_GDIR = ioremap(0x0209C004, 4); GPIO1_DR = ioremap(0x0209C000, 4); /* 使能GPIO1時鐘 */ iowrite32(0xffffffff, IMX6U_CCM_CCGR1); /* 設置GPIO1_IO04復用為普通GPIO */ iowrite32(5, SW_MUX_GPIO1_IO04); /* 設置GPIO屬性 */ iowrite32(0x10B0, SW_PAD_GPIO1_IO04); /* 設置GPIO1_IO04為輸出功能 */ iowrite32(1<<4, GPIO1_GDIR); /* LED輸出高電平 */ iowrite32(1<<4, GPIO1_DR); /* 註冊字元設備驅動 */ major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops); printk(KERN_ALERT "led major:%d\n", major); return 0; } static void __exit led_exit(void) { /* 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO04); iounmap(SW_PAD_GPIO1_IO04); iounmap(GPIO1_GDIR); iounmap(GPIO1_DR); /* 註銷字元設備驅動 */ unregister_chrdev(major, DEV_NAME); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL2"); MODULE_AUTHOR("couvrir"); MODULE_DESCRIPTION("led module"); MODULE_ALIAS("led module");
make。
make copy。