xv6操作系统
开发环境构建
以mac系统为例
-
安装virtual box,并加载 ubuntu 22.04镜像
- virtual box 开启端口映射
- 主机端口:虚拟机端口
2222:22
- 通过
ssh -p 2222 bing@localhost
访问虚拟机
- 主机端口:虚拟机端口
- virtual box 开启端口映射
-
安装依赖
- 需要的依赖
sudo apt-get install -y gcc build-essential gdb gcc-multilib qemu-system-i386
- b站作者git仓库
git clone https://github.com/Jeanhwea/xv6-course.git
- 官方xv6仓库
git clone https://github.com/mit-pdos/xv6-public.git
- 安装后进行
make
和make qemu-nox-gdb
- 需要的依赖
开篇-操作系统接口
标准输入输出
按照惯例,进程从文件描述符0读入(标准输入),从文件描述符1输出(标准输出),从文件描述符2输出错误(标准错误输出)
ls existing-file non-exsiting-file > tmp1 2>&1
2>&1
告诉 shell 给这条命令一个复制描述符1的描述符2。
这样 existing-file 的名字和 non-exsiting-file 的错误输出都将出现在 tmp1 中
为何 fork 和 exec 是单独的两种系统调用?
这种区分使得 shell 可以在子进程执行指定程序之前对子进程进行修改。
举例
int main()
{
char *argv[2];
argv[0] = "cat";
if (fork() == 0) {
close(0); // 关闭标准输入
int fd = open("input.txt", O_RDONLY); // 返回0
printf("fd %d\n", fd); // fd 0
execv("/bin/cat", argv);
}
return 0;
}
为啥 kernel.s 可以在qemu里关中断,但在用户态程序里不行?
# kernal.s文件
kernel.out: file format elf32-i386
Disassembly of section .text:
00007c00 <start>:
7c00: fa cli
7c01: b8 01 00 mov $0x1,%ax
7c04: fb sti
00007c05 <spin>:
7c05: eb fe jmp 7c05 <spin>
在操作系统中,关中断与能否执行特权指令有关。关中断通常是由操作系统的核心部分,也就是内核来处理的。内核运行在特权态(在x86架构中通常是0级),可以执行诸如修改中断控制器状态之类的特权指令。
在你所提供的 kernel.s 中的 cli 指令和 sti 指令,分别表示关中断和开中断,它们都是特权指令,只有在内核态(Ring 0)中才能执行。 而用户态程序运行在非特权态(在x86架构中通常是3级),不能直接执行特权指令,包括开关中断等操作,否则会触发异常。这是因为用户程序可以由任何人编写和执行,如果允许用户态程序随意关闭或打开中断,那么系统的稳定性和安全性就无法保障。
所以,kernel.s 能在 qemu(模拟的内核态环境)中关闭中断,而在用户态程序中不能关闭中断。
页表
dir、页目录
- pgdir: 页目录基地址,是虚拟地址
- PDX: PDX(va) 获取虚拟地址(32位)的前10位
- pde: 页目录项,pgdir[0-1023]的一个元素,存储的是 (pgtab的物理地址|权限)
table、页表
- pgtab: 页表基地址,是虚拟地址
- PTX: PTX(va) 获取虚拟地址(32位)的前11-20位
- pte: 页表项,pgtab[0-1023]的一个元素,存储的是(元素的物理地址|权限)
- PTE_ADDR: 根据pde,获取实际pgtab的物理地址
整体启动流程
- 启动流程
- 从磁盘加载kernel文件到内存
- 通过 elf的entry 调用 main
- 虚拟内存申请与分配
- 创建kernel的页表,进行虚拟地址和物理地址映射
- 运行用户进程
- userinit
- 申请并初始化进程
- 将initcode.S 代码加载到进程的页目录内存
- mpmain:scheduler
- 获取当前cpu,以及可运行的进程
- 切换用户进程的页表
- 保存cpu寄存器的数据,栈切换到进程
- forkret 释放进程锁
- trapret 栈上取出寄存器的值
- initcode
- 执行exec("/init")
- init
pid=1
- 创建console
- fork后执行 exec("sh")
- sh
pid=2
- 循环监听用户键盘输入
cd
命令,改变目录- 其他命令,fork后exec(
pid=3
),执行后子进程退出,sh程序继续运行
- userinit
xv6启动流程详细分析
- bootasm.S
- 加载lgdt表
- 调用 bootmain.c:bootmain()
- bootmain.c
- 加载 kernel 到内存
- 解析 elf 头
- 跳转 elf->entry()
- entry.S
- 初始化
- 跳转 main.c:main()
- main
syscall流程
- initcode.asm
- int $T_SYSCALL (0x40)
- vector.S
- vector64
- alltraps
- call trap
- trap
- syscall
- sys_exec
- exec
- load elf
- alloc uvm(virtual memory)
- fill ustack
- replace old uvm to new uvm
用户态程序
不同的用户程序
- cat:读取fd,输出
- ls: 扫描当前文件/目录,输出
- echo: 写入fd
- find: 递归扫描目录,找到符合条件的文件
- grep: 文件内容按行切分,找到match pattern的行
- xargs: 根据输入,截取换行符,每个程序依次执行
- find . b | xargs grep hello => xargs grep hello ./a/b
scause 0x000000000000000d sepc=0x0000000080002052 stval=0x0000000000000000
b *0x0000000080002052
固件,bios,rom
固件,ROM 和 BIOS 是计算机硬件和软件关键组成部分,它们在系统中扮演不同角色: - 固件(Firmware): 固件是嵌入到硬件设备中的软件,例如:路由器,网卡等。它提供了硬件设备的低级控制,用于硬件中的特定模型或设备类型。固件能够在设备电源关闭后保持存储,但可被特殊程序更新或替换。 - ROM(只读存储器): ROM是一种存储媒介,用于计算机,它存储的数据是永久性的,即使在电源关断时也不会消失。ROM中的数据是在制造过程中写入的,含有初始启动程序(BIOS)或者其他系统必需的固件。 - BIOS(基本输入/输出系统): BIOS是系统启动时第一个运行的软件,存在于ROM中,它初始化并测试硬件,然后从硬盘或其他设备加载操作系统。BIOS可能包含额外的软件服务和诊断程序。 综上,固件,ROM和BIOS三者关系紧密。固件是一种特殊类型的软件,通常被烧录到ROM中,这样硬件就可以在电源打开时立刻开始工作。而其中的BIOS固件负责在启动时初始化硬件并加载操作系统。
进程
the trampoline page contains the code to transition in and out of the kernel mapping the trapframe is necessary to save/restore the state of the user proces
the state of a thread (local variables, function call return addresses) is stored on the thread’s stacks.
页表
三层结构允许以一种节省内存的方式记录 PTE。在大范围虚拟地址没有映射的常见情况下,三级结构可以省略整个页目录。
为什么是三级页表?
- 存储成本考虑
- 只有一层需要2^27的索引项,但是如果是三层,是按需分配,用到多少就分配多少,比如只用了[0][0][0] 最终只生成了3*2^9个索引项
- cpu性能
- 避免从物理内存加载 PTE 的成本,RISC-V CPU 将页表条目缓存在转换后备缓冲区 (TLB) 中
pgTable 是4096字节;int64指针占用8字节;因此一页可以分配512(2^9)个PTE
(pte >> 10) << 12
一个PTE指向一个4096字节的物理页面,右移10位是为了去掉flags位,左移12位是,对齐一个4096的页面