構建Linux內核驅動demo子系統示例

一般在編寫嵌入式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

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

2、demo_device

struct demo_device
{
	struct device dev;
	struct module *owner;

	int id;
	char name[DEMO_DEVICE_NAME_SIZE];

	const struct demo_class_ops *ops;
	struct mutex ops_lock;

	struct cdev char_dev;
	unsigned long flags;

	unsigned long irq_data;
	spinlock_t irq_lock;
	wait_queue_head_t irq_queue;
	struct fasync_struct *async_queue;

	/* some demo data */
	struct demo_data demo_data;
};
該結構體中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

struct demo_data
{
	unsigned long text_data;
	/* something else */
};

該結構包含在demo_device結構體中,包含了一些demo驅動中通用性的參數以及demo子系統爲了屏蔽底層差異並向用戶提供統一接口時需要而提取出來的需要統一管理的參數等等,這裏僅示例性的列了一個demo_data數據。


4、demo_class_ops

struct demo_class_ops {
	int (*open)(struct device *);
	void (*release)(struct device *);
	int (*ioctl)(struct device *, unsigned int, unsigned long);
	int (*set_data)(struct device *, struct demo_ctl_data *);
	int (*get_data)(struct device *, struct demo_ctl_data *);
	int (*proc)(struct device *, struct seq_file *);
	int (*read_callback)(struct device *, int data);
};
該結構爲demo驅動程序的註冊函數接口,由具體的xxx_demo驅動負責實現並向demo子系統註冊,但並不是每個實例都需要實現,僅需要實現驅動所支持的功能即可,在demo子系統中會根據用戶的需要進行調用。這裏的open、release和ioctl接口都已經非常熟悉了,下面的set_data和get_data僅作爲示例使用,分別用來設置和獲取驅動中的具體數據,proc接口用於proc文件系統調用,後面會具體看到用法。


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


1、demo子系統初始化



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

/* demo子系統初始化 */
static int __init demo_core_init(void)
{ 
	/* 創建 demo class */
	demo_class = class_create(THIS_MODULE, "demo");
	if (IS_ERR(demo_class)) {
		pr_err("couldn't create class\n");
		return PTR_ERR(demo_class);
	}

	/* demo 設備驅動初始化 */
	demo_dev_init();

	/* demo proc初始化 */
	demo_proc_init();
	
	/* demo sysfs初始化 */
	demo_sysfs_init(demo_class);

	pr_info("demo subsys init success\n");	
	return 0;
}
首先創建了一個class,具體作用詳見Linux設備驅動模型,主要的用於在後續自動創建設備文件和在sysfs目錄下創建控制節點。然後調用的demo_dev_init()函數:

void __init demo_dev_init(void)
{
	int err;

	err = alloc_chrdev_region(&demo_devt, 0, DEMO_DEV_MAX, "demo");
	if (err < 0)
		pr_err("failed to allocate char dev region\n");
}
該函數向內核申請了主設備號和最大DEMO_DEV_MAX個次設備號,也即最多可以支持DEMO_DEV_MAX個demo字符設備,可以根據需要動態調整(佔8位,上限255)。接着初始化函數調用demo_proc_init()函數:

void __init demo_proc_init(void)
{
	/* 創建 demo proc 目錄 */
	demo_proc = proc_mkdir("driver/demo", NULL);
}
這個函數的作用非常簡單就是在文件系統的/proc/driver/目錄下創建demo子目錄,後續驅動程序的proc節點都將保存在這個目錄下。最後初始化函數調用demo_sysfs_init()函數:

void __init demo_sysfs_init(struct class *demo_class)
{
	/* 綁定通用sys節點,在註冊設備時會依次生成 */
	demo_class->dev_groups = demo_groups;
}
該函數綁定了sysfs節點操作函數的集合,它不會立即在/sys目錄下生成節點,會在後面驅動程序註冊設備時生成。這裏的demo_group是一個attribute函數指針數組,如下:

static struct attribute *demo_attrs[] = {
	&dev_attr_demo_name.attr,
	&dev_attr_demo_data.attr,
	NULL,
};
ATTRIBUTE_GROUPS(demo);
其中的dev_attr_demo_name和dev_attr_demo_data由宏DEVICE_ATTR_RO和DEVICE_ATTR_RW生成,他們分別定義了只讀的和可讀可寫的attribute節點並綁定了對應的函數。其中箇中細節詳見《Linux設備驅動模塊自加載示例與原理解析》。

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

