最近一直在寻找有关于系统的项目,偶然了解到了这个来自麻省理工大学2020年操作系统课程秋季项目,该项目虽然使用英文传授,但貌似很多前辈着手做了翻译和尝试,项目难度较大,lab设计也比较巧妙,总体评价还是不错的。以下是项目相关记录资料。

翻译课程链接:MIT 6.S081 2020 操作系统 [中英文字幕]

如果喜欢看文字,有大哥将课程所有内容整理出来了: github课程翻译

GitBook版本排版更加舒服:GitBook课程翻译

xv6 Book合实验手册:https://xv6.dgs.zone

环境配置

编程环境

  • Win10 SSH+虚拟机Ubuntu 18.04

虚拟机安装SSH服务:

1
sudo apt-get install openssh-server
启用服务:
1
sudo service ssh start
开机自启
1
sudo systemctl enable ssh
关闭防火墙
1
sudo ufw disable

使用Win终端工具XShell、mabaXterm、Vscode等进行SSH连接即可。

RISCV工具链环境

xv6是一个类Unix系统,但比Linux等简单许多,整个系统基于RISCV指令集上,与ARM指令集不同,RISCV是一个开源的指令集,广泛被用于服务器和嵌入式设备,通过RISCV工具链将c语言等编译成RISCV指令集可执行文件,而且这种文件不能直接被Linux、MacOS、Windows等系统直接运行,需要再套一层虚拟机环境,Linux下为qemu虚拟机环境,MacOS则对应spike。

对Ubuntu20.04以上版本,RISCV安装似乎比较简单: 相关依赖:

1
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

riscv64编译工具链:

1
sudo apt install gcc-riscv64-unknown-elf  #这是一种用于生成riscV执行文件的gcc

如果是Ubuntu18.04,则可能需要自己编译RISCV的工具链:

1
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

clone下N个G,有可能因为某些原因导致文件没办法clone全,运行:

1
git submodule update --init --recursive

安装依赖,注意将前面提到的依赖也一并安装,以免遗漏

1
sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev

新增一个依赖:这个不影响编译和gdb运行,但是缺少了gdb的layout无法正常运行,见Q&A 3;

1
sudo apt-get install  libncurses5-dev

开始编译:

1
sudo make
然后就开始漫长的等待,我的电脑AMD R7 4800H编译时间大概为2h。

验证是否编译成功:

1
riscv64-unknown-elf-gcc --version
如果出现版本信息,说明riscv64工具链编译完毕;

安装qemu

安装依赖:

1
2
3
sudo apt-get install libglib2.0-dev ninja-build  build-essential zlib1g-dev pkg-config libglib2.0-dev  \
binutils-dev libboost-all-dev autoconf libtool libssl-dev libpixman-1-dev libpython-dev \
virtualenv libmount-dev libpixman-1-dev

安装qemu,注意18.04要按照官网qemu版本标准(见Q&A 2):

1
wget https://download.qemu.org/qemu-5.1.0.tar.xz
解压
1
tar xf qemu-5.1.0.tar.xz

1
./configure --disable-kvm --disable-werror --prefix=/usr/local --target-list="riscv64-softmmu"

编译:

1
make
安装
1
sudo make install

验证:

1
qemu-system-riscv64 --version
出现对应版本消息,说明qemu正常安装。

回到xv6项目文件,

1
git clone https://github.com/mit-pdos/xv6-riscv.git

在项目文件夹下运行xv6系统:

1
make qemu

如果出现RISCV命令符,说明系统已经成功运行了,输入ls可以得到回显。

退回Linux终端,只需先按ctrl+a,再按x即可。

至此,基本的虚拟环境已经搭好,总体而言坑不算多,需要保持耐心,如果有不当地方也不能骂博主www。

一个Hello World程序

编译一个RISCV可执行程序:hello.c(自己去默写)不要使用return 0,包含头文件stdlib.h使用exit(0),这也是后面常用的写法。

1
riscv64-unknown-elf-gcc hello.c
这个编译文件是不能直接在Linux运行的,只能在虚拟机上运行。xv6项目里内置了一个makefile文件,指定好了编译规则,只需要根据以下操作: 将hello.c放入user文件夹,在MakefileUPROGS项中添加:
1
$U/_hello\
注意这个变量名一定保持和c文件一致,不然qemu就不会找了。通过make qemu,会发现虚拟机下出现了hello命令,输入即可得到输出: 结果

