Linux内核驱动程序初始化顺序的调整(1)

        Linux内核驱动程序初始化顺序的调整 ,当在做一个驱动的时候要用到另一个驱动提供的API时,此时内核初始化时便碰到了一个依赖问题。

而这也是面试老生常谈的问题:

  经常让你说下你做的驱动,然后你说XXX驱动后,很可能问下驱动细节后,会有另外一个问题:  
  1.你写了一个XXX驱动,那么它是怎么被内核执行的呢?

  2.我有a,b,c三个设备的驱动,怎么让他们按照b,a,c的顺序加载

        那么首先我们就得搞清楚驱动程序是如何被内核初始化的,这个问题很明显就得追溯到内核启动流程上了。简单的讲Linux的启动过程可以分为两个部分:架构/开发板相关的引导过程、后续的通用启动过程。在这里我们不多谈内核启动,为了使问题能够解释清楚,我们直接跳到第二阶段init/main.cstart_kernel()函数(内核启动流程的第一个C函数)说起。

        而驱动程序的初始化正是在这个阶段的rest_init()函数中完成的,rest_init()函数主要目标有:加载驱动程序,挂载根文件系统,执行用户空间第一个进程init进程(pid=1)。

让我们看看rest_init()主要流程:

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

static void noinline __init_refok rest_init(void) __releases(kernel_lock)
{
 int pid;

 kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
 numa_default_policy();
 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
 kthreadd_task = find_task_by_pid(pid);
 unlock_kernel();

 /*
  * The boot idle thread must execute schedule()
  * at least one to get things moving:
  */
 preempt_enable_no_resched();
 schedule();
 preempt_disable();

 /* Call into cpu_idle with preempt disabled */
 cpu_idle();
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------

从上面看出,rest_init()函数创建两个内核线程:1)kernel_init;2)kthreadd

kernel_init线程主要就用来加载驱动程序、挂载根文件系统、由内核线程蜕变成第一个用户进程init(pid=1)

kthreadd线程将是所有其它内核线程的父线程(ps -aux 命令查看,所有由[]括起的那些)

----------------------------------------------------------------------------------------------------------------------------------------------------------------------

static int __init kernel_init(void * unused)
{
 lock_kernel();

   。。。。。。
  do_basic_setup();          //加载驱动程序工作的地方

 /*
  * check if there is an early userspace init.  If yes, let it do all
  * the work
  */

 if (!ramdisk_execute_command)
  ramdisk_execute_command = "/init";

 if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
  ramdisk_execute_command = NULL;
  prepare_namespace();        //挂载根文件系统
 } 

 init_post();   //由内核线程蜕变成第一个用户进程init
 return 0;
}

---------------------------------------------------------------------------------------------------------------------------------------------------------------------

让我们来看看do_basic_setup()函数是如何加载驱动的:

--------------------------------------------------------------------------------------------------------------------------------------------------------------------

static void __init do_basic_setup(void)

{
 /* drivers will send hotplug events */
 init_workqueues();                 //初始化工作队列
 usermodehelper_init();        //用户态的khelper守护进程
 driver_init();                           //设备模型中一些基础结构体的初始化和设备的注册
init_irq_proc();                       //irq程序的初始化
do_initcalls();                        //(*call)()* 具体的initcall函数调用
}

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

现在开始我们有点眉目了。等不及了,进来看看吧!

----------------------------------------------------------------------------------------------------------------------------------------------------------------

static void __init do_initcalls(void)
{
 initcall_t *call;
 int count = preempt_count();

 for (call = __initcall_start; call < __initcall_end; call++) {
  ktime_t t0, t1, delta;
  char *msg = NULL;
  char msgbuf[40];
  int result;

  。。。。。。

   result = (*call)();

 。。。。。。

     }

 /* Make sure there is no pending stuff from the initcall sequence */
 flush_scheduled_work();
}

-----------------------------------------------------------------------------------------------------------------------------------------------------------

这个__initcall_start是在文件        arch/arm/kernel/vmlinux.lds
这个文件是内核ld的时候使用的.其中定义了各个sectioin,看看就明白了。
在这个文件中有个.initcall.init, 代码如下:
----------------------------------------------------------------------------------------------------------------------------------------------------------

