手把手带你写个最精简的 docker
闪客
《Linux 源码趣读》作者,公号「无聊的闪客」作者。
49 人已学习
立即订阅
课程目录
已更新 3 讲/共 3 讲
手把手带你写个最精简的 docker
15
15
1.0x
00:00/00:00
登录|注册

02 | 请不要动我的文件(chroot)

闪客 · 手把手带你写个最精简的 docker
上一讲的最后我们提出了一个问题,就是如何才能让你的两个买家都以为自己独占了整个机器的文件系统或者说根目录呢?
最直观的一个冲突例子就是,其中一个人通过 shell 在根目录下添加了一个文件,另一个人的 shell 中查看根目录是可以看到这个文件的。
把这句人话翻译得上档次一点,就是说:在操作系统级别,文件系统是一个全局资源,被系统上运行的所有进程共享。
那我们现在的问题就很明确了,如何让两个进程不去共享同一个文件系统,而是有一定隔离性呢?
当然这个不共享并不是说通过加个文件锁,让不同进程无法同一时间访问同一个文件。这个不共享是说两个进程压根看到的就不是同一个文件系统的视图,也就根本不存在改不改同一个文件的说法,隔离得更彻底。
有一个简单的办法可以做到这一点,就是在主机的根目录中创建两个目录,分别给这两个用户使用,让他们以为自己的根目录就是你创建的这两个目录。
那我们的问题此时更加明确了,就是如何让某个进程的根目录 / 变成我们指定的一个位置,而不是全部进程都共享同一个根目录呢?

大名鼎鼎的 chroot

这个事情非常容易,如果 Linux 内核提供这样的支持,即在每个进程的结构中记录了根目录的位置,而不是所有进程都使用同一个全局变量,那这个事情才有解。
否则如果 Linux 内核不支持,你上层再怎么折腾也无法完美实现这一目标。
那 Linux 内核是否有支持这个能力呢?答案出乎你所料。
chroot 最早是在 1979 年引入到 Unix 系统中的,而 Linux 借鉴了大量的 Unix 设计,所以在 1991 年发布的第一个版本 Linux 0.01 中就支持了这个能力。
具体怎么支持的呢?和刚刚说的一样,就是在每个进程的结构体 task_struct 中用 root 字段记录了根目录的位置,这就做到不同进程可以不一样了。
struct task_struct {
...
struct m_inode * root;
...
};
为了修改这个字段的值,内核还提供了一个 chroot 的系统调用供上层用户调用,逻辑非常简单,就是把这个 root 字段修改为入参中传入的值。
int sys_chroot(const char * filename) {
struct m_inode * inode = namei(filename);
...
current->root = inode;
return (0);
}
我画张图方便你理解。
现在的 shell 和 glibc 库中也有对应的同名命令和库函数方便开发者调用,你可以分别 man 1 和 man 2 来查看详情。

使用 shell 更改根目录

