【深入Linux内核驱动】CLK_OF_DECLARE使用及其内在机制

本文解析CLK_OF_DECLARE宏在Linux内核驱动中的作用机制,介绍如何使用section关键字构建时钟驱动初始化函数表,并通过实例说明内核如何自动调用这些初始化函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【深入Linux内核驱动】CLK_OF_DECLARE使用及其内在机制

在clk驱动中有出现这样的一句宏定义调用

CLK_OF_DECLARE(s3c2410_clk, "samsung,s3c2410-clock", s3c2410_clk_init)

通过查找kernel路径下,发现没有其他地方去调用这个s3c2410_clk,s3c2410_clk_init.很疑惑不知道这个接口是怎么被调用的.

CLK_OF_DECLARE通过百度查找,发现这个宏经常会在系统时钟初始化的时候进行使用:

Declare the compatible clocks and associate it with an initialization function using CLK_OF_DECLARE

使用CLK_OF_DECLARE声明兼容时钟并将其与初始化函数关联

CLK_OF_DECLARE 定义

#ifdef CONFIG_OF
#define _OF_DECLARE(table, name, compat, fn, fn_type)           \
    static const struct of_device_id __of_table_##name      \
        __used __section(__##table##_of_table)          \
         = { .compatible = compat,              \ 
             .data = (fn == (fn_type)NULL) ? fn : fn  }
#else 
#define _OF_DECLARE(table, name, compat, fn, fn_type)           \
    static const struct of_device_id __of_table_##name      \
        __attribute__((unused))                 \
         = { .compatible = compat,              \
             .data = (fn == (fn_type)NULL) ? fn : fn }
#endif


#define OF_DECLARE_1(table, name, compat, fn) \
        _OF_DECLARE(table, name, compat, fn, of_init_fn_1)
#define OF_DECLARE_2(table, name, compat, fn) \
        _OF_DECLARE(table, name, compat, fn, of_init_fn_2)


#define CLK_OF_DECLARE(name, compat, fn) OF_DECLARE_1(clk, name, compat, fn)

通过_OF_DECLARE宏的实现发现其实CLK_OF_DECLARE是声明定义的时钟信息.转化之后:

static const struct of_device_id __of_table_s3c2410_clk __used __section(__clk_of_table) {...}

由上可知是声明了一个struct of_device_id类型的变量__of_table_s3c2410_clk.但是后面的__section(__clk_of_table)如何解释?

通过查询了解到:

section关键字可以将变量定义到指定的输入段中。例如

int a __attribute__((section(“list”))) = 0;

这句话的意思是将一个int型的变量a放到名为list的输入段中。

那么static const struct of_device_id __of_table_s3c2410_clk __used __section(__clk_of_table) {...}即可理解为将一个struct of_device_id类型的变量__of_table_s3c2410_clk放入到名为__clk_of_table的输入段中.

那为什么要这样操作,定义一个变量,最终再通过section放入到指定的段中呢?

传统的应用编写时,每添加一个模块,都需要在main中添加新模块的初始化

1

使用__attribute__((section()))构建初始化函数表后,由模块告知main:“我要初始化“,添加新模块再也不需要在main代码中显式调用模块初始化接口。

2

以此实现main与模块之间的隔离,main不再关心有什么模块,模块的删减也不需要修改main。

内核其实就是使用__attribute__((section()))从而实现的初始化函数表

1. module_init的定义

module_init定义在<include/linux/init.h>。代码如下:

图片

代码中使用的“_section_”,是一层层的宏,为了简化,把其等效理解为“section”

分析上述代码,我们发现module_init__attribute__((section(“name”)))实现,把初始化函数地址保存到名为".initcall6.init" 的数据段中。

2. 链接内核使用自定义的链接脚本

我们看到内核目录最上层的Makefile,存在如下代码:

# Rule to link vmlinux - also used during CONFIG_KALLSYMS
# May be overridden by arch/$(ARCH)/Makefile
quiet_cmd_vmlinux__ ?= LD      $@  
cmd_vmlinux__ ?= $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux) -o $@ \
      -T $(vmlinux-lds) $(vmlinux-init)                          \
      --start-group $(vmlinux-main) --end-group                  \
      $(filter-out $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o FORCE ,$^)