SECTIONS
{
 .init : {   /* Init code and data  */
   *(.init.text)
  _einittext = .;
  __proc_info_begin = .;
   *(.proc.info.init)
  __proc_info_end = .;
  __arch_info_begin = .;
   *(.arch.info.init)
  __arch_info_end = .;
  __tagtable_begin = .;
   *(.taglist.init)
  __tagtable_end = .;
  . = ALIGN(16);
  __setup_start = .;
   *(.init.setup)
  __setup_end = .;
  __early_begin = .;
   *(.early_param.init)
  __early_end = .;
  __initcall_start = .;
    *(.initcall1.init)
   *(.initcall2.init)
   *(.initcall3.init)
   *(.initcall4.init)
   *(.initcall5.init)
   *(.initcall6.init)
   *(.initcall7.init)
  __initcall_end = .;
  __con_initcall_start = .;
   *(.con_initcall.init)
  __con_initcall_end = .;

。。。。。。

  }

。。。。。。

}

--------------------------------------------------------------------------------------------------------------------------------------------------------

这里有7个初始化的优先级,内核会按照这个优先级的顺序依次加载.
这些优先级是在文件include/linux/init.h 中定义的. 你注意一下宏 __define_initcall的实现就明白了.
相关代码如下:
#define __define_initcall(level,fn) \
        static initcall_t __initcall_##fn __attribute_used__ \
        __attribute__((__section__(".initcall" level ".init"))) = fn

#define core_initcall(fn)                __define_initcall("1",fn)
#define postcore_initcall(fn)                __define_initcall("2",fn)
#define arch_initcall(fn)                __define_initcall("3",fn)
#define subsys_initcall(fn)                __define_initcall("4",fn)
#define fs_initcall(fn)                        __define_initcall("5",fn)
#define device_initcall(fn)                __define_initcall("6",fn)
#define late_initcall(fn)                __define_initcall("7",fn)

---------------------------------------------------------------------------------------------------------------------------------------------------------

而我们经常写的设备驱动程序中就常用的module_init进行修饰,其实就是对应了优先级 6
#define __initcall(fn) device_initcall(fn)

#define module_init(x)        __initcall(x);

正是这样系统中的驱动程序会在内核启动过程中,被链接在一个段中统一被加载。

----------------------------------------------------------------------------------------------------------------------------------------------------------

         其实所以的__init函数在区段.initcall.init中保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些__init函数指针,并在整个初始化完成后,释放整个init区段(包括.init.text,.initcall.init等)。

注意,这些函数在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关,各个子区段之间的顺序是确定的,即先调用. initcall1.init中的函数指针,再调用.initcall2.init中的函数指针,等等。而在每个子区段中的函数指针的顺序是和链接顺序相关的,是不确定的。

未完待续……

 

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux系统驱动程序的调用顺序通常是由系统内核控制的。当设备被插入到系统时,内核会自动加载相应的驱动程序,并将其加入到设备驱动程序的调用链表驱动程序的调用顺序一般包括以下几个步骤: 1. 设备的探测(Probe):内核通过设备的信息来确定相应的驱动程序,并调用该驱动程序的probe函数。 2. 驱动程序的注册:当probe函数成功完成时,驱动程序会向系统内核注册设备,并分配相应的资源。 3. 设备的初始化(Init):驱动程序进行设备的初始化操作,如设置设备的工作模式、配置设备寄存器、启动设备等。 4. 设备的使用(Open):当设备被打开时,内核调用驱动程序的open函数,让驱动程序准备好相应的数据结构和处理器资源,以便于后续的读取和写入操作。 5. 设备的读取(Read):当用户进程调用read函数时,内核将请求传递到驱动程序,并调用驱动程序的read函数进行数据读取操作。 6. 设备的写入(Write):当用户进程调用write函数时,内核将请求传递到驱动程序,并调用驱动程序的write函数进行数据写入操作。 7. 设备的关闭(Close):当设备被关闭时,内核调用驱动程序的close函数,释放设备资源并清除相应的数据结构。 8. 设备的移除(Remove):当设备被拔出时,内核调用驱动程序的remove函数,释放设备资源并从设备驱动程序的调用链表移除相应的驱动程序。 需要注意的是,不同的设备驱动程序可能存在一些特殊的调用顺序,具体的实现可能会有所不同。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值