UP | HOME

内核入口

Table of Contents

start_kernel函数是与体系架构无关的通用处理入口函数,尽管在此初始化过程中要无数次的返回arch 文件夹

如果仔细看看start_kernel函数的内容,将发现此函数涉及内容非常广泛

在此过程中约包含了86个调用函数,是的,它真的是非常庞大

但是此部分并不是全部的初始化过程,在当前阶段只看这些就可以了

此章节以及后续所有在内核初始化过程章节的内容将涉及并详述它

start_kernel 函数的主要目的是完成内核初始化并启动 始祖进程 1号进程

在始祖进程启动之前start_kernel函数做了很多事情:

如锁验证器,根据处理器标识ID初始化处理器,开启cgroups子系统,设置每CPU区域环境

初始化VFS Cache机制,初始化内存管理,rcu,vmalloc,scheduler(调度器),IRQs(中断向量表),ACPI(中断可编程控制器)以及其它很多子系统

只有经过这些步骤才看到本章最后一部分祖先进程启动的过程

GCC attribute

start_kernel函数是定义在 init/main.c 从已知代码中能看到此函数使用了 __init特性 。在内核初始化阶段这个机制在所有的函数中都是有必要的:

#define __init      __section(.init.text) __cold notrace

在初始化过程完成后,内核将通过调用 free_initmem 释放这些 sections 。__init属性是通过 __coldnotrace 两个属性来定义的:

  • cold的目的是标记此函数很少使用,所以编译器必须优化此函数的大小
  • notrace定义如下:

    #define notrace __attribute__((no_instrument_function))
    

    含有no_instrument_function:告诉编译器函数调用不产生环境变量(堆栈空间)

在start_kernel函数的定义中,也可以看到 __visible 属性的扩展:

#define __visible __attribute__((externally_visible))

含有 externally_visible 意思就是告诉编译器有一些过程在使用该函数或者变量。可以在此 include/linux/init.h 处查到这些属性表达式的含义

start_kernel 初始化

在 start_kernel的初始之初可以看到这两个变量:

char *command_line; // 内核命令行的全局指针
char *after_dashes; // 包含parse_args函数通过输入字符串中的参数'name=value',寻找特定的关键字和调用正确的处理程序
这个时候不参与这两个变量的相关细节,但是会在接下来的章节看到

lockdep_init

接着往下走,下一步看到了此函数:

lockdep_init();

lockdep_init 初始化 lock validator . 其实现是相当简单的,它只是初始化了两个哈希表 list_head 并设置 lockdep_initialized 全局变量为 1

关于自旋锁 spinlock 以及 互斥锁mutex 如何获取

请参考链接 https://zh.wikipedia.org/wiki/%E8%87%AA%E6%97%8B%E9%94%81
https://zh.wikipedia.org/wiki/%E4%BA%92%E6%96%A5%E9%94%81 

set_task_stack_end_magic

下一个函数是 set_task_stack_end_magic ,参数为 init_task 初始化进程(任务) 数据结构。这个函数会为栈底设置一个魔术数字 STACK_END_MAGIC : 0x57AC6E9D 来防止栈溢出攻击

struct task_struct init_task = INIT_TASK(init_task);

task_struct 存储了进程的所有相关信息,可以查看调度相关数据结构定义头文件 include/linux/sched.h

因为它很庞大,这本书并不会去介绍

现在 task_struct已经包含了超过100个字段!

虽然不会在这本书中看到关于task_struct的解释,但是会经常使用它

因为它是介绍在Linux内核进程的基本知识,接下来将描述这个结构中字段的一些含义