Q&A

  1. xv6文件夹下user/sh.cdruncmd函数报错
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    user/sh.c: In function 'runcmd':
    user/sh.c:58:1: error: infinite recursion detected [-Werror=infinite-recursion]
    58 | runcmd(struct cmd *cmd)
    | ^~~~~~
    user/sh.c:89:5: note: recursive call
    89 | runcmd(rcmd->cmd);
    | ^~~~~~~~~~~~~~~~~
    user/sh.c:109:7: note: recursive call
    109 | runcmd(pcmd->left);
    | ^~~~~~~~~~~~~~~~~~
    user/sh.c:116:7: note: recursive call
    116 | runcmd(pcmd->right);
    | ^~~~~~~~~~~~~~~~~~~
    user/sh.c:95:7: note: recursive call
    95 | runcmd(lcmd->left);
    | ^~~~~~~~~~~~~~~~~~
    user/sh.c:97:5: note: recursive call
    97 | runcmd(lcmd->right);
    | ^~~~~~~~~~~~~~~~~~~
    user/sh.c:127:7: note: recursive call
    127 | runcmd(bcmd->cmd);
    | ^~~~~~~~~~~~~~~~~
    cc1: all warnings being treated as errors
    <内置>: recipe for target 'user/sh.o' failed

这不算是一个错误,需要在对应文件user/sh.c的56行添加:\__attribute__((noreturn)),不会出现报错,此后所有初始分支都需要这个操作。

  1. make qemu卡在

    1
    qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
    在官方文档也提到这个问题,原因是qemu版本过高不兼容,例如我下的6.2就不work了,需要装回5.1;官方给出的方案是;
    1
    2
    sudo apt-get remove qemu-system-misc
    sudo apt-get install qemu-system-misc=1:4.2-3ubuntu6
    也是将qemu降版本,但qemu-system-misc=1:4.2-3ubuntu6只在20.04及更高版本存在,18.04无法更换,因此还是直接换5.1即可。

  2. gdb调试layout命令找不到:

    1
    Undefined command: “layout”. Try “help”
    缺少了一个库:
    1
    2
    3
    sudo apt-get install  libncurses5-dev
    sudo make clean
    sudo make #重新编译貌似只需要几十分钟

版本控制及提交

首先介绍必要性:xv6将实验用git分支(master)将工作目录隔绝开来,保证前面的实验不会对后面的目录产生干扰。每次实验完成,必须的是使用commit或者stash其中一种方法将更改贮存起来,防止你的更改丢失。其次push到自己仓库就不是必须的,权当学习。

创建自己的代码仓库,不要使用原始仓库,在github创建一个新的仓库,使用命令。

1
git remote add github (换成你自己的仓库地址)https://github.com/EdenMoxe/mit-xv6-2020.git
然后你的xv6目录下.git/config就会出现一个"github"项指向你的仓库,可以向仓库添加代码.

在每次实验开始时切换到当前分支,或者新建一个测试分支更加稳健:

1
2
3
git checkout util
#也可以创建新的测试分支
#git checkout -b util_test

commit提交

完成实验,在切换下个分支前,需要对编辑过的文件进行保存放到暂存区,使用命令:

1
git add . #.代表所有更改
如果你忘记自己更改的是什么文件了,可以使用diff命令查看两个分支了文件差异:
1
2
git diff util(需要比较的分支)  #源码级比较
git diff util --stat #文件级比较

如果其中出现了不想提交的文件,可以使用reset命令移除

1
2
git reset HEAD   #移除所有add
git reset HEAD +文件名/文件夹 #移除某项

提交:提交它会向git提交一个版本,在提交前要指明用户、邮箱来作为给git的标识:谁谁谁在何时提交了什么版本,修改了什么:因此要在.git/config文件末尾中添加:

1
2
3
[user]  #换成自己的信息
name=EdenMoxe
email=2436444815@qq.com
运行:
1
git commit -m "Description" #提交+描述,这样可以在
在其他分支也能够查看这个分支的历史版本:
1
git log <master> #git log util_test
如果其他分支想采用相同的文件配置,需要合并两个分支:
1
git merge <master>   #(master是原来的分支环境)
上传仓库,这对于实验而言不是必须的:
1
2
git push <远程主机名> <本地分支>:<远程分支>
#上面我定义的主机名是github,这里就是git push github util:util(实验1的分支命名)

reset回滚

1
2
3
4
5
6
#回滚到某次提交,哈希值到当前暂存区的代码修改历史都会被“隐去”,存入暂存区
git reset --soft HEAD~ #最近一次的提交
git reset --soft <commit-hash>

####注意一个破坏性的操作
git reset --hard <commit-hash> #该命令会将工作区、暂存区重置为哈希值指定环境,所有未提交操作都会丢失,哈希值后面的版本都被清除

rebase变基

rebase变基是一个很强大的命令:

场景1:和merge功能比较:

例如假设主分支是A->B->C->D,我们之前在C分支pull了代码,产生我们自己的分支C->E->F,现在我们想将F分支合并到D分支,

使用merge的效果就是在分支D执行merge F,处理代码冲突,产生的历史是A->B->C->D->M,其中C->E->F->M作为支线也会存在与merge的历史记录中,使用命令git log --oneline(每个历史一行) --graph(图形) --decorate(关联标签)可以直观看到主分支和合并分支记录,这种方法很好地保留了修改和merge的历史记录;

