Linux内核学习随笔

       前段时间去Qualcomm面试,被面试官问到一个问题给呛住了:说一下内核启动时内核是如何正确加载驱动到对应的硬件上的,说一下过程。当时第一反应是糟糕,这个过程的确没有去细想过啊......,于是我真诚的吃瘪了。回来之后就决定把这个问题整明白。对于这个问题其实网上有人回答过的,我也真的是学艺不精令自己汗颜啊。写这篇博文一是为了反省二是为了攒攒人品。废话不多说了,进正题。
    对于arm或X86的启动过程就不再详细描述,具体可以学习:
    针对我的这个问题,以平台设备(platform_device)为例,真正要关心的就是start_kernel()函数开始,该函数位于/linux/init/mian.c。刚开始学习驱动移植的时候(以NAND Flash为例),看网上的帖子都是把平台设备添加到(以s3c6410为例)/arch/arm/mach-s3c64xx/mach-s3c6410.c中。当时只知道跟着帖子的教程走,但是没有去想到底为什么。现在追踪下代码。先说下内核是何时把这些平台设备给注册上的,具体说是注册到bus上。
   首先,在mach-s3c6410.c中,必须有MACHINE_START(SMDK6410,"SMDK6410")和MACHINE_END以及二者中间一些域的赋值。来看宏定义(linux/arch/arm/include/asm/mach/arch.h):
 68 #define MACHINE_START(_type,_name)                      \
 69 static const struct machine_desc __mach_desc_##_type    \
 70  __used                                                 \
 71  __attribute__((__section__(".arch.info.init"))) = {    \
 72         .nr             = MACH_TYPE_##_type,            \
 73         .name           = _name,
 74 
 75 #define MACHINE_END                             \
 76 };
   也就是说MACHINE_START(SMDK6410,"SMDK6410")被宏定义成了一个叫machine_desc_SMDK6410的结构体,该结构体被放在了.arch.info.init段。其中两个域nr和name直接被替换成相应的类型码和字符串。MACH_TYPE_SMDK6410,它位于/arch/arm/tools/mach-types中,对应一个类型码,它的作用之后将会提及。再来看machine_desc是个什么东东:
18 struct machine_desc {
 19         unsigned int            nr;             /* architecture number  */
 20         const char              *name;          /* architecture name    */
 21         unsigned long           atag_offset;    /* tagged list (relative) */
 22         const char *const       *dt_compat;     /* array of device tree
 23                                                  * 'compatible' strings */
 24 
 25         unsigned int            nr_irqs;        /* number of IRQs */
 26 
 27 #ifdef CONFIG_ZONE_DMA
 28         unsigned long           dma_zone_size;  /* size of DMA-able area */
 29 #endif
 30 
 31         unsigned int            video_start;    /* start of video RAM   */
 32         unsigned int            video_end;      /* end of video RAM     */
 33 
 34         unsigned char           reserve_lp0 :1; /* never has lp0        */
 35         unsigned char           reserve_lp1 :1; /* never has lp1        */
 36         unsigned char           reserve_lp2 :1; /* never has lp2        */
 37         char                    restart_mode;   /* default restart mode */
 38         void                    (*fixup)(struct tag *, char **,
 39                                          struct meminfo *);
 40         void                    (*reserve)(void);/* reserve mem blocks  */
 41         void                    (*map_io)(void);/* IO mapping function  */
 42         void                    (*init_early)(void);
 43         void                    (*init_irq)(void);
 44         struct sys_timer        *timer;         /* system tick timer    */
 45         void                    (*init_machine)(void);
 46 #ifdef CONFIG_MULTI_IRQ_HANDLER
 47         void                    (*handle_irq)(struct pt_regs *);
 48 #endif
 49         void                    (*restart)(char, const char *);
 50 };
