linux内核初始化调用顺序

x86_64平台,上个图先,5种颜色各代表1个阶段:

图片转自:https://blog.csdn.net/xxiomg/article/details/100558790

For x86 64bits architecture platform, linux-2.6.35.4

  1. arch/x86/boot/header.S: the first instruction that runs in kernel image in real mode is at label "_start", then call "start_of_setup", then call "main".
  2. arch/x86/boot/main.c: "main" function is called by "start_of_setup", after "main" doing lots of operation, at last, call "go_to_protected_mode", 
  3. arch/x86/boot/pm.c: "go_to_protected_mode" is defined here, it will call "protected_mode_jump".
  4. arch/x86/boot/pmjump.S: "protected_mode_jump" is in this file. After executing this function, the processor is in protected mode.
  5. arch/x86/boot/compressed/head_64.S: the entry point is "startup_32", call "decompress_kernel"
  6. arch/x86/boot/compressed/misc.c: executing "decompress_kernel".
  7. arch/x86/boot/compressed/head_64.S: jump to the decompressed kernel entry point "startup_64"
  8. arch/x86/kernel/head_64.S: start to execute at label "startup_64", then call "x86_64_start_kernel".
  9. arch/x86/kernel/head64.c: executing "x86_64_start_kernel", it call "x86_64_start_reservations", "x86_64_start_reservations" call "start_kernel".
  10. init/main.c: start executing "start_kernel".

即:

arch/x86/boot/header.S   -->   _start   -->   calll   main

arch/x86/boot/main.c   -->   main -- >   go_to_protected_mode

arch/x86/boot/pm.c   -->   go_to_protected_mode   -->   protected_mode_jump

arch/x86/boot/pmjump.S   -->   GLOBAL(protected_mode_jump)   -->   jmpl    *%eax

arch/x86/boot/compressed/head_64.S   -->   ENTRY(startup_32)   -->   ENTRY(startup_64)   -->    jmp *%rax

arch/x86/kernel/head_64.S   -->   startup_64   -->   ENTRY(secondary_startup_64)   -->   lretq (%rax)

arch/x86/kernel/head64.c   --> x86_64_start_kernel   -->   start_kernel

init/main.c   -->   start_kernel   -->   rest_init

 

现在的 Linux 启动过程一般分成了两步,也就是首先调用 GRUB 作为通用的启动服务,然后可以选择 Windows 或者 Linux 加载。

接下来,看看 Linux 的加载过程。

启动加载

Bootloader 执行完之后,将内核加载到物理地址 0x100000(1MB) 处,并将执行权限交给了内核。然后执行的是 setup.bin 的入口函数 _start() ( arch/x86/boot/header.S,链接文件为 setup.lds ) 。

第一条是一个调转指令,调转到 start_of_setup (start_of_setup-1f是setup头部的长度),在这之间的代码是一个庞大的数据结构,与 bootparam.h 中的 struct setup_header 一一对应。这个数据结构定义了启动时所需的默认参数,其中一些参数可以通过命令选项 overwrite。很多值是在形成 bzImage 时,由 arch/x86/boot/tools/build 程序赋值。

接着实际是设置 C 语言环境,主要是设置堆栈,然后调转到 main()@arch/x86/boot/main.c,现在仍然运行在实模式。setup.bin 主要作用就是完成一些系统检测和环境准备的工作,其函数调用顺序为:

_start()@arch/x86/boot/header.S
  |-start_of_setup()@arch/x86/boot/header.S
    |-main()@arch/x86/boot/main.c
      |-copy_boot_params()                        // 将进行参数复制
      |-detect_memory()                           // 探测物理内存,第一次与内存相关
      |-go_to_protect_mode
        |-realmode_switch_hook()                  // 如果boot_params.hdr.realmode_swtch有非空的hook函数地址则执行之
        |-enable_a20()
        |-reset_coprecessor()                     // 重启协处理器
        |-protected_mode_jump()@arch/x86/boot/pmjump.S   // 进入 32-bit 模式

main.c 主要功能为检测系统参数如: Detect memory layoutset video mode 等,后面会详细分析,最后调用 goto_protect_mode,设置 32-bit 或 64-bit 保护段式寻址模式,注意: main.o 编译为16位实模式程序。