man 1 chroot 先看一下 chroot 命令的用法。
红框标出的就是两个重点。总结来说,chroot 这条命令会执行你传入的 command 命令,同时改变进程的根目录。如果你没传具体的 command,就默认执行 $SHELL -i 命令,也就是启动一个新的 shell 进程。
别废话,直接创建一个空目录先尝试一下。
[shanke ~] # mkdir empty
[shanke ~] # chroot empty
chroot: failed to run command ‘/bin/bash’: No such file or directory
啊哦,报错了。不过这正说明我们的 chroot 生效了!因为在新的根目录下还什么都没有嘛,自然无法找到 /bin/bash 这个程序来执行 shell 进程了。
那简单,我们把主机上的 bash 拷贝过来,再次执行。
[shanke ~] # mkdir empty/bin
[shanke ~] # cp /bin/bash ./empty/bin
[shanke ~] # chroot empty
chroot: failed to run command ‘/bin/bash’: No such file or directory
诶?怎么还是报同样的错误?
这个需要点额外的知识了,不过别慌。我们先执行下 file 命令看看文件的具体类型。
[shanke ~] # file /bin/bash
/bin/bash: ELF 64-bit LSB executable ... dynamically linked ...
首先这是个可执行文件(ELF),同时可以看到这里有个 dynamically linked 表示动态链接,说明这个程序依赖了外部的动态链接库。
ELF 和动态链接的知识是 Linux 如何加载并执行一个程序的关键,是个庞大的知识点,如果你不了解的话,这里完全可以简单理解为 /bin/bash 这个程序的运行,需要有一些叫做动态链接库的文件存在才行,否则就运行不起来。
所以这个报错并不是 /bin/bash 不存在,而是它所依赖的动态链接库文件不存在。就好比你运行 java 程序的时候忘记把依赖的 jar 包导进来,导致了 Class Not Found 相关的错误差不多。
那现在我们的目标就变成了,找到这个程序所依赖的动态链接库有哪些。
有现成的命令,很简单,通过 ldd 命令可以查看到它所依赖的动态链接库。
[shanke ~] # ldd /bin/bash
libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007fd34c3fc000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fd34c1f8000)
libc.so.6 => /lib64/libc.so.6 (0x00007fd34be2a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd34c626000)
那解决上面的问题就很简单了,我们把这些 so 文件分别拷贝到我们的 empty 目录下同样的位置。
[shanke ~] # mkdir empty/lib64
[shanke ~] # cp /lib64/libtinfo.so.5 empty/lib64
[shanke ~] # cp /lib64/libdl.so.2 empty/lib64
[shanke ~] # cp /lib64/libc.so.6 empty/lib64
[shanke ~] # cp /lib64/ld-linux-x86-64.so.2 empty/lib64
新目录变成了这个样子
[shanke ~] # tree empty
empty
├── bin
│ └── bash
└── lib64
├── ld-linux-x86-64.so.2
├── libc.so.6
├── libdl.so.2
└── libtinfo.so.5
再次执行 chroot 命令,发现成功启动了一个新的 shell(命令提示符的前缀变成了 bash-4.2)!
[shanke ~] # chroot empty
bash-4.2# /bin/ls
bash: ls: command not found
刚想庆祝一下,发现在这个新的 shell 里执行下 ls 命令,又失败了。这回你知道该怎么解决了吧?你可以自己尝试一下,这里就不演示具体过程了。
解决好依赖之后,再次查看 ls 命令,顺带手也把 PATH 改了方便命令的执行,此时发现根目录已经变成了全新的样子!
bash-4.2# PATH=/bin
bash-4.2# ls
bin lib64

通过代码更改根目录

