linux启动代码分析

linux版本3.0.8+  

最开始时几乎每个硬件设备都处于一种随机的,不可预知的状态。此外,启动过程在很大程度上都依赖于计算机体系结构。

linux一旦进入保护模式,就不再使用BIOS,而是为计算机上的每个硬件设备提供各自的设备驱动程序。

引导装入程序boot loader是由BIOS用来把操作系统的内核镜像装载到RAM中所调用的一个程序。

硬盘的第一个扇区称为主引导纪录,该扇区中包括分区表和一个小程序,这个小程序用来装载被启动的操作系统所在分区的第一个扇区。



因为之前看驱动程序的代码遇到了很多自己不能理解的问题,跟代码最后很多都跟到了系统初始化的地方,所以打算将系统启动代码完整地看一遍。

  在bootloader将linux的uImage搬到指定内存空间地址并且引导linux内核启动后,linux首先从arch/xxx_cpu/kernel/head.S先一步一步执行汇编代码。

  (1)加载bootloader_tags的地址

  (2)使能指令cache

  (3)使能数据cache

  (4)使能MMU

  (5)初始化TLB

  (6)建立中断异常向量表

  (7)加载当前任务栈。

最后调用start_kernel()函数,由汇编代码变为C语言。之前的代码也看不懂,但是CPU厂商通常会事先将head.S给我们,让我们进行之后的事情。

  一般的start_kernel()函数在init/main.c中,接下来我们慢慢向下看:

  有很多函数都是空函数,可能是如果需要我们自己去完善我们想要完善的部分。

  首先我们要

local_irq_disable();最终调用arch_local_irq_disable();该函数在arch/xxx_cpu/include/asm/irqflags.h中由cpu厂商汇编实现

tick_init();会调用clockevents_register_notifier(&tick_notifier);将clock_event注册到clockevents_chain中.而在tick_notifier中包含了tick处理事件的函数。

boot_cpu_init();

 static void __init boot_cpu_init(void)
 {
         int cpu = smp_processor_id();
         /* Mark the boot cpu "present", "online" etc for SMP and UP case */
         set_cpu_online(cpu, true);
         set_cpu_active(cpu, true);
         set_cpu_present(cpu, true);
         set_cpu_possible(cpu, true);
 }
在跟代码的时候遇到了疑问DECLARE_BITMAP();???

接下来关键函数***setup_arch();//该函数由cpu厂商设定好.

         init_mm.start_code = (unsigned long) &_stext;
         init_mm.end_code = (unsigned long) &_etext;
         init_mm.end_data = (unsigned long) &_edata;
         init_mm.brk = (unsigned long) 0;
init_mm为初始化的内存,上述代码中的_stext,_etext,_edata在arch/xxx_cpu/kernel/head.S中定义好了.

cpu_probe();实际上就是填充cpuinfo_xxx里面的信息(cpu类型,icache大小,dcache大小);
config_BSP();实际上是注册回调函数,提供之后的函数调用.

          mach_time_init = xxx_timer_init;//定时器的初始化
          mach_hwclk = xxx_hwclk;//RTC的时间初始化
          mach_init_IRQ = xxx_init_IRQ;//中断初始化
          mach_get_auto_irqno = xxx_get_auto_irqno;
          mach_reset = xxx_machine_restart;
  #ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
          mach_tick = xxx_tick;
          mach_gettimeoffset = xxx_timer_offset;
  #endif  
  #ifdef CONFIG_CPU_USE_FIQ
          mach_init_FIQ = xxx_init_FIQ;
  #endif
          prom_meminit();//分配DSP内存,解析mem=参数???还是有点没看懂
setup_irq();该函数是在早期boot阶段将中断号和中断处理函数进行绑定.

struct clock_event_device;

struct clocksource;

clockevents_register_device(&clock_event_device);

clocksource_register_hz(&clock_source,BUSCLK);
在early_param();中调用__setup_param();将传入的函数指针放入.init.setup中,在系统初始化是执行该函数。

注意default_command_line为传入的.config中CONFIG_CMDLINE的字符串.

parse_early_param();

IDR机制适用在需要把某个整数和特定指针关联在一起的地方,内部采用红黑树实现,可很方便将整数和指针关联起来,具有很高搜索效率。

prio_tree在linux内核中,被应用于反向内存映射,prio_tree是一棵查找树,查找的是一个区间。

__attribute__可设置函数属性,变量属性和类型属性。


伪文件系统proc在处理大数据结构(大于一页的数据)有比较大的局限性。如果当前的内核缓冲区不足以容纳一条记录,那么内核缓冲区大小加倍后再次尝试,直到可容纳至少一条记录,然后持续调用next和show,直到内核缓冲区无法容纳新的整条记录后调用stop终止。

注意用户仅需要设置open函数,其他都是seq_file提供的函数,对于简单的输出,seq_file用户并不需要定义和设置,那么多函数和结构,仅需要定义一个open函数,然后使用single_open来定义open函数就可以了。

如果open函数使用了single_open,release函数必须为single_reslease而不是seq_release.

linux内核的等待队列,是以双循环链表为基础的数据结构,与进程调度机制紧密结合,能够实现核心的异步事件通知机制。


wait_event_interruptible():修改task状态为TASK_INTERRUPTIBLE,意味着该进程将不会继续进行直到被唤醒,然后被添加到等待队列中。

内存屏障主要解决的问题是编译器的优化和CPU的乱序执行。编译器在优化的时候,生成的汇编指令可能与C语言程序的执行顺序不一样,在需要严格按照C语言执行时,需显式告诉编译器不需要优化,这在linux下通过barrier()宏完成的。

CPU执行会通过乱序提高性能,汇编指令不一定是按照我们看到的顺序执行的。linux中通过mb()系列宏来保持执行顺序的。