内核的日志信息可以通过 printk() 打印,不过只有信息等级小于 console loglevel 的值时,这些信息才会被打印到 console 上,可以通过如下方法修改日志等级。

  1. 修改内核启动时的参数选项,loglevel=level。
  2. 运行时执行如下命令 dmesg -n level 或者 echo $level > /proc/sys/kernel/printk 。
  3. 写程序使用 syslog 系统调用。

goto_protect_modea() 则会调用 protected_mode_jump(),进入 32-bit 的兼容模式,也就是 IA32-e 模式,在最后会执行 jmpl *%eax 调转到 32-bits 的入口,也就是 0x100000 处。

startup_32()

可以看到 arch/x86/boot/compressed/head_64.S 的入口函数 startup_32,该函数会解压缩 Kernel、设置内存地址转换、进入 64bit 模式。

关于转换的具体步骤,可以参考 《Intel 64 and IA-32 Architectures Software Developer’s Manual》 中的 9.8.5 Initializing IA-32e Mode 部分 。

然后会调转到 arch/x86/kernel/head_64.S 的 startup_64 。

准备环境

这个函数主要是为第一个 Linux 进程 (进程0) 建立执行环境,该函数主要执行以下操作:

bootstrap

最先执行的是 arch/x86/boot/header.S (由最早的bootsect.S和setup.S修改而来),一般是汇编语言。最早的时候 Linux 可以自己启动,因此包含了 Boot Sector 的代码,但是现在如果执行的话只会输出 "bugger_off_msg" ,并重启,该段为 .bstext 。

现在的 Bootloader 会忽略这段代码。上述无效的 Boot Sector 之后还有 15Bytes 的 Real-Mode Kernel Header ,这两部分总计 512Bytes 。

512 字节之后,也就是在偏移 0x200 处,是 Linux Kernel 实模式的入口,也即 _start 。第一条指令是直接用机器码写的跳转指令 (0xeb+[start_of_setup-1]),因此会直接跳转到 start_of_setup 。这个函数主要是设置堆栈、清零 bss 段,然后跳转到 arch/x86/boot/main.c:main() 。

main() 函数主要完成一些必要的清理工作,例如探测内存的分布、设置 Video Mode 等,最后调用 go_to_protected_ mode()@arch/x86/boot/pm.c 。

初始化

在进入保护模式之前,还需要一些初始化操作,最主要的两项是 中断 和 内存。

  • 中断
    在实模式中,中断向量表保存在地址 0 处,而在保护模式中,中断向量表的地址保存在寄存器 IDTR 中。
  • 内存
    实模式和保护模式的从逻辑地址到实地址的转换不同。在保护模式中,需要通过 GDTR 定位全局描述符表(Global Descriptor Table) 。

因此,进入保护模式之前,在 go_to_protected_mode() 中会通过调用 setup_idt() 和 setup_gdt() 创建临时的中断描述符表和全局描述符表。最后通过 protected_mode_jump() (同样为汇编语言) 进入保护模式。

protected_mode_jump() 会设置 CR0 寄存器的 PE 位。此时,不需要分页功能,因此分页功能是关闭的。最主要的是现在不会受 640K 的限制, RAM 的访问空间达到了 4GB 。

然后会调用 32-bit 的内核入口,即 startup_32 。该函数会初始化寄存器,并通过 decompress_kernel() 解压内核。

decompress_kernel() 输出 "Decompressing Linux..." ,然后 in-place 解压,解压后的内核镜像会覆盖上图中的压缩镜像。因此,解压后的内容也是从 1MB 开始。解压完成后将会输出 "done.",然后输出 "Booting the kernel." 。

此时将会跳转到保护模式下的入口处 (0x100000),该入口同样为 startup_32 ,但与上面的在不同的目录下。

第二个 startup_32 同样为汇编语言,包含了 32-bit 的初始化函数。包括清零保护模式下的 Kernel bss 段、建立全局描述符、建立中断描述符表,然后跳转到目标相关的启动入口,start_kernel() ,过程如下所示。

bootstrap protect mode

正式启动

