devm_kmalloc 实现与使用

2018年7月30日 11.89k 次阅读 0 条评论 2 人点赞

Linux 为驱动开发提供了专门的内存申请函数 kmalloc/kfree,但是偶尔有时候在内存申请完成之后或者设备卸载之后,这一段内存并没有被释放掉,从而导致内存泄露。或者在某些时候内存申请失败时,需要通过多个 goto 来保证不内存泄露,你一定见过类似的代码:

int *data, *resource;

int hello_init(void)
{
        data = kmalloc(sizeof(int), GFP_KERNEL);
        if (!data)
            goto out;
        resource = kmalloc(sizeof(int), GFP_KERNEL);
        if (!resource)
            goto exit;

        do_something();
        return 0;
exit:
        kfree(data);
out:
        return -1;
}

void hello_exit(void)
{
        if (resource)
            kfree(resource);
        if (data)
            kfree(data);
}

在 C++ 中为了解决这种类型的问题,有一种严格限定资源对象生命周期的处理办法 RAII(资源获取即初始化),名字可能比较难以理解,其实实现起来比较容易,就是利用 C++ 的入口构造函数初始化资源然后利用析构函数释放资源。那么内核是不是也可以考虑借鉴一下这样的办法呢?

答案当然是肯定的,我们都知道内核虽然是使用 C 语言编写的,但是他的设计思想是接近与 C++ 的对象方式,为了防止开发人员忘记释放资源,那么就让他严格限定在一个设备之上吧,这就是今天要分析的 devm_xxx 系列函数。

设备内存申请 devm_kmalloc

那么通过 devm_kmalloc 优化过的代码有会是什么样的呢?

int *data, *resource;

int hello_probe(struct platform_device *pdev)
{
        data = devm_kmalloc(pdev->dev, sizeof(int), GFP_KERNEL);
        if (!data)
            return -ENOMEM;
        resource = devm_kmalloc(pdev->dev, sizeof(int), GFP_KERNEL);
        if (!resource)
            return -ENOMEM;

        do_something();
        return 0;
}

void hello_exit(void)
{
        /* noop */
}

是不是不需要对申请到的内存进行管理?代码也简洁许多。这就是 devm_xxx 函数的魔力,他让我们更加的关注逻辑的实现而不是对资源的申请/释放花太多精力。

如何实现?

了解到他的便利,那么 devm_kmalloc 内部又是怎么实现的呢?他引进了一种资源管理的办法,通过对申请的内存与相对应的 device 做一个捆绑,当设备 probe 时申请到的内存在设备 detach 时隐形释放掉。我们来关注一下他的具体实现,代码位于 drivers/base/devres.c

/**
 * devm_kmalloc - Resource-managed kmalloc
 * @dev: Device to allocate memory for
 * @size: Allocation size
 * @gfp: Allocation gfp flags
 *
 * Managed kmalloc.  Memory allocated with this function is
 * automatically freed on driver detach.  Like all other devres
 * resources, guaranteed alignment is unsigned long long.
 *
 * RETURNS:
 * Pointer to allocated memory on success, NULL on failure.
 */
void * devm_kmalloc(struct device *dev, size_t size, gfp_t gfp)
{
        struct devres *dr;

        /* use raw alloc_dr for kmalloc caller tracing */
        dr = alloc_dr(devm_kmalloc_release, size, gfp, dev_to_node(dev));
        if (unlikely(!dr))
            return NULL;

        /*
         * This is named devm_kzalloc_release for historical reasons
         * The initial implementation did not support kmalloc, only kzalloc
         */
        set_node_dbginfo(&dr->node, "devm_kzalloc_release", size);
        devres_add(dev, dr->data);
        return dr->data;
}
EXPORT_SYMBOL_GPL(devm_kmalloc);

其实不需要再过更多的深入就已经理解了该机制的一个实现逻辑,通过 alloc_dr 申请一个设备资源,然后调用 devres_add 添加到设备的一个资源列表中,就完成了一个设备资源的申请,更多细节可以自行深入代码,其实没有什么复杂的东西了,那么在设备卸载又是如何释放的呢? device_release_driver --> __device_release_driver --> devres_release_all. 代码参见 drivers/base/devres.c:

/**
 * devres_release_all - Release all managed resources
 * @dev: Device to release resources for
 *
 * Release all resources associated with @dev.  This function is
 * called on driver detach.
 */
int devres_release_all(struct device *dev)
{
        unsigned long flags;

        /* Looks like an uninitialized device structure */
        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);
}

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;
}

