1. 程式人生 > 其它 >構建Linux核心驅動demo子系統示例(轉載)

構建Linux核心驅動demo子系統示例(轉載)

原文:https://blog.csdn.net/luckyapple1028/article/details/50642906?spm=1001.2014.3001.5501
原文作者實在厲害,轉載備份,學習膜拜。

一般在編寫嵌入式Linux核心驅動時,最簡單的情況下往往只需要寫一個簡單的misc驅動,它僅需要相容一種硬體外設和一種特定的晶片平臺即可,這種驅動的最大缺點就是可擴充套件性和可移植性較差,往往在單板硬體上存在小幅的改動就需要更改驅動原始碼,有時在甚至在硬體上增加了一個相同的外設時需要重新為其寫一個幾乎一模一樣的驅動。

一個好的Linux核心驅動是要求在儘量小的改動下能夠快速適配於不同的平臺,且能夠支援多裝置。Linux核心針對沒有掛接在物理匯流排(PCI、I2C和USB等等)上的嵌入式裝置設計了一套platform匯流排驅動框架,這種框架能夠很好的解決滿足一般嵌入式外設驅動的要求。但如果存在較多功能類似但又不盡相同的的外設就需要為其設計對應的子系統來統一管理,Linux核心裡存在許多各式各樣的子系統,有相當複雜的也有比較簡單的,它們不僅統一管理了不同型別的外設驅動也遮蔽了他們的差異並向用戶空間提供了統一的介面。例如統一管理RTC驅動的RTC子系統、統一管理圖形顯示裝置的framebuffer子系統,甚至非常龐大且複雜的網路子系統

檔案系統子系統等等。

今次本文參考核心RTC子系統並提取出一個簡單的demo驅動子系統框架示例程式,可適用於一些簡單的Linux裝置驅動開發。

示例環境:

交叉編譯工具鏈:arm-bcm2708-linux-gnueabi-
Linux核心:linux-rpi-4.1.y

單板硬體:樹莓派b

原始碼連結:https://github.com/luckyapple1028/linux-demo-subsys-module


一、demo子系統框架

框架結構圖:


示例程式列表:demo_core.c、demo_dev.c、demo_interface.c、demo_proc.c、demo_sysfs.c、xxx_demo_driver.c、xxx_demo_device.c、demo-core.h、demo_dev.h、demo.h

1、demo_core

demo驅動程式的管理的核心,向裝置驅動程式提供介面,完成驅動程式向核心的的註冊和解除安裝。

2、demo_dev

負責demo驅動的字元裝置檔案的管理,包括註冊和登出字元裝置,定義了包括read、write、ioctl等一系列裝置控制介面。

3、demo_interface.c

提供了demo裝置iotcl控制的函式介面,負責對下層實際的驅動介面進行統一管理和呼叫。

4、demo_proc.c

提供了proc檔案系統的demo裝置查詢和控制介面。

5、demo_sysfs.c

提供了sys檔案系統的demo裝置查詢和控制介面,依賴於標準的Linux裝置驅動模型。

6、xxx_demo_driver.c

具體外設的驅動程式,不同的外設針對自己的特點可分別實現,是真正具有差異性的部分,然後統一向demo子系統註冊。本文中作為示例驅動依賴於platform驅動模型。

7、xxx_demo_device.c

具體外設,同本文中裝置驅動xxx_demo_driver匹配,負責註冊platform裝置並向驅動程式傳入具體的物理外設引數。(注:本文這裡使用的是傳統的方法,在新的Linux中可用Device Tree代替)。

二、結構體

1、xxx_demo

  1. struct xxx_demo {
  2. struct demo_device *demo; /* demo子系統通用裝置指標 */
  3. struct timer_list xxx_demo_timer;
  4. unsigned long xxx_demo_data;
  5. /* something else */
  6. };
xxx_demo結構是具體的demo驅動的控制結構,不同的外設驅動不盡相同,可以包括自己需要的一些資料和特殊結構。例如本示例結構中包含了一個核心定時器結構和一個data資料。除了具體的驅動資料之外,最重要的是demo_device指標,它是demo子系統的核心,負責了穿針引線的作用,在本驅動向demo子系統完成註冊之後就會生成具體的物件了。下面來具體分析這個結構。

2、demo_device

  1. struct demo_device
  2. {
  3. struct device dev;
  4. struct module *owner;
  5. int id;
  6. char name[DEMO_DEVICE_NAME_SIZE];
  7. const struct demo_class_ops *ops;
  8. struct mutex ops_lock;
  9. struct cdev char_dev;
  10. unsigned long flags;
  11. unsigned long irq_data;
  12. spinlock_t irq_lock;
  13. wait_queue_head_t irq_queue;
  14. struct fasync_struct *async_queue;
  15. /* some demo data */
  16. struct demo_data demo_data;
  17. };
該結構體中struct device dev 結構體為Linux裝置驅動模型的裝置結構,在向核心註冊裝置時需要進行初始化;struct module *owner指標一般設定為THIS_MODULE即可;id為demo裝置號,由於子系統可支援多個demo驅動裝置,顧需要進行編號便於管理;name為裝置驅動的名字;struct demo_class_ops *ops為demo驅動的控制函式集,為驅動程式向子系統註冊的控制函式介面,在驅動進行註冊時會進行賦值,以後由子系統負責呼叫具體的驅動功能介面;struce cdev char_dev即是demo裝置的字元裝置結構體了;其他struct mutex ops_lock和spinlock_t irq_lock鎖分別用於保護iotcl操作和保護中斷操作到的臨界區資料,irq_queue為等待佇列,用於實現阻塞式的io操作,async_queue用於訊號式非同步io操作,最後的demo_data為demo子系統所管理的示例資料結構(該以上幾項可更具驅動子系統的具體需要自行刪減)。