start_kernel()@init/main.c 的大部分代码为 C ,而且与平台相关。该函数会初始化内核的各个子系统和数据结构,包括了调度器(Scheduler)、内存、时钟等。

然后 start_kernel() 会调用 rest_init() ,该函数将会创建一个内核线程,并将 kernel_init() 作为一个入口传入。

dmesg 中的信息是从 start_kernel() 之后记录的。

像 USB、ACPI、PCI 这样的系统,会通过 subsys_initcall() 定义一个入口,当然还有一些其它的类似入口,可以参考 include/linux/init.h ,实际上时在内核镜像中定义了 .initcall.init 字段 (连接脚本见vmlinux.lds)。

这些函数会在 do_initcalls() 中调用。

rest_init() 随后会调用 schedule() 启动任务调度器,并通过 cpu_idle() 进入睡眠,这是 Linux 中的空闲线程。当没有可运行的进程时,该线程会调用,否则运行可运行的线程。

此时,之前启动的线程将会替代进程0(process 0) 即空闲线程。kernel_init() 将会初始化其他的 CPUs ,在此之前,这些 CPUs 并没有运行。负责初始化的 CPU 被称为 boot processor ,此时启动的 CPUs 被称为 application processors ,这些 CPUs 同样从实模式启动,因此也需要类似的初始化。

最后 kernel_init() 调用 init_post() ,该函数将会尝试执行一个用户进程,执行顺序如下 /sbin/init/etc/init/bin/init 和 /bin/sh 。如果所有的都失败了,那么内核将会停止。

此时执行的进程 PID 为 1 ,它将会检查配置文件,然后运行相应的进程,如 X11 Windows、登录程序、网络服务等。

至此,全部启动过程完成。

bootstrap all

详细流程

从代码角度,介绍启动时的调用流程。

start_kernel()
 |-smp_setup_processor_id()                  ← 返回启动时的CPU号
 |-local_irq_disable()                       ← 关闭当前CPU的中断
 |-setup_arch()                              ← 完成与体系结构相关的初始化工作
 | |-setup_memory_map()                      ← 建立内存图
 | |-e820_end_of_ram_pfn()                   ← 找出最大的可用页帧号
 | |-init_mem_mapping()                      ← 初始化内存映射机制
 | |-initmem_init()                          ← 初始化内存分配器
 | |-x86_init.paging.pagetable_init()        ← 建立完整的页表
 |-parse_early_param()
 | |-parse_args()                            ← 调用两次parse_args()处理bootloader传递的参数
 |-parse_args()
 |-init_IRQ()                                ← 硬件中断初始化
 |-softirq_init()                            ← 软中断初始化
 |
 |-vfs_caches_init_early()
 |-vfs_caches_init()                         ← 根据参数计算可以作为缓存的页面数,并建立一个存放文件名称的slab缓存
 | |-kmem_cache_create()                     ← 创建slab缓存
 | |-dcache_init()                           ← 建立dentry和dentry_hashtable的缓存
 | |-inode_init()                            ← 建立inode和inode_hashtable的缓存
 | |-files_init()                            ← 建立filp的slab缓存,设置内核可打开的最大文件数
 | |-mnt_init()                              ← 完成sysfs和rootfs的注册和挂载
 |   |-kernfs_init()
 |   |-sysfs_init()                          ← 注册挂载sysfs
 |   | |-kmem_cache_create()                 ← 创建缓存
 |   | |-register_filesystem()
 |   |-kobject_create_and_add()              ← 创建fs目录
 |   |-init_rootfs()                         ← 注册rootfs文件系统
 |   |-init_mount_tree()                     ← 建立目录树,将init_task的命名空间与之联系起来
 |     |-vfs_kern_mount()                    ← 挂载已经注册的rootfs文件系统
 |     | |-alloc_vfsmnt()
 |     |-create_mnt_ns()                     ← 创建命名空间
 |     |-set_fs_pwd()                        ← 设置init的当前目录
 |     |-set_fs_root()                       ← 以及根目录
 |
 |-rest_init()
   |-kernel_init()                           ← 通过kernel_thread()创建独立内核线程
   | |-kernel_init_freeable()
   | | |-do_basic_setup()
   | | | |-do_initcalls()                    ← 调用子模块的初始化
   | | |   |-do_initcall_level()
   | | |     |-do_one_initcall()             ← 调用一系列初始化函数
   | | |       |-populate_rootfs()
   | | |         |-unpack_to_rootfs()
   | | |
   | | |-prepare_namespace()
   | |   |-wait_for_device_probe()
   | |   |-md_run_setup()
   | |   |-initrd_load()                     ← 加载initrd
   | |   | |-create_dev()
   | |   | |-rd_load_image()
   | |   |   |-identify_ramdisk_image()      ← 检查映像文件的magic确定格式,minux、ext2等;并返回解压方法
   | |   |   | |-decompress_method()
   | |   |   |-crd_load()                    ← 解压
   | |   |     |-deco()
   | |   |
   | |   |-mount_root()
   | |
   | |-run_init_process()                    ← 执行init,会依次查看ramdisk、命令行指定、/sbin/init等
   |
   |-kthreadd()                              ← 同样通过kernel_thread()创建独立内核线程

