系统初始化

本文详细介绍了计算机从实模式到保护模式的启动过程,涉及地址总线、数据总线的作用,BIOS、GRUB2在启动中的角色,以及32位和64位处理器对内存访问的差异。重点讲述了系统调用、内核初始化和启动流程,包括RAMdisk的使用和内核服务的初始化。
摘要由CSDN通过智能技术生成

工作模式

实模式

因为早期地址总线位数限制,所以一个段的大小只有64K,最多只能访问1M的内存空间,还要分成多个段

  • CPU 和其他设备连接,依靠总线
  • 最重要的设备是内存,很多复杂的计算任务需要将中间结果保存下来
  • CPU由三部分组成:运算单元(只做运算),数据单元(CPU内部的缓存和寄存器组,减少每次经过总线访问内存的消耗,暂时存放数据和运算结果),控制单元(指令控制)

  • cpu和内存来回传送数据,靠的都是总线,总线上有两类数据:地址数据(想拿内存中哪个位置的数据),这类总线叫地址总线;另一类是真正的数据,这类总线叫数据总线
  • 地址总线的位数:决定了能访问的地址范围有多广,位数越多,能访问 的位置就越多,能管理的内存的范围就越广
  • 地址总线的位数:决定了一次性能拿多少个数据,位数越多,一次拿的数据越多,访问速度就越快

保护模式:

32位的处理器,能够访问到更多的内存

  • AX、BX、CX、DX分成两个8位寄存器来使用,分别是AH、AL、BH、BL、CH、CL、DH、DL,H就是High(高位)
  • 为了指向不同进程的地址空间,有四个 16 位的段寄存器,分别是 CS(代码寄存器)、DS(数据段寄存器)、SS(栈寄存器)、ES

BIOS时期

早期计算机主板上有一段只读存储器,叫ROM,上面固化了一段初始化程序,也就是BIOS(基本输入输出系统)

当电脑刚加电的时候,会做一些重制动作,将CS设置成0XFFFF,将IP设置为0X0000,所以第一条指令就会指向0XFFFF0,正是ROM的范围内,这里有一个JMP命令会跳到ROM中做初始化工作的代码,于是BIOS开始进行初始化的工作:

    • 检查硬件
    • 提供简单服务菜单
    • 建立中断向量表和中断服务程序

bootloader时期

BIOS启动完后,会通过启动盘,查找操作系统

启动盘:一般在磁盘的第一个扇区,占用512字节,而且以 0xAA55 结束。这是一个约定,当满足这个条件的时候,就说明这是一个启动盘,在 512 字节以内会启动相关的代码

这512字节的代码来自哪里?

linux的一个工具:Grub2(Grand Unified Bootloader Version 2)

可以通过grub2-mkconfig -o /boot/grub2/grub.cfg 来配置系统启动的选项,这里配置的选项会在系统启动时,成为一个列表,让你选择从哪个系统开始启动

使用grub2-install /dev/sda 可以将启动程序安装到相应的位置

  • grub2第一个安装的就是boot.img,由boot.S编译而成,一共512字节,正式安装到启动盘的第一个扇区,这个扇区通常称为MBR(Master Boot Record 主引导记录/扇区)
  • BIOS任务完成任务后,会将boot.img从硬盘加载到内存中的0X7c00来运行
  • boot.img一共512字节,能做的事情有限,能做的最重要的一件事就是加载grub2的另一个镜像core.img
  • core.img 由 lzma_decompress.img、diskboot.img、kernel.img 和一系列的模块组成

  • boot.img将控制权交给diskboot.img之后,diskboot.img的任务是将core.img的别的模块加载进来
    • 解压缩lzma_decompress.img,kernel.img,最后是各个模块module对应的映像,这里是grub的内核,不是linux的内核
    • 在这之前,这些都是实模式下的运行的,但随着加载的东西越多,1M地址空间放不下,所以lzma_decompress.img在在真正解压缩之前,会调用real_to_prot切换到保护模式,这样就是在更大的寻址空间里,加载更多东西

从实模式切换到保护模式

切换保护模式的函数 DATA32 call real_to_prot 会打开 Gate A20,也就是第 21 根地址线的控制线。

  • 解压缩kernel.img,并跳转到kernel.img开始运行
    • kernel.img对应的代码是startup.S以及一堆c文件,startup.S会调用grub_main,是grub kernel的主函数
    • 这个函数里面,grub_load_config开始解析,前面所说的grub.conf文件里的配置信息
    • 如果正常启动,grub_main最后会调用grub_command_execute(“naomal”,0,0),最终会调用grub_normal_execute函数,在这个函数里面,grub_show_menu会显示出让你选择那个操作系统的列表
    • 选择了之后,就开始调用grub_menu_execute_entry解析执行,比如里面的linux16命令,表示装载指定的内核文件,并传递内核启动参数,也是grub_cmd_linux函数会被调用,这个函数会先读取linux内核镜像头部的一些数据结构,放到内存中的数据结构中,进行检查,如果检查通过,则会读取整个linux内核镜像到内存。
    • 如果配置文件里面还有 initrd 命令,用于为即将启动的内核传递 init ramdisk 路径。于是 grub_cmd_initrd() 函数会被调用,将 initramfs 加载到内存中来。
    • 当这些事情做完之后,grub_command_execute (“boot”, 0, 0) 才开始真正地启动内核