3、demo_data

  1. struct demo_data
  2. {
  3. unsigned long text_data;
  4. /* something else */
  5. };

該結構包含在demo_device結構體中,包含了一些demo驅動中通用性的引數以及demo子系統為了遮蔽底層差異並向用戶提供統一介面時需要而提取出來的需要統一管理的引數等等,這裡僅示例性的列了一個demo_data資料。


4、demo_class_ops

  1. struct demo_class_ops {
  2. int (*open)(struct device *);
  3. void (*release)(struct device *);
  4. int (*ioctl)(struct device *, unsigned int, unsigned long);
  5. int (*set_data)(struct device *, struct demo_ctl_data *);
  6. int (*get_data)(struct device *, struct demo_ctl_data *);
  7. int (*proc)(struct device *, struct seq_file *);
  8. int (*read_callback)(struct device *, int data);
  9. };
該結構為demo驅動程式的註冊函式介面,由具體的xxx_demo驅動負責實現並向demo子系統註冊,但並不是每個例項都需要實現,僅需要實現驅動所支援的功能即可,在demo子系統中會根據使用者的需要進行呼叫。這裡的open、release和ioctl介面都已經非常熟悉了,下面的set_data和get_data僅作為示例使用,分別用來設定和獲取驅動中的具體資料,proc介面用於proc檔案系統呼叫,後面會具體看到用法。

三、demo子系統和驅動程式流程分析


1、demo子系統初始化



這裡demo子系統的初始化符合大多數Linux外設驅動子系統初始化方案,來簡單走讀一下程式碼:

  1. /* demo子系統初始化 */
  2. static int __init demo_core_init(void)
  3. {
  4. /* 建立 demo class */
  5. demo_class = class_create(THIS_MODULE, "demo");
  6. if (IS_ERR(demo_class)) {
  7. pr_err("couldn't create class\n");
  8. return PTR_ERR(demo_class);
  9. }
  10. /* demo 裝置驅動初始化 */
  11. demo_dev_init();
  12. /* demo proc初始化 */
  13. demo_proc_init();
  14. /* demo sysfs初始化 */
  15. demo_sysfs_init(demo_class);
  16. pr_info("demo subsys init success\n");
  17. return 0;
  18. }
首先建立了一個class,具體作用詳見Linux裝置驅動模型,主要的用於在後續自動建立裝置檔案和在sysfs目錄下建立控制節點。然後呼叫的demo_dev_init()函式:
  1. void __init demo_dev_init(void)
  2. {
  3. int err;
  4. err = alloc_chrdev_region(&demo_devt, 0, DEMO_DEV_MAX, "demo");
  5. if (err < 0)
  6. pr_err("failed to allocate char dev region\n");
  7. }
該函式向核心申請了主裝置號和最大DEMO_DEV_MAX個次裝置號,也即最多可以支援DEMO_DEV_MAX個demo字元裝置,可以根據需要動態調整(佔8位,上限255)。接著初始化函式呼叫demo_proc_init()函式:
  1. void __init demo_proc_init(void)
  2. {
  3. /* 建立 demo proc 目錄 */
  4. demo_proc = proc_mkdir("driver/demo", NULL);
  5. }
這個函式的作用非常簡單就是在檔案系統的/proc/driver/目錄下建立demo子目錄,後續驅動程式的proc節點都將儲存在這個目錄下。最後初始化函式呼叫demo_sysfs_init()函式:
  1. void __init demo_sysfs_init(struct class *demo_class)
  2. {
  3. /* 繫結通用sys節點,在註冊裝置時會依次生成 */
  4. demo_class->dev_groups = demo_groups;
  5. }
該函式綁定了sysfs節點操作函式的集合,它不會立即在/sys目錄下生成節點,會在後面驅動程式註冊裝置時生成。這裡的demo_group是一個attribute函式指標陣列,如下:
  1. static struct attribute *demo_attrs[] = {
  2. &dev_attr_demo_name.attr,
  3. &dev_attr_demo_data.attr,
  4. NULL,
  5. };
  6. ATTRIBUTE_GROUPS(demo);
其中的dev_attr_demo_name和dev_attr_demo_data由巨集DEVICE_ATTR_RO和DEVICE_ATTR_RW生成,他們分別定義了只讀的和可讀可寫的attribute節點並綁定了對應的函式。其中箇中細節詳見《 Linux裝置驅動模組自載入示例與原理解析》。

這裡的demo子系統初始化流程只是一個簡單的框架模型,對於具體的裝置子系統還會進行一些其他部件的初始化。分析完初始化流程後來簡單看一下反初始化流程:

  1. static void __exit demo_core_exit(void)
  2. {
  3. demo_proc_exit();
  4. demo_dev_exit();
  5. class_destroy(demo_class);
  6. ida_destroy(&demo_ida);
  7. pr_info("demo subsys exit success\n");
  8. }

反初始化流程可以視為初始化流程的一個逆過程,非常直白。下面來分析一個具體的驅動程式是如何完成初始化並註冊到demo子系統中的。


2、驅動註冊總體流程


具體驅動的初始化流程視具體的物理特性可以千變萬化,對於依賴於I2C通訊的外設可以通過I2C匯流排完成初始化、對於依賴於SPI通訊的外設則可以通過SPI匯流排完成初始化,本文為了簡單起見使用了虛擬的platform匯流排來進行驅動的匹配和註冊操作,來詳細分析一下程式碼:

xxx_demo_driver:

  1. static struct platform_driver xxx_demo_driver = {
  2. .driver = {
  3. .name = "xxx_demo_device",
  4. .owner = THIS_MODULE,
  5. },
  6. .probe = xxx_demo_driver_probe,
  7. .remove = xxx_demo_driver_remove,
  8. };