本文的关注点在于:-T $(vmlinux-lds),通过“ld -T <script>”使用了定制的链接脚本。定制的链接脚本在哪里呢?在Makefile存在如下代码:

vmlinux-lds := arch/$(SRCARCH)/kernel/vmlinux.lds

我们以”ARCH=arm“ 为例,查看链接脚本:arch/arm/kernel/vmlinux.lds

图片

在上述代码中,我们聚焦于两个地方:

__initcall6_start = .; : 由__initcall6_start指向当前地址

*(.initcall6.init): 所有.o文件的.initcall6.init数据段放到当前位置

如此,“__initcall6_start”指向“.initcall6.init”数据段的开始地址,在应用代码中就可通过“__initcall6_start”访问数据段“.initcall6.init”。

是不是如此呢?我们再聚焦到文件<init/main.c>中。

<init/main.c>中,有如下代码:

static initcall_t *initcall_levels[] __initdata = {
   __initcall0_start,
   __initcall1_start,
   __initcall2_start,
   __initcall3_start,
   __initcall4_start,
   __initcall5_start,
   __initcall6_start,
   __initcall7_start,
   __initcall_end,
};
......
int __init_or_module do_one_initcall(initcall_t fn)
{......
  if (initcall_debug)
​    ret = do_one_initcall_debug(fn);
  else
​    ret = fn();......
}
......
static void __init do_initcall_level(int level)
{
  ......
  for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)do_one_initcall(*fn);
}

按0-7的初始化级别,依次调用各个级别的初始化函数表,而驱动module_init的初始化级别为6。在“for (fn = initcall_levels[level]; fn <initcall_levels[level+1]; fn++)”的for循环调用中,实现了遍历当前初始化级别的所有初始化函数。

通过上述的代码追踪,我们发现module_init的实现有以下关键步骤:

  1. 通过module_init的宏,在编译时,把初始化函数放到了数据段:.initcall6.init
  2. 在链接成内核的时候,链接脚本规定好了.initcall6.init的数据段以及指向数据段地址的变量:_initcall6_start
  3. init/main.c中的for循环,通过_initcall6_start的指针,调用了所有注册的驱动模块的初始化接口
  4. 最后通过Kconfig/Makefile选择编译的驱动,实现只要编译了驱动代码,则自动把驱动的初始化函数构建到统一的驱动初始化函数表

回到linux的clk子系统驱动里来,clk子系统的注册,初始化等函数操作与前述module_init实现类似;我们在clk驱动中使用__section(__##table##_of_table)对所用的时钟驱动进行声明初始化,并将该时钟传递给__clk_of_table的内核时钟数据段中,后续内核在初始化时钟时,就会发现内核直接调用匹配好使用即可

/**
 * of_clk_init() - Scan and init clock providers from the DT
 * @matches: array of compatible values and init functions for providers.
 *
 * This function scans the device tree for matching clock providers
 * and calls their initialization functions. It also does it by trying
 * to follow the dependencies.
 */
void __init of_clk_init(const struct of_device_id *matches)
{
    const struct of_device_id *match;
    struct device_node *np;
    struct clock_provider *clk_provider, *next;
    bool is_init_done;
    bool force = false;
    LIST_HEAD(clk_provider_list);
    
    if (!matches)
        matches = &__clk_of_table;

    /* First prepare the list of the clocks providers */
    for_each_matching_node_and_match(np, matches, &match) {
        struct clock_provider *parent;

本文重点在于咱们自己编写的clk子系统驱动如何传递给内核,内部如何传递,如何匹配,内核自定义链接脚本的这里不做过多叙述.

内容部分摘自:廖威雄: 利用__attribute__((section()))构建初始化函数表与Linux内核init的实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Van.Ghylivan

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

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

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

打赏作者

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

抵扣说明:

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

余额充值