Linux设备模型系列文章之六(设备资源管理)

本文围绕Linux设备资源管理展开,介绍了其解决的资源申请与释放难题,通过devm_xxx接口简化操作。阐述了设备资源的概念,如供电、时钟等。分析了DRM软件框架,它以链表组织资源。还对相关代码的数据结构、零长度数组及接口进行了详细解读。

优快云链接:

Linux设备模型剖析系列一(基本概念、kobject、kset、kobj_type)

Linux设备模型剖析系列之二(uevent、sysfs)

Linux设备模型剖析系列之三(device和device driver)

Linux设备模型剖析系列之四(BUS)

Linux设备模型剖析系列之五(class)

Linux设备模型系列文章之六(设备资源管理)

Linux设备模型剖析系列文章之七(kobj、kset)

九、设备资源管理

1. 前言

先看一个例子:

/* drivers/media/platform/soc_camera/mx1_camera.c, line 695 */
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你只管申请就行了,不用考虑释放,我设备模型帮你释放。最终,我们的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指针。

2. 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);

3. 什么是“设备资源”

一个设备能工作,需要依赖很多的外部条件,如供电、时钟等等,这些外部条件称作设备资源(device resouce)。对于现代计算机的体系结构,可能的资源包括:

  1. power,供电。
  2. clock,时钟。
  3. memory,内存,在kernel中一般使用kzalloc分配。
  4. GPIO,用户和CPU交换简单控制、状态等信息。
  5. IRQ,触发中断。
  6. DMA,无CPU参与情况下进行数据传输。
  7. 虚拟地址空间,一般使用ioremap、request_region等分配。
  8. 其它资源

而在Linux kernel的眼中,“资源”的定义更为广义,比如PWM、RTC、reset,都可以抽象为资源,供driver使用。

在较早的kernel中,系统还不是特别复杂,且各个framework还没有成型,因此大多的资源都由driver自行维护。但随着系统复杂度的增加,driver之间共用资源的情况越来越多,同时电源管理的需求也越来越迫切。于是kernel就将各个resource的管理权收回,基于“device resource management”的框架,由各个framework统一管理,包括分配和回收。

4. DRM(device resource management)的软件框架

1117305-20200322204749526-998410095

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之上,使用它们提供的机制和接口,开发起来就非常方便了。

5. 代码分析

5.1 数据结构

先从struct device开始吧!该结构中有一个名称为devres_head的链表头,用于保存该设备申请的所有资源,如下:

struct device {
    ...
    spinlock_t devres_lock;
    struct list_head devres_head;
    ...
}

那资源的数据结构呢?在“drivers/base/devres.c”中,名称为struct devres,如下:

struct devres {
    struct devres_node node;
	/* -- 3 pointers */
    /* 确保以ULL长度对齐 */
    /*data零长度数组就表示资源,资源是什么类型就分配多大的空间,然后将其转换成对应的类型*/
	unsigned long long  data[]; 
};

咋一看非常简单,一个struct devres_node的变量node,一个零长度数组data,但其中有无穷奥妙,让我们继续分析。

node用于将devres组织起来,方便插入到device结构的devres_head链表中,因此一定也有一个list_head(见下面的entry)。另外,资源的存在形式到底是什么,device resource management并不知情,因此需要上层模块提供一个release的回调函数,用于release资源,如下:

struct devres_node {
	struct list_head                entry;
	dr_release_t                    release;
#ifdef CONFIG_DEBUG_DEVRES
	const char                      *name;
 	size_t                          size;
#endif
};

抛开用于debug的变量不说,也很简单,一个名为entry的 list_head链表,一个release回调函数。看不出怎么抽象资源啊!别急,奥妙都在data这个零长度数组上面呢。

5.2 零长度数组

零长度数组的英文原名为Arrays of Length Zero,是GNU C的规范,主要用途是用来作为结构体的最后一个成员,然后用它来访问此结构体对象之后的一段内存(通常是动态分配的内存)。什么意思呢?