static void __exit demo_core_exit(void)
{
	demo_proc_exit();
	demo_dev_exit();
	class_destroy(demo_class);
	ida_destroy(&demo_ida);
	
	pr_info("demo subsys exit success\n");
}

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


2、驅動註冊總體流程


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

xxx_demo_driver:

static struct platform_driver xxx_demo_driver = {
	.driver	= {
		.name    = "xxx_demo_device",   
		.owner	 = THIS_MODULE,
	},
	.probe   = xxx_demo_driver_probe,
	.remove  = xxx_demo_driver_remove,
};
demo0_device和demo1_device:

static struct platform_device demo0_device = {
	.name		= "xxx_demo_device",
	.id		    = 0,
	.dev		= {
		.release	= demo_device_release,
	}
};

static struct platform_device demo1_device = {
	.name		= "xxx_demo_device",
	.id		    = 1,
	.dev		= {
		.release	= demo_device_release,
	}
};
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()函數:

static int xxx_demo_driver_probe(struct platform_device *pdev)
{
	struct xxx_demo *xxx_demo = NULL;
	int ret = 0;
	
	/* 申請驅動結構內存並保存爲platform的私有數據 */
	xxx_demo = devm_kzalloc(&pdev->dev, sizeof(struct xxx_demo), GFP_KERNEL);
	if (!xxx_demo)
		return -ENOMEM;

	platform_set_drvdata(pdev, xxx_demo);

	/* 獲取平臺資源 */
	/* do something */

	
	/* 執行驅動相關初始化(包括外設硬件、鎖、隊列等)*/
	xxx_demo->xxx_demo_data = 0;
	/* do something */
	init_timer(&xxx_demo->xxx_demo_timer);
	xxx_demo->xxx_demo_timer.function = xxx_demo_time;
	xxx_demo->xxx_demo_timer.data = (unsigned long)xxx_demo;
	xxx_demo->xxx_demo_timer.expires = jiffies + HZ;
	add_timer(&xxx_demo->xxx_demo_timer);

	/* 向 demo 子系統註冊設備 */
	xxx_demo->demo = devm_demo_device_register(&pdev->dev, "xxx_demo",
				&xxx_demo_ops, THIS_MODULE);
	if (IS_ERR(xxx_demo->demo)) {
		dev_err(&pdev->dev, "unable to register the demo class device\n");
		ret = PTR_ERR(xxx_demo->demo);
		goto err;
	}
	
	return 0;

err:
	del_timer_sync(&xxx_demo->xxx_demo_timer);
	return ret;
}
該函數首先向內核申請了xxx_demo的結構實例內存空間,然後並將該結構體設置爲了pedv的私有數據(注意這一步很關鍵,後面用於通過pdev查找對應的xxx_demo結構實例);然後可以獲取一些platform resource及一些物理外設參數和資源(本示例程序中沒有詳細寫出);接下來就可以開始執行驅動軀體的初始化了,可以設置一些驅動外設的物理寄存器或者如同這裏初始化可一個內核定時器,接下來最爲關鍵的就是調用devm_demo_device_register函數完成向demo子系統的註冊工作。

/* demo設備註冊澹(使用devm機制) */
struct demo_device *devm_demo_device_register(struct device *dev,
					const char *name,
					const struct demo_class_ops *ops,
					struct module *owner)
{
	struct demo_device **ptr, *demo;

	ptr = devres_alloc(devm_demo_device_release, sizeof(*ptr), GFP_KERNEL);
	if (!ptr)
		return ERR_PTR(-ENOMEM);

	/* 註冊 demo 設備 */
	demo = demo_device_register(name, dev, ops, owner);
	if (!IS_ERR(demo)) {
		*ptr = demo;
		devres_add(dev, ptr);
	} else {
		devres_free(ptr);
	}

	return demo;
}
該函數利用了內核的devm機制,它是一種錯誤回收機制,在初始化某步出錯的時候不需要驅動程序員再逐次調用反向去初始化,下面把注意點放到最關鍵的demo_device_register()函數:

/* demo設備註冊 */
struct demo_device *demo_device_register(const char *name, struct device *dev,
					const struct demo_class_ops *ops,
					struct module *owner)
{
	struct demo_device *demo;
	int of_id = -1, id = -1, err;