demo0_device和demo1_device:
  1. static struct platform_device demo0_device = {
  2. .name = "xxx_demo_device",
  3. .id = 0,
  4. .dev = {
  5. .release = demo_device_release,
  6. }
  7. };
  8. static struct platform_device demo1_device = {
  9. .name = "xxx_demo_device",
  10. .id = 1,
  11. .dev = {
  12. .release = demo_device_release,
  13. }
  14. };
xxx_demo_driver定義了probe函式xxx_demo_driver_probe,它會在platform裝置和platform驅動匹配時被platform_drv_probe()函式呼叫;platform驅動由驅動模組初始化函式xxx_demo_driver_init()呼叫platform_driver_register(&xxx_demo_driver)函式註冊;platform裝置由驅動裝置模組初始化函式demo_device_init()呼叫platform_device_register(&demoX_device)函式註冊,這裡註冊了兩個xxx_demo裝置用來示例模擬多設備註冊。值得說明的是,這裡註冊platform裝置的方法是使用較為傳統的方法,較為妥當的方式是使用Linux device tree來完成裝置的註冊(後續會補上)。下面來分析probe()函式:
  1. static int xxx_demo_driver_probe(struct platform_device *pdev)
  2. {
  3. struct xxx_demo *xxx_demo = NULL;
  4. int ret = 0;
  5. /* 申請驅動結構記憶體並儲存為platform的私有資料 */
  6. xxx_demo = devm_kzalloc(&pdev->dev, sizeof(struct xxx_demo), GFP_KERNEL);
  7. if (!xxx_demo)
  8. return -ENOMEM;
  9. platform_set_drvdata(pdev, xxx_demo);
  10. /* 獲取平臺資源 */
  11. /* do something */
  12. /* 執行驅動相關初始化(包括外設硬體、鎖、佇列等)*/
  13. xxx_demo->xxx_demo_data = 0;
  14. /* do something */
  15. init_timer(&xxx_demo->xxx_demo_timer);
  16. xxx_demo->xxx_demo_timer.function = xxx_demo_time;
  17. xxx_demo->xxx_demo_timer.data = (unsigned long)xxx_demo;
  18. xxx_demo->xxx_demo_timer.expires = jiffies + HZ;
  19. add_timer(&xxx_demo->xxx_demo_timer);
  20. /* 向 demo 子系統註冊裝置 */
  21. xxx_demo->demo = devm_demo_device_register(&pdev->dev, "xxx_demo",
  22. &xxx_demo_ops, THIS_MODULE);
  23. if (IS_ERR(xxx_demo->demo)) {
  24. dev_err(&pdev->dev, "unable to register the demo class device\n");
  25. ret = PTR_ERR(xxx_demo->demo);
  26. goto err;
  27. }
  28. return 0;
  29. err:
  30. del_timer_sync(&xxx_demo->xxx_demo_timer);
  31. return ret;
  32. }
該函式首先向核心申請了xxx_demo的結構例項記憶體空間,然後並將該結構體設定為了pedv的私有資料(注意這一步很關鍵,後面用於通過pdev查詢對應的xxx_demo結構例項);然後可以獲取一些platform resource及一些物理外設引數和資源(本示例程式中沒有詳細寫出);接下來就可以開始執行驅動軀體的初始化了,可以設定一些驅動外設的物理暫存器或者如同這裡初始化可一個核心定時器,接下來最為關鍵的就是呼叫devm_demo_device_register函式完成向demo子系統的註冊工作。
  1. /* demo設備註冊澹(使用devm機制) */
  2. struct demo_device *devm_demo_device_register(struct device *dev,
  3. const char *name,
  4. const struct demo_class_ops *ops,
  5. struct module *owner)
  6. {
  7. struct demo_device **ptr, *demo;
  8. ptr = devres_alloc(devm_demo_device_release, sizeof(*ptr), GFP_KERNEL);
  9. if (!ptr)
  10. return ERR_PTR(-ENOMEM);
  11. /* 註冊 demo 裝置 */
  12. demo = demo_device_register(name, dev, ops, owner);
  13. if (!IS_ERR(demo)) {
  14. *ptr = demo;
  15. devres_add(dev, ptr);
  16. } else {
  17. devres_free(ptr);
  18. }
  19. return demo;
  20. }
該函式利用了核心的devm機制,它是一種錯誤回收機制,在初始化某步出錯的時候不需要驅動程式設計師再逐次呼叫反向去初始化,下面把注意點放到最關鍵的demo_device_register()函式:
  1. /* demo設備註冊 */
  2. struct demo_device *demo_device_register(const char *name, struct device *dev,
  3. const struct demo_class_ops *ops,
  4. struct module *owner)
  5. {
  6. struct demo_device *demo;
  7. int of_id = -1, id = -1, err;
  8. /* 獲取ID號 */
  9. if (dev->of_node)
  10. of_id = of_alias_get_id(dev->of_node, "demo");
  11. else if (dev->parent && dev->parent->of_node)
  12. of_id = of_alias_get_id(dev->parent->of_node, "demo");
  13. if (of_id >= 0) {
  14. id = ida_simple_get(&demo_ida, of_id, of_id + 1, GFP_KERNEL);
  15. if (id < 0)
  16. dev_warn(dev, "/aliases ID %d not available\n", of_id);
  17. }
  18. if (id < 0) {
  19. id = ida_simple_get(&demo_ida, 0, 0, GFP_KERNEL);
  20. if (id < 0) {
  21. err = id;
  22. goto exit;
  23. }
  24. }
  25. /* 開始分配記憶體 */
  26. demo = kzalloc(sizeof(struct demo_device), GFP_KERNEL);
  27. if (demo == NULL) {
  28. err = -ENOMEM;
  29. goto exit_ida;
  30. }
  31. /* demo 結構初始化 */
  32. demo->id = id;
  33. demo->ops = ops;
  34. demo->owner = owner;
  35. demo->dev.parent = dev;
  36. demo->dev.class = demo_class;
  37. demo->dev.release = demo_device_release;
  38. mutex_init(&demo->ops_lock);
  39. spin_lock_init(&demo->irq_lock);
  40. init_waitqueue_head(&demo->irq_queue);
  41. strlcpy(demo->name, name, DEMO_DEVICE_NAME_SIZE);
  42. dev_set_name(&demo->dev, "demo%d", id);
  43. /* 字元裝置初始化 */
  44. demo_dev_prepare(demo);
  45. err = device_register(&demo->dev);
  46. if (err) {
  47. put_device(&demo->dev);
  48. goto exit_kfree;
  49. }
  50. /* 字元裝置、sysfs裝置和proc設備註冊新增 */
  51. demo_dev_add_device(demo);
  52. demo_sysfs_add_device(demo);
  53. demo_proc_add_device(demo);
  54. dev_notice(dev, "demo core: registered %s as %s\n", demo->name, dev_name(&demo->dev));
  55. return demo;
  56. exit_kfree:
  57. kfree(demo);
  58. exit_ida:
  59. ida_simple_remove(&demo_ida, id);
  60. exit:
  61. dev_err(dev, "demo core: unable to register %s, err = %d\n", name, err);
  62. return ERR_PTR(err);
  63. }