以struct devres为例,devres_node变量的长度为3个指针的长度,而struct devres的长度也是3个指针的长度,data只是一个标记,当有人分配了大于3个指针长度的空间并把它转换为struct devres类型的变量后,我们就可以通过data来访问多出来的memory。也就是说,有了零长度数组data,struct devres结构的长度可以不定,完全依赖于你分配的空间的大小。有什么用呢?

以本文的应用场景为例,多出来的、可通过data访问的空间,正是具体的device resource所占的空间。资源的类型不同,占用的空间的多少也不同,但devres模块的主要功能又是释放资源所占的资源。这是就是零长度数组的功能之一,因为整个memory空间是连续的,因此可以通过释放devres指针,进而释放所有的空间,包括data所指的那片不定长度的、具体资源所用的空间。

零长度数组(data[0]),在不同的C版本中,有不同的实现方案,包括1长度数组(data[1])和不定长度数组(data[],本文所描述就是这一种),具体可参考GCC规范

5.3 向上层framework提供的接口:devres_alloc/devres_free、devres_add/devres_remove

先看一个使用device resource management的例子(IRQ模块):

/* include/linux/interrupt.h */
static inline int __must_check devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,\
                                                unsigned long irqflags, const char *devname, void *dev_id)
{
    return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags, devname, dev_id);
}

/* kernel/irq/devres.c */
int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn,\
                              unsigned long irqflags, const char *devname, void *dev_id)
{
    struct irq_devres *dr;
    int rc;

    /*分配data的大小是sizeof(struct irq_devres),对返回的data直接转换成irq_devres结构*/
    dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres), GFP_KERNEL);
    if (!dr)
        return -ENOMEM;

    rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname, dev_id);
    if (rc) {
        devres_free(dr);
        return rc;
    }

    dr->irq = irq;
    dr->dev_id = dev_id;
    /*再将devres结构放到device结构中的链表上*/
    devres_add(dev, dr);

    return 0;
}
EXPORT_SYMBOL(devm_request_threaded_irq);

/*可以直接调用这个函数释放,也可以不调用让其自动释放*/
void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id)
{
    struct irq_devres match_data = { irq, dev_id };

    WARN_ON(devres_destroy(dev, devm_irq_release, devm_irq_match, &match_data));
    free_irq(irq, dev_id);
}
EXPORT_SYMBOL(devm_free_irq);

前面我们提过,上层的IRQ framework,会提供两个和request_irq/free_irq基本兼容的接口,这两个接口的实现非常简单,就是在原有的实现之上,封装一层devres的操作,如要包括:

  1. 一个自定义的数据结构(struct irq_devres),用于保存和resource有关的信息(对中断来说,就是IRQ num),如下:
/*
* device resource management aware IRQ request/free implementation.
*/
struct irq_devres {
	unsigned int irq;
	void *dev_id;
};
  1. 一个用于release resource的回调函数(这里的release,和memory无关,例如free IRQ),如下:
static void devm_irq_release(struct device *dev, void *res)
{
	struct irq_devres *this = res;
	free_irq(this->irq, this->dev_id);
}

因为回调函数是由devres模块调用的,由它的参数可知,struct irq_devres变量就是实际的“资源”,但对devres而言,它并不知道该资源的实际形态,因而是void类型指针。也只有这样,devres模块才可以统一的处理所有类型的资源。

  1. 以回调函数、resource的size为参数,调用devres_alloc接口,为resource分配空间。该接口的定义如下:
void * devres_alloc(dr_release_t release, size_t size, gfp_t gfp)
{
    struct devres *dr;
    dr = alloc_dr(release, size, gfp);
    if (unlikely(!dr))
        return NULL;
    return dr->data;
}

调用alloc_dr,分配一个struct devres类型的变量,并返回其中的data指针(data变量实际上是资源的代表)。alloc_dr的定义如下:

static __always_inline struct devres * alloc_dr(dr_release_t release, size_t size, gfp_t gfp)
{
    size_t tot_size = sizeof(struct devres) + size;
    struct devres *dr;

    dr = kmalloc_track_caller(tot_size, gfp);
    if (unlikely(!dr))
        return NULL;

    memset(dr, 0, tot_size);
    INIT_LIST_HEAD(&dr->node.entry);
    dr->node.release = release; /*release回调赋值过去*/
    return dr;
}

看第一句就可以了,在资源size之前,加一个struct devres的size,就是total分配的空间。除去struct devres的,就是资源的(由data指针访问)。之后是初始化struct devres变量的node。

  1. 调用原来的中断注册接口(这里是request_threaded_irq),注册中断。该步骤和device resource management无关。

  2. 注册成功后,以设备指针(dev)和资源指针(dr)为参数,调用devres_add,将资源添加到设备的资源链表头(devres_head)中,该接口定义如下:

void devres_add(struct device *dev, void *res)
{
    struct devres *dr = container_of(res, struct devres, data);
    unsigned long flags;
    spin_lock_irqsave(&dev->devres_lock, flags);
    add_dr(dev, &dr->node);
    spin_unlock_irqrestore(&dev->devres_lock, flags);
}

从资源指针中,取出完整的struct devres指针,调用add_dr接口。add_dr也很简单,把struct devres指针挂到设备的devres_head中即可:

static void add_dr(struct device *dev, struct devres_node *node)
{
    devres_log(dev, node, "ADD");
    BUG_ON(!list_empty(&node->entry));
    list_add_tail(&node->entry, &dev->devres_head); /*注意这里是尾插法插入设备的资源链表的*/
}
  1. 如果失败,可以通过devres_free接口释放资源占用的空间,devm_free_irq接口中,会调用devres_destroy接口,将devres从devres_head中移除,并释放资源。这里就不详细描述了。

5.4 向设备模型提供的接口:devres_release_all

这里是重点,用于自动释放资源。

先回忆一下设备模型中probe的流程,devres_release_all接口被调用的时机有两个:

  1. probe失败时,调用过程为:

__driver_attach / __device_attach–>driver_probe_device—>really_probe,really_probe调用driver或者bus的probe接口,如果失败,则会调用devres_release_all。

  1. deriver dettach时(就是driver remove时)

driver_detach/bus_remove_device–>__device_release_driver–>devres_release_all

devres_release_all的实现如下:

int devres_release_all(struct device *dev)
{
    unsigned long flags;
    /* Looks like an uninitialized device structure */
    /* 如果在函数probe()失败时驱动中自己已经释放了,这里就是NULL,就不再次释放了*/
    if (WARN_ON(dev->devres_head.next == NULL))
        return -ENODEV;
    spin_lock_irqsave(&dev->devres_lock, flags);
    return release_nodes(dev, dev->devres_head.next, &dev->devres_head, flags);
}

以设备指针为参数,直接调用release_nodes:

/*此为内核代码静态分析工具Sparse的annotation。Sparse通过gcc的扩展属性__attribute__ 以及自己定义的__context__来对代码进行静态检查。*/
static int release_nodes(struct device *dev, struct list_head *first, struct list_head *end, unsigned long flags)
 __releases(&dev->devres_lock) 
{
    LIST_HEAD(todo);
    int cnt;
    struct devres *dr, *tmp;

    cnt = remove_nodes(dev, first, end, &todo);

    spin_unlock_irqrestore(&dev->devres_lock, flags);
   /*
    * Release.  Note that both devres and devres_group are
    * handled as devres in the following loop.  This is safe.
    */
    /*最后申请的资源最先释放*/
    list_for_each_entry_safe_reverse(dr, tmp, &todo, node.entry) {
        devres_log(dev, &dr->node, "REL");
        dr->node.release(dev, dr->data);
        kfree(dr);
    }

    return cnt;
}
  • 首先,会先调用remove_nodes,将设备所有的struct devres指针从设备的devres_head中移除。

  • 然后,调用所有资源的release回调函数,回调函数会回收具体的资源。

  • 最后,调用free,释放devres以及资源所占的空间。

  • 【参考资料】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值