之前我们一直通过各种 shell 命令来达到我们的目标,但最终我们是要手写一个容器实现的程序,所以不妨从这个最简单的需求入手,开始写我们的代码吧。
我们使用 C 语言来实现刚刚的效果,即创建一个新的 shell 进程,并更改它的根目录。
我们先实现一个最简单的版本,功能仅仅是运行后会创建一个新的 shell 进程,其他的什么都不做(这里我们不考虑任何的异常捕捉、内存释放等,就先写一个最 low 版本的代码,突出重点)
源代码在仓库 demos/02-01-hello-shell.c 中。
#include <stdlib.h>
#include <sys/wait.h>
int child();
// 直接运行 ./a.out
int main(int argc, char *argv[]) {
// 创建新进程
int pid = clone(child, malloc(4096) + 4096, SIGCHLD, NULL);
// 等待子进程结束的信号
waitpid(pid, NULL, 0);
return 0;
}
int child() {
char *args[] = {"sh", NULL};
// 替换并执行指定的程序
execvp("/bin/sh", args);
return 1;
}
如果你稍微懂一点 C 语言,这段代码非常简单明了,就是用 clone 创建一个子进程,在子进程里通过 exec 替换为 /bin/sh 程序,仅此而已。
clone 和 exec 的原理又是一个庞大的知识点,但这里你只需要知道,它们结合起来的效果和我们在命令行里直接输入 /bin/sh 敲击回车一样,就是创建了一个新的进程并运行指定的程序。
直接编译执行一下,可以看到会弹出一个新的 sh 进程(还记得刚刚的命令提示符的前缀变成了 bash-4.2,现在这个是 sh-4.2,这也说明了 /bin/bash 和 /bin/sh 是不同的,但通通都是 shell 的实现,感兴趣可以下来研究下具体哪里不同)。
[shanke demos] # gcc 02-01-hello-shell.c
[shanke demos] # ./a.out
sh-4.2# ps -f
UID PID PPID C STIME TTY TIME CMD
root 8733 8731 0 14:44 pts/0 00:00:00 bash
root 8826 8733 0 14:44 pts/0 00:00:00 ./a.out
root 8827 8826 0 14:44 pts/0 00:00:00 sh
root 9658 8827 0 14:47 pts/0 00:00:00 ps -f
好了,接下来我们在这个代码的基础上,再多实现一步,把进程的根目录给改掉。
我们再顺带手做点事儿,把原来写死的 /bin/sh 改成通过命令行参数传入,这样我们可以灵活控制创建的新进程执行什么命令,或者使用哪一款具体的 shell。
源代码在仓库 demos/02-02-chroot.c 中。
#include <stdlib.h>
#include <sys/wait.h>
int child(void *argv);
// 直接运行 ./a.out empty /bin/bash
int main(int argc, char *argv[]) {
// 创建新进程
int pid = clone(child, malloc(4096) + 4096, SIGCHLD, argv);
// 等待子进程结束的信号
waitpid(pid, NULL, 0);
return 0;
}
int child(void *arg) {
char **argv = (char **)arg;
// 设置根目录 这里 argv[1] 就是 empty
chroot(argv[1]);
// 把当前目录设置为根目录
chdir("/");
// 替换并执行指定的程序,这里 argv[2] 就是 /bin/bash
execvp(argv[2], argv + 2);
return 1;
}
这里我们只多加了两行代码,分别是 chroot() 和 chdir() 方法。
chroot() 不用多说,和命令行的 chroot 一样,只不过它改变的是当前进程的根目录。
chdir() 的意思是改变当前进程的工作目录。配合 chroot() 一块就是把根目录改了,再把当前目录指向根目录的意思。
实际上命令行的 chroot 内部就包含着把工作目录一块修改的逻辑,不然我们当时执行完 chroot 之后,为什么会很自然地直接跳转到新的根目录下呢。
其实很多我们习以为常的效果,背后都隐藏着很多细节的处理,达到让使用者觉得很符合直觉的效果,往往并不是件容易的事儿。
通过 strace 来追踪 chroot 命令触发的系统调用可以看到这一点。
[shanke ~] # strace chroot empty
...
chroot("empty") = 0
...
chdir("/") = 0
...
好家伙,这顺序简直和我们代码实现的一模一样。
好了,回到主题,我们赶快执行下我们的新代码。
[shanke ~] # gcc 02-02-chroot.c
[shanke ~] # ./a.out empty /bin/bash
bash-4.2# /bin/ls
bin lib64
yes,没问题!我们终于用代码实现了 chroot 命令同样的效果!
虽然这么一大坨代码和一个简简单单的 chroot 命令效果一样,但有了这个根基,对我们接下来想要实现的更多功能,就方便多了。
好了,这一讲的代码开始变多了,你可以先消化一下。
现在我们回到最初的场景。你已经成功让两个人使用的 shell 分别指向了不同的根目录,他们之间对文件的操作就只在自己的小围墙里进行,再也影响不到对方了。
但此时你给他们创建的根目录基本上是空的,太简陋了,用户随便执行个命令就露馅了。
这可怎么办呢?难道说要把本机上的所有文件全都拷贝一份到他们俩各自的根目录下才能以假乱真么?
别急,我们下讲再一起来探索。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《手把手带你写个最精简的 docker》
立即购买
登录 后留言

精选留言

由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论
大纲
固定大纲
大名鼎鼎的 chroot
使用 shell 更改根目录
通过代码更改根目录
显示
设置
留言
收藏
1
沉浸
阅读
分享
手机端
快捷键
回顶部