該函式首先向dev(此處為pedv->dev)和dev->parent的of節點查詢獲取of_id號,然後以該of_id為基數獲取一個新的id號,這裡由於並不存在of_node所以會直接從demo_ida中獲取空閒的ida,前文中註冊的兩個demo驅動裝置,這裡會分別分配0和1。這裡的demo_ida由巨集定義:static DEFINE_IDA(demo_ida);

註冊函式接下來向核心申請demo_device結構的記憶體並進行賦值,分別賦值了id號、demo驅動操作函式指標ops,初始化了demo->dev結構,指定了parent為pedv->dev、class為demo_class、並指定了結構釋放回調函式,然後初始化了鎖和等待佇列。注意這裡的函式指標ops指向了xxx_demo_driver中實現的xxx_demo_ops:

  1. struct demo_class_ops xxx_demo_ops = {
  2. .open = xxx_demo_open,
  3. .release = xxx_demo_release,
  4. .ioctl = xxx_demo_ioctl,
  5. .set_data = xxx_demo_set_data,
  6. .get_data = xxx_demo_get_data,
  7. .proc = xxx_demo_proc,
  8. .read_callback = xxx_demo_read,
  9. };

然後對裝置名進行賦值,demo->name為“xxx_demo”,demo->dev.name為“demoX”,後一個名字會作為/dev/目錄裝置檔案、procfs節點檔案及sysfs節點目錄的名字。同樣的可以針對不同的需求進行其他不同元件的初始化。接下來呼叫demo_dev_prepare函式根據分配的id號和字元裝置號初始化字元裝置結構:

  1. void demo_dev_prepare(struct demo_device *demo)
  2. {
  3. if (!demo_devt)
  4. return;
  5. if (demo->id >= DEMO_DEV_MAX) {
  6. dev_warn(&demo->dev, "%s: too many demo devices\n", demo->name);
  7. return;
  8. }
  9. /* 字元裝置結構初始化 */
  10. demo->dev.devt = MKDEV(MAJOR(demo_devt), demo->id);
  11. cdev_init(&demo->char_dev, &demo_dev_fops);
  12. demo->char_dev.owner = demo->owner;
  13. }

可以看到字元裝置號由子系統初始化時分配的主裝置號和id號作為次裝置好組成,然後繫結fops函式結構demo_dev_fops:

  1. static const struct file_operations demo_dev_fops = {
  2. .owner = THIS_MODULE,
  3. .llseek = no_llseek,
  4. .read = demo_dev_read,
  5. .poll = demo_dev_poll,
  6. .unlocked_ioctl = demo_dev_ioctl,
  7. .open = demo_dev_open,
  8. .release = demo_dev_release,
  9. .fasync = demo_dev_fasync,
  10. };

初始化完字元裝置結構後接下來呼叫device_register()函式向Linux核心註冊裝置,註冊完成後就會在/sys目錄下生成對應的目錄並生成前文中繫結的attr屬性檔案demo_data和 demo_name。接著呼叫demo_dev_add_device()函式向核心新增字元裝置,新增完成後會在/dev目錄下生成對應的/demoX裝置節點。

  1. void demo_dev_add_device(struct demo_device *demo)
  2. {
  3. /* 註冊字元裝置 */
  4. if (cdev_add(&demo->char_dev, demo->dev.devt, 1))
  5. dev_warn(&demo->dev, "%s: failed to add char device %d:%d\n",
  6. demo->name, MAJOR(demo_devt), demo->id);
  7. else
  8. dev_dbg(&demo->dev, "%s: dev (%d:%d)\n", demo->name,
  9. MAJOR(demo_devt), demo->id);
  10. }

註冊函式接著呼叫demo_sysfs_add_device函式用來建立一些特殊性的attr節點。

  1. void demo_sysfs_add_device(struct demo_device *demo)
  2. {
  3. int err;
  4. /* 條件判斷 */
  5. /* do something */
  6. /* 為需要的裝置建立一些特殊的 sys 節點 */
  7. err = device_create_file(&demo->dev, &dev_attr_demodata);
  8. if (err)
  9. dev_err(demo->dev.parent,
  10. "failed to create alarm attribute, %d\n", err);
  11. }

