前言 为什么Linux内核要引入设备资源管理框架
每当driver probe一个具体的device实例的时候,都需要建立一些私有的数据结构来保存该device的一些具体的硬件信息。
在过去,驱动工程师多半使用kmalloc或者kzalloc来分配内存,但这会带来一些潜在的问题。例如:在初始化过程中,有各种各样可能的失败情况,这时候就依靠driver工程师小心的撰写代码,释放之前分配的内存。
当然,初始化过程中,除了memory,driver会为probe的device分配各种资源,例如IRQ 号,io memory map、DMA等等。当初始化需要管理这么多的资源分配和释放的时候,很多驱动程序都出现了资源管理的issue。
而且,由于这些issue是异常路径上的issue,不是那么容易测试出来,更加重了解决这个issue的必要性。
内核解决这个问题的模式(所谓解决一类问题的设计方法就叫做设计模式)是Devres,即device resource management
模块。
先看下以前的内核的驱动代码中资源的管理方式:
// drivers/media/platform/soc_camera/mx1_camera.c static int __init mx1_camera_probe(struct platform_device *pdev) { // ... res = platform_get_resource(pdev, IORESOURCE_MEM, 0); irq = platform_get_irq(pdev, 0); if (!res || (int)irq <= 0) { err = -ENODEV; goto exit; } clk = clk_get(&pdev->dev, "csi_clk"); if (IS_ERR(clk)) { err = PTR_ERR(clk); goto exit; } pcdev = kzalloc(sizeof(*pcdev), GFP_KERNEL); if (!pcdev) { dev_err(&pdev->dev, "Could not allocate pcdev\n"); err = -ENOMEM; goto exit_put_clk; } // ... /* * Request the regions. */ if (!request_mem_region(res->start, resource_size(res), DRIVER_NAME)) { err = -EBUSY; goto exit_kfree; } base = ioremap(res->start, resource_size(res)); if (!base) { err = -ENOMEM; goto exit_release; } // ... /* request dma */ pcdev->dma_chan = imx_dma_request_by_prio(DRIVER_NAME, DMA_PRIO_HIGH); if (pcdev->dma_chan < 0) { dev_err(&pdev->dev, "Can't request DMA for MX1 CSI\n"); err = -EBUSY; goto exit_iounmap; } // ... /* request irq */ err = claim_fiq(&fh); if (err) { dev_err(&pdev->dev, "Camera interrupt register failed\n"); goto exit_free_dma; } // ... err = soc_camera_host_register(&pcdev->soc_host); if (err) goto exit_free_irq; dev_info(&pdev->dev, "MX1 Camera driver loaded\n"); return 0; exit_free_irq: disable_fiq(irq); mxc_set_irq_fiq(irq, 0); release_fiq(&fh); exit_free_dma: imx_dma_free(pcdev->dma_chan); exit_iounmap: iounmap(base); exit_release: release_mem_region(res->start, resource_size(res)); exit_kfree: kfree(pcdev); exit_put_clk: clk_put(clk); exit: return err; }
相信每一个写过Linux driver的工程师,都在probe函数中遇到过上面的困惑:
- 要顺序申请多种资源(IRQ、Clock、memory、regions、ioremap、dma、等等),只要任意一种资源申请失败,就要回滚释放之前申请的所有资源。
- 于是函数的最后,一定会出现很多的goto标签(如上面的exit_free_irq、exit_free_dma、等等),并在申请资源出错时,小心翼翼的goto到正确的标签上,以便释放已申请资源。
正像上面代码一样,整个函数被大段的、重复的“if (condition) { err = xxx; goto xxx; }
”充斥,浪费精力,容易出错,不美观。
最终,Linux设备模型借助device resource management(设备资源管理),帮我们解决了这个问题。driver你只管申请就行了,不用考虑释放,我设备模型帮你释放。既然你驱动需要用的资源都是是设备的资源,那么资源的管理归于device
,也就是说不需要driver
过多的参与。当device和driver detach的时候,device会自动的释放其所有的资源。
最终,我们的driver可以这样写:
static int __init mx1_camera_probe(struct platform_device *pdev) { // ... res = platform_get_resource(pdev, IORESOURCE_MEM, 0); irq = platform_get_irq(pdev, 0); if (!res || (int)irq <= 0) { return -ENODEV; } clk = devm_clk_get(&pdev->dev, "csi_clk"); if (IS_ERR(clk)) { return PTR_ERR(clk); } pcdev = devm_kzalloc(&pdev->dev, sizeof(*pcdev), GFP_KERNEL); if (!pcdev) { dev_err(&pdev->dev, "Could not allocate pcdev\n"); return -ENOMEM; } // ... /* * Request the regions. */ if (!devm_request_mem_region(&pdev->dev, res->start, resource_size(res), DRIVER_NAME)) { return -EBUSY; } base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); if (!base) { return -ENOMEM; } // ... /* request dma */ pcdev->dma_chan = imx_dma_request_by_prio(DRIVER_NAME, DMA_PRIO_HIGH); if (pcdev->dma_chan < 0) { dev_err(&pdev->dev, "Can't request DMA for MX1 CSI\n"); return -EBUSY; } // ... /* request irq */ err = claim_fiq(&fh); if (err) { dev_err(&pdev->dev, "Camera interrupt register failed\n"); return err; } // ... err = soc_camera_host_register(&pcdev->soc_host); if (err) return err; dev_info(&pdev->dev, "MX1 Camera driver loaded\n"); return 0; }
怎么做到呢?注意上面“devm_
”开头的接口,答案就在那里。
不要再使用那些常规的资源申请接口,用devm_xxx
的接口代替。
为了保持兼容,这些新接口和旧接口的参数保持一致,只是名字前加了“devm_
”,并多加一个struct device
指针。
devm_xxx接口
下面列举一些常用的资源申请接口,它们由各个framework(如clock、regulator、gpio、等等)基于device resource management实现。
使用时,直接忽略“devm_”的前缀,后面剩下的部分,driver工程师都很熟悉。
只需记住一点,driver可以只申请,不释放,设备模型会帮忙释放。
不过如果为了严谨,在driver remove时,可以主动释放(也有相应的接口,这里没有列出)。
extern void *devm_kzalloc(struct device *dev, size_t size, gfp_t gfp); void __iomem *devm_ioremap_resource(struct device *dev, struct resource *res); void __iomem *devm_ioremap(struct device *dev, resource_size_t offset, unsigned long size); struct clk *devm_clk_get(struct device *dev, const char *id); int devm_gpio_request(struct device *dev, unsigned gpio, const char *label); static inline struct pinctrl * devm_pinctrl_get_select( struct device *dev, const char *name) static inline struct pwm_device *devm_pwm_get(struct device *dev, const char *consumer); struct regulator *devm_regulator_get(struct device *dev, const char *id); static inline int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id); struct reset_control *devm_reset_control_get(struct device *dev, const char *id);
设备资源
一个设备能工作,需要依赖很多的外部条件,如供电、时钟等等,这些外部条件称作设备资源(device resouce)。
对于现代计算机的体系结构,可能的资源包括:
- power,供电。
- clock,时钟。
- memory,内存,在kernel中一般使用kzalloc分配。
- GPIO,用户和CPU交换简单控制、状态等信息。
- IRQ,触发中断。
- DMA,无CPU参与情况下进行数据传输。
- 虚拟地址空间,一般使用ioremap、request_region等分配。
- 略
而在Linux kernel的眼中,“资源”的定义更为广义,比如PWM、RTC、Reset,都可以抽象为资源,供driver使用。
在较早的kernel中,系统还不是特别复杂,且各个framework还没有成型,因此大多的资源都由driver自行维护。但随着系统复杂度的增加,driver之间共用资源的情况越来越多,同时电源管理的需求也越来越迫切。
于是kernel就将各个resource的管理权收回,基于“device resource management”的框架,由各个framework统一管理,包括分配和回收。
device resource management的软件框架
位于“drivers/base/devres.c
”中,它的实现非常简单,为什么呢?因为资源的种类有很多,表现形式也多种多样,而devres不可能一一知情,也就不能进行具体的分配和回收。
因此,devres能做的(也是它的唯一功能),就是:
- 提供一种机制,将系统中某个设备的所有资源,以链表的形式,组织起来,以便在driver detach的时候,自动释放。
而更为具体的事情,如怎么抽象某一种设备,则由上层的framework负责。这些framework包括:regulator framework(管理power资源),clock framework(管理clock资源),interrupt framework(管理中断资源)、gpio framework(管理gpio资源),pwm framework(管理PWM),等等。
其它的driver,位于这些framework之上,使用它们提供的机制和接口,开发起来就非常方便了。
device原型中的devres_head
先从struct device开始吧!该结构中有一个名称为“devres_head
”的链表头,用于保存该设备申请的所有资源。
devres: device resource struct device { // ... spinlock_t devres_lock; struct list_head devres_head; //... }
devres原型
devres
代表了资源的数据结构。
不知道您是否注意到,devres有关的数据结构,是在devres.c中定义的。
换句话说,是对其它模块透明的。这真是优雅的设计(尽量屏蔽细节)!
当然了,有关的接口还是公开的
// drivers/base/devres.c struct devres { struct devres_node node; /* -- 3 pointers */ unsigned long long data[]; /* guarantee ull alignment */ };
咋一看非常简单,一个struct devres_node的
变量node
,一个零长度数组data
,但其中有无穷奥妙,让我们继续分析。
- data是一个零长数组,用于存放所申请的不定长内存;因为整个memory空间是连续的,因此可以通过释devres指针,释放所有的空间,包括data所指的那片不定长度的、具体资源所用的空间。
- 而node用于将devres组织起来,方便插入到device结构的devres_head链表中
devres_node原型
// base/devres.c struct devres_node { struct list_head entry; dr_release_t release; #ifdef CONFIG_DEBUG_DEVRES const char *name; size_t size; #endif };
entry:刚刚说了,devres
使用node
用于将devres组织起来,方便插入到device结构的devres_head链表中
release:资源的存在形式到底是什么,device resource management并不知情,因此需要上层模块提供一个release的回调函数,用于release资源。
抛开用于debug的变量不说,也很简单,一个entry list_head,一个release回调函数。看不出怎么抽象资源啊!别急,奥妙都在data这个零长度数组上面呢。
向上层framework提供的接口
其实有两对:
devres_alloc/devres_free devres_add/devres_remove
devres_alloc/devres_free
// drivers/base/devres.c /** * devres_alloc - Allocate device resource data * @release: Release function devres will be associated with * @size: Allocation size * @gfp: Allocation flags * * Allocate devres of @size bytes. The allocated area is zeroed, then * associated with @release. The returned pointer can be passed to * other devres_*() functions. * * RETURNS: * Pointer to allocated devres on success, NULL on failure. */ void * devres_alloc(dr_release_t release, size_t size, gfp_t gfp) { struct devres *dr; dr = alloc_dr(release, size, gfp | __GFP_ZERO); if (unlikely(!dr)) return NULL; return dr->data; } EXPORT_SYMBOL_GPL(devres_alloc); /** * devres_free - Free device resource data * @res: Pointer to devres data to free * * Free devres created with devres_alloc(). */ void devres_free(void *res) { if (res) { struct devres *dr = container_of(res, struct devres, data); BUG_ON(!list_empty(&dr->node.entry)); kfree(dr); } } EXPORT_SYMBOL_GPL(devres_free);
devres_alloc
调用alloc_dr
,分配一个struct devres
类型的变量,并返回其中的data指针(data变量实际上是资源的代表)。