通过 release_nodes 函数遍历整个设备的资源列表,然后一个个释放掉。这个有一个在函数体外的 __releases(&dev->devres_lock),可能对于非驱动开发熟手而言看不懂,他并不是标准的 C 语言的什么新的方式,其实他就是一种静态的语法检测,是配合 Sparse 这个代码检查工具使用的。下面是他的定义,代码参见 include/linux/compiler.h

# define __user             __attribute__((noderef, address_space(1)))
# define __kernel           __attribute__((address_space(0)))
# define __safe             __attribute__((safe))
# define __force            __attribute__((force))
# define __nocast           __attribute__((nocast))
# define __iomem            __attribute__((noderef, address_space(2)))
# define __must_hold(x)     __attribute__((context(x,1,1)))
# define __acquires(x)      __attribute__((context(x,0,1)))
# define __releases(x)      __attribute__((context(x,1,0)))
# define __acquire(x)       __context__(x,1)
# define __release(x)       __context__(x,-1)
# define __cond_lock(x,c)   ((c) ? ({ __acquire(x); 1; }) : 0)
# define __percpu           __attribute__((noderef, address_space(3)))
# define __pmem             __attribute__((noderef, address_space(5)))

那如果不想等到设备卸载才释放设备资源,而是想尽早的释放这个已经捆绑到设备的资源该怎么办呢?没错,那么 free 掉吧。

/**
 * devm_kfree - Resource-managed kfree
 * @dev: Device this memory belongs to
 * @p: Memory to free
 *
 * Free memory allocated with devm_kmalloc().
 */
void devm_kfree(struct device *dev, void *p)
{
        int rc;

        rc = devres_destroy(dev, devm_kmalloc_release, devm_kmalloc_match, p);
        WARN_ON(rc);
}
EXPORT_SYMBOL_GPL(devm_kfree);

One More Thing

内核不仅仅只给我们提供了强大的 devm_malloc 同样的他还把整个框架开放给了所有的驱动开发者,devm_kmalloc 很好用,但是他还是不能满足一些自定义要求,比如说有些资源在释放之前还需要做一些其他的检查或者清理工作,这个可不是 devm_malloc 能够做到的,有什么办法呢?是的,那就是 devres_alloc 这个函数的到来,那么该如何使用?我们来看一下 rtc 驱动是如何使用的,代码参见 drivers/rtc/rtc-ds2404.c

static int rtc_probe(struct platform_device *pdev)
{
        struct ds2404_platform_data *pdata = dev_get_platdata(&pdev->dev);
        struct ds2404 *chip;
        int retval = -EBUSY;

        chip = devm_kzalloc(&pdev->dev, sizeof(struct ds2404), GFP_KERNEL);
        if (!chip)
            return -ENOMEM;

        chip->ops = &ds2404_gpio_ops;

        retval = chip->ops->map_io(chip, pdev, pdata);
        if (retval)
            goto err_chip;

        dev_info(&pdev->dev, "using GPIOs RST:%d, CLK:%d, DQ:%d\n",
             chip->gpio[DS2404_RST].gpio, chip->gpio[DS2404_CLK].gpio,
             chip->gpio[DS2404_DQ].gpio);

        platform_set_drvdata(pdev, chip);

        chip->rtc = devm_rtc_device_register(&pdev->dev, "ds2404",
                        &ds2404_rtc_ops, THIS_MODULE);
        if (IS_ERR(chip->rtc)) {
            retval = PTR_ERR(chip->rtc);
            goto err_io;
        }

        ds2404_enable_osc(&pdev->dev);
        return 0;

err_io:
        chip->ops->unmap_io(chip);
err_chip:
        return retval;
}

可以看到 chip 是通过 devm_kmalloc 申请的内存,但是也还有一个名字叫做 devm_rtc_device_register 的函数,当然,这个肯定不是框架提供的函数,必须是驱动开发者自定义的一个与 devm_xxx 簇的函数。

struct rtc_device *devm_rtc_device_register(struct device *dev,
                    const char *name,
                    const struct rtc_class_ops *ops,
                    struct module *owner)
{
        struct rtc_device **ptr, *rtc;

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

        rtc = rtc_device_register(name, dev, ops, owner);
        if (!IS_ERR(rtc)) {
            *ptr = rtc;
            devres_add(dev, ptr);
        } else {
            devres_free(ptr);
        }

        return rtc;
}
EXPORT_SYMBOL_GPL(devm_rtc_device_register);

You see it?RTC 的设备注册直接封装为了一个 devm_xxx 簇的函数,而且在这一个资源释放时会主动去调用 devm_rtc_device_release 函数进行资源释放准备工作,一切都是那么完美,这就是 devm_xxx 系列函数。Enjoy it.

标签:
最后编辑:2020年12月30日