关于linux的一点好奇心(一):linux启动过程
阅读原文时间:2022年01月04日阅读:1

  一直很好奇,操作系统是如何工作的?我们知道平时编程,是如何让代码跑起来的,但那些都是比较高层次的东西。越往后,你会越觉得,像是空中楼阁,或者说只是有人帮你铺平了许多道理,而你却对此一无所知。

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
#include
#include
#include
#include
#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
#include
#include
#include

.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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

/*
* 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
#include
#include
#include

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#include
#include
#include
#include
#include
// 平台无关启动代码入口
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之旅愉快啊!