關於linux的一點好奇心(一):linux啟動過程
一直很好奇,作業系統是如何工作的?我們知道平時程式設計,是如何讓程式碼跑起來的,但那些都是比較高層次的東西。越往後,你會越覺得,像是空中樓閣,或者說只是有人幫你鋪平了許多道理,而你卻對此一無所知。
1. 作業系統的困惑
當然了,也不是真的一無所知。因為有很多的作業系統方面的書籍,教你瞭解作業系統是如何如何工作的,它的各種原理。但總有一種任督二脈不通的感覺。好像說的都知道一點,但好像知道這是什麼,在哪裡用,和為什麼了。
我曾經看過一系列關於一個如何自制作業系統的文章,非常棒。https://wiki.0xffffff.org/ 裡面完全展示了一個求知者的過程,硬體載入,軟體接管,作業系統,記憶體,中斷,驅動,執行緒等等方面的知道。可以說,是一個用於解惑,不可多得的文章了。應該說,很多關於作業系統的困惑,在這裡找到答案,當然你得自己總結下才行。
但是,我還是會有那麼一種感覺,原理看得再多,還是很空虛的。上面那個demo雖然把所有的東西都講了一遍,好像已經把所有問題都講了,但還畢竟只是demo。也許實際情況並非如此呢?至少不會那麼簡單。這著實困擾著自己跳動的心。
再後來,遇到了一篇講關於epoll的文章: https://bbs.gameres.com/thread_842984_1_1.html 。 經過這篇文章的講解,可以說把整個io的原理講得非常之透徹了。而我本人的確也從這裡出發,給團隊內部做了一次分享。不知道他們感覺怎麼樣,反正我是感覺挺通透的。這關於io東西,可以說是作業系統中的一個小點。但我個人覺得,框架也許只需要你瞭解一次就好,但小點卻是需要反覆琢磨的。我們需要帶著問題去找答案,這個問題往往是關於小點的多。
2. 敢不敢啃一啃作業系統的硬骨頭?
說實話,我是不敢的。原因是,它太複雜,太巨集大,這是比較大方向的困難。其次是,我單就語言這一關,可能就難以過去,因為你至少彙編、C之類的語言要足夠好才行,而自己卻只算是皮毛。正所謂一生清貧怎敢入繁華,兩袖清風怎敢誤佳人。
難道就這樣得過且過麼?但心裡總是有一些疑問,不知道怎麼去解決。一是問不了許多人,二是自己也不知道咋問,三是即使別人告訴了你你就能懂嗎?(就像教書一樣)
所以,還是自己找答案吧。其實網上有太多零零散散的答案,好像都能看懂,但又好像都不是很明白。
最好的文件,都在官方資料裡。最好的答案,都在程式碼裡。所以,去看看又何妨。
3. linux核心原始碼地址
也許大家一般都是在github上去看這些原始碼。但在國內,github的速度實在是不敢恭維。
gitee地址: https://gitee.com/mirrors/linux
github地址: https://github.com/torvalds/linux
至於閱讀工具嘛,純粹打醬油的,使用 sublime 之類的就可以了,如果想更好一點,就eclipse也行,當然可能還要設定其他好些環境問題。
4. linux框架結構
關於閱讀技巧,可參考文章:https://www.cnblogs.com/fanzhidongyzby/archive/2013/03/20/2970624.html
整體目錄結構如下:
細節簡略描述如下:
arch——與體系結構相關的程式碼。 對應於每個支援的體系結構,有一個相應的目錄如x86、 arm、alpha等。每個體系結構子目錄下包含幾個主要的子目錄: kernel、mm、lib。
Documentation——核心方面的相關文件。
drivers——裝置驅動程式碼。每類裝置有相應的子目錄,如char、 block、net等 fs 檔案系統程式碼。每個支援檔案系統有相應的子目錄, 如ext2、proc等。
fs——檔案系統實現。如fat, ext4...
include——核心標頭檔案。 對每種支援的體系結構有相應的子目錄,如asm-x86、 asm-arm、asm-alpha等。
init——核心初始化程式碼。提供main.c,包含start_kernel函式。
ipc——程序間通訊程式碼。
kernel——核心管理程式碼。
lib——與體系結構無關的核心庫程式碼,特定體系結構的庫程式碼儲存在arch/*/lib目錄下。
mm——記憶體管理程式碼。
net——核心的網路程式碼。
samples——一些使用功能介面的樣例,有點類似於單元測試。
scripts——此目錄包含了核心設定時用到的指令碼。
security——安全相關的實現。
tools——一些附帶工具類實現。
其中,Documentation目錄可能是原始碼不相關的目錄,但對我們理解系統卻是非常重要的地方。(因為我們多半隻能看得懂文字的表面意思)
5. linux-86啟動過程
以x86的實現為例,其啟動過程大致如下:以 header.S 開始,以main.c結束(我們自認為看得懂的地方)。
/arch/x86/boot/header.S -> calll main -> /arch/x86/boot/main.c -> go_to_protected_mode() -> /arch/x86/boot/pmjump.S -> jmpl *%eax -> /arch/x86/kernel/head_32.S -> .long i386_start_kernel -> /arch/x86/kernel/head32.c -> start_kernel() -> /init/main.c (C語言入口)
細節程式碼樣例如下:
// /arch/x86/boot/header.S #include <asm/segment.h> #include <asm/boot.h> #include <asm/page_types.h> #include <asm/setup.h> #include <asm/bootparam.h> #include "boot.h" #include "voffset.h" #include "zoffset.h" ... 6: # Check signature at end of setup cmpl $0x5a5aaa55, setup_sig jne setup_bad # Zero the bss movw $__bss_start, %di movw $_end+3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl # Jump to C code (should not return) calll main // /arch/x86/boot/main.c void main(void) { /* First, copy the boot header into the "zeropage" */ copy_boot_params(); /* Initialize the early-boot console */ console_init(); if (cmdline_find_option_bool("debug")) puts("early console in setup code\n"); /* End of heap check */ init_heap(); /* Make sure we have all the proper CPU support */ if (validate_cpu()) { puts("Unable to boot - please use a kernel appropriate " "for your CPU.\n"); die(); } /* Tell the BIOS what CPU mode we intend to run in. */ set_bios_mode(); /* Detect memory layout */ detect_memory(); /* Set keyboard repeat rate (why?) and query the lock flags */ keyboard_init(); /* Query Intel SpeedStep (IST) information */ query_ist(); /* Query APM information */ #if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE) query_apm_bios(); #endif /* Query EDD information */ #if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE) query_edd(); #endif /* Set the video mode */ set_video(); /* Do the last things and invoke protected mode */ go_to_protected_mode(); } // /arch/x86/boot/pmjump.S /* * The actual transition into protected mode */ #include <asm/boot.h> #include <asm/processor-flags.h> #include <asm/segment.h> #include <linux/linkage.h> .text .code16 ... 2: .long in_pm32 # offset .word __BOOT_CS # segment ENDPROC(protected_mode_jump) .code32 .section ".text32","ax" GLOBAL(in_pm32) # Set up data segments for flat 32-bit mode movl %ecx, %ds movl %ecx, %es movl %ecx, %fs movl %ecx, %gs movl %ecx, %ss # The 32-bit code sets up its own stack, but this way we do have # a valid stack if some debugging hack wants to use it. addl %ebx, %esp # Set up TR to make Intel VT happy ltr %di # Clear registers to allow for future extensions to the # 32-bit boot protocol xorl %ecx, %ecx xorl %edx, %edx xorl %ebx, %ebx xorl %ebp, %ebp xorl %edi, %edi # Set up LDTR to make Intel VT happy lldt %cx jmpl *%eax # Jump to the 32-bit entrypoint ENDPROC(in_pm32) // /arch/x86/kernel/head_32.S .text #include <linux/threads.h> #include <linux/init.h> #include <linux/linkage.h> #include <asm/segment.h> #include <asm/page_types.h> #include <asm/pgtable_types.h> #include <asm/cache.h> #include <asm/thread_info.h> #include <asm/asm-offsets.h> #include <asm/setup.h> #include <asm/processor-flags.h> #include <asm/msr-index.h> #include <asm/cpufeatures.h> #include <asm/percpu.h> #include <asm/nops.h> #include <asm/bootparam.h> #include <asm/export.h> #include <asm/pgtable_32.h> ... /* * 32-bit kernel entrypoint; only used by the boot CPU. On entry, * %esi points to the real-mode code as a 32-bit pointer. * CS and DS must be 4 GB flat segments, but we don't depend on * any particular GDT layout, because we load our own as soon as we * can. */ __HEAD ENTRY(startup_32) ... hlt_loop: hlt jmp hlt_loop ENDPROC(early_ignore_irq) __INITDATA .align 4 GLOBAL(early_recursion_flag) .long 0 __REFDATA .align 4 ENTRY(initial_code) .long i386_start_kernel ENTRY(setup_once_ref) .long setup_once // /arch/x86/kernel/head32.c #include <linux/init.h> #include <linux/start_kernel.h> #include <linux/mm.h> #include <linux/memblock.h> #include <asm/desc.h> #include <asm/setup.h> #include <asm/sections.h> #include <asm/e820/api.h> #include <asm/page.h> #include <asm/apic.h> #include <asm/io_apic.h> #include <asm/bios_ebda.h> #include <asm/tlbflush.h> #include <asm/bootparam_utils.h> ... asmlinkage __visible void __init i386_start_kernel(void) { /* Make sure IDT is set up before any exception happens */ idt_setup_early_handler(); cr4_init_shadow(); sanitize_boot_params(&boot_params); x86_early_init_platform_quirks(); /* Call the subarch specific early setup function */ switch (boot_params.hdr.hardware_subarch) { case X86_SUBARCH_INTEL_MID: x86_intel_mid_early_setup(); break; case X86_SUBARCH_CE4100: x86_ce4100_early_setup(); break; default: i386_default_early_setup(); break; } start_kernel(); } // /init/main.c #define DEBUG /* Enable initcall_debug */ #include <linux/types.h> #include <linux/extable.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/binfmts.h> #include <linux/kernel.h> #include <linux/syscalls.h> #include <linux/stackprotector.h> #include <linux/string.h> #include <linux/ctype.h> #include <linux/delay.h> #include <linux/ioport.h> #include <linux/init.h> #include <linux/initrd.h> #include <linux/bootmem.h> #include <linux/acpi.h> #include <linux/console.h> #include <linux/nmi.h> #include <linux/percpu.h> #include <linux/kmod.h> #include <linux/vmalloc.h> #include <linux/kernel_stat.h> #include <linux/start_kernel.h> #include <linux/security.h> #include <linux/smp.h> #include <linux/profile.h> #include <linux/rcupdate.h> #include <linux/moduleparam.h> #include <linux/kallsyms.h> #include <linux/writeback.h> #include <linux/cpu.h> #include <linux/cpuset.h> #include <linux/cgroup.h> #include <linux/efi.h> #include <linux/tick.h> #include <linux/sched/isolation.h> #include <linux/interrupt.h> #include <linux/taskstats_kern.h> #include <linux/delayacct.h> #include <linux/unistd.h> #include <linux/utsname.h> #include <linux/rmap.h> #include <linux/mempolicy.h> #include <linux/key.h> #include <linux/buffer_head.h> #include <linux/page_ext.h> #include <linux/debug_locks.h> #include <linux/debugobjects.h> #include <linux/lockdep.h> #include <linux/kmemleak.h> #include <linux/pid_namespace.h> #include <linux/device.h> #include <linux/kthread.h> #include <linux/sched.h> #include <linux/sched/init.h> #include <linux/signal.h> #include <linux/idr.h> #include <linux/kgdb.h> #include <linux/ftrace.h> #include <linux/async.h> #include <linux/sfi.h> #include <linux/shmem_fs.h> #include <linux/slab.h> #include <linux/perf_event.h> #include <linux/ptrace.h> #include <linux/pti.h> #include <linux/blkdev.h> #include <linux/elevator.h> #include <linux/sched_clock.h> #include <linux/sched/task.h> #include <linux/sched/task_stack.h> #include <linux/context_tracking.h> #include <linux/random.h> #include <linux/list.h> #include <linux/integrity.h> #include <linux/proc_ns.h> #include <linux/io.h> #include <linux/cache.h> #include <linux/rodata_test.h> #include <linux/jump_label.h> #include <linux/mem_encrypt.h> #include <asm/io.h> #include <asm/bugs.h> #include <asm/setup.h> #include <asm/sections.h> #include <asm/cacheflush.h> // 平臺無關啟動程式碼入口 asmlinkage __visible void __init start_kernel(void) { char *command_line; char *after_dashes; set_task_stack_end_magic(&init_task); smp_setup_processor_id(); debug_objects_early_init(); cgroup_init_early(); local_irq_disable(); early_boot_irqs_disabled = true; /* * Interrupts are still disabled. Do necessary setups, then * enable them. */ boot_cpu_init(); page_address_init(); pr_notice("%s", linux_banner); setup_arch(&command_line); /* * Set up the the initial canary and entropy after arch * and after adding latent and command line entropy. */ add_latent_entropy(); add_device_randomness(command_line, strlen(command_line)); boot_init_stack_canary(); mm_init_cpumask(&init_mm); setup_command_line(command_line); setup_nr_cpu_ids(); setup_per_cpu_areas(); smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */ boot_cpu_hotplug_init(); build_all_zonelists(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, NULL, &unknown_bootoption); if (!IS_ERR_OR_NULL(after_dashes)) parse_args("Setting init args", after_dashes, NULL, 0, -1, -1, NULL, set_init_arg); jump_label_init(); /* * These use large bootmem allocations and must precede * kmem_cache_init() */ setup_log_buf(0); vfs_caches_init_early(); sort_main_extable(); trap_init(); mm_init(); ftrace_init(); /* trace_printk can be enabled here */ early_trace_init(); /* * Set up the scheduler prior starting any interrupts (such as the * timer interrupt). Full topology setup happens at smp_init() * time - but meanwhile we still have a functioning scheduler. */ sched_init(); /* * Disable preemption - early bootup scheduling is extremely * fragile until we cpu_idle() for the first time. */ preempt_disable(); if (WARN(!irqs_disabled(), "Interrupts were enabled *very* early, fixing it\n")) local_irq_disable(); radix_tree_init(); /* * Set up housekeeping before setting up workqueues to allow the unbound * workqueue to take non-housekeeping into account. */ housekeeping_init(); /* * Allow workqueue creation and work item queueing/cancelling * early. Work item execution depends on kthreads and starts after * workqueue_init(). */ workqueue_init_early(); rcu_init(); /* Trace events are available after this */ trace_init(); if (initcall_debug) initcall_debug_enable(); context_tracking_init(); /* init some links before init_ISA_irqs() */ early_irq_init(); init_IRQ(); tick_init(); rcu_init_nohz(); init_timers(); hrtimers_init(); softirq_init(); timekeeping_init(); time_init(); sched_clock_postinit(); printk_safe_init(); 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(); /* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init(); if (panic_later) panic("Too many boot %s vars at `%s'", panic_later, panic_param); lockdep_info(); /* * Need to run this when irqs are enabled, because it wants * to self-test [hard/soft]-irqs on/off lock inversion bugs * too: */ locking_selftest(); /* * This needs to be called before any devices perform DMA * operations that might use the SWIOTLB bounce buffers. It will * mark the bounce buffers as decrypted so that their usage will * not cause "plain-text" data to be decrypted when accessed. */ mem_encrypt_init(); #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_ext_init(); kmemleak_init(); debug_objects_mem_init(); setup_per_cpu_pageset(); numa_policy_init(); acpi_early_init(); if (late_time_init) late_time_init(); calibrate_delay(); pid_idr_init(); anon_vma_init(); #ifdef CONFIG_X86 if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_enter_virtual_mode(); #endif thread_stack_cache_init(); cred_init(); fork_init(); proc_caches_init(); uts_ns_init(); buffer_init(); key_init(); security_init(); dbg_late_init(); vfs_caches_init(); pagecache_init(); signals_init(); seq_file_init(); proc_root_init(); nsfs_init(); cpuset_init(); cgroup_init(); taskstats_init_early(); delayacct_init(); check_bugs(); acpi_subsystem_init(); arch_post_acpi_subsys_init(); sfi_init_late(); if (efi_enabled(EFI_RUNTIME_SERVICES)) { efi_free_boot_services(); } /* Do the rest non-__init'ed, we're now alive */ rest_init(); }
本篇不做深入探討,僅為梳理來龍去脈。其中深意還需各自領悟了。反正大致就是,上電啟動後,進入BIOS,交許可權轉交特殊地址,然後轉到系統啟動處,載入對應平臺彙編指令,做各種硬體設定,最後轉到我們熟悉一點的C程式碼入口的過程。這個過程中,更多的是記憶體地址,暫存器之類的操作,可以說涉及到的東西相當廣泛,所以我們不能要求太多可能也沒有必要要求太多。
老鐵,linux之旅愉快啊!
不要害怕今日的苦,你要相信明天,更苦!