可以查看 宏INIT_TASK 的初始化流程。这个宏指令来自于 include/linux/init_task.h 这里只是设置和初始化了第一个进程来(0号进程)的值。例如:

  • 初始化进程状态为 zero 或者 runnable. 一个可运行进程即为等待CPU去运行
  • 初始化仅存的标志位 PF_KTHREAD: 意思为 内核线程
  • 一个可运行的任务列表
  • 进程地址空间
  • 初始化进程堆栈 &init_thread_info:
    • init_thread_union.thread_info 和 init_thread_union 使用union thread_union

      • thread_union 包含了 thread_info进程信息以及进程栈:
      union thread_union {
                      struct thread_info thread_info;
                      unsigned long stack[THREAD_SIZE/sizeof(long)];
      };
      
      每个进程都有其自己的堆栈,x86_64架构的CPU一般支持的页表是16KB or 4个页框大小
      
      注意:stack变量被定义为数据并且类型是unsigned long
           thread_union代表一个联合体union而不是结构体
      
      • thread_info 定义如下:

        struct thread_info {
                struct task_struct      *task;
                struct exec_domain      *exec_domain;
                __u32                   flags; 
                __u32                   status;
                __u32                   cpu;
                int                     saved_preempt_count;
                mm_segment_t            addr_limit;
                struct restart_block    restart_block;
                void __user             *sysenter_return;
                unsigned int            sig_on_uaccess_error:1;
                unsigned int            uaccess_err:1;
        };
        
        thread_info结构包含了特定体系架构相关的线程信息,此结构占用52个字节
        
        在X86_64架构上内核栈是逆生成而thread_union.thread_info结构则是正生长
        
        进程进程栈是16KB并且thread_info是在栈底,因此可以使用的是:16KB - 52 bytes = 16332 bytes
        
    • 用一张图来描述栈内存空间。 如下图所示:

      e+-----------------------+
      |                       |
      |                       |
      |        stack          |
      |                       |
      |_______________________|
      |          |            |
      |          |            |
      |          |            |
      |__________↓____________|             +--------------------+
      |                       |             |                    |
      |      thread_info      |<----------->|     task_struct    |
      |                       |             |                    |
      +-----------------------+             +--------------------+
      

现在回到set_task_stack_end_magic函数,这个函数被定义在 kernel/fork.c 功能为设置 init 进程堆栈以检测堆栈溢出 :

void set_task_stack_end_magic(struct task_struct *tsk)
{
                unsigned long *stackend;
                stackend = end_of_stack(tsk);
                *stackend = STACK_END_MAGIC; /* for overflow detection */
}

先通过 end_of_stack 函数获取堆栈并赋给 task_struct 。因为学习的是x86架构的初始化,堆栈是逆生成,所以堆栈底部为:

(unsigned long *)(task_thread_info(p) + 1);

在进程的栈底,写入STACK_END_MAGIC这个值

smp_setup_processor_id

下一个函数是 smp_setup_processor_id .此函数在x86_64架构上是空函数:

void __init __weak smp_setup_processor_id(void)
{
}

在此架构上没有实现此函数,但在别的体系架构的实现可以参考 arm64

debug_object_early_init

debug_object_early_init 函数的执行几乎和lockdep_init是一样的,但是填充的哈希对象是调试相关

boot_init_stack_canary

task_struct->canary 的值利用了GCC特性

但是此特性需要先使能内核CONFIG_CC_STACKPROTECTOR宏后才可以使用,否则什么也不做

boot_init_stack_canary 基于随机数和随机池产生 TSC :

get_random_bytes(&canary, sizeof(canary));
tsc = __native_read_tsc();
canary += tsc + (tsc << 32UL);

给当前字段的 stack_canary 字段赋值:

current->stack_canary = canary;

然后将此值写入IRQ 堆栈的顶部:

this_cpu_write(irq_stack_union.stack_canary, canary);
关于IRQ的章节这里也不会详细剖析

local_irq_disable

canary被设置后, 关闭本地中断 interrupts for current CPU 使用 local_irq_disable 函数,展开后原型为 arch_local_irq_disable 函数 include/linux/percpu-defs.h:

static inline notrace void arch_local_irq_disable(void)
{
        native_irq_disable();
}

激活第一个CPU

当前已经走到start_kernel函数中的 boot_cpu_init 函数,此函数主要为了通过 掩码 初始化 每一个 CPU 。首先需要获取当前处理器的ID通过下面函数:

int cpu = smp_processor_id();

smp_processor_id 的值就来自于 raw_smp_processor_id 函数,原型如下:

#define raw_smp_processor_id() (this_cpu_read(cpu_number))

this_cpu_read 函数与其它很多函数一样如(this_cpu_write, this_cpu_add 等等…) 被定义在 include/linux/percpu-defs.h 这里函数主要为对 this_cpu 进行操作. 这些操作提供不同的对每cpuper-cpu 变量相关访问方式. 譬如来看看这个函数 this_cpu_read:

__pcpu_size_call_return(this_cpu_read_, pcp)

现在看看 __pcpu_size_call_return 的执行:

#define __pcpu_size_call_return(stem, variable)                 \
                ({                                                      \
                                typeof(variable) pscr_ret__;                    \
                                __verify_pcpu_ptr(&(variable));                 \
                                switch(sizeof(variable)) {                      \
                                case 1: pscr_ret__ = stem##1(variable); break;  \
                                case 2: pscr_ret__ = stem##2(variable); break;  \
                                case 4: pscr_ret__ = stem##4(variable); break;  \
                                case 8: pscr_ret__ = stem##8(variable); break;  \
                                default:                                        \
                                                __bad_size_call_parameter(); break;     \
                                }                                               \
                                pscr_ret__;                                     \
                }) 

是的,此函数虽然看起起奇怪但是它的实现是简单的:

  1. pscr_ret__ 变量的定义是 int类型 , variable参数 是common_cpu 它声明了每cpu(per-cpu)变量:

    DECLARE_PER_CPU_READ_MOSTLY(int, cpu_number);
    
  2. 调用了 __verify_pcpu_ptr 通过使用一个有效的 per-cpu变量指针 来取地址得到 cpu_number
  3. 通过pscr_ret__ 函数设置变量的大小,common_cpu变量是int,所以它的大小是4字节

    意思就是通过this_cpu_read4(common_cpu)获取cpu变量,其大小被pscr_ret__决定
    
  4. 在__pcpu_size_call_return的结束 调用了 __pcpu_size_call_return

    #define this_cpu_read_4(pcp)       percpu_from_op("mov", pcp)
    
    • 需要调用 percpu_from_op 并且通过 mov 指令来传递每cpu变量,percpu_from_op的内联扩展如下:

      asm("movl %%gs:%1,%0" : "=r" (pfo_ret__) : "m" (common_cpu))
      
      gs段寄存器包含每个CPU区域的初始值,这里通过mov指令copy common_cpu到内存中去
      

此函数还有另外的形式:

this_cpu_read(common_cpu)

等价于:

movl %gs:$common_cpu, $pfo_ret__
由于没有设置每个CPU的区域, 并且只有一个

所以当前CPU的值zero 通过此函数 smp_processor_id返回

boot_cpu_init 函数设置了CPU的在线, 激活:

set_cpu_online(cpu, true);
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);

上述所有使用的这些CPU的配置称之为 CPU掩码 cpumask

  • cpu_possible 则是设置支持CPU热插拔时候的CPU ID
  • cpu_present 表示当前热插拔的CPU
  • cpu_online表示当前所有在线的CPU
  • cpu_present 来决定被调度出去的CPU.
CPU热插拔的操作需要打开内核配置宏 CONFIG_HOTPLUG_CPU

并且将 possible == present 以及active == online选项禁用

这些功能都非常相似,每个函数都需要检查第二个参数,如果设置为true,需要通过调用 cpumask_set_cpucpumask_clear_cpu 来改变状态。譬如可以通过第二个参数 true 来这么调用:

cpumask_set_cpu(cpu, to_cpumask(cpu_possible_bits));

继续尝试理解 to_cpumask宏 指令,此宏指令转化为一个位图:通过 struct cpumask 指针,CPU掩码提供了位图集代表了当前系统中所有的CPU's,每CPU都占用1bit,CPU掩码相关定义通过cpu_mask结构定义:

typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;

下面一组函数定义了位图宏指令:

#define DECLARE_BITMAP(name, bits) unsigned long name[BITS_TO_LONGS(bits)]
// static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;
DECLARE_BITMAP宏指令的原型是一个unsigned long的数组

再来查看如何执行to_cpumask:

#define to_cpumask(bitmap)                                              \
        ((struct cpumask *)(1 ? (bitmap)                                \
                            : (void *)sizeof(__check_is_bitmap(bitmap))))
其实就是一个条件判断语句当条件为真的时候,但是为什么执行__check_is_bitmap?

看看 __check_is_bitmap 的定义:

static inline int __check_is_bitmap(const unsigned long *bitmap)
{
        return 1;
}

原来此函数始终返回1

事实上需要这样的函数才达到目的:

它在编译时给定一个bitmap,换句话将就是检查bitmap的类型是否是unsigned long *

因此通过 to_cpumask 宏指令将类型为unsigned long的数组转化为struct cpumask *

现在可以调用cpumask_set_cpu 函数,这个函数仅仅是一个 set_bit给CPU掩码的功能函数

所有的这些set_cpu_*函数的原理都是一样的

