2018-2019-1 20189215 《Linux內核原理與分析》第四周作業
《庖丁解牛》第三章書本知識總結
計算機的三大法寶
存儲程序計算機
函數調用堆棧
中斷操作系統的兩把寶劍
中斷上下文的切換——保存現場和恢復現場
進程上下文的切換Linux內核源碼的目錄結構如下所示。
關鍵的目錄
arch:arch目錄在Linux內核目錄中占比相當龐大,主要原因是arch目錄中的代碼可以使Linux內核支持不同的CPU和體系結構。
block:存放Linux存儲體系中關於塊設備管理的代碼。
crypto:存放常見的加密算法的C語言代碼,譬如crc32、md5、sha1等。
drives:驅動目錄。
fs:文件系統(File System)。
init:init是初始化的意思,存放Linux內核啟動時的初始化代碼。
ipc:Linux系統支持的IPC(進程間通信)的代碼實現。
kernel:存放內核本身需要的一些核心代碼文件,其中有很多關鍵代碼,包括pid——進程號等。
lib:公用的庫文件,裏面是一些公用的庫函數。需要註意的 是在內核編程中不能用C函數標準庫函數
mm: 內存管理,存放Linux的內存管理代碼。
net:存放網絡相關的代碼,譬如TCP/IP的協議棧等。init
目錄下有main.c
源文件,它是整個Linux內核啟動的起點,但它的起點不是main函數,而是start_kernel
函數,start_kernel
函數是初始化Linux內核啟動的起點,start_kerne
前的代碼使用匯編語言來進行硬件初始化。qemu
仿真kernel。bzImage
是vmlinux
經過壓縮後的文件,是壓縮的內核映像;vmlinux
是編譯出來的最原始的內核ELF文件。qemu
命令中的-s
、-S
參數,-s
表示用1234端口上的gdb-server連接,可以用-gdb tcp:xxxx
-S
表示在CPU初始化之前凍結,使用c
繼續執行。
實驗:跟蹤分析Linux內核的啟動過程
- 運行內核
- 重新運行,加上參數,內核被凍結
- 調試內核
- 設置斷點到
start_kernel
設置斷點到
rest_init
代碼分析
start_kernel()
(為了減少點空間,我把所有註釋和空行刪掉了)
500 asmlinkage __visible void __init start_kernel(void) { char *command_line; char *after_dashes; lockdep_init(); 510 set_task_stack_end_magic(&init_task); smp_setup_processor_id(); debug_objects_early_init(); boot_init_stack_canary(); cgroup_init_early(); local_irq_disable(); early_boot_irqs_disabled = true; boot_cpu_init(); page_address_init(); pr_notice("%s", linux_banner); setup_arch(&command_line); mm_init_cpumask(&init_mm); setup_command_line(command_line); setup_nr_cpu_ids(); setup_per_cpu_areas(); smp_prepare_boot_cpu(); build_all_zonelists(NULL, NULL); page_alloc_init(); pr_notice("Kernel command line: %s\n", boot_command_line); parse_early_param(); after_dashes = parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, -1, -1, &unknown_bootoption); if (!IS_ERR_OR_NULL(after_dashes)) parse_args("Setting init args", after_dashes, NULL, 0, -1, -1, set_init_arg); jump_label_init(); setup_log_buf(0); pidhash_init(); vfs_caches_init_early(); sort_main_extable(); trap_init(); mm_init(); sched_init(); preempt_disable(); if (WARN(!irqs_disabled(), "Interrupts were enabled *very* early, fixing it\n")) local_irq_disable(); idr_init_cache(); rcu_init(); context_tracking_init(); radix_tree_init(); early_irq_init(); init_IRQ(); tick_init(); rcu_init_nohz(); init_timers(); hrtimers_init(); softirq_init(); timekeeping_init(); time_init(); sched_clock_postinit(); perf_event_init(); profile_init(); call_function_init(); WARN(!irqs_disabled(), "Interrupts were enabled early\n"); early_boot_irqs_disabled = false; local_irq_enable(); kmem_cache_init_late(); console_init(); if (panic_later) panic("Too many boot %s vars at `%s‘", panic_later, panic_param); lockdep_info(); locking_selftest(); #ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok && page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) { pr_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 page_cgroup_init(); debug_objects_mem_init(); kmemleak_init(); setup_per_cpu_pageset(); numa_policy_init(); if (late_time_init) late_time_init(); sched_clock_init(); calibrate_delay(); pidmap_init(); anon_vma_init(); acpi_early_init(); #ifdef CONFIG_X86 if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_enter_virtual_mode(); #endif #ifdef CONFIG_X86_ESPFIX64 init_espfix_bsp(); #endif thread_info_cache_init(); cred_init(); fork_init(totalram_pages); proc_caches_init(); buffer_init(); key_init(); security_init(); dbg_late_init(); vfs_caches_init(totalram_pages); signals_init(); page_writeback_init(); proc_root_init(); cgroup_init(); cpuset_init(); taskstats_init_early(); delayacct_init(); check_bugs(); sfi_init_late(); if (efi_enabled(EFI_RUNTIME_SERVICES)) { efi_late_init(); efi_free_boot_services(); } ftrace_init(); rest_init(); }
main.c
中沒有main函數,start_kernel
是一切的起點,可以看到本函數中全是初始化的調用。
不論分析內核的哪一部分,都會涉及到start_kernel
,因為基本上所有模塊的初始化都在main.c
的start_kernel
來調用。
- 標註510行的代碼
set_task_stack_end_magic(&init_task);
init_task
即手工創建的PCB,是0號進程,也是最終的idle進程,init_task
是唯一的沒有通過fork方式產生的進程。 rest_init()
static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
kernel_thread(kernel_init, NULL, CLONE_FS);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
complete(&kthreadd_done);
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
schedule_preempt_disabled();
/* 當系統沒有進程需要執行時,就調度到idle進程 */
cpu_startup_entry(CPUHP_ONLINE);
}
通過rest_init()
新建kernel_init
和kernel_thread()
內核線程。調用kernel_thread()
創建1號內核線程。
kthreadd()
位於linux-3.18.6/kernel/kthread.c #483
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
這行代碼調用kernel_thread
執行kthreadd
,創建PID為2的內核線程。trap_init()
涉及到一些中斷的初始化
一些補充
Linux下有3個特殊的進程,idle進程(PID = 0), init進程(PID = 1)和kthreadd(PID = 2)
- idle進程由系統自動創建, 運行在內核態
idle進程其pid=0,其前身是系統創建的第一個進程,也是唯一一個沒有通過fork或者kernel_thread產生的進程。完成加載系統後,演變為進程調度、交換。- init進程由idle通過kernel_thread創建,在內核空間完成初始化後, 加載init程序, 並最終到用戶空間
由0進程創建,完成系統的初始化. 是系統中所有其它用戶進程的祖先進程。
Linux中的所有進程都是有init進程創建並運行的。首先Linux內核啟動,然後在用戶空間中啟動init進程,再啟動其他系統進程。在系統啟動完成完成後,init將變為守護進程監視系統其他進程。- kthreadd進程由idle通過kernel_thread創建,並始終運行在內核空間, 負責所有內核線程的調度和管理。
它的任務就是管理和調度其他內核線程kernel_thread, 會循環執行一個kthread的函數,該函數的作用就是運行kthread_create_list全局鏈表中維護的kthread, 當我們調用kernel_thread創建的內核線程會被加入> 到此鏈表中,因此所有的內核線程都是直接或者間接的以kthreadd為父進程。
其他想法
本次實驗和書本學習,我受益匪淺,真正接觸到了Linux內核的源碼,感覺需要更多接觸,之後在自己的機器上面構建一個MenuOS,構建成功後把截圖發在下面。
參考資料
《庖丁解牛Linux》
Linux下0號進程的前世(init_task進程)今生(idle進程)
2018-2019-1 20189215 《Linux內核原理與分析》第四周作業