	/* 獲取ID號 */
	if (dev->of_node)
		of_id = of_alias_get_id(dev->of_node, "demo");
	else if (dev->parent && dev->parent->of_node)
		of_id = of_alias_get_id(dev->parent->of_node, "demo");

	if (of_id >= 0) {
		id = ida_simple_get(&demo_ida, of_id, of_id + 1, GFP_KERNEL);
		if (id < 0)
			dev_warn(dev, "/aliases ID %d not available\n", of_id);
	}

	if (id < 0) {
		id = ida_simple_get(&demo_ida, 0, 0, GFP_KERNEL);
		if (id < 0) {
			err = id;
			goto exit;
		}
	}

	/* 開始分配內存 */
	demo = kzalloc(sizeof(struct demo_device), GFP_KERNEL);
	if (demo == NULL) {
		err = -ENOMEM;
		goto exit_ida;
	}

	/* demo 結構初始化 */
	demo->id = id;
	demo->ops = ops;
	demo->owner = owner;
	demo->dev.parent = dev;
	demo->dev.class = demo_class;
	demo->dev.release = demo_device_release;

	mutex_init(&demo->ops_lock);
	spin_lock_init(&demo->irq_lock);
	init_waitqueue_head(&demo->irq_queue);

	strlcpy(demo->name, name, DEMO_DEVICE_NAME_SIZE);
	dev_set_name(&demo->dev, "demo%d", id);

	/* 字符設備初始化 */
	demo_dev_prepare(demo);

	err = device_register(&demo->dev);
	if (err) {
		put_device(&demo->dev);
		goto exit_kfree;
	}

	/* 字符設備、sysfs設備和proc設備註冊添加 */
	demo_dev_add_device(demo);
	demo_sysfs_add_device(demo);
	demo_proc_add_device(demo);

	dev_notice(dev, "demo core: registered %s as %s\n", demo->name, dev_name(&demo->dev));

	return demo;

exit_kfree:
	kfree(demo);

exit_ida:
	ida_simple_remove(&demo_ida, id);

exit:
	dev_err(dev, "demo core: unable to register %s, err = %d\n", name, err);
	return ERR_PTR(err);
}
該函數首先向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:

struct demo_class_ops xxx_demo_ops = {
	.open		= xxx_demo_open,
	.release	= xxx_demo_release,
	.ioctl		= xxx_demo_ioctl,
	.set_data	= xxx_demo_set_data,
	.get_data	= xxx_demo_get_data,
	.proc		= xxx_demo_proc,
	.read_callback	= xxx_demo_read,
};

然後對設備名進行賦值,demo->name爲“xxx_demo”,demo->dev.name爲“demoX”,後一個名字會作爲/dev/目錄設備文件、procfs節點文件及sysfs節點目錄的名字。同樣的可以針對不同的需求進行其他不同組件的初始化。接下來調用demo_dev_prepare函數根據分配的id號和字符設備號初始化字符設備結構:

void demo_dev_prepare(struct demo_device *demo)
{
	if (!demo_devt)
		return;

	if (demo->id >= DEMO_DEV_MAX) {
		dev_warn(&demo->dev, "%s: too many demo devices\n", demo->name);
		return;
	}

	/* 字符設備結構初始化 */
	demo->dev.devt = MKDEV(MAJOR(demo_devt), demo->id);

	cdev_init(&demo->char_dev, &demo_dev_fops);
	demo->char_dev.owner = demo->owner;
}

可以看到字符設備號由子系統初始化時分配的主設備號和id號作爲次設備好組成,然後綁定fops函數結構demo_dev_fops:

static const struct file_operations demo_dev_fops = {
	.owner		= THIS_MODULE,
	.llseek		= no_llseek,
	.read		= demo_dev_read,
	.poll		= demo_dev_poll,
	.unlocked_ioctl	= demo_dev_ioctl,
	.open		= demo_dev_open,
	.release	= demo_dev_release,
	.fasync		= demo_dev_fasync,
};

初始化完字符設備結構後接下來調用device_register()函數向Linux內核註冊設備,註冊完成後就會在/sys目錄下生成對應的目錄並生成前文中綁定的attr屬性文件demo_data和 demo_name。接着調用demo_dev_add_device()函數向內核添加字符設備,添加完成後會在/dev目錄下生成對應的/demoX設備節點。

