linux內核啓動分析

-----如下內容爲從網絡上整理所得------linux

 

嵌入式Linux啓動後,會先運行系統的bootloader,通常是用開源的U-boot;數組

Broadcom芯片的bootloader則是本身公司的CFE。緩存

 

bootloader前面文章有大致介紹了一下,如下主要分析Linux內核的啓動過程,以MIPS芯片爲例。安全

內核版本:2.6.31網絡

 

Bootloader將Linux內核映像拷貝到RAM中某個空閒地址處,而後通常有個內存移動操做,目的地址在數據結構

arch/mips/Makefile內指定:架構

load-$(CONFIG_MIPS_PB1550) += 0xFFFFFFFF80100000,
則最終bootloader定會將內核移到物理地址   0x00100000  處。框架

上面Makefile 裏指定的的 load 地址,最後會被編譯系統寫入到 arch/mips/kernel/vmlinux.lds 中:less

OUTPUT_ARCH(mips) ENTRY(kernel_entry) jiffies = jiffies_64; SECTIONS { . = 0xFFFFFFFF80100000; /* read-only */ _text = .; /* Text and read-only data */ .text : { *(.text) ...

這個文件最終會以參數 -Xlinker --script -Xlinker vmlinux.lds 的形式傳給 gcc,並最終傳給連接器 ld 來控制其行爲。
ld 會將 .text 節的地址連接到 0xFFFFFFFF80100000 處。ssh

關於內核 ELF 文件的入口地址(Entry point),即 bootloader 移動完內核後,直接跳轉到的地址,由ld 寫入 ELF的頭中,

其會依次用下面的方法嘗試設置入口點,當遇到成功時則中止:
a. 命令行選項 -e entry
b. 腳本中的 ENTRY(symbol)
c. 若是有定義 start 符號,則使用start符號(symbol)
d. 若是存在 .text 節,則使用第一個字節的地址。
e. 地址0

注意到上面的 ld script 中,用 ENTRY 宏設置了內核的 entry point 是 kernel_entry,
所以內核取得控制權後執行的第一條指令是在 kernel_entry 處。

 

linux  內核啓動的第一個階段是從  /arch/mips/kernel/head.s文件開始的。
而此處正是內核入口函數kernel_entry(),該函數定義在 /arch/mips/kernel/head.s文件裏。

kernel_entry()函數是體系結構相關的彙編語言,它首先初始化內核堆棧段,來爲建立系統中的第一個進程進行準備,
接着用一段循環將內核映像的未初始化數據段(bss段,在_edata和_end之間)清零,
最後跳轉到  /init/main.c 中的 start_kernel()初始化硬件平臺相關的代碼。

 

/*asmlinkage: This is a #define for some gcc magic that tells the 
 *compiler that the function should not expect to find any of its arguments 
 *in registers (a common optimization), but only on the CPU's stack.意思是如
 *果函數定義前加宏asmlinkage ,表示這些函數經過堆棧而不是經過寄存器傳遞參數。 
 *init: 宏定義__init,用於告訴編譯器相關函數或變量的僅用於初始化。
 *編譯器將標有—init的全部代碼存在初始化段中,初始化結束後就釋放這段內存
 */
asmlinkage void __init start_kernel(void)
{
    char * command_line;
    extern struct kernel_param __start___param[], __stop___param[];
    //定義了核的參數數據結構

    lockdep_init();
    //初始化核依賴關係哈希表,classhash_table 和 chainhash_table

    smp_setup_processor_id();
    //獲取當前正在執行初始化的處理器ID,若是kernel是單處理器,則此函數是空的,不做任何處理

    debug_objects_early_init();
  //這個函數主要做用是對調試對象進行早期的初始化,其實就是HASH鎖和靜態對象池進行初始化。
   
  boot_init_stack_canary();
  //stack_canary的是帶防止棧溢出攻擊保護的堆棧

  cgroup_init_early();
  //控制組進行早期的初始化

  local_irq_disable();
    //關閉當前CPU全部中斷響應

  early_boot_irqs_off();
  /*標記內核還在早期初始化代碼階段,而且中斷在關閉狀態,若是有任何中斷打開或請求中斷的事情出現,都是會提出警告,以便跟蹤代碼錯誤狀況。
   *早期代碼初始化結束以後,就會調用函數early_boot_irqs_on來設置這個標誌爲真。
  **/
  
  early_init_irq_lock_class();
  //每個中斷都有一個IRQ 描述符(struct irq_desc)來進行描述,這個函數的主要做用是設置全部的IRQ 描述符(struct irq_desc)的鎖是統一的鎖,
  //仍是每個IRQ 描述符(struct irq_desc)都有一個小鎖。

  lock_kernel();
  //這個函數主要做用是初始化大內核鎖。在對稱多處理器的系統裏,每個CPU均可以運行內核的代碼,但有時須要只能一個CPU運行內核代碼,那麼怎麼辦呢?
  //要解決這個問題,就須要給內核配備一把鎖  //只要擁有這把鎖的CPU才能夠運行內核的代碼,而且同一個CPU能夠遞歸地運行內核。

  tick_init();
  //這個函數主要做用是初始化時鐘事件管理器的回調函數,好比當時鍾設備添加時處理

  boot_cpu_init();
  //這個函數主要做用是設置當前引導系統的CPU在物理上存在,在邏輯上可使用,而且初始化準備好。

  page_address_init();
  //這個函數主要做用是初始化高端內存的映射表。在32位系統裏,內核爲了訪問超過1G的物理內存空間,須要使用高端內存映射表。
  //好比當內核須要讀取1G的緩存數據時,就須要分配高端內存來使用,這樣才能夠管理起來。  
  //使用高端內存以後,32位的系統也能夠訪問達到64G內存。在移動操做系統裏,目前尚未這個必要,最多才1G多內存。

  printk(KERN_NOTICE "%s", linux_banner);
  //輸出終端上顯示版本信息、編譯的電腦用戶名稱、編譯器版本、編譯時間

  setup_arch(&command_line);
  //每種體系結構都有setup_arch函數,主要是再次獲取CPU類型和系統架構,分析引導程序傳入的命令行參數,
  //進行頁面內存初始化,處理器初始化,中斷早期初始化等等。

  mm_init_owner(&init_mm, &init_task);
  //這個函數主要做用是設置最開始的初始化任務屬於init_mm內存

  setup_command_line(command_line);
  //保存命令行

  setup_nr_cpu_ids();
  //設置最多有多少個nr_cpu_ids結構

  setup_per_cpu_areas();
  //設置SMP體系每一個CPU使用的內存空間,同時拷貝初始化段裏數據

  smp_prepare_boot_cpu();
  //爲SMP系統裏引導CPU進行準備工做

  build_all_zonelists();
  //初始化全部內存管理節點列表,以便後面進行內存管理初始化

  page_alloc_init();
  //設置內存頁分配通知器

  printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
  //輸出命令參數到顯示終端

  parse_early_param();
  //分析命令行最先使用的參數

  parse_args("Bootingkernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption);
  //對傳入內核參數進行解釋,若是不能識別的命令就調用最後參數的函數

  pidhash_init();
  //進程ID的HASH表初始化,這樣能夠提供通PID進行高效訪問進程結構的信息。LINUX裏共有四種類型的PID,所以就有四種HASH表相對應。

  vfs_caches_init_early();
  //虛擬文件系統的早期初始化

  sort_main_extable();
  //對內核內部的異常表進行堆排序,以便加速訪問

  trap_init();
  //對異常進行初始化

  mm_init();
  //設置內核內存分配器

  sched_init();
  //這個函數主要做用是對進程調度器進行初始化,好比分配調度器佔用的內存,初始化任務隊列,設置當前任務的空線程,當前任務的調度策略爲CFS調度器。

  preempt_disable();
  //這個函數主要做用是關閉優先級調度。因爲每一個進程任務都有優先級,目前系統尚未徹底初始化,還不能打開優先級調度。
  
  if(!irqs_disabled() {
    printk(KERN_WARNING "start_kernel(): bug: interrupts were "
              "enabled *very* early, fixing it\n");
    local_irq_disable();
  }
  //判斷是否過早打開中斷,若是是這樣,就會提示,並把中斷關閉。

  rcu_init();
  //初始化直接讀拷貝更新的鎖機制。RCU主要提供在讀取數據機會比較多,但更新比較的少的場合,這樣減小讀取數據鎖的性能低下的問題。

  early_irq_init();
  //用於管理中斷的irq_desc[NR_IRQS]數組的每一個元素的部分字段設置爲肯定的狀態,它設置每個成員的中斷號

  init_IRQ();
  //是一個與特定體系結構相關的函數,用於管理中斷的irq_desc[NR_IRQS]結構數組各成員字段設置爲IRQ——NOREQUEST | IRQ——NOPROBE,
  //也就是未請求和未探測狀態,而後調用特定平臺的中斷初始化init_arch_irq()函數
  
  prio_tree_init();
  //優先搜索樹的初始化,主要用在內在反向搜索方面

  init_timers();
  //初始化引導CPU的時鐘相關的體系結構,註冊時鐘的回調函數,當時鍾到達時能夠回調時鐘處理函數,最後初始化時鐘軟件中斷處理。

  hrtimers_init();
  //初始化高精度的定時器,並設置回調函數

  softirq_init();
  //這個函數是初始化軟件中斷,軟件中斷與硬件中斷區別就是中斷髮生時,軟件中斷是使用線程來監視中斷信號,而硬件中斷是使用CPU硬件來監視中斷。

  timekeeping_init();
  //初始化系統時鐘計時,而且初始化內核裏與時鐘計時相關的變量。

  time_init();
  //初始化系統時鐘

  sched_clock_init();
  //系統進程調度時鐘初始化

  profile_init();
  //分配內核性能統計保存的內存,以便統計的性能變量能夠保存到這裏

  if (!irqs_disabled())
    printk(KERN_CRIT "start_kernel(): bug: interrupts were "
            "enabled early\n");

  early_boot_irqs_on();
  //設置內核還在早期初始化階段的標誌,以便用來調試時輸出信息

  local_irq_enable();
  //打開本CPU的中斷,也即容許本CPU處理中斷事件,在這裏打開引CPU的中斷處理。若是有多核心,別的CPU尚未打開中斷處理。

  set_gfp_allowed_mask(__GFP_BITS_MASK);
  //Interrupts are enabled now so all GFP allocations are safe.

  kmem_cache_init_late();
  //slab分配器的緩存機制
  
  console_init();
  //初始化控制檯,從這個函數以後就能夠輸出內容到控制檯了
  //在這個函數初化以前,都沒有辦法輸出內容,就是輸出,也是寫到輸出緩衝區裏,緩存起來,等到這個函數調用以後,就當即輸出內容。

  if (panic_later)
    panic(panic_later, panic_param);
  //判斷分析輸入的參數是否出錯,若是有出錯,就啓動控制檯輸出以後,當即打印出錯的參數,以便用戶當即看到出錯的地方。

  lockdep_info();
  //打印鎖的依賴信息,用來調試鎖。

  locking_selftest();
  //用來測試鎖的API是否使用正常,進行自我測試。好比測試自旋鎖、讀寫鎖、通常信號量和讀寫信號量。

#ifdef CONFIG_BLK_DEV_INITRD
  if (initrd_start && !initrd_below_start_ok &&
    page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
      printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
        "disabling it.\n",
        page_to_pfn(virt_to_page((void *)initrd_start)),
        min_low_pfn);
      initrd_start = 0;
  }
#endif
  //這段代碼是要支持初始RAM 磁盤,內核必需要使用CONFIG_BLK_DEV_RAM 和CONFIG_BLK_DEV_INITRD 選項進行編譯。

  page_cgroup_init();
  //容器組的頁面內存分配。

  enable_debug_pagealloc();
  //設置內存分配是否須要輸出調試信息,若是調用這個函數,當分配內存時,不會輸出一些相關的信息。

  kmemtrace_init();
  //

  kmemleak_init();
  //memory lead偵測初始化

  debug_objects_mem_init();
  //在kmem_caches以後表示創建一個高速緩衝池,創建起SLAB_DEBUG_OBJECTS標誌。

  idr_init_cache();
  //建立IDR機制的內存緩存對象。所謂的IDR就是整數標識管理機制(integerIDmanagement)。
  //引入的主要緣由是管理整數的ID與對象的指針的關係,
  //因爲這個ID能夠達到32位,也就是說,若是使用線性數組來管理,那麼分配的內存太大了;
  //若是使用線性表來管理,又效率過低了,因此就引用IDR管理機制來實現這個需求。

  setup_per_cpu_pageset();
  //建立每一個CPU的高速緩存集合數組。由於每一個CPU都不定時須要使用一些頁面內存和釋放頁面內存,
  //爲了提升效率,就預先建立一些內存頁面做爲每一個CPU的頁面集合。

  numa_policy_init();
  //初始化NUMA的內存訪問策略。所謂NUMA,它是NonUniform Memory AccessAchitecture的縮寫,
  //主要用來提升多個CPU訪問內存的速度。由於多個CPU訪問同一個節點的內存速度遠遠比訪問多個節點的速度來得快。

  if(late_time_init)
    late_time_init();
  //主要運行時鐘相關後期的初始化功能。

  calibrate_delay();
  //這個函數是主要計算CPU須要校準的時間,這裏說的時間是CPU執行時間。若是是引導CPU,
  //這個函數計算出來的校準時間是不須要使用的,主要使用在非引導CPU上,由於非引導CPU執行的頻率不同,致使時間計算不許確。
  
  pidmap_init();
  //進程位圖初始化,通常狀況下使用一頁來表示全部進程佔用狀況

  anon_vma_init();
  //這個函數是初始化反向映射的匿名內存,提供反向查找內存的結構指針位置,快速地回收內存。

#ifdef CONFIG_X86
  if (efi_enabled)
    efi_enter_virtual_mode();
#endif
  //這段代碼是初始化EFI的接口,並進入虛擬模式。EFI是ExtensibleFirmware Interface的縮寫,就是INTEL公司新開發的BIOS接口。

  thread_info_cache_init();
  //這個函數是線程信息的緩存初始化。

  cred_init();
  //證書初始化

  fork_init(num_physpages);
  //根據當前物理內存計算出來能夠建立進程(線程)的數量,並進行進程環境初始化

  proc_caches_init();
  //進程緩存初始化。

  buffer_init();
  //初始化文件系統的緩衝區,並計算最大可使用的文件緩存。

  key_init();
  //初始化安全鍵管理列表和結構。

  security_init();
  //初始化安全管理框架,以便提供訪問文件/登陸等權限。

  vfs_caches_init(num_physpages);
  //虛擬文件系統進行緩存初始化,提升虛擬文件系統的訪問速度。

  radix_treee_init();
  //初始化radix樹,radix樹基於二進制鍵值的查找樹。

  signals_init();
  //初始化信號隊列緩存。

  page_writeback_init();
  //計算當前系統vm-radio等,設置是否須要回寫操做

#ifdef CONFIG_PROC_FS
  proc_root_init();
#endif
  //初始化系統進程文件系統,主要提供內核與用戶進行交互的平臺,方便用戶實時查看進程的信息。

  cgroup_init();
  //初始化進程控制組,主要用來爲進程和其子程提供性能控制。好比限定這組進程的CPU使用率爲20%。

  cpuset_init();
  //初始化CPUSET,CPUSET主要爲控制組提供CPU和內存節點的管理的結構。

  taskstats_init_early();
  //初始化任務狀態相關的緩存、隊列和信號量。任務狀態主要向用戶提供任務的狀態信息。

  delayacct_init();
  //初始化每一個任務延時計數。當一個任務等CPU運行,或者等IO同步時,都須要計算等待時間。

  check_bugs();
  //用來檢查CPU配置、FPU等是否非法使用不具有的功能。

  acpi_early_init();
  //這個函數是初始化ACPI電源管理。高級配置與能源接口(ACPI)ACPI規範介紹ACPI能使軟、硬件、操做系統(OS),
  //主機板和外圍設備,依照必定的方式管理用電狀況,系統硬件產生的Hot-Plug事件,讓操做系統從用戶的角度上直接支配即插即用設備,
  //不一樣於以往直接經過基於BIOS 的方式的管理。

  ftrace_init();
  //初始化內核跟蹤模塊,ftrace的做用是幫助開發人員瞭解Linux 內核的運行時行爲,以便進行故障調試或性能分析。

  /* Do the rest non-__init'ed, we're now alive */
  rest_init();
  //這個函數是後繼初始化,主要是建立內核線程init,並運行。
}

 

在上面已經對基本的硬件、系統的結構初始化完成,接着下來系統要作的工做,就是建立進 程,對進程進行管理,纔可讓系統生生不息,處理各類各樣的任務。雖然大部份的初始化工做已經完成,但還須要更進一步初始化,所以建立一個內核初始化線程 來繼續初始化。爲了有一個乾淨,又能夠拷貝,又方便建立線程的方法,就是建立一個特別的內核線程kthreadd,這樣全部之後須要建立的線程都是由這個線程建立出來的,能夠說這個線程爲其他內核線程之母。建立完這兩個線程以後,初始化進程還須要對線程調度器的狀態進行運行一次,以便初始化到合適的值。最後,這個初始化進程就退化爲一個IDLE空閒進程了,完成了引導系統全部的工做,進入系統正常運行的狀態。

 

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

    kernel_thread(kernel_init,NULL, CLONE_FS | CLONE_SIGHAND);
    //這行代碼是調用建立內核線程函數kernel_thread來建立內核第二階段的初始化線程。

    numa_default_policy();
    //這行代碼是設置當前進程使用缺省的內存管理策略。

    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    //這行代碼是建立一個乾淨內核線程,以便之後其它全部內核線程所有拷貝它,並由它來建立,這樣達到更方便建立線程,不用設置太多參數。

    kthreadd_task= find_task_by_pid_ns(pid, &init_pid_ns);
    //這行代碼是經過進程ID查找內核線程的任務地址,並保存起來,方便訪問。

    unlock_kernel();
    //這行代碼是釋放大內核鎖,以便多CPU能夠訪問內核代碼。

  /*
   * The boot idle thread must execute schedule()
   * at least once to get things moving:
   */

  init_idle_bootup_task(current);
  //這行代碼是初始化空閒進程的調度器,以便讓空閒進程知道怎麼樣調度任務列表裏的進程。current是指向當前IDLE任務的結構。

  rcu_scheduler_starting();
  //內核RCU鎖機制調度啓動

  preempt_enable_no_resched();
  //這行代碼是減小內核搶先計數,而且不進行調度操做。這樣運行後,以便IDLE進程可讓別的高優先級的進程運行。

  schedule();
  //這行代碼是調用進程調度函數schedule,主要初始化調度器能夠切換回到空閒任務。

  preempt_disable();
  //這行代碼是增長內核搶先計數。

  /*Call into cpu_idle with preempt disabled */
  cpu_idle();
  //這行代碼是調用CPU空閒任務進程運行,再也不返回來。
}

 

系統已經完成了整個初始化過程,那麼這個初始化進程最好的歸宿是那裏呢?顯然它就是進化爲一個空閒進程,當系統沒有其它任務處理時,就會經過進程管理器選擇這個優先級最低,沒有什麼事情作的任務,以便整個CPU還有事情可作。也許你也會問,爲何必定要一個空閒進程,不要這個進程不行嗎?確定回答是不行的,整個系統裏的CPU資源總須要使用的,若是不使用CPU資源,那麼這個CPU就意味着再也不執行指令了,CPU就已經停機了,當有新的任務到來時就沒有辦法切換過去。若是選擇CPU停機的方式,那麼這個CPU須要再運行任務時,就須要喚醒。而喚醒的方法,須要外界施加條件才能夠,通常都是物理條件,就是觸發CPU的中斷信號。

空閒進程主要作的工做,就是不斷查找是否有新的任務能夠運行,若是沒有新的任務運行,就繼續執行空閒任務處理的事情。下面就來仔細地分析這個函數的具體工做,代碼以下:

 

void cpu_idle(void)
{
  local_fiq_enable();
  //這行代碼是打開ARM系統的快速中斷,所謂的FIQ是相對於普通的IRQ來講的,FIQ是能夠打斷普通的IRQ中斷,反之不行。

  //下面就進入無限循環的空閒進程處理:
  /* endless idle loopwith no priority at all */
  while (1) {
    void (*idle)(void)= pm_idle;
    //這行代碼是調用定製的空閒處理函數。

#ifdefCONFIG_HOTPLUG_CPU
    if(cpu_is_offline(smp_processor_id())) {
      leds_event(led_idle_start);
      cpu_die();
    }
#endif
    //這段代碼是處理熱插拔的CPU機制,當容許當前這個CPU進入睡眠狀態,就能夠進入。

    if (!idle)
      idle =default_idle;
    //這段代碼是當沒有用戶定義的空閒處理函數時,就調用缺省的空閒處理函數。缺省的空閒處理函數,就會調用系統架構的空閒處理函數。

    leds_event(led_idle_start);
    //這行代碼是打開LED顯示空閒運行狀態。

    tick_nohz_stop_sched_tick(1);
    //這行代碼是中止進程調度計數。

    while(!need_resched())
      idle();
    //這段代碼是當須要調度標誌爲空時,就不斷調用空閒處理函數進行運行。

    leds_event(led_idle_end);
    //這行代碼是當須要調度其它進行運行了,LED結束顯示空閒運行狀態。
  
    tick_nohz_restart_sched_tick();
    //這行代碼是從新開始計算調度運行計數。

    preempt_enable_no_resched();
    //這行代碼是減小搶佔計數,不須要從新調度,下面立刻就開始調度。

    schedule();
    //這行代碼是對進程任務進行調度,以便當即運行正在等待的任務。

    preempt_disable();
    //這行代碼是增長搶佔計數,禁止調度發生。
  }
}
相關文章
相關標籤/搜索