初始化

在初始化时通常可以分为两种:A) 一种是关键而其必须按照特定顺序来完成,通常在 start_kernel() 中直接调用;B) 以子系统、模块实现,通过 do_initcalls() 完成。

在 do_initcalls() 中调用时,会按照等级,从 level0 ~ level7 来初始化,其宏定义在 include/linux/init.h 中实现,简单分为了两类,内核以及模块的实现。

下面以 inet_init 的初始化为例,末行为最后的展开格式。

fs_initcall(inet_init);                                           // net/ipv4/af_inet.c
#define fs_initcall(fn) __define_initcall(fn, 5)                  // include/linux/init.h
#define __define_initcall(fn, id) \                               // 同上
      static initcall_t __initcall_##fn##id __used \
      __attribute__((__section__(".initcall" #id ".init"))) = fn

static initcall_t __initcall_inet_init5 __used __attribute__((__section__(".initcall5.init"))) = inet_init;

do_initcalls() 是从特定的内存区域取出初始化函数的指针,然后调用该函数,通过 "vmlinux.lds.h" 定义的宏。

 

ipv4内核初始化相关

所在文件:
net/ipv4/af_inet.c
初始化函数定义:

  1. static int __init inet_init ( void )

初始化函数调用:

  1. fs_initcall ( inet_init ) ; //#define fs_initcall(fn)                 __define_initcall("5",fn,5)

这里的fs_initcall和module_init这样的函数是一样的功能,就是给系统内核添加一个功能函数。

这个宏的定义位于inlcludelinuxinit.h中:

  1. # define __define_initcall ( level , fn , id )
  2. static initcall_t __initcall_ ## fn ## id __used
  3. __attribute__ (( __section__ ( " .initcall "   level " .init " ))) = fn

其中 initcall_t 是个函数指针类型:typedef int (*initcall_t)(void);

而属性 __attribute__((__section__())) 则表示把对象放在一个这个由括号中的名称所指代的section中。
以这个宏定义的的含义是:
1) 声明一个名称为__initcall_##fn##id的函数指针(其中##表示替换连接,);
2) 将这个函数指针初始化为fn;
3) 编译的时候需要把这个函数指针变量放置到名称为 “.initcall” level “.init”的section中(比如level=”1″,代表这个section的名称是 “.initcall1.init”)。

这些衍生宏宏的定义也位于 inlcludelinuxInit.h 中:

  1. # define pure_initcall ( fn )                __define_initcall ( " 0 " , fn , 0 )
  2. # define   core_initcall ( fn )                __define_initcall ( " 1 " , fn , 1 )
  3. # define   core_initcall_sync ( fn )           __define_initcall ( " 1s " , fn , 1 s )
  4. # define   postcore_initcall ( fn )            __define_initcall ( " 2 " , fn , 2 )
  5. # define   postcore_initcall_sync ( fn )       __define_initcall ( " 2s " , fn , 2 s )
  6. # define   arch_initcall ( fn )                __define_initcall ( " 3 " , fn , 3 )
  7. # define   arch_initcall_sync ( fn )           __define_initcall ( " 3s " , fn , 3 s )
  8. # define   subsys_initcall ( fn )              __define_initcall ( " 4 " , fn , 4 )
  9. # define   subsys_initcall_sync ( fn )         __define_initcall ( " 4s " , fn , 4 s )
  10. # define   fs_initcall ( fn )                  __define_initcall ( " 5 " , fn , 5 )
  11. # define   fs_initcall_sync ( fn )             __define_initcall ( " 5s " , fn , 5 s )
  12. # define   rootfs_initcall ( fn )              __define_initcall ( " rootfs " , fn , rootfs )
  13. # define   device_initcall ( fn )              __define_initcall ( " 6 " , fn , 6 )
  14. # define   device_initcall_sync ( fn )         __define_initcall ( " 6s " , fn , 6 s )
  15. # define   late_initcall ( fn )                __define_initcall ( " 7 " , fn , 7 )
  16. # define   late_initcall_sync ( fn )           __define_initcall ( " 7s " , fn , 7 s )