merge最大的特点是保留了大量的分支记录,使得commit记录不直观。

而如果使用rebase,则在分支F上执行rebase D,则产生的历史就是A->B->C->D->E'->F',E'和F'是E和F分别解决与D的冲突而来的新哈希值版本,这种方法使得提交记录更加线性,没有那么多合并记录(尽管有时候不是好的优点)。

场景2:融合上次版本;

这也是我第一次rebase用到的场景,首先我们有提交历史A->B,现在我又提交了C,现在我希望将BC两个版本合并,使得一次的提交更加完整,可以采用:

1
git rebase -i A  #B的父版本
此时会出现vim格式的文档,每个哈希前面都有字母,这时候能做的事情就很多,我们可以对A后面某一个版本进行操作:
1
2
3
4
5
6
7
p, pick = 使用提交
r, reword = 使用提交,但修改提交说明
e, edit = 使用提交,但停止以便进行提交修补
s, squash = 使用提交,但和前一个版本融合
f, fixup = 类似于 "squash",但丢弃提交说明日志
x, exec = 使用 shell 运行命令(此行剩余部分)
d, drop = 删除提交
这里我们将C版本前面的pick修改成s,就能融合历史;

stash存储

commit是作为版本镜像提供给git的,另一种方法是stash暂存修改,如果其他分支想用也可以恢复这个修改:

1
git stash  
这时候stash就存入了一个栈,这个栈存放了修改过的文件记录,可以进行查看,注意这个命令会清空所有修改,只能使用apply重新应用。
1
git stash list
栈是后进先出的结构,因此0编号对应的是最新的stash,如果想删除某项记录,就是:
1
git stash drop stash@{x}
清空暂存记录(请谨慎):
1
git stash clear
这时即使没有commit也能切换到其他分支了,如果其他分支需要某项修改:
1
git stash apply stash@{x}

其他命令

  1. 查看当前是哪个分支:

    1
    git branch

  2. 查看追踪的文件、修改文件:

    1
    git status

  3. 查看两次commit版本差异并且push到远程分支

有时候我们希望当前代码(或者新的commit)和某次commit的版本差异,这两个版本之间可能还间隔了多个版本,可以通过补丁之间呈现其差异。假设我们现在处于最新版本的new分支,首先将旧版本应用到一个测试分支:

1
git checkout -b test [Hash]
这个命令将创建test分支,并且应用某次commit的哈希值,并切换到test分支。

1
git diff test..pgtbl>changes.patch

这个代码会将diff差异重定向到patch文件,使用vim、cat可以查看具体的源码差异;如果只是关心哪些文件有改变,可以使用

1
diffstat changes.patch

如果有需求将这两次版本放在相邻的commit,可以参考以下方法:

首先将补丁文件应用到当前的旧版本分支:

1
2
git apply --check changes.patch  #不会修改当前分支,会进行文件检查是否匹配,没有输出代表ok
git apply changes.patch #会更新分支
这样两个版本就直接体现到commit上了:
1
2
git status  #查看更改的文件是否符合预期
git commit -m "Your Description"

如果这两个版本的commit记录是关心的,而其之间的版本是不关心的,可以更新到远程分支,但是此时进行push test是会失败的,因为一般远程分支和现在分支是不匹配的,这时候有两个方法:

如果确定远程分支的commit记录并不是所需要的,可以使用force参数直接覆盖:

1
git push --force <主机名> <本地分支>:<远程分支>
注意这个命令会覆盖远程分支的所有commit记录;

如果需要兼顾远程分支和本地分支的commit记录,可以使用git pull拉取远程分支,如果两个分支没有冲突(例如没有改变,或者commit是可融合的),git pull会将他们融合;如果分支存在冲突,就需要编辑产生冲突的文件再进行add和重新commit。

  1. 一种安全的提交:git revert
    1
    2
    git revert <commit-hash>
    git revert HEAD #撤销前一次更改
    git revert是一种安全地撤回更改的方式。例如现在有提交历史A->B->C,使用git revert C的后果是,现在会新建版本D,版本C到D变化就是B到C的变化的相反,因此效果和直接reset到A->B是一致的,但是这种方式安全地保留了版本C,在push远程仓库这种场合更加安全和利于协作者。

Q&A

git push/pull/clone 命令运行有时候错误,让git走某软件代理端口,global代表该代理会加入全局的.git文件(Windows下的user/用户名/.gitconfig,Linux下/home/用户名/.gitconfig,可以使用locate .gitconfig全局查找)

1
2
3
4
5
6
git config --global http.proxy http://127.0.0.1:监听端口
git config --global https.proxy http://127.0.0.1:监听端口

#取消代理
git config --global --unset http.proxy
git config --global --unset https.proxy

参考博客:
1. http://t.csdnimg.cn/eR2Y5
2. https://zhuanlan.zhihu.com/p/504164986
3. http://t.csdnimg.cn/Tq7DT