内核启动

内核的启动从入口函数 start_kernel() 开始。在 init/main.c 文件中,start_kernel 相当于内核的 main 函数

  • 0号进程初始化 struct task_struct init_task = INIT_TASK(init_task)
  • 中断初始化,trap_init(),里面设置了很多中断门,用于处理各种中断,set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32),这是32位系统调用的中断门。系统调用也是通过发送中断的方式进行的
  • 内存管理初始化,mm_init()
  • 调度模块初始化,sched_init()
  • vfs_caches_init() 会用来初始化基于内存的文件系统 rootfs。在这个函数里面,会调用 mnt_init()->init_rootfs()。这里面有一行代码,register_filesystem(&rootfs_fs_type)。在 VFS 虚拟文件系统里面注册了一种类型,我们定义为 struct file_system_type rootfs_fs_type。
  • rest_init()其他初始化
    • 初始化1号进程,用 kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是 1 号进程,第一个用户态的进程,是所有用户进程的祖先
      • kernel_thread执行的时候还是内核态,通过do_execve系统调用,运行一个执行文件,它会尝试运行 ramdisk 的“/init”,或者普通文件系统上的“/sbin/init”“/etc/init”“/bin/init”“/bin/sh”。
      • do_execve->do_execveat_common->exec_binprm->search_binary_handler运行一个程序,需要加载这个二进制文件,格式是ELF
int search_binary_handler(struct linux_binprm *bprm)
{
  ......
  struct linux_binfmt *fmt;
  ......
  retval = fmt->load_binary(bprm);
  ......
}


static struct linux_binfmt elf_format = {
.module  = THIS_MODULE,
.load_binary  = load_elf_binary,
.load_shlib  = load_elf_library,
.core_dump  = elf_core_dump,
.min_coredump  = ELF_EXEC_PAGESIZE,
};
// 先调用load_elf_binary,最后在调用start_thread
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs  = 0;
regs->ds  = __USER_DS;
regs->es  = __USER_DS;
regs->ss  = __USER_DS;
regs->cs  = __USER_CS;
regs->ip  = new_ip;
regs->sp  = new_sp;
regs->flags  = X86_EFLAGS_IF;
force_iret();
}
EXPORT_SYMBOL_GPL(start_thread);
      • start_thread将寄存器的用户态的代码段 CS 设置为 __USER_CS,将用户态的数据段 DS 设置为 __USER_DS,以及指令指针寄存器 IP、栈指针寄存器 SP,最后调用iret从系统调用中返回时,恢复寄存器,从之前补好的寄存器的值恢复,指向用户态函数栈的栈顶。所以,下一条指令,就从用户态开始运行了。
      • ramdisk的作用:
        • 一开始到用户态的是ramdisk的init,后面会启动真正根文件系统上的init,称为所有用户进程的祖先
        • 如果存储系统数目很有限,那驱动可以直接放到内核里面,但是随着存储系统越来越多,如果市场上所有的存储系统的驱动都默认放进内核,那内核旧太大了,所以只好先弄一个基于内存的文件系统,内存访问不要驱动,这个就是ramdisk,这时,ramdisk就是跟文件系统
        • 等运行完ramdisk上的/init,就已经在用户态了。/init这个程序会先根据存储系统的类型夹杂驱动,有了驱动就可以设置真正的根文件系统,有了根文件系统,ramdisk上的/init会启动文件系统上的init
      • 接下来就是各种系统的初始化。启动系统的服务,启动控制台,用户就可以登录进来了。
    • 初始化2号进程
      • kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 又一次使用 kernel_thread 函数创建2号进程
      • 函数 kthreadd,负责所有内核态的线程的调度和管理,是内核态所有线程运行的祖先

系统调用

  • 32位系统调用过程
  • 64位系统调用过程
  • 系统调用表sys_call_table
    • 32位的系统调用表定义在arch/x86/entry/syscalls/syscall_32.tbl、
      5 i386 open sys_open compat_sys_open

    • 64位的系统调用表定义在arch/x86/entry/syscalls/syscall_64.tbl
      2 common open sys_open
      • 第一列是系统调用号
      • 第三列是系统调用的名字
      • 第四列是系统调用在内核的实现函数

系统调用在内核中的实现函数要有一个声明,声明往往在include/linux/syscalls.h 文件中

asmlinkage long sys_open(const char __user *filename,
                                int flags, umode_t mode);

真正实现,一般在.c文件中

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
        if (force_o_largefile())
                flags |= O_LARGEFILE;
        return do_sys_open(AT_FDCWD, filename, flags, mode);
}

SYSCALL_DEFINE3是一个宏系统调用,最多6个参数,根据参数的数据选择宏

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)


#define SYSCALL_DEFINEx(x, sname, ...)                          \
        SYSCALL_METADATA(sname, x, __VA_ARGS__)                 \
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)


#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...)                                 \
        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))       \
                __attribute__((alias(__stringify(SyS##name))));         \
        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));      \
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))       \
        {                                                               \
                long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));  \
                __MAP(x,__SC_TEST,__VA_ARGS__);                         \
                __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));       \
                return ret;                                             \
        }                                                               \
        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)

  • 46
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值