Linux設(shè)備管理(二)_從cdev_add說(shuō)起(超詳細(xì))
這里我們來(lái)探討一下Linux內(nèi)核(以4.8.5內(nèi)核為例)是怎么管理字符設(shè)備的,即當(dāng)我們獲得了設(shè)備號(hào),分配了cdev結(jié)構(gòu),注冊(cè)了驅(qū)動(dòng)的操作方法集,最后進(jìn)行cdev_add()的時(shí)候,究竟是將哪些內(nèi)容告訴了內(nèi)核,內(nèi)核又是怎么管理我的cdev結(jié)構(gòu)的,這就是本文要討論的內(nèi)容。我們知道,Linux內(nèi)核對(duì)設(shè)備的管理是基于kobject的(參見Linux設(shè)備管理(一)_kobject,、kset、ktype分析(超詳細(xì))),這點(diǎn)從我們的cdev結(jié)構(gòu)中就可以看出,所以,接下來(lái),你將看到"fs/char_dev.c"中實(shí)現(xiàn)的操作字符設(shè)備的函數(shù)都是基于"lib/kobject.c"以及"drivers/base/map.c"中對(duì)kobject操作的函數(shù)。好,現(xiàn)在我們從cdev_add()開始一層層的扒。
cdev_map對(duì)象
內(nèi)核中關(guān)于字符設(shè)備的操作函數(shù)的實(shí)現(xiàn)放在"fs/char_dev.c"中,打開這個(gè)文件,首先注意到就是這個(gè)在內(nèi)核中不常見的靜態(tài)全局變量cdev_map(27),我們知道,為了提高軟件的內(nèi)聚性,Linux內(nèi)核在設(shè)計(jì)的時(shí)候盡量避免使用全局變量作為函數(shù)間數(shù)據(jù)傳遞的方式,而建議多使用形參列表,而這個(gè)結(jié)構(gòu)體變量在這個(gè)文件中到處被使用,所以它應(yīng)該是描述了系統(tǒng)中所有字符設(shè)備的某種信息,帶著這樣的想法,我們可以在"drivers/base/map.c"中找到kobj_map結(jié)構(gòu)的定義:
從中可以看出,kobj_map的核心就是一個(gè)struct probe類型、大小為255的數(shù)組,而在這個(gè)probe結(jié)構(gòu)中,第一個(gè)成員next(21)顯然是將這些probe結(jié)構(gòu)通過(guò)鏈表的形式連接起來(lái),dev_t類型的成員dev顯然是設(shè)備號(hào),get(25)和lock(26)分別是兩個(gè)函數(shù)接口,最后的重點(diǎn)來(lái)了,void作為C語(yǔ)言中的萬(wàn)金油類型,在這里就是我們cdev結(jié)構(gòu)(通過(guò)后面的分析可以看出),所以,這個(gè)cdev_map是一個(gè)struct kobj_map類型的指針,其中包含著一個(gè)struct probe*類型、大小為255的數(shù)組,數(shù)組的每個(gè)元素指向的一個(gè)probe結(jié)構(gòu)封裝了一個(gè)設(shè)備號(hào)和相應(yīng)的設(shè)備對(duì)象(這里就是cdev),下圖中體現(xiàn)兩種常見的對(duì)設(shè)備號(hào)和cdev管理的方式,其一是一個(gè)cdev對(duì)象對(duì)應(yīng)這一個(gè)/多個(gè)設(shè)備號(hào)的情況, 在cdev_map中, 一個(gè)probes對(duì)象就對(duì)應(yīng)一個(gè)主設(shè)備號(hào),多個(gè)設(shè)備號(hào)對(duì)應(yīng)一個(gè)cdev時(shí),其實(shí)只是次設(shè)備號(hào)在變,主設(shè)備號(hào)還是一樣的,所以是同一個(gè)probes對(duì)象;其二是當(dāng)主設(shè)備號(hào)超過(guò)255時(shí),會(huì)進(jìn)行probe復(fù)用,此時(shí)probe->next就派上了用場(chǎng),比如probe[200],可以表示設(shè)備號(hào)200,455...3895等所有對(duì)255取余是200的數(shù)字, 參見下文的kobj_map--58--。

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


