我在Linux字符設備驅動框架一文中已經簡單的介紹了字符設備驅動的基本的編程框架,這里我們來探討一下Linux內核(以4.8.5內核為例)是怎么管理字符設備的,即當我們獲得了設備號,分配了cdev結構,注冊了驅動的操作方法集,最后進行cdev_add()的時候,究竟是將哪些內容告訴了內核,內核又是怎么管理我的cdev結構的,這就是本文要討論的內容。我們知道,Linux內核對設備的管理是基於kobject的(參見Linux設備管理(一)_kobject_kset_kobj_type),這點從我們的cdev結構中就可以看出,所以,接下來,你將看到"fs/char_dev.c"中實現的操作字符設備的函數都是基於"lib/kobject.c"以及"drivers/base/map.c"中對kobject操作的函數。好,現在我們從cdev_add()開始一層層的扒。
cdev_map對象
//fs/char_dev.c
27 static struct kobj_map *cdev_map;
內核中關於字符設備的操作函數的實現放在"fs/char_dev.c"中,打開這個文件,首先注意到就是這個在內核中不常見的靜態全局變量cdev_map(27),我們知道,為了提高軟件的內聚性,Linux內核在設計的時候盡量避免使用全局變量作為函數間數據傳遞的方式,而建議多使用形參列表,而這個結構體變量在這個文件中到處被使用,所以它應該是描述了系統中所有字符設備的某種信息,帶着這樣的想法,我們可以在"drivers/base/map.c"中找到kobj_map結構的定義:
//drivers/base/map.c
19 struct kobj_map {
20 struct probe {
21 struct probe *next;
22 dev_t dev;
23 unsigned long range;
24 struct module *owner;
25 kobj_probe_t *get;
26 int (*lock)(dev_t, void *);
27 void *data;
28 } *probes[255];
29 struct mutex *lock;
30 };
從中可以看出,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),下圖中體現兩種常見的對設備號和cdev管理的方式,其一是一個cdev對象對應這一個/多個設備號的情況, 在cdev_map中, 一個probes對象就對應一個主設備號,多個設備號對應一個cdev時,其實只是次設備號在變,主設備號還是一樣的,所以是同一個probes對象;其二是當主設備號超過255時,會進行probe復用,此時probe->next就派上了用場,比如probe[200],可以表示設備號200,455...3895等所有對255取余是200的數字, 參見下文的kobj_map--58--。
cdev_add
了解了cdev_map的功能,我們就可以一探cdev_add()。從中可以看出,其工作顯然是交給了kobj_map()
cdev_add()
--460-->就是將我們之前獲得設備號和設備號長度填充到cdev結構中,
--468-->kobject_get()將kobject的計數減一,並返回struct kobject*
//fs/char_dev.c
456 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
457 {
458 int error;
459
460 p->dev = dev;
461 p->count = count;
462
463 error = kobj_map(cdev_map, dev, count, NULL,
464 exact_match, exact_lock, p);
465 if (error)
466 return error;
467
468 kobject_get(p->kobj.parent);
469
470 return 0;
471 }
kobj_map()
這個函數在內核的設備管理中占有重要的地位,這里我們只從字符設備的角度分析它的功能,這個函數的設計也很單純,就是封裝好一個probe結構並將它的地址放入probes數組進而封裝進cdev_map,。
kobj_map()
--48-55-->根據傳入的設備號的個數,將設備號和cdev依次封裝到kmalloc_array()分配的n個probe結構中
--57-63-->就是遍歷probs數組,直到找到一個值為NULL的元素,再將probe的地址存入probes, 將設備號對255取余后與probes的下標對應。至此,我們就將我們的cdev放入的內核的數據結構
//drivers/base/map.c
32 int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
33 struct module *module, kobj_probe_t *probe,
34 int (*lock)(dev_t, void *), void *data)
35 {
36 unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
37 unsigned index = MAJOR(dev);
38 unsigned i;
39 struct probe *p;
...
44 p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
...
48 for (i = 0; i < n; i++, p++) {
49 p->owner = module;
50 p->get = probe;
51 p->lock = lock;
52 p->dev = dev;
53 p->range = range;
54 p->data = data;
55 }
56 mutex_lock(domain->lock);
57 for (i = 0, p -= n; i < n; i++, p++, index++) {
58 struct probe **s = &domain->probes[index % 255];
59 while (*s && (*s)->range < range)
60 s = &(*s)->next;
61 p->next = *s;
62 *s = p;
63 }
64 mutex_unlock(domain->lock);
65 return 0;
66 }
chrdev_open()
將設備放入的內核,我們再來看看內核是怎么找到一個特定的cdev的。
首先,在一個字符設備文件被創建的時候,內核會構造相應的inode,作為一種特殊文件,其inode初始化的時候,就會做一些准備工作
//fs/char_dev.c
429 const struct file_operations def_chr_fops = {
430 .open = chrdev_open,
431 .llseek = noop_llseek,
432 };
//fs/inode.c
1923 void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
1924 {
1925 inode->i_mode = mode;
1926 if (S_ISCHR(mode)) {
1927 inode->i_fop = &def_chr_fops; //Here
1928 inode->i_rdev = rdev; //and Here
1929 } else if (S_ISBLK(mode)) {
1930 inode->i_fop = &def_blk_fops;
1931 inode->i_rdev = rdev;
1932 } else if (S_ISFIFO(mode))
1933 inode->i_fop = &pipefifo_fops;
1934 else if (S_ISSOCK(mode))
1935 ; /* leave it no_open_fops */
1936 else
1937 printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
1938 " inode %s:%lu\n", mode, inode->i_sb->s_id,
1939 inode->i_ino);
1940 }
由此可見,對一個字符設備的訪問流程大概是:文件路徑=>inode=>chrdev_open()=>(kobj_lookup=>)inode.i_cdev=>cdev.fops.my_chr_open()。所以只要通過VFS找到了inode,就可以找到chrdev_open(),這里我們就來關注一個chrdev_open()是怎么從內核的數據結構中找到我們的cdev並執行其中的my_chr_open()的。比較有意思的是,雖然我們有了字符設備的設備文件,inode也被構造並初始化了, 但是在第一次調用chrdev_open()之前,這個inode和具體的chr_dev對象並沒有直接關系,而只是通過設備號建立的"間接"關系。在第一次調用chrdev_open()之后, inode->i_cdev才被根據設備號找到的cdev對象賦值,此后inode才和具體的cdev對象直接聯系在了一起
//fs/char_dev.c
326 static struct kobject *cdev_get(struct cdev *p)
327 {
328 struct module *owner = p->owner;
329 struct kobject *kobj;
330
331 if (owner && !try_module_get(owner))
332 return NULL;
333 kobj = kobject_get(&p->kobj);
...
336 return kobj;
337 }
351 static int chrdev_open(struct inode *inode, struct file *filp)
352 {
353 const struct file_operations *fops;
354 struct cdev *p;
355 struct cdev *new = NULL;
356 int ret = 0;
...
359 p = inode->i_cdev;
360 if (!p) {
361 struct kobject *kobj;
362 int idx;
...
364 kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
...
367 new = container_of(kobj, struct cdev, kobj);
369 /* Check i_cdev again in case somebody beat us to it while
370 we dropped the lock. */
371 p = inode->i_cdev;
372 if (!p) {
373 inode->i_cdev = p = new;
374 list_add(&inode->i_devices, &p->list);
375 new = NULL;
376 } else if (!cdev_get(p))
377 ret = -ENXIO;
378 } else if (!cdev_get(p))
379 ret = -ENXIO;
...
386 fops = fops_get(p->ops);
...
390 replace_fops(filp, fops);
391 if (filp->f_op->open) {
392 ret = filp->f_op->open(inode, filp);
...
395 }
396
397 return 0;
398
399 out_cdev_put:
400 cdev_put(p);
401 return ret;
402 }
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();如果我們沒有實現自己的open接口,就什么都不做,也不是錯
扒完了字符設備的注冊過程,不知各位看官有沒有發現,全程沒有一個初始化cdev.kobj的函數!到此為止,我們都是通過cdev_map來管理系統里的字符設備的,所以,我們並不能在sysfs找到我們此時注冊的字符設備,更深層的原因是內核中並不直接使用cdev作為一個設備,而是將其作為一個設備接口,使用這個接口我們可以派生出misc設備,輸入設備,LCD等等,當初始化這些具體的字符設備的時候,相應的list_head對象才可能被打開掛接到相應的鏈表,並初始化kobj。即如果希望sysfs中找到我們的字符設備,我們就必須對cdev.kobj進行初始化,掛接到合適的kset,這也就是導出設備信息到sysfs以便自動創建設備文件的原理。
彩蛋
Linux中幾乎所有的"設備"都是"device"的子類,無論是平台設備還是i2c設備還是網絡設備,但唯獨字符設備不是,從"Linux字符設備驅動框架"一文中我們可以看出cdev並不是繼承自device,從"Linux設備管理(二)_從cdev_add說起"一文中我們可以看出注冊一個cdev對象到內核其實只是將它放到cdev_map中,直到"Linux設備管理(四)_從sysfs回到ktype"一文中對device_create的分析才知道此時才創建device結構並將kobj掛接到相應的鏈表,,所以,基於歷史原因,當下cdev更合適的一種理解是一種接口(使用mknod時可以當作設備),而不是而一個具體的設備,和platform_device,i2c_device有着本質的區別