該函式可以在進入後執行一些條件判斷,用來判別是否需要建立attr屬性檔案。本示例程式中建立了一個dev_attr_demodata屬性檔案並繫結show和set函式為demo_sysfs_show_demodata()和demo_sysfs_set_demodata()。註冊函式最後呼叫demo_proc_add_device()在/proc/driver/demo目錄下建立名為“demoX”(dev_name(&demo->dev))的檔案並繫結檔案操作函式:

  1. void demo_proc_add_device(struct demo_device *demo)
  2. {
  3. /* 為新註冊的裝置分配 proc */
  4. proc_create_data(dev_name(&demo->dev), 0, demo_proc, &demo_proc_fops, demo);
  5. }
  1. static const struct file_operations demo_proc_fops = {
  2. .open = demo_proc_open,
  3. .read = seq_read,
  4. .llseek = seq_lseek,
  5. .release = demo_proc_release,
  6. };
分析完xxx_demo驅動程式的初始化流程,來簡單看一下去初始化流程,去初始化流程在xxx_demo驅動模組的remove介面中執行:
  1. static int xxx_demo_driver_remove(struct platform_device *pdev)
  2. {
  3. struct xxx_demo *xxx_demo = platform_get_drvdata(pdev);
  4. /* 執行驅動相關去初始化(包括外設硬體、鎖、佇列等)*/
  5. del_timer_sync(&xxx_demo->xxx_demo_timer);
  6. /* do something */
  7. /* 向 demo 子系統登出裝置 */
  8. devm_demo_device_unregister(&pdev->dev, xxx_demo->demo);
  9. /* 釋放驅動結構記憶體 */
  10. devm_kfree(&pdev->dev, xxx_demo);
  11. return 0;
  12. }
該函式首先執行具體驅動程式元件的去初始化和物理硬體功能的關閉(本示例程式銷燬前文中初始化的核心定時器),然後呼叫devm_demo_device_unregister()->devres_release()函式向demo子系統執行登出流程,最後devm_kfree釋放驅動結構例項。
  1. void devm_demo_device_unregister(struct device *dev, struct demo_device *demo)
  2. {
  3. int res;
  4. /* 登出 demo 裝置 */
  5. res = devres_release(dev, devm_demo_device_release,
  6. devm_demo_device_match, demo);
  7. WARN_ON(res);
  8. }
該函式呼叫devm_demo_device_match函式找到需要的demo_device結構,然後呼叫devm_demo_device_release執行demo設備註銷
  1. static void devm_demo_device_release(struct device *dev, void *res)
  2. {
  3. struct demo_device *demo = *(struct demo_device **)res;
  4. demo_device_unregister(demo);
  5. }
  1. void demo_device_unregister(struct demo_device *demo)
  2. {
  3. if (get_device(&demo->dev) != NULL) {
  4. mutex_lock(&demo->ops_lock);
  5. demo_sysfs_del_device(demo);
  6. demo_dev_del_device(demo);
  7. demo_proc_del_device(demo);
  8. device_unregister(&demo->dev);
  9. demo->ops = NULL;
  10. mutex_unlock(&demo->ops_lock);
  11. put_device(&demo->dev);
  12. }
  13. }
這裡的去初始函式同樣非常的直觀,首先獲取裝置(增加裝置的引用計數),然後上鎖並釋放proc和sysfs屬性檔案,然後呼叫device_unregister登出裝置,最後put_device釋放裝置引用計數,在裝置引用計數降到0後會呼叫前面初始化時註冊的release函式demo_device_release():
  1. static void demo_device_release(struct device *dev)
  2. {
  3. struct demo_device *demo = to_demo_device(dev);
  4. ida_simple_remove(&demo_ida, demo->id);
  5. kfree(demo);
  6. }
這裡回收了id號並釋放demo_device結構。驅動程式註冊成功了後,下面來分析應用層是如何呼叫該驅動中的機制的。


3、應用呼叫總體流程


應用層可以通過裝置檔案/dev/demoX裝置檔案、procfs和sys屬性檔案同核心demo子系統進行互動。首先來看前面註冊的demo_dev_fops函式集合中的open函式

  1. static int demo_dev_open(struct inode *inode, struct file *file)
  2. {
  3. struct demo_device *demo = container_of(inode->i_cdev, struct demo_device, char_dev);
  4. const struct demo_class_ops *ops = demo->ops;
  5. int err;
  6. if (test_and_set_bit_lock(DEMO_DEV_BUSY, &demo->flags))
  7. return -EBUSY;
  8. file->private_data = demo;
  9. /* 呼叫驅動層 open 實現 */
  10. err = ops->open ? ops->open(demo->dev.parent) : 0;
  11. if (err == 0) {
  12. spin_lock_irq(&demo->irq_lock);
  13. /* do something while open */
  14. demo->irq_data = 0;
  15. spin_unlock_irq(&demo->irq_lock);
  16. return 0;
  17. }
  18. clear_bit_unlock(DEMO_DEV_BUSY, &demo->flags);
  19. return err;
  20. }
該函式首先找到對應的demo_device結構例項,然後通過該結構就可以找到驅動程式註冊的ops函式集合並嘗試呼叫,由於本文中xxx_demo_driver驅動程式並沒有實現該函式介面,因此不會呼叫,對於具體的驅動程式可以視情況進行實現。

另外值得注意的是,這裡是如何找到對應的demo_device結構例項的?若驅動註冊了多個結構例項會有何影響呢?