RCU和读写锁相似,但RCU的读者占锁没有任何的系统开锁,写者和读者必须要保持同步,且写者必须要等到它之前的读者全部退出之后才能释放之前的资源。

RCU保护的是指针,因为指针赋值是一条单指令,也就是一个原子操作,更改指针指向没有必要考虑它的同步,只考虑cache的影响。

读者在持有rcu_read_lock()的时候,不能发生进程上下文的切换,否则因为写者需要等待读者完成,写者进程会一直被阻塞。

在每一次进程切换的时候,都会调用rcu_gsctr_inc();

每次时钟中断update_process_times,都会检查是否需要更新的RCU需要处理,就会调用rcu_check_callbacks();


页高速缓存是由RAM中的物理页组成的,缓存中的每一页对应着多个块。

页高速缓存通过两个参数address_space对象和一个偏移量进行搜索,每个address_space对象都有唯一的基树,它保证在page_tree结构体中,基树是一个二叉树,主要指定了文件偏移量就可以在基树中迅速检索到希望的数据。

SMP共享存书型多处理器分成三种模型:1均匀存储器存取模型(UMA);2非均匀存储器模型(NUMA)3只用高速缓存的存储器结构(COMA)主要区别在于存储器和外围资源如何共享和分布

一个内存节点代表了一片连续的内存区域。

UBOOT中会将板子上的内存物理地址和内存大小通过tag参数传递到内核,内核将这个参数保存到meminfo这边的全局变量中。


实际的物理内存,只有当进程真的去访问新获取的虚拟地址时,产生“缺页异常”,从而进入分配实际物理地址的过程,,就是分配的实际的page frame并建立page table。之后系统返回产生异常的地址重新执行内存访问,一切好像没有发生过。

如果由内存访问一场情况,就会转到do_page_fault()中处理。一旦内核初始化完成之后,就不会使用init_mm(),也就是说init_mm中的映射关系该没有更新到当前的内核页目录中去。

一个vma就是一块连续的线性地址空间的抽象,它拥有自身的权限(可读,可写,可执行),每个虚拟内存空间都由一个相关的vm_area_struct结构体来描述。

缺页异常被触发通常有两种情况:1程序设计不当导致访问非法地址 2访问地址合法但该地址未分配物理页框

一个总线主设备访问位于同一个节点中的任意内存单元所花的代价相同,而访问任意两个不同节点的内存单元所花代价不同。

PGD:page global directory SHIFT

PMD:page middle directory SIZE

PTE:page table entry MASK

PFN是物理内存map的偏移量,以page为单位。

每个进程都有自己的PGD,是一个物理页,并包含一个pgd_t数组

每个pte_t指向一个物理页地址,并且所有地址都是页对齐的。first fit分配器是基本的内存分配器,使用bitmap而不是空闲块列表来表示内存。

物理内存分配器,通过boot memery分配器来初始化自己。每个CPU架构被要求提供setup_arch();负责获取boot memery分配器的必要参数。

堆栈这些内存的虚拟地址段都是没有磁盘文件与之对应的就是ananymous page。

在系统初始化期间,所有页都被标记为MOVABLE。

页面回收的方式有页回写,页交换和页丢弃三种方式

kswapd是内核为每个内存node创建内核回收线程。

linux采用伙伴系统解决外部碎片问题,采用slab解决内部碎片问题。伙伴系统宗旨是用最小的内存块来满足内核 对内存的需求。

struct zone中的struct free_area是用来描述该管理区伙伴系统的空闲内存块的。


sys_mount主要实现vfsmnt结构体的填充,这个结构体中比较重要的是super_block的填充,然后将vfsmnt添加到相应进程PCB的namespace成员所指向的namespace结构体中,大部分都指向这个namespace,所以挂载对大部分可见。

reset_init创建内核线程kernel_init,调用do_basic_setup()将initrd内容解压到rootfs文件系统下接着调用init_post执行initrd下的init脚本。

__initcall_start和__initcall_end界定存放了初始化函数指针区域的起始地址。

不挂载文件系统,这个文件系统在内核以一个filesystem_type的形式存在,仅仅是存在这么一个type并没有构建到全局文件系统树中去,在全局文件系统树上要确定一个位置不能由dentry唯一确定(需要)。

所有标示为__init的函数在链接的时候都放在.init.text这个区段内,在这个区段中,函数的摆放顺序和链接顺序有关,是不确定的。

所有的__init函数在区段.initcall.init中还保存了一份指针,在初始化时内核会通过这些函数指针调用这个__init函数指针,并在整个初始化完成后,释放整个init区段。


start_kernel()两次调用parse_args()解析启动配置字符串的原因是启动选项事实上分为两类,且每次调用只能够兼顾到其中一类。1缺省选项 2先期处理选项


uImage在zImage头添加64字节的镜像信息(操作系统类型,映像是否压缩,映像加载地址和压缩地址)提供给uboot解析使用。

Initrd是一个被压缩过的小型根目录,,这个目录包含了启动阶段中必须的驱动模块,可执行文件和启动脚本。当系统启动时,bootloader会将initrd文件读到内存中,然后把iinitrd文件在内存中起始文件地址和大小传递给内核,然后执行根目录中的linuxrc脚本,在该脚本中加载真实文件系统存放设备驱动程序及在/dev目录下建立必要的设备节点。

调通了system level的driver(timer,interrupt,clock)以及串口的terminal之后,linux kernel基本是可以起来了,后续各种driver不断地添加,直到系统支持所有硬件。

在linux内核代码里,运用了subsys_initcall来进行各种子系统的初始化。

.init,setup段总存放的是kernel通用参数和对应处理函数的映射表。


你可能感兴趣的:(linux子系统)