从域的名字来看,machine_desc描述的是架构的名字和类型码(上面刚刚赋过值了),其他是些初始化的程序入口和IO端口映射、时钟、重启等等方法。针对我的问题,其中最值得注意的就是init_machine域。在板级支持文件mach-s3c6410.c中,夹在MACHINE_START和MACHINE_END之间的代码如下:
.phys_io    = S3C24XX_PA_UART,
.io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params    = S3C_SDRAM_PA + 0x100,
.init_irq    = s3c_init_irq,
.map_io   = smdk6410_map_io,
.fixup       = smdk6410_fixup,
.timer       = &s3c_timer,
.init_machine   = smdk6410_machine_init,
如此看来我们的init_machine域在此就被赋值成为了smdk6410_machine_init函数。该函数主要是调用了platform_add_devices函数来注册所有之前在smdk6410_devices中列出的所有平台设备。然后platform_add_devices调用platform_device_register函数,该函数初始化设备然后调用platform_device_add函数(名字真够绕的)。platform_device_add函数对平台设备结构体的关键域赋值,比如bus_type,然后调用device_add来注册设备到总线。注册设备时内核要为这个设备在总线上搜索是否存在和其匹配的驱动。这是通过调用bus_probe_device函数来实现的。从该函数一路向下找经过bus_probe_device-->device_attach-->bus_for_each_dev-->__device_attach-->driver_match_device。driver_match_device是一个巧妙的内联函数:
114 static inline int driver_match_device(struct device_driver *drv,
115                                       struct device *dev)
116 {
117         return drv->bus->match ? drv->bus->match(dev, drv) : 1;
118 }
所以问题来到了match这个函数身上。之前刚说过:platform_device_add函数对平台设备结构体的关键域赋值,所有平台设备的总线类型都被赋予成了platform_bus_type。这个结构体的match域被初始化为platform_match函数。每个硬件注册时,platform_match函数都会被调用来遍历驱动程序中的id_table指向的platform_device_id结构体数组,进而判断在驱动支持的id列表中是否存在和设备名字相同的名字。如果平台驱动中没有给id_table域赋值,那么直接比较驱动和设备的名字:
660 static int platform_match(struct device *dev, struct device_driver *drv)
661 {
662         struct platform_device *pdev = to_platform_device(dev);
663         struct platform_driver *pdrv = to_platform_driver(drv);
664 
665         /* Attempt an OF style match first */
666         if (of_driver_match_device(dev, drv))
667                 return 1;
668 
669         /* Then try to match against the id table */
670         if (pdrv->id_table)
671                 return platform_match_id(pdrv->id_table, pdev) != NULL;
672 
673         /* fall-back to driver name match */
674         return (strcmp(pdev->name, drv->name) == 0);
675 }
存在则表示有驱动支持该设备,不存在则表示不支持。但是此时驱动并不认识设备。只有当我们自己的驱动程序中的probe函数被调用时,驱动程序才会真正去尝试驱动硬件。这个过程由__device_attach函数在调用driver_match_device之后的driver_probe_device函数来实现。driver_probe_device首先复查下设备是否注册,然后调用really_probe函数。该函数会调用驱动程序的probe函数,这样就绕回来了。问题也解决了,就是个字符串的比较问题。
其实在启动阶段,是现有设备后有驱动的,所以设备注册时是找不到驱动的,那为什么多此一举的去找驱动呢?原因很简单:热插拔。热插拔在内核起来之后,可能导值驱动先存在而硬件后出现的情况。问题又来了,为何启动时先有设备后有驱动呢?硬件何时注册,也就是smdk6410_machine_init何时被调用?
回到最开始提到的start_kernel()函数。start_kernel()函数对于内核启动来说功不可没,它在关中断的情况下完成了很多必要的初始化工作。在其中我找到了一个叫做setup_arch()的函数,看着名字像那么回事,于是溯源发现核心就是一个叫setup_machine_tags()的函数把结果赋给了一个叫mdesc的machine_desc结构体。setup_machine_tags之前叫setup_machine,在名字上有所区别。它主要的工作就是遍历内核支持的所有架构的machie_desc结构体并与当前的架构描述结构体做匹配(对比nr域,之前的MACH_TYPE_SMDK6410),如果找到了一致的则表示内核支持该架构,反之则表示不支持。但是我并未找到对smdk6410_machine_init的调用。在/arch/arm/kernel/setup.c中,有一个函数:
794 static int __init customize_machine(void)
795 {
796         /* customizes platform devices, or adds new ones */
797         if (machine_desc->init_machine)
798                 machine_desc->init_machine();
799         return 0;
800 }
801 arch_initcall(customize_machine);
arch_initcall(customize_machine)被定义成名为__initcall_customize_machine3,类型为initcall_t的函数,该函数位于.initcall3.init段,被链接到.initcall段中。level表示优先级,越小优先级越高,也就是优先加载。所以相比驱动加载函数module_init来说(level为6),在启动阶段设备是优先加载的。再扯回到start_kernel,既然setup_arch没有调用smdk6410_machine_init,继续往下找,找到rest_init。这个函数里,内核生成了两个内核进程后就去休眠了,其中一个进程叫kernel_init,该进程继续完成初始化。在这个进程中,一个叫do_basicsetup()函数被调用:
178 #define __define_initcall(level,fn,id) \
179         static initcall_t __initcall_##fn##id __used \
180         __attribute__((__section__(".initcall" level ".init"))) = fn
201 #define arch_initcall(fn)               __define_initcall("3",fn,3)
208 #define device_initcall(fn)             __define_initcall("6",fn,6)
213 #define __initcall(fn) device_initcall(fn)
267 #define module_init(x)  __initcall(x);