關鍵就在本函式的第一條語句中,在inode的i_cdev指標中儲存了使用者想要開啟裝置檔案的cdev結構地址,然而該結構又包含在demo_device結構中,通過它即可找到對應的demo_device例項了。同時由於Linux程序在每開啟一個檔案後都會建立一個file結構,這裡將找到的demo_device例項繫結為該file結構的私有資料,以後使用者空間對該裝置節點的其他系統呼叫操作都將能夠準確的找到該demo_device結構,不會出現錯誤。例如,前文中註冊了兩個demo裝置,在/dev目錄下就會生成兩個裝置檔案demo0和demo1。當用戶程式open demo0,就會呼叫到這裡的open函式並通過儲存在該demo0檔案中的裝置號找到對應的cdev結構,最後通過該cedv結構找到對應的demo_device結構,而不會對demo1存在影響。使用者程序在open了裝置後就可以呼叫read、ioctl等系統呼叫了,來分別看一下。

  1. static ssize_t demo_dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
  2. {
  3. struct demo_device *demo = file->private_data;
  4. DECLARE_WAITQUEUE(wait, current);
  5. unsigned long data;
  6. ssize_t ret;
  7. /* 對讀取資料量進行保護 */
  8. if (count != sizeof(unsigned int) && count < sizeof(unsigned long))
  9. return -EINVAL;
  10. /* 等待資料就緒 */
  11. add_wait_queue(&demo->irq_queue, &wait);
  12. do {
  13. __set_current_state(TASK_INTERRUPTIBLE);
  14. spin_lock_irq(&demo->irq_lock);
  15. data = demo->irq_data;
  16. demo->irq_data = 0;
  17. spin_unlock_irq(&demo->irq_lock);
  18. if (data != 0) {
  19. ret = 0;
  20. break;
  21. }
  22. if (file->f_flags & O_NONBLOCK) {
  23. ret = -EAGAIN;
  24. break;
  25. }
  26. if (signal_pending(current)) {
  27. ret = -ERESTARTSYS;
  28. break;
  29. }
  30. schedule();
  31. } while (1);
  32. set_current_state(TASK_RUNNING);
  33. remove_wait_queue(&demo->irq_queue, &wait);
  34. /* 從domo驅動層中讀取資料並傳輸到應用層 */
  35. if (ret == 0) {
  36. if (demo->ops->read_callback)
  37. data = demo->ops->read_callback(demo->dev.parent,
  38. data);
  39. if (sizeof(int) != sizeof(long) &&
  40. count == sizeof(unsigned int))
  41. ret = put_user(data, (unsigned int __user *)buf) ?:
  42. sizeof(unsigned int);
  43. else
  44. ret = put_user(data, (unsigned long __user *)buf) ?:
  45. sizeof(unsigned long);
  46. }
  47. return ret;
  48. }
該read函式同時實現了同步非阻塞和同步阻塞式的介面。如果使用者配置了O_NONBLOCK,在資料沒有ready的情況下就會直接返回失敗,否則將使程序睡眠知道資料就緒。當資料就緒後就嘗試呼叫驅動的read_callback函式來獲取資料,最後把資料傳回給應用層。
  1. static unsigned int demo_dev_poll(struct file *file, poll_table *wait)
  2. {
  3. struct demo_device *demo = file->private_data;
  4. unsigned long data;
  5. /* 加入等待佇列 */
  6. poll_wait(file, &demo->irq_queue, wait);
  7. /* 讀取資料並判斷條件是否滿足(若不滿足本呼叫程序會睡眠) */
  8. data = demo->irq_data;
  9. return (data != 0) ? (POLLIN | POLLRDNORM) : 0;
  10. }
該介面實現了非同步阻塞式介面,當用戶看空間呼叫了poll、select或epoll介面就會在此處判斷資料是否ready,若ready則以上系統呼叫直接返回,否則就會睡眠在此處準備的等待佇列中(並非在此處睡眠)。
  1. static long demo_dev_ioctl(struct file *file,
  2. unsigned int cmd, unsigned long arg)
  3. {
  4. struct demo_device *demo = file->private_data;
  5. struct demo_ctl_data demo_ctl;
  6. const struct demo_class_ops *ops = demo->ops;
  7. void __user *uarg = (void __user *) arg;
  8. int err = 0;
  9. err = mutex_lock_interruptible(&demo->ops_lock);
  10. if (err)
  11. return err;
  12. switch (cmd) {
  13. case DEMO_IOCTL_SET:
  14. /* 程序許可權限制(可選), 詳見capability.h */
  15. if (!capable(CAP_SYS_RESOURCE)) {
  16. err = -EACCES;
  17. goto done;
  18. }
  19. mutex_unlock(&demo->ops_lock);
  20. if (copy_from_user(&demo_ctl, uarg, sizeof(demo_ctl)))
  21. return -EFAULT;
  22. /* demo 示例設定命令函式 */
  23. return demo_test_set(demo, &demo_ctl);
  24. case DEMO_IOCTL_GET:
  25. mutex_unlock(&demo->ops_lock);
  26. /* demo 示例獲取命令函式 */
  27. err = demo_test_get(demo, &demo_ctl);
  28. if (err < 0)
  29. return err;
  30. if (copy_to_user(uarg, &demo_ctl, sizeof(demo_ctl)))
  31. return -EFAULT;
  32. return err;
  33. default:
  34. /* 嘗試使用驅動程式的 ioctl 介面 */
  35. if (ops->ioctl) {
  36. err = ops->ioctl(demo->dev.parent, cmd, arg);
  37. if (err == -ENOIOCTLCMD)
  38. err = -ENOTTY;
  39. } else
  40. err = -ENOTTY;
  41. break;
  42. }
  43. done:
  44. mutex_unlock(&demo->ops_lock);
  45. return err;
  46. }