cdev_add
了解了cdev_map的功能,我們就可以一探cdev_add()。從中可以看出,其工作顯然是交給了kobj_map()
cdev_add()--460-->就是將我們之前獲得設(shè)備號(hào)和設(shè)備號(hào)長(zhǎng)度填充到cdev結(jié)構(gòu)中, --468-->kobject_get()將kobject的計(jì)數(shù)減一,并返回struct kobject*
kobj_map()
這個(gè)函數(shù)在內(nèi)核的設(shè)備管理中占有重要的地位,這里我們只從字符設(shè)備的角度分析它的功能,這個(gè)函數(shù)的設(shè)計(jì)也很單純,就是封裝好一個(gè)probe結(jié)構(gòu)并將它的地址放入probes數(shù)組進(jìn)而封裝進(jìn)cdev_map,。
kobj_map()--48-55-->根據(jù)傳入的設(shè)備號(hào)的個(gè)數(shù),將設(shè)備號(hào)和cdev依次封裝到kmalloc_array()分配的n個(gè)probe結(jié)構(gòu)中 --57-63-->就是遍歷probs數(shù)組,直到找到一個(gè)值為NULL的元素,再將probe的地址存入probes, 將設(shè)備號(hào)對(duì)255取余后與probes的下標(biāo)對(duì)應(yīng)。至此,我們就將我們的cdev放入的內(nèi)核的數(shù)據(jù)結(jié)構(gòu)
chrdev_open()
將設(shè)備放入的內(nèi)核,我們?cè)賮?lái)看看內(nèi)核是怎么找到一個(gè)特定的cdev的。 首先,在一個(gè)字符設(shè)備文件被創(chuàng)建的時(shí)候,內(nèi)核會(huì)構(gòu)造相應(yīng)的inode,作為一種特殊文件,其inode初始化的時(shí)候,就會(huì)做一些準(zhǔn)備工作
由此可見,對(duì)一個(gè)字符設(shè)備的訪問(wèn)流程大概是:文件路徑=>inode=>chrdev_open()=>(kobj_lookup=>)inode.i_cdev=>cdev.fops.my_chr_open()。所以只要通過(guò)VFS找到了inode,就可以找到chrdev_open(),這里我們就來(lái)關(guān)注一個(gè)chrdev_open()是怎么從內(nèi)核的數(shù)據(jù)結(jié)構(gòu)中找到我們的cdev并執(zhí)行其中的my_chr_open()的。比較有意思的是,雖然我們有了字符設(shè)備的設(shè)備文件,inode也被構(gòu)造并初始化了, 但是在第一次調(diào)用chrdev_open()之前,這個(gè)inode和具體的chr_dev對(duì)象并沒有直接關(guān)系,而只是通過(guò)設(shè)備號(hào)建立的"間接"關(guān)系。在第一次調(diào)用chrdev_open()之后, inode->i_cdev才被根據(jù)設(shè)備號(hào)找到的cdev對(duì)象賦值,此后inode才和具體的cdev對(duì)象直接聯(lián)系在了一起
chrdev_open()--359-->嘗試將inode->i_cdev(一個(gè)cdev結(jié)構(gòu)指針)保存在局部變量p中, --360-->如果p為空,即inode->i_cdev為空, --364-->我們就根據(jù)inode->i_rdev(設(shè)備號(hào))通過(guò)kobj_lookup()搜索cdev_map,并返回與之對(duì)應(yīng)kobj, --367-->由于kobject是cdev的父類,我們根據(jù)container_of很容易找到相應(yīng)的cdev結(jié)構(gòu)并將其保存在inode->i_cdev中, --374-->找到了cdev,我們就可以將inode->devices掛接到inode->i_cdev的管理鏈表中,這樣下次就不用重新搜索, --378-->直接cdev_get()即可。 --386-->找到了我們的cdev結(jié)構(gòu),我們就可以將其中的操作方法集inode->i_cdev->ops傳遞給filp->f_ops(386-390), --392-->這樣,我們就可以回調(diào)我們的設(shè)備打開函數(shù)my_chr_open();如果我們沒有實(shí)現(xiàn)自己的open接口,就什么都不做,也不是錯(cuò)
扒完了字符設(shè)備的注冊(cè)過(guò)程,不知各位看官有沒有發(fā)現(xiàn),全程沒有一個(gè)初始化cdev.kobj的函數(shù)!到此為止,我們都是通過(guò)cdev_map來(lái)管理系統(tǒng)里的字符設(shè)備的,所以,我們并不能在sysfs找到我們此時(shí)注冊(cè)的字符設(shè)備,更深層的原因是內(nèi)核中并不直接使用cdev作為一個(gè)設(shè)備,而是將其作為一個(gè)設(shè)備接口,使用這個(gè)接口我們可以派生出misc設(shè)備,輸入設(shè)備,LCD等等,當(dāng)初始化這些具體的字符設(shè)備的時(shí)候,相應(yīng)的list_head對(duì)象才可能被打開掛接到相應(yīng)的鏈表,并初始化kobj。即如果希望sysfs中找到我們的字符設(shè)備,我們就必須對(duì)cdev.kobj進(jìn)行初始化,掛接到合適的kset,這也就是導(dǎo)出設(shè)備信息到sysfs以便自動(dòng)創(chuàng)建設(shè)備文件的原理。
彩蛋
Linux中幾乎所有的"設(shè)備"都是"device"的子類,無(wú)論是平臺(tái)設(shè)備還是i2c設(shè)備還是網(wǎng)絡(luò)設(shè)備,但唯獨(dú)字符設(shè)備不是,可以看出cdev并不是繼承自device,我們可以看出注冊(cè)一個(gè)cdev對(duì)象到內(nèi)核其實(shí)只是將它放到cdev_map中,直到對(duì)device_create的分析才知道此時(shí)才創(chuàng)建device結(jié)構(gòu)并將kobj掛接到相應(yīng)的鏈表,,所以,基于歷史原因,當(dāng)下cdev更合適的一種理解是一種接口(使用mknod時(shí)可以當(dāng)作設(shè)備),而不是而一個(gè)具體的設(shè)備,和platform_device,i2c_device有著本質(zhì)的區(qū)別.
