本课程是理论和代码相结合的授课模式,在先讲解一般操作系统的通用运行逻辑后再结合Linux内核源码具体学习相关操作系统功能逻辑的实现。
计算机系统的基本工作原理1. 计算机的三个关键性方法机制
当代所有计算机平台的基础–冯诺伊曼机
先了解一下什么是存储程序计算机?
主要思想是讲程序存放在计算机存储器中,然后按照存储器中的存储程序的首地址执行程序的第一条指令,按照该程序中编写好的指令执行,直至程序结 束。
冯诺伊曼机的三大特点:
包含运算器、存储器、控制器、I/O
内部采用二进制来表示指令和数据。
将编写好的程序和数据先放入计算机中,然后启动。
核心是存储程序计算机。
2. 堆栈
作用:记录调用框架,传递参数,保存返回值,暂时存储局部变量
相关寄存器:ESP+EBP(堆栈相关寄存器),CS:EIP(函数调用相关),EAX(返回值地址),还有传递参数相关
运行机制:通过pop,push两种操作,结合以上各种寄存器,实现函数调用,返回地址的记录,并且在函数调用完之后可以返回到原文中继续执行。
3. 中断–系统调用(这里只写相关部分的中断内容)
系统调用的总过程:
触发系统调用时,用户态使用int$0x80指令触发中断,跳转到汇编代码system_call,触发中断机制,当前进程用户态的堆栈SS:ESP,CS:EIP EFLAGS保存在内核中,接下来执行到system_call的位置,将这段汇编用于保存现场,执行系统调用内核函数,函数调用完之后返回,恢复现场。最后iret将关键现场恢复到对应的寄存器中去,并回到int$0x80之后的下一条指令继续执行。
初始化过程:
系统加载 start_kernel 开始,调用了 trap.c 中的 trap_init()函数对中断进行初始化。并且调用同一文件下的set_trap_gate()/set_system_gate()/set_intr_gate()等对中断描述符进行初始化。在进入保护模式之前, IDT 再次通过 setup_idt()函数进行初始化,在这里使用了 ignore_int()函数,是为了保护未初始化完成时发生异常不出错。然后调用 init_IRQ()函数,把中断描述附表的中断处理代码段地址设在在 interrupt 数组中,该数组指向同一个函数处理 common_interrup。
为什么需要分级?
系统体系架构分为用户态和内核态,用户态的权限低于核心态,也就是说代码掌控的范围收到了限制,如果没有权限级别的划分,程序员所有代码都可以使用特权指令,系统很容易出现崩溃的情况。
1. 进程的的概念:
运行一个程序的时候,操作系统需要描述其的运行过程,通过结构体task_struct{}来描述,统称为PCB。
2. 进程的描述信息有:
PID,进程状态,优先级,sp,上下文,内存指针。
3.创建进程–fork()
fork函数不同于其他函数,因为其返回次数的特殊性:fork会有两个返回值,一个为父亲进程的返回值,一个为子进程的
子进程返回0,父进程返回子进程的进程ID。
4.fork运行机制:
触发系统调用时,用户态使用int$0x80指令触发中断,跳转到汇编代码system_call,触发中断机制后把当前进程用户态的堆栈SS:ESP,CS:EIP EFLAGS,接下来执行到system_call的位置,将这段汇编用于保存现场,执行系统调用内核函数,函数调用完之后返回,恢复现场。最后iret将关键现场恢复到对应的寄存器中去,并回到int$0x80之后的下一条指令继续执行。
fork系统调用创建了一个子进程,子进程复制了父进程中所有的进程信息(内核堆栈,进程描述符),但有些信息是不一样的,比如pid和内核堆栈,还有将新的进程连接到各种链表中,要保存进程执行到哪个位置,父子进程共享存储空间。
5.相关函数:
fork,vfork,和clone3个系统调用和kernel_thread内核函数都是通过do_fork()来创建进程的。
6.进程状态:
阻塞态,运行态,就绪态。
7.进程调度策略:
7.1 实时调度策略:SCHED_FIFO和SCHED_RR,实时系统的优先级是静态设定的,在就绪队列没有实时进程的情况下,普通进程才能获得调度,二者分别为先 进先出和时间片轮转。
7.2 根据负载变化发生变化:SCHED_NORMAL,每个进程分配到的cpu时间占比有比例跟系统当前的负载有关。
8.调度算法:
CFS:基于权重的动态优先级调度算法,已使用的虚拟时间越少,进程排序就越靠前。
9.进程上下文切换
包括两个部分:切换页全局目录CR3和切换内核态堆栈和硬件上下文。
最后给出一个linux系统概念模型:
举例验证:
读写文件,在读写文件之前,必须使用oepn打开一个文件,打开文件首先open会执行到C库,C库里有INT $0x80指令,然后在中断向量表中找到128项,中断向量表里有中断描述符,可以找到中断处理程序入口,128项是系统调用处理函数,进入系统调用处理函数,保存现场,系统调用号存储eax中,根据系统调用号执行系统调用表中对应那一项的函数。系统调用表是相应的函数指针,这里会执行sys_open。sys_open进行命令查找,找到文件控制块,根据不同文件类型,调用文件打开函数,文件打开函数会创建一个系统文件打开表file,file的很多内容来自文件控制块,填完之后,进程也有一个进程文件打开表,这个结构里面有fd数组,fd数组是指针,找个空闲的,把它指向之前已经创建的file结构,最后返回那一项的索引号,即fd。
当进程使用read系统调用读这个文件,就会根据fd数组的下标 找到fd数组的对应项,然后找到指向之前创建的file结构的指针,再找到这个file结构,最后找到file结构里的file_operations里的具体的read函数来读取文件。
当进程使用write系统调用写数据到这个文件时也会根据fd数组的下标 找到fd数组的对应项,然后找到之前创建的file结构的指针,再找到这个file结构,进而找到file结构里的file_operations里的具体的write函数来写文件。
二、课程实验 2.1 实验一:mykernel实验目的:搭建虚拟的x86-64 CPU实验平台mykernel
实验环境:VMware+Ubuntu1804
详细步骤:
//下载孟宁老师的mykernel2.0补丁 wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch //安装axel工具 sudo apt install axel //下载linux-5.4.34内核源码 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz //解压 xz -d linux-5.4.34.tar.xz tar -xvf linux-5.4.34.tar //进入linux内核源码目录,进行后续操作 cd linux-5.4.34 //patch一下补丁,可以看到哪些文件被更改了 patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch //安装调试工具 sudo apt install build-essential gcc-multilib sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev //配置文件 make defconfig # Default configuration is based on 'x86_64_defconfig' //编译,需要五分钟左右时间,make -j2(核数) make -j$(nproc) qemu-system-x86_64 -kernel arch/x86/boot/bzImage
在qemu窗口可以看到my_start_kernel在执行,同时my_timer_handler时钟中断处理程序周期性执行。
2.2 实验二:debug mykernel(1)安装开发工具
-
安装vscode在官网https://code.visualstudio.com/Download下载并安装,注意,ubuntu下要下载.deb后缀的文件哦
-
还要安装VSCode插件C/C++ Intellisense和C/C++ Themes。由于插件C/C++ Intellisense需要GNU Global,还需要使用如下命令安装GNU Global。
sudo apt install global
-
修改VScode的配置文件c_cpp_properties.json使得VSCode能自动搜索跳转到函数定义了,阅读代码会方便很多。由于Linux内核代码非常庞大,为加速搜索速度还可以设置排除一些干扰文件,比如修改配置文件settings.json
配置文件的路径是home/butterfly/.config/Code/User/seetings.json。配置文件参见https://github.com/mengning/linuxkernel/tree/master/src/kerneldebuging -
安装调试工具
sudo apt install build-essential gcc-multilib sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev(2)下载内核源代码
sudo apt install axel axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz xz -d linux-5.4.34.tar.xz tar -xvf linux-5.4.34.tar cd linux-5.4.34
由于 Linux 内核高度定制化,所以没有办法直接通过配置 includePath 等让 Intellisense 正常提示,这里借助一个 Python 脚本来生成 compile_commands.json 文件帮助 Intellisense 正常提示(包括头文件和宏定义等)。在Linux源代码目录下直接运行如下命令就可以生成 compile_commands.json 了。
python ./scripts/gen_compile_commands.py //如果没有安装python,使用如下命令即可 sudo apt install python(3)配置内核选项
make defconfig # Default configuration is based on 'x86_64_defconfig' make menuconfig #打开debug相关选项
Kernel hacking —>
Compile-time checks and compiler options —>
[] Compile the kernel with debug info
[] Provide GDB scripts for kernel debugging
[*] Kernel debugging
关闭KASLR,否则会导致打断点失败
Processor type and features ---->
[] Randomize the address of the kernel image (KASLR)
make -j$(nproc) # nproc 是核数 make -j2 # 测试一下内核能不能正常加载运行,因为没有文件系统最终会kernel panic qemu-system-x86_64 -kernel arch/x86/boot/bzImage(5)生成initramfs根文件系统镜像,也习惯于命名为rootfs
电脑加电启动首先由bootloader加载内核,内核紧接着需要挂载内存根文件系统,其中包含必要的设备驱动和工具,bootloader加载根文件系统到内存中,内核会将其挂载到根目录/下,然后运行根文件系统中init脚本执行一些启动任务,最后才挂载真正的磁盘根文件系统。
我们这里为了简化实验环境,仅制作内存根文件系统。这里借助BusyBox 构建极简内存根文件系统,提供基本的用户态可执行程序。
首先从https://www.busybox.net下载 busybox源代码解压,解压完成后,跟内核一样先配置编译,并安装。
axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
tar -jxvf busybox-1.31.1.tar.bz2
cd busybox-1.31.1
make menuconfig
# 记得要编译成静态链接,不用动态链接库。
Settings --->
[*] Build static binary (no shared libs)
# 然后编译安装,默认会安装到源码目录下的 _install 目录中。
make -j$(nproc) && make install
# 然后制作内存根文件系统镜像,大致过程如下:
mkdir rootfs
cd rootfs
cp ../_install/* ./ -rf
mkdir dev proc sys home
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
# 准备init脚本文件放在根文件系统跟目录下(rootfs/init) touch init #添加如下内容到init文件。 #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome MengningOS!" echo "--------------------" cd home /bin/sh 给init脚本添加可执行权限 chmod +x init
打包成内存根文件系统镜像 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz 测试挂载根文件系统,看内核启动完成后是否执行init脚本 qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz(6)命令行debug linux kernel
下面具体看看如何使用gdb跟踪调试Linux内核。使用gdb跟踪调试内核,加两个参数,一个是-s,在TCP 1234端口上创建了一个gdb-server。可以另外打开一个窗口,用gdb把带有符号表的内核镜像vmlinux加载进来,然后连接gdb server,设置断点跟踪内核。若不想使用1234端口,可以使用-gdb tcp:xxxx来替代-s选项),另一个是-S代表启动时暂停虚拟机,等待 gdb 执行 continue指令(可以简写为c)。
# 启动虚拟机 qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
用以上命令先启动,然后可以看到虚拟机一启动就暂停了。加-nographic -append "console=ttyS0"参数启动不会弹出QEMU虚拟机窗口,可以在纯命令行下启动虚拟机,此时可以通过“killall qemu-system-x86_64”命令强行关闭虚拟机。
再打开一个窗口,启动gdb,把内核符号表加载进来,建立连接:
cd linux-5.4.34/ gdb vmlinux (gdb) target remote:1234 (gdb) b start_kernel c、bt、list、next、step....(7)VScode debug linux kernel
配置.vscode/tasks.json和.vscode/launch.json。配置文件参见https://github.com/mengning/linuxkernel/tree/master/src/kerneldebuging,修改方法示例:
此时将断点设在init/main.c中的start_kernel函数中,然后Qemu 开启GDB调试,vscode start debug即可开始调试内核。
参考资料《庖丁解牛Linux操作系统分析》 孟宁



