工作模式
实模式:
因为早期地址总线位数限制,所以一个段的大小只有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
- 32位的系统调用表定义在arch/x86/entry/syscalls/syscall_32.tbl、
-
- 64位的系统调用表定义在arch/x86/entry/syscalls/syscall_64.tbl
2 common open sys_open
- 64位的系统调用表定义在arch/x86/entry/syscalls/syscall_64.tbl
-
-
- 第一列是系统调用号
- 第三列是系统调用的名字
- 第四列是系统调用在内核的实现函数
-
系统调用在内核中的实现函数要有一个声明,声明往往在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__)