void demo_dev_add_device(struct demo_device *demo)
{
	/* 註冊字符設備 */
	if (cdev_add(&demo->char_dev, demo->dev.devt, 1))
		dev_warn(&demo->dev, "%s: failed to add char device %d:%d\n",
			demo->name, MAJOR(demo_devt), demo->id);
	else
		dev_dbg(&demo->dev, "%s: dev (%d:%d)\n", demo->name,
			MAJOR(demo_devt), demo->id);
}

註冊函數接着調用demo_sysfs_add_device函數用來創建一些特殊性的attr節點。

void demo_sysfs_add_device(struct demo_device *demo)
{
	int err;

	/* 條件判斷 */
	/* do something */

	/* 爲需要的設備創建一些特殊的 sys 節點 */
	err = device_create_file(&demo->dev, &dev_attr_demodata);
	if (err)
		dev_err(demo->dev.parent,
			"failed to create alarm attribute, %d\n", err);
}

該函數可以在進入後執行一些條件判斷,用來判別是否需要創建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))的文件並綁定文件操作函數:

void demo_proc_add_device(struct demo_device *demo)
{
	/* 爲新註冊的設備分配 proc */
	proc_create_data(dev_name(&demo->dev), 0, demo_proc, &demo_proc_fops, demo);
}
static const struct file_operations demo_proc_fops = {
	.open		= demo_proc_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= demo_proc_release,
};
分析完xxx_demo驅動程序的初始化流程,來簡單看一下去初始化流程,去初始化流程在xxx_demo驅動模塊的remove接口中執行:

static int xxx_demo_driver_remove(struct platform_device *pdev)
{
	struct xxx_demo *xxx_demo = platform_get_drvdata(pdev);

	/* 執行驅動相關去初始化(包括外設硬件、鎖、隊列等)*/
	del_timer_sync(&xxx_demo->xxx_demo_timer);	
	/* do something */

	/* 向 demo 子系統註銷設備 */
	devm_demo_device_unregister(&pdev->dev, xxx_demo->demo);

	/* 釋放驅動結構內存 */
	devm_kfree(&pdev->dev, xxx_demo);
	
	return 0;
}
該函數首先執行具體驅動程序組件的去初始化和物理硬件功能的關閉(本示例程序銷燬前文中初始化的內核定時器),然後調用devm_demo_device_unregister()->devres_release()函數向demo子系統執行註銷流程,最後devm_kfree釋放驅動結構實例。
void devm_demo_device_unregister(struct device *dev, struct demo_device *demo)
{
	int res;

	/* 註銷 demo 設備 */
	res = devres_release(dev, devm_demo_device_release,
				devm_demo_device_match, demo);
	
	WARN_ON(res);
}
該函數調用devm_demo_device_match函數找到需要的demo_device結構,然後調用devm_demo_device_release執行demo設備註銷
static void devm_demo_device_release(struct device *dev, void *res)
{
	struct demo_device *demo = *(struct demo_device **)res;

	demo_device_unregister(demo);
}
void demo_device_unregister(struct demo_device *demo)
{
	if (get_device(&demo->dev) != NULL) {
		mutex_lock(&demo->ops_lock);
		demo_sysfs_del_device(demo);
		demo_dev_del_device(demo);
		demo_proc_del_device(demo);
		device_unregister(&demo->dev);
		demo->ops = NULL;
		mutex_unlock(&demo->ops_lock);
		put_device(&demo->dev);
	}
}
這裏的去初始函數同樣非常的直觀,首先獲取設備(增加設備的引用計數),然後上鎖並釋放proc和sysfs屬性文件,然後調用device_unregister註銷設備,最後put_device釋放設備引用計數,在設備引用計數降到0後會調用前面初始化時註冊的release函數demo_device_release():

static void demo_device_release(struct device *dev)
{
	struct demo_device *demo = to_demo_device(dev);
	ida_simple_remove(&demo_ida, demo->id);
	kfree(demo);
}
這裏回收了id號並釋放demo_device結構。驅動程序註冊成功了後,下面來分析應用層是如何調用該驅動中的機制的。


3、應用調用總體流程


應用層可以通過設備文件/dev/demoX設備文件、procfs和sys屬性文件同內核demo子系統進行交互。首先來看前面註冊的demo_dev_fops函數集合中的open函數