因此通过宏 core_initcall() 来声明的函数指针,将放置到名称为.initcall1.init的section中,而通过宏 postcore_initcall() 来声明的函数指针,将放置到名称为.initcall2.init的section中,依次类推。
在:include/asm-generic/vmlinux.lds.h:

  1. # define INITCALLS                                                       
  2. * ( . initcallearly . init )                                          
  3. VMLINUX_SYMBOL ( __early_initcall_end ) = .;    //注意这里的__early_initcall_end标志
  4. * ( . initcall0 . init )                                              
  5. * ( . initcall0s . init )                                             
  6. * ( . initcall1 . init )                                              
  7. * ( . initcall1s . init )                                             
  8. * ( . initcall2 . init )                                              
  9. * ( . initcall2s . init )                                             
  10. * ( . initcall3 . init )                                              
  11. * ( . initcall3s . init )                                             
  12. * ( . initcall4 . init )                                              
  13. * ( . initcall4s . init )                                             
  14. * ( . initcall5 . init )                                              
  15. * ( . initcall5s . init )                                             
  16. * ( . initcallrootfs . init )                                         
  17. * ( . initcall6 . init )                                              
  18. * ( . initcall6s . init )                                             
  19. * ( . initcall7 . init )                                              
  20. * ( . initcall7s . init )
  21. # define   INIT_CALLS                                                      
  22. VMLINUX_SYMBOL ( __initcall_start ) = .;                  
  23. INITCALLS                                               
  24. VMLINUX_SYMBOL ( __initcall_end ) = .;    //还有这里的__initcall_end

最终跟踪之后这个初始化的段会在arch/x86/kernel/vmlinux.lds.S这样的体系结构中内核二进制文件结构组织的配置文件中。
而在内核Makefile文件中有这样的编译语句:

  1. vmlinux : $ ( vmlinux - lds ) $ ( vmlinux - init ) $ ( vmlinux - main ) vmlinux . o $ ( kallsyms . o ) FORCE
  2. 。。。
  3. vmlinux - lds   := arch /$ ( SRCARCH ) / kernel / vmlinux . lds
  4. 。。。

而在init/main.c 中:

  1. static void __init do_initcalls ( void )
  2. {
  3. initcall_t * call ;
  4.  
  5. for   ( call = __early_initcall_end ; call & lt ; __initcall_end ; call ++ )
  6. do_one_initcall ( * call ) ;
  7.  
  8. /* Make sure there is no pending stuff from the initcall sequence */
  9. flush_scheduled_work () ;
  10. }

该函数的调用关系如下:

  1. start_kernel --& gt ; rest_init -& gt ; kernel_thread ( kernel_init , NULL , CLONE_FS | CLONE_SIGHAND ) ;
  2. |
  3. -& gt ; kernel_init ( void * unused ) -& gt ; do_initcalls ( void )

也就是说对于所有的内核模块或是其它的以类似该形式加入到内核中的程序,都最终在内核所在的二进制文件中是有一个固定的段来存放的,而且内核在初始化的过程中也是找到这些段的地址让后做相应的加载和执行。

参考:

https://blog.csdn.net/xxiomg/article/details/100558790

https://gohalo.me/post/kernel-bootstrap.html

https://gohalo.me/post/kernel-bootstrap.html

https://www.cnblogs.com/lcw/p/3337937.html

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值