該ioctl介面首先上鎖,然後如果是執行寫操作的情況判斷使用者程序的許可權,最後呼叫demo_interface.c中的介面函式demo_test_set()和demo_test_get()執行具體的操作。
  1. int demo_test_set(struct demo_device *demo, struct demo_ctl_data *demo_ctl)
  2. {
  3. int err = 0;
  4. err = mutex_lock_interruptible(&demo->ops_lock);
  5. if (err)
  6. return err;
  7. if (demo->ops == NULL)
  8. err = -ENODEV;
  9. else if (!demo->ops->set_data)
  10. err = -EINVAL;
  11. else {
  12. /* do somerhing */
  13. demo->demo_data.text_data = demo_ctl->data;
  14. /* 呼叫驅動層介面 */
  15. err = demo->ops->set_data(demo->dev.parent, demo_ctl);
  16. }
  17. mutex_unlock(&demo->ops_lock);
  18. return err;
  19. }
  20. int demo_test_get(struct demo_device *demo, struct demo_ctl_data *demo_ctl)
  21. {
  22. int err = 0;
  23. err = mutex_lock_interruptible(&demo->ops_lock);
  24. if (err)
  25. return err;
  26. if (demo->ops == NULL)
  27. err = -ENODEV;
  28. else if (!demo->ops->get_data)
  29. err = -EINVAL;
  30. else {
  31. /* do somerhing */
  32. demo_ctl->data = demo->demo_data.text_data;
  33. /* 呼叫驅動層介面 */
  34. err = demo->ops->get_data(demo->dev.parent, demo_ctl);
  35. }
  36. mutex_unlock(&demo->ops_lock);
  37. return err;
  38. }

本示例程式這兩個函式實現的較為簡單,分別是設定和返回demo_data中的text_data值,然後如果驅動程式實現了自己的get_data和set_data函式介面則會呼叫它們,這裡的思想有點類似面向物件中的派生。在前文中已經看到xxx_demo_driver驅動程式中已經實現了這兩個介面:

  1. static int xxx_demo_set_data(struct device *dev, struct demo_ctl_data *ctrl_data)
  2. {
  3. struct xxx_demo *xxx_demo = dev_get_drvdata(dev);
  4. printk(KERN_INFO "xxx demo set data\n");
  5. xxx_demo->xxx_demo_data = ctrl_data->data;
  6. return 0;
  7. }
  8. static int xxx_demo_get_data(struct device *dev, struct demo_ctl_data *ctrl_data)
  9. {
  10. struct xxx_demo *xxx_demo = dev_get_drvdata(dev);
  11. printk(KERN_INFO "xxx demo get data\n");
  12. ctrl_data->data = xxx_demo->xxx_demo_data;
  13. return 0;
  14. }
驅動程式中的這兩個函式會設定和獲取驅動程式自己的xxx_demo_data,來替代demo子系統中的通用demo_data,如果此處沒有實現這兩個介面,則不會改變demo子系統中的值。然後再來看一下fops中的最後一個release介面:
  1. static int demo_dev_release(struct inode *inode, struct file *file)
  2. {
  3. struct demo_device *demo = file->private_data;
  4. /* do something while exit */
  5. /* 呼叫驅動層 release 實現 */
  6. if (demo->ops->release)
  7. demo->ops->release(demo->dev.parent);
  8. clear_bit_unlock(DEMO_DEV_BUSY, &demo->flags);
  9. return 0;
  10. }
這個介面會在應用程式呼叫close(fd)時呼叫執行,首先執行一些通用的release操作,然後同樣的如果驅動程式實現了自己的release介面則呼叫驅動自己的介面實現驅動的close操作。下面來看一下使用者通過procfs和demo子系統互動的流程,首先來看proc檔案系統的介面demo_proc_fops
  1. static const struct file_operations demo_proc_fops = {
  2. .open = demo_proc_open,
  3. .read = seq_read,
  4. .llseek = seq_lseek,
  5. .release = demo_proc_release,
  6. };
這裡幾個函式的實現基於seq_file子系統,在demo_proc_open中建立了seq_operations結構和seq_file結構例項並綁定了show函式為demo_proc_show,當用戶讀取/proc/driver/demo/demoX時,會呼叫seq_read()->demo_proc_show()函式:
  1. static int demo_proc_show(struct seq_file *seq, void *offset)
  2. {
  3. int err = 0;
  4. struct demo_device *demo = seq->private;
  5. const struct demo_class_ops *ops = demo->ops;
  6. /* 輸出需要的subsys proc資訊 */
  7. seq_printf(seq, "demo_com_data\t: %ld\n", demo->demo_data.text_data);
  8. seq_printf(seq, "\n");
  9. /* 輸出驅動層proc資訊 */
  10. if (ops->proc)
  11. err = ops->proc(demo->dev.parent, seq);
  12. return err;
  13. }
這裡可以依次呼叫seq_printf輸出一些子系統方面的通用資訊,如果驅動程式也需要輸出並提供了proc介面則呼叫它:
  1. static int xxx_demo_proc(struct device *dev, struct seq_file *seq)
  2. {
  3. struct xxx_demo *xxx_demo = dev_get_drvdata(dev);
  4. seq_printf(seq, "xxx_demo_data\t: %ld\n", xxx_demo->xxx_demo_data);
  5. seq_printf(seq, "\n");
  6. return 0;
  7. }
通過該proc介面,應用程式可以非常方便的獲取核心demo子系統和驅動上的資訊。最後來看下面來看一下使用者通過sysfs和demo子系統的互動流程,前文中驅動註冊流程中註冊了dev_attr_demo_name和dev_attr_demo_data兩個通用的屬性檔案和一個
dev_attr_demodata特殊屬性檔案。其中dev_attr_demo_name屬性檔案是隻讀的,所繫結的show介面函式為
  1. static ssize_t
  2. demo_name_show(struct device *dev, struct device_attribute *attr, char *buf)
  3. {
  4. return sprintf(buf, "%s\n", to_demo_device(dev)->name);
  5. }
  6. static DEVICE_ATTR_RO(demo_name);