在该函数中发现了smdk6410_machine_init的调用。但是customize_machine()函数却并未被显式调用过,而是被定义成了arch_initcall(801行)。那arch_initcall又是什么呢?在/include/linux/init.h中找到其定义:








772 static void __init do_basic_setup(void)
773 {
774         cpuset_init_smp();
775         usermodehelper_init();
776         shmem_init();
777         driver_init();
778         init_irq_proc();
779         do_ctors();
780         usermodehelper_enable();
781         do_initcalls();
782 }
在781行上一个函数叫do_initcalls(),这个函数就负责遍历执行所有的处于.initcall段中的函数。也就是这时smdk6410_machine_init被执行,硬件都被注册到总线上,然后驱动也被注册到总线上,当然硬件先于驱动出现。
驱动加载时也要去总线上找设备来匹配。这样才能保证万无一失,
不管是谁先出现,只要有新来的都要去总线上看看有没有能和自己匹配的另一半。
当do_initcalls函数遍历到module_init的加载级别时,也就是驱动加载的时刻。
拿平台设备驱动来说,一个驱动程序会去执行platform_driver_register函数来注册初始化好的platform_driver。
在platform_driver_register函数中,platform_driver的driver域同样会像platform_device一样被赋予总线类型,
和其他一些match、probe等方法。
然后driver_register函数会被调用,该函数先去总线上看看有没有相同的驱动已经被注册过,
如果有则要丢弃这个驱动,如果没有可以继续调用bus_add_driver来注册驱动。
接下来就是调用driver_attach函数(还记得之前的device_attach吗?名字很像,就是有了设备找驱动)
来找设备区匹配,之后调用bus_for_each_dev(对应bus_for_each_drv)
来遍历总线上的所有设备循环调用__driver_attach(对应__device_attach)。
__driver_attach和__device_attach最后殊途同归到driver_match_device上。
之后的过程就和上面的一样了。好了,问题解决了。顺带提一下的就是既然设备和驱动都要注册到总线上,
而且在注册时需要被赋予总线类型,那么总线肯定是在这两者之前就的注册好。
没错的,总线的注册在do_basic_setup函数中的driver_init()中进行,先于do_initcalls出现。
这是我第一次写技术类的博文,哪有错误还望大牛们指正和提点。
最后再分享篇描述BUS、DEVICE和DRIVER三者关系的帖子,写的很贴切很幽默:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值