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

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


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