如果还不确定set_cpu_*这些函数的操作并且不能理解 cpumask的概念,不要担心。可以通过读取这些章节 cpumask 或者 cpu-hotplug.来继续了解和学习这些函数的原理

现在已经激活第一个CPU,继续接着start_kernel函数往下走

下面的函数是page_address_init, 但是此函数不执行任何操作,因为只有当所有内存不能直接映射的时候才会执行

内核的第一条打印信息

下面调用了 pr_notice 函数:

#define pr_notice(fmt, ...) \
    printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)

pr_notice其实是 printk 的扩展,这里使用它打印了Linux 的banner:

pr_notice("%s", linux_banner);

打印的是内核的版本号以及编译环境信息:

Linux version 4.0.0-rc6+ (alex@localhost) (gcc version 4.9.1 (Ubuntu 4.9.1-16ubuntu6) ) #319 SMP

依赖于体系结构的初始化部分

下个步骤就要进入到指定的体系架构的初始函数,Linux 内核初始化体系架构相关调用 setup_arch 函数

这又是一个类型于start_kernel的庞大函数,这里仅仅简单描述

在下一节将继续深入,指定体系架构的内容

setup_arch函数定义在 arch/x86/kernel/setup.c 文件中,此函数就一个参数:内核命令行

这里使用 memblock 来解析内存块:

memblock_reserve(__pa_symbol(_text), (unsigned long)__bss_stop - (unsigned long)_text);

。此函数解析 内核段 _text_data 来自于 _text符号和 _bss_stop

_text 和 _bss_stop 符号来自于文件arch/x86/kernel/head_64.S

memblock_reserve函数的两个参数:

  • base physical address of a memory block
  • size of a memory block

关于memblock的相关内容在 Linux kernel memory management Part 1

通过 __pa_symbol宏 指令来获取符号表 _text段 中的 物理地址

#define __pa_symbol(x) \
        __phys_addr_symbol(__phys_reloc_hide((unsigned long)(x)))
  • 调用 __phys_reloc_hide 宏 指令来填充参数,这个宏指令在x86_64上返回的参数是给定的
  • 宏指令 __phys_addr_symbol 的执行是简单的,只是减去从 “_text符号表中读到的内核的符号映射地址” 并且加上 “物理地址的基地址”
#define __phys_addr_symbol(x) \
 ((unsigned long)(x) - __START_KERNEL_map + phys_base)

memblock_reserve函数对内存页进行分配

保留可用内存初始化initrd

在内核text和data段中保留内存用来初始化initrd

暂时不去了解initrd的详细信息,仅仅只需要知道根文件系统就是通过这种方式来进行初始化

这就是 early_reserve_initrd 函数的工作,此函数获取 RAM DISK的基地址RAM DISK的大小 以及 RAM DISK的结束地址

u64 ramdisk_image = get_ramdisk_image();
u64 ramdisk_size  = get_ramdisk_size();
u64 ramdisk_end   = PAGE_ALIGN(ramdisk_image + ramdisk_size);
如果阅读过前面Linux启动过程 ,就知道所有的这些参数都来自于boot_params

boot_params在boot期间已经被赋值,包含了一下几个字段用来描述RAM DISK:

Field name:	ramdisk_image
Type:		write (obligatory)
Offset/size:	0x218/4
Protocol:	2.00+

  The 32-bit linear address of the initial ramdisk or ramfs.  Leave at
  zero if there is no initial ramdisk/ramfs.

具体查看 get_ramdisk_image :

static u64 __init get_ramdisk_image(void)
{
        u64 ramdisk_image = boot_params.hdr.ramdisk_image;

        ramdisk_image |= (u64)boot_params.ext_ramdisk_image << 32;

        return ramdisk_image;
}

关于32位的ramdisk的地址,可以阅读此部分内容来获取zero-page.txt:

0C0/004	ALL	ext_ramdisk_image ramdisk_image high 32bits

获取64位的ramdisk原理一样,为此可以检查bootloader 提供的ramdisk信息:

if (!boot_params.hdr.type_of_loader ||
    !ramdisk_image || !ramdisk_size)
                return;

校验成功后保留内存块,并将ramdisk传输到最终的内存地址,然后进行初始化:

memblock_reserve(ramdisk_image, ramdisk_end - ramdisk_image);
Next: 与系统架构有关的初始化 Previous: 进入内核入口点之前最后的准备工作 Home:内核初始化