該函式將裝置的名字輸出到使用者空間。另一個dev_attr_demo_data屬性檔案為可讀可寫的,其繫結的show介面和store介面為:
  1. static ssize_t
  2. demo_data_show(struct device *dev, struct device_attribute *attr, char *buf)
  3. {
  4. return sprintf(buf, "%ld\n", to_demo_device(dev)->demo_data.text_data);
  5. }
  6. static ssize_t
  7. demo_data_store(struct device *dev, struct device_attribute *attr,
  8. const char *buf, size_t n)
  9. {
  10. struct demo_device *demo = to_demo_device(dev);
  11. unsigned long val = simple_strtoul(buf, NULL, 0);
  12. if (val >= 4096 || val == 0)
  13. return -EINVAL;
  14. demo->demo_data.text_data = (unsigned long)val;
  15. return n;
  16. }

這裡分別設定和輸出子系統中的demo_data。最後來看dev_attr_demodata的store介面:

  1. static ssize_t
  2. demo_sysfs_set_demodata(struct device *dev, struct device_attribute *attr,
  3. const char *buf, size_t n)
  4. {
  5. struct demo_device *demo = to_demo_device(dev);
  6. struct demo_ctl_data demo_ctl;
  7. unsigned long val = 0;
  8. ssize_t retval;
  9. val = simple_strtoul(buf, NULL, 0);
  10. if (val >= 4096 || val == 0)
  11. retval = -EINVAL;
  12. /* 呼叫interface介面寫入驅動資料 */
  13. demo_ctl.data = (unsigned long)val;
  14. retval = demo_test_set(demo, &demo_ctl);
  15. return (retval < 0) ? retval : n;
  16. }
這裡接收到使用者輸入的資料後然後封裝呼叫interface中的demo_test_set介面,就同ioctl相同,通過sysfs介面使用者也可以非常方便的同驅動驅動程式互動。


三、demo驅動和子系統演示


首先使用如下Makefile程式進行編譯:
  1. ifneq ($(KERNELRELEASE),)
  2. obj-m := demo.o
  3. demo-objs := demo_core.o demo_dev.o demo_interface.o demo_proc.o demo_sysfs.o
  4. obj-m += xxx_demo_driver.o
  5. obj-m += xxx_demo_device.o
  6. else
  7. KDIR := /home/apple/raspberry/build/linux-rpi-4.1.y
  8. all:prepare
  9. make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-bcm2708-linux-gnueabi-
  10. cp *.ko ./release/
  11. prepare:
  12. mkdir release
  13. modules_install:
  14. make -C $(KDIR) M=$(PWD) modules_install ARCH=arm CROSS_COMPILE=arm-bcm2708-linux-gnueabi-
  15. clean:
  16. rm -f *.ko *.o *.mod.o *.mod.c *.symvers modul*
  17. rm -f ./release/*
  18. endif
編譯後在relseae目錄下得到3個模組ko檔案demo.ko、xxx_demo_device.ko和xxx_demo_driver.ko,將他們拷貝到樹莓派中依次載入:
root@apple:~# insmod demo.ko
root@apple:~# insmod xxx_demo_driver.ko
root@apple:~# insmod xxx_demo_device.ko
root@apple:~# lsmod
Module Size Used by
xxx_demo_device 1604 0
xxx_demo_driver 2935 0
demo 9989 1 xxx_demo_driver 載入完成後可以在/dev/目錄下看到生成的裝置檔案: root@apple:/dev# ls /dev/demo*
/dev/demo0 /dev/demo1 然後在/proc/driver/demo/目錄下看到生成的屬性檔案: root@apple:/dev# ls /proc/driver/demo/demo*
/proc/driver/demo/demo0 /proc/driver/demo/demo1

讀取其中一個就夠可以看到輸出了:

root@apple:/dev# cat /proc/driver/demo/demo0
demo_com_data : 0

xxx_demo_data : 206

這裡的xxx_demo_data會一直持續累加(約每1s累加1)

最後在/sys/class/demo目錄下可以看到生成的兩個目錄,他們是指向device目錄下對應的連結檔案:

root@apple:/sys/class/demo# ls
demo0 demo1

root@apple:/sys/class/demo# ls -l
total 0
lrwxrwxrwx 1 root root 0 Jan 1 01:26 demo0 -> ../../devices/platform/xxx_demo_device.0/demo/demo0
lrwxrwxrwx 1 root root 0 Jan 1 01:27 demo1 -> ../../devices/platform/xxx_demo_device.1/demo/demo1

開啟其中一個可以看到:

root@apple:/sys/class/demo/demo0# ls
demo_data demo_name demodata dev device subsystem uevent

其中demo_data、demo_name和demodata就是前文中程式生成的屬性檔案了,可以讀取和寫入數值

root@apple:/sys/class/demo/demo0# cat demodata
0

root@apple:/sys/class/demo/demo0# echo 100 > demo_data
root@apple:/sys/class/demo/demo0# cat demodata
100


四、總結


最後總結一下,設計demo子系統的核心用意就在於能夠提取出不同demo驅動中相似的部分進行歸一化。底層可以有xxx_demo_driver、yyy_demo_driver、zzz_demo_drive等等,他們主要都是為Linux應用提供一個特定的服務,但是它們可能擁有不同的通訊總線,不同的暫存器配置,甚至功能也有細微的差別,但是隻要能夠設計出類似上文中的這麼一個demo子系統就可以把這些驅動外設的差異遮蔽起來,子系統要求驅動程式用一套標準的介面與其對接(不支援的功能可以不用實現),這樣子系統就可以對這些驅動進行歸一化的管理,結構更為清晰,層次更為分明,程式碼的重複率大大降低,最終為應用程式提供一套標準的介面,應用層序的設計也可以大大的簡化。














本文來自部落格園,作者:whilewell,轉載請註明原文連結:https://www.cnblogs.com/viiv/p/15613279.html