static int demo_dev_open(struct inode *inode, struct file *file)
{
	struct demo_device *demo = container_of(inode->i_cdev, struct demo_device, char_dev);
	const struct demo_class_ops *ops = demo->ops;
	int err;

	if (test_and_set_bit_lock(DEMO_DEV_BUSY, &demo->flags))
		return -EBUSY;

	file->private_data = demo;

	/* 調用驅動層 open 實現 */
	err = ops->open ? ops->open(demo->dev.parent) : 0;
	if (err == 0) {
		spin_lock_irq(&demo->irq_lock);
		/* do something while open */
		demo->irq_data = 0;
		spin_unlock_irq(&demo->irq_lock);

		return 0;
	}

	clear_bit_unlock(DEMO_DEV_BUSY, &demo->flags);
	return err;
}
該函數首先找到對應的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等系統調用了,來分別看一下。

static ssize_t demo_dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	struct demo_device *demo = file->private_data;
	
	DECLARE_WAITQUEUE(wait, current);
	unsigned long data;
	ssize_t ret;

	/* 對讀取數據量進行保護 */
	if (count != sizeof(unsigned int) && count < sizeof(unsigned long))
		return -EINVAL;

	/* 等待數據就緒 */
	add_wait_queue(&demo->irq_queue, &wait);
	do {
		__set_current_state(TASK_INTERRUPTIBLE);

		spin_lock_irq(&demo->irq_lock);
		data = demo->irq_data;
		demo->irq_data = 0;
		spin_unlock_irq(&demo->irq_lock);

		if (data != 0) {
			ret = 0;
			break;
		}
		if (file->f_flags & O_NONBLOCK) {
			ret = -EAGAIN;
			break;
		}
		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			break;
		}
		schedule();
	} while (1);
	set_current_state(TASK_RUNNING);
	remove_wait_queue(&demo->irq_queue, &wait);

	/* 從domo驅動層中讀取數據並傳輸到應用層 */
	if (ret == 0) {
		if (demo->ops->read_callback)
			data = demo->ops->read_callback(demo->dev.parent,
						       data);

		if (sizeof(int) != sizeof(long) &&
		    count == sizeof(unsigned int))
			ret = put_user(data, (unsigned int __user *)buf) ?:
				sizeof(unsigned int);
		else
			ret = put_user(data, (unsigned long __user *)buf) ?:
				sizeof(unsigned long);
	}
	return ret;
}
該read函數同時實現了同步非阻塞和同步阻塞式的接口。如果用戶配置了O_NONBLOCK,在數據沒有ready的情況下就會直接返回失敗,否則將使進程睡眠知道數據就緒。當數據就緒後就嘗試調用驅動的read_callback函數來獲取數據,最後把數據傳回給應用層。

static unsigned int demo_dev_poll(struct file *file, poll_table *wait)
{
	struct demo_device *demo = file->private_data;
	unsigned long data;

	/* 加入等待隊列 */
	poll_wait(file, &demo->irq_queue, wait);

	/* 讀取數據並判斷條件是否滿足(若不滿足本調用進程會睡眠) */
	data = demo->irq_data;

	return (data != 0) ? (POLLIN | POLLRDNORM) : 0;
}
該接口實現了異步阻塞式接口,當用戶看空間調用了poll、select或epoll接口就會在此處判斷數據是否ready,若ready則以上系統調用直接返回,否則就會睡眠在此處準備的等待隊列中(並非在此處睡眠)。

static long demo_dev_ioctl(struct file *file,
		unsigned int cmd, unsigned long arg)
{
	struct demo_device *demo = file->private_data;
	struct demo_ctl_data demo_ctl;
	const struct demo_class_ops *ops = demo->ops;
	void __user *uarg = (void __user *) arg;
	int err = 0;

	err = mutex_lock_interruptible(&demo->ops_lock);
	if (err)
		return err;

	switch (cmd) {

	case DEMO_IOCTL_SET:
		/* 進程權限限制(可選), 詳見capability.h */
		if (!capable(CAP_SYS_RESOURCE)) {
			err = -EACCES;
			goto done;
		}

		mutex_unlock(&demo->ops_lock);

		if (copy_from_user(&demo_ctl, uarg, sizeof(demo_ctl)))
			return -EFAULT;

		/* demo 示例設置命令函數 */
		return demo_test_set(demo, &demo_ctl);
		
	case DEMO_IOCTL_GET:
		mutex_unlock(&demo->ops_lock);

		/* demo 示例獲取命令函數 */
		err = demo_test_get(demo, &demo_ctl);
		if (err < 0)
			return err;

		if (copy_to_user(uarg, &demo_ctl, sizeof(demo_ctl))) 
			return -EFAULT;

		return err;
		
	default:
		/* 嘗試使用驅動程序的 ioctl 接口 */
		if (ops->ioctl) {
			err = ops->ioctl(demo->dev.parent, cmd, arg);
			if (err == -ENOIOCTLCMD)
				err = -ENOTTY;
		} else
			err = -ENOTTY;
		break;
	}

done:
	mutex_unlock(&demo->ops_lock);
	return err;
}
該ioctl接口首先上鎖,然後如果是執行寫操作的情況判斷用戶進程的權限,最後調用demo_interface.c中的接口函數demo_test_set()和demo_test_get()執行具體的操作。

int demo_test_set(struct demo_device *demo, struct demo_ctl_data *demo_ctl)
{
	int err = 0;

	err = mutex_lock_interruptible(&demo->ops_lock);
	if (err)
		return err;	

	if (demo->ops == NULL)
		err = -ENODEV;
	else if (!demo->ops->set_data)
		err = -EINVAL;
	else {
		/* do somerhing */
		demo->demo_data.text_data = demo_ctl->data;

		/* 調用驅動層接口 */
		err = demo->ops->set_data(demo->dev.parent, demo_ctl);
	}
	mutex_unlock(&demo->ops_lock);

	return err;	
}

int demo_test_get(struct demo_device *demo, struct demo_ctl_data *demo_ctl)
{
	int err = 0;

	err = mutex_lock_interruptible(&demo->ops_lock);
	if (err)
		return err;	

	if (demo->ops == NULL)
		err = -ENODEV;
	else if (!demo->ops->get_data)
		err = -EINVAL;
	else {
		/* do somerhing */
		demo_ctl->data = demo->demo_data.text_data;

		/* 調用驅動層接口 */
		err = demo->ops->get_data(demo->dev.parent, demo_ctl);
	}
	mutex_unlock(&demo->ops_lock);

	return err;	
}

本示例程序這兩個函數實現的較爲簡單,分別是設置和返回demo_data中的text_data值,然後如果驅動程序實現了自己的get_data和set_data函數接口則會調用它們,這裏的思想有點類似面向對象中的派生。在前文中已經看到xxx_demo_driver驅動程序中已經實現了這兩個接口:

static int xxx_demo_set_data(struct device *dev, struct demo_ctl_data *ctrl_data)
{
	struct xxx_demo *xxx_demo = dev_get_drvdata(dev);
	
	printk(KERN_INFO "xxx demo set data\n");

	xxx_demo->xxx_demo_data = ctrl_data->data;
	return 0;
}

static int xxx_demo_get_data(struct device *dev, struct demo_ctl_data *ctrl_data)
{
	struct xxx_demo *xxx_demo = dev_get_drvdata(dev);
	
	printk(KERN_INFO "xxx demo get data\n");

	ctrl_data->data = xxx_demo->xxx_demo_data;
	return 0;
}
驅動程序中的這兩個函數會設置和獲取驅動程序自己的xxx_demo_data,來替代demo子系統中的通用demo_data,如果此處沒有實現這兩個接口,則不會改變demo子系統中的值。然後再來看一下fops中的最後一個release接口:

static int demo_dev_release(struct inode *inode, struct file *file)
{
	struct demo_device *demo = file->private_data;

	/* do something while exit */

	/* 調用驅動層 release 實現 */
	if (demo->ops->release)
		demo->ops->release(demo->dev.parent);

	clear_bit_unlock(DEMO_DEV_BUSY, &demo->flags);
	return 0;
}
這個接口會在應用程序調用close(fd)時調用執行,首先執行一些通用的release操作,然後同樣的如果驅動程序實現了自己的release接口則調用驅動自己的接口實現驅動的close操作。下面來看一下用戶通過procfs和demo子系統交互的流程,首先來看proc文件系統的接口demo_proc_fops
static const struct file_operations demo_proc_fops = {
	.open		= demo_proc_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= demo_proc_release,
};
這裏幾個函數的實現基於seq_file子系統,在demo_proc_open中創建了seq_operations結構和seq_file結構實例並綁定了show函數爲demo_proc_show,當用戶讀取/proc/driver/demo/demoX時,會調用seq_read()->demo_proc_show()函數:

static int demo_proc_show(struct seq_file *seq, void *offset)
{
	int err = 0;
	struct demo_device *demo = seq->private;
	const struct demo_class_ops *ops = demo->ops;

	/* 輸出需要的subsys proc信息 */
	seq_printf(seq, "demo_com_data\t: %ld\n", demo->demo_data.text_data);
	seq_printf(seq, "\n");

	/* 輸出驅動層proc信息 */
	if (ops->proc)
		err = ops->proc(demo->dev.parent, seq);

	return err;
}
這裏可以依次調用seq_printf輸出一些子系統方面的通用信息,如果驅動程序也需要輸出並提供了proc接口則調用它:

static int xxx_demo_proc(struct device *dev, struct seq_file *seq)
{
	struct xxx_demo *xxx_demo = dev_get_drvdata(dev);

	seq_printf(seq, "xxx_demo_data\t: %ld\n", xxx_demo->xxx_demo_data);
	seq_printf(seq, "\n");

	return 0;
}
通過該proc接口,應用程序可以非常方便的獲取內核demo子系統和驅動上的信息。最後來看下面來看一下用戶通過sysfs和demo子系統的交互流程,前文中驅動註冊流程中註冊了dev_attr_demo_name和dev_attr_demo_data兩個通用的屬性文件和一個
dev_attr_demodata特殊屬性文件。其中dev_attr_demo_name屬性文件是隻讀的,所綁定的show接口函數爲
static ssize_t
demo_name_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%s\n", to_demo_device(dev)->name);
}
static DEVICE_ATTR_RO(demo_name);
該函數將設備的名字輸出到用戶空間。另一個dev_attr_demo_data屬性文件爲可讀可寫的,其綁定的show接口和store接口爲:

static ssize_t
demo_data_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%ld\n", to_demo_device(dev)->demo_data.text_data);
}

static ssize_t
demo_data_store(struct device *dev, struct device_attribute *attr,
		const char *buf, size_t n)
{
	struct demo_device *demo = to_demo_device(dev);
	unsigned long val = simple_strtoul(buf, NULL, 0);

	if (val >= 4096 || val == 0)
		return -EINVAL;

	demo->demo_data.text_data = (unsigned long)val;

	return n;
}

這裏分別設置和輸出子系統中的demo_data。最後來看dev_attr_demodata的store接口:

static ssize_t
demo_sysfs_set_demodata(struct device *dev, struct device_attribute *attr,
		const char *buf, size_t n)
{
	struct demo_device *demo = to_demo_device(dev);
	struct demo_ctl_data demo_ctl;	
	unsigned long val = 0;
	ssize_t retval;

	val = simple_strtoul(buf, NULL, 0);

	if (val >= 4096 || val == 0)
		retval = -EINVAL;

	/* 調用interface接口寫入驅動數據 */
	demo_ctl.data = (unsigned long)val;

	retval = demo_test_set(demo, &demo_ctl);

	return (retval < 0) ? retval : n;
}
這裏接收到用戶輸入的數據後然後封裝調用interface中的demo_test_set接口,就同ioctl相同,通過sysfs接口用戶也可以非常方便的同驅動驅動程序交互。


三、demo驅動和子系統演示


首先使用如下Makefile程序進行編譯:
ifneq ($(KERNELRELEASE),)

obj-m := demo.o
demo-objs := demo_core.o demo_dev.o demo_interface.o demo_proc.o demo_sysfs.o

obj-m += xxx_demo_driver.o

obj-m += xxx_demo_device.o

else
	
KDIR := /home/apple/raspberry/build/linux-rpi-4.1.y
all:prepare
	make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-bcm2708-linux-gnueabi-
	cp *.ko ./release/	
prepare:
	mkdir release
modules_install:
	make -C $(KDIR) M=$(PWD) modules_install ARCH=arm CROSS_COMPILE=arm-bcm2708-linux-gnueabi-
clean:
	rm -f *.ko *.o *.mod.o *.mod.c *.symvers  modul*
	rm -f ./release/*

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子系統就可以把這些驅動外設的差異屏蔽起來,子系統要求驅動程序用一套標準的接口與其對接(不支持的功能可以不用實現),這樣子系統就可以對這些驅動進行歸一化的管理,結構更爲清晰,層次更爲分明,代碼的重複率大大降低,最終爲應用程序提供一套標準的接口,應用層序的設計也可以大大的簡化。














發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章