https://m.toutiao.com/is/R3wEfuv/?=淺談Linux操作系統(tǒng)學(xué)習(xí)之字符設(shè)備 一. 前言上文中我們分析了虛擬文件系統(tǒng)的結(jié)構(gòu)以及常見的文件操作從用戶態(tài)到虛擬文件系統(tǒng)再到底層實際文件系統(tǒng)的過程。而實際上我們并沒有說明實際的文件系統(tǒng)如ext4是如何和磁盤進行交互的,這就是本文和下篇文章的重點:I/O之塊設(shè)備和字符設(shè)備。輸入輸出設(shè)備我們大致可以分為兩類:塊設(shè)備(Block Device)和字符設(shè)備(Character Device)。
本文首先介紹虛擬文件系統(tǒng)下層直至硬件輸入輸出設(shè)備的結(jié)構(gòu)關(guān)系,然后重點分析字符設(shè)備相關(guān)的整體邏輯情況。 二. I/O架構(gòu)由于各種輸入輸出設(shè)備具有不同的硬件結(jié)構(gòu)、驅(qū)動程序,因此我們采取了設(shè)備控制器這一中間層對上提供統(tǒng)一接口。設(shè)備控制器通過緩存來處理CPU和硬件I/O之間的交互關(guān)系,通過中斷進行通知,因此我們需要有中斷處理器對各種中斷進行統(tǒng)一。由于每種設(shè)備的控制器的寄存器、緩沖區(qū)等使用模式,指令都不同,所以對于操作系統(tǒng)還需要一層對接各個設(shè)備控制器的設(shè)備驅(qū)動程序。 這里需要注意的是,設(shè)備控制器不屬于操作系統(tǒng)的一部分,但是設(shè)備驅(qū)動程序?qū)儆诓僮飨到y(tǒng)的一部分。操作系統(tǒng)的內(nèi)核代碼可以像調(diào)用本地代碼一樣調(diào)用驅(qū)動程序的代碼,而驅(qū)動程序的代碼需要發(fā)出特殊的面向設(shè)備控制器的指令,才能操作設(shè)備控制器。設(shè)備驅(qū)動程序中是一些面向特殊設(shè)備控制器的代碼,不同的設(shè)備不同。但是對于操作系統(tǒng)其它部分的代碼而言,設(shè)備驅(qū)動程序有統(tǒng)一的接口。 設(shè)備驅(qū)動本身作為一個內(nèi)核模塊,通常以ko文件的形式存在,它有著其獨特的代碼結(jié)構(gòu):
#include <linux/module.h>#include <linux/init.h>
在下文的分析中,我們就將按照此順序來剖析字符設(shè)備的源碼,以弄懂字符設(shè)備的一般運行邏輯。關(guān)于設(shè)備驅(qū)動代碼編寫的詳細(xì)知識可以參考《Linux設(shè)備驅(qū)動》一書,本文重點不在于如何編寫代碼,而是在于操作系統(tǒng)中的字符設(shè)備和塊設(shè)備如何工作。 更多Linux內(nèi)核視頻教程文檔資料免費領(lǐng)取后臺私信【內(nèi)核】自行獲取。 內(nèi)核學(xué)習(xí)網(wǎng)站: 三. 字符設(shè)備基本構(gòu)成一個字符設(shè)備由3個部分組成:
cdev結(jié)構(gòu)體還有另一個相關(guān)聯(lián)的結(jié)構(gòu)體char_device_struct。這里首先會定義主設(shè)備號和次設(shè)備號:主設(shè)備號用來標(biāo)識與設(shè)備文件相連的驅(qū)動程序,用來反映設(shè)備類型。次設(shè)備號被驅(qū)動程序用來辨別操作的是哪個設(shè)備,用來區(qū)分同類型的設(shè)備。這里minorct指的是分配的區(qū)域,用于主設(shè)備號和次設(shè)備號的分配工作。 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]; cdev_map用于維護所有字符設(shè)備驅(qū)動,實際是結(jié)構(gòu)體kobj_map,主要包括了一個互斥鎖lock,一個probes[255]數(shù)組,數(shù)組元素為struct probe的指針,該結(jié)構(gòu)體包括鏈表項、設(shè)備號、設(shè)備號范圍等。所以我們將字符設(shè)備驅(qū)動最后保存為一個probe,并用cdev_map/kobj_map進行統(tǒng)一管理。
四. 打開字符設(shè)備字符設(shè)備有很多種,這里以打印機設(shè)備為輸出設(shè)備的例子,源碼位于drivers/char/lp.c。以鼠標(biāo)為輸入設(shè)備的例子,源碼位于 4.1 加載字符設(shè)備的使用從加載開始,通常我們會使用insmod命令或者modprobe命令加載ko文件,ko文件的加載則從module_init調(diào)用該設(shè)備自定義的初始函數(shù)開始。對于打印機來說,其初始化函數(shù)定義為lp_init_module(),實際調(diào)用lp_init()。lp_init()會初始化打印機結(jié)構(gòu)體,并調(diào)用register_chardev()注冊該字符設(shè)備。 module_init(lp_init_module);static int __init lp_init_module(void){...... return lp_init();}static int __init lp_init(void){...... if (register_chrdev(LP_MAJOR, 'lp', &lp_fops)) { printk(KERN_ERR 'lp: unable to get major %d\n', LP_MAJOR); return -EIO; }......} register_chrdev()實際調(diào)用__register_chrdev(),該函數(shù)會進行字符設(shè)備的注冊操作。其主要邏輯如下
對于鼠標(biāo)來說,加載也是類似的:注冊為logibm_init()函數(shù)。但是這里沒有調(diào)用register_chrdev()而是使用input_register_device(),原因在于輸入設(shè)備會統(tǒng)一由input_init()初始化,之后加入的輸入設(shè)備通過input_register_device()注冊到input的管理結(jié)構(gòu)體中進行統(tǒng)一管理。 module_init(logibm_init);static int __init logibm_init(void){...... err = input_register_device(logibm_dev);......} 4.2 創(chuàng)建文件設(shè)備加載完ko文件后,Linux內(nèi)核會通過mknod在/dev目錄下創(chuàng)建一個設(shè)備文件,只有有了這個設(shè)備文件,我們才能通過文件系統(tǒng)的接口對這個設(shè)備文件進行操作。mknod本身是一個系統(tǒng)調(diào)用,主要邏輯為調(diào)用user_path_create()為該設(shè)備文件創(chuàng)建dentry,然后對于S_IFCHAR或者S_IFBLK會調(diào)用vfs_mknod()去調(diào)用對應(yīng)文件系統(tǒng)的操作。
對于/dev目錄下的設(shè)備驅(qū)動來說,所屬的文件系統(tǒng)為devtmpfs文件系統(tǒng),即設(shè)備驅(qū)動臨時文件系統(tǒng)。devtmpfs對應(yīng)的文件系統(tǒng)定義如下 static struct file_system_type dev_fs_type = { .name = 'devtmpfs', .mount = dev_mount, .kill_sb = kill_litter_super,};static struct dentry *dev_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data){#ifdef CONFIG_TMPFS return mount_single(fs_type, flags, data, shmem_fill_super);#else return mount_single(fs_type, flags, data, ramfs_fill_super);#endif} 從這里可以看出,devtmpfs 在掛載的時候有兩種模式:一種是 ramfs,一種是 shmem ,都是基于內(nèi)存的文件系統(tǒng)。這兩個 mknod 雖然實現(xiàn)不同,但是都會調(diào)用到同一個函數(shù) init_special_inode()。顯然這個文件是個特殊文件,inode 也是特殊的。這里這個 inode 可以關(guān)聯(lián)字符設(shè)備、塊設(shè)備、FIFO 文件、Socket 等。我們這里只看字符設(shè)備。這里的 inode 的 file_operations 指向一個 def_chr_fops,這里面只有一個 open,就等著你打開它。另外,inode 的 i_rdev 指向這個設(shè)備的 dev_t。通過這個 dev_t,可以找到我們剛剛加載的字符設(shè)備 cdev。
由此我們完成了/dev下文件的創(chuàng)建,并利用rdev和生成的字符設(shè)備進行了關(guān)聯(lián)。 4.3 打開字符設(shè)備如打開普通文件一樣,打開字符設(shè)備也會首先分配對應(yīng)的文件描述符fd,然后生成struct file結(jié)構(gòu)體與其綁定,并將file關(guān)聯(lián)到對應(yīng)的dentry從而可以接觸inode。在進程里面調(diào)用 open() 函數(shù),最終會調(diào)用到這個特殊的 inode 的 open() 函數(shù),也就是 chrdev_open()。 chrdev_open()主要邏輯為;
/* * Called every time a character special file is opened */static int chrdev_open(struct inode *inode, struct file *filp){ const struct file_operations *fops; struct cdev *p; struct cdev *new = NULL;...... p = inode->i_cdev;...... kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);...... fops = fops_get(p->ops);...... replace_fops(filp, fops); if (filp->f_op->open) { ret = filp->f_op->open(inode, filp);......} 上述過程借用極客時間中的圖來作為總結(jié)。 五. 寫入字符設(shè)備寫入字符設(shè)備和寫入普通文件一樣,調(diào)用write()函數(shù)執(zhí)行。該函數(shù)在內(nèi)核里查詢系統(tǒng)調(diào)用表最終調(diào)用sys_write(),并根據(jù)fd描述符獲取對應(yīng)的file結(jié)構(gòu)體,接著調(diào)用vfs_write()去調(diào)用對應(yīng)的文件系統(tǒng)自定義的寫入函數(shù)file->f_op->write()。對于打印機來說,最終調(diào)用的是自定義的lp_write()函數(shù)。 這里寫入的重點在于調(diào)用 copy_from_user() 將數(shù)據(jù)從用戶態(tài)拷貝到內(nèi)核態(tài)的緩存中,然后調(diào)用 parport_write() 寫入外部設(shè)備。這里還有一個 schedule() 函數(shù),也即寫入的過程中,給其他線程搶占 CPU 的機會。如果寫入字節(jié)數(shù)多,不能一次寫完,就會在循環(huán)里一直調(diào)用 copy_from_user() 和 parport_write(),直到寫完為止。
六. 字符設(shè)備的控制在Linux中,我們常用ioctl()來對I/O設(shè)備進行一些讀寫之外的特殊操作。其參數(shù)主要由文件描述符fd,命令cmd以及命令參數(shù)arg構(gòu)成。其中cmd由幾個部分拼接成整型,主要結(jié)構(gòu)為
ioctl()也是一個系統(tǒng)調(diào)用,其中fd 是這個設(shè)備的文件描述符,cmd 是傳給這個設(shè)備的命令,arg 是命令的參數(shù)。主要調(diào)用do_vfs_ioctl()完成實際功能。 SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg){ int error; struct fd f = fdget(fd);...... error = do_vfs_ioctl(f.file, fd, cmd, arg); fdput(f); return error;} ??do_vfs_ioctl()對于已經(jīng)定義好的 cmd進行相應(yīng)的處理。如果不是默認(rèn)定義好的 cmd,則執(zhí)行默認(rèn)操作:對于普通文件,調(diào)用 file_ioctl,對于其他文件調(diào)用 vfs_ioctl。
??對于字符設(shè)備驅(qū)動程序,最終會調(diào)用vfs_ioctl()。這里面調(diào)用的是 struct file 里 file_operations 的 unlocked_ioctl() 函數(shù)。我們前面初始化設(shè)備驅(qū)動的時候,已經(jīng)將 file_operations 指向設(shè)備驅(qū)動的 file_operations 了。這里調(diào)用的是設(shè)備驅(qū)動的 unlocked_ioctl。對于打印機程序來講,調(diào)用的是 lp_ioctl()。 long vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){ int error = -ENOTTY; if (!filp->f_op->unlocked_ioctl) goto out; error = filp->f_op->unlocked_ioctl(filp, cmd, arg); if (error == -ENOIOCTLCMD) error = -ENOTTY; out: return error;} EXPORT_SYMBOL(vfs_ioctl); ??打印機的lp_do_ioctl()主要邏輯也是針對cmd采用switch()語句分情況進行處理。主要包括使用LP_XXX()宏定義賦值標(biāo)記位和調(diào)用copy_to_user()將用戶想得到的信息返回給用戶態(tài)。
總結(jié)本文簡單介紹了設(shè)備驅(qū)動程序的結(jié)構(gòu),并在此基礎(chǔ)上介紹了字符設(shè)備從創(chuàng)建到打開、寫入以及控制的整個流程。 |
|