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

01 | 回到一切的起点(shell)

闪客·手把手带你写个最精简的 docker
你是不是以为第一章节要先介绍一下 docker 的发展史了?不,现在我需要你清空你的大脑,忘掉 docker,忘掉镜像,忘掉所有 namespace、cgroup 这些听过无数次但又说不清楚是什么的概念。跟着我一起进入一个沉浸式的场景中。
话不多说,我们开始吧~

从一个奸商开始做起

想象一下,你是一个卖云服务器的老板,现在有两个人找你来买,但你手里只有一台机器,怎么办呢?
最简单的办法,什么都不用做,直接给他们俩 root 账号和密码。他们通过 ssh 登录你的服务器,通过一个 shell 进程终端来操作服务器,都以为自己独占了这台机器。
两个用户和你自己都通过 shell 进程操作着主机,从进程视角看如下图所示。
这张图里的每一个红色的方框,都表示一个进程,并且箭头的指向代表进程之间的父子关系,它们形成了一个树状的结构。
具体说来,你的主机上运行着一个 sshd 的守护进程,每当一个用户通过 ssh 连接到主机时,这个 sshd 守护进程会创建一个 sshd 子进程,这个 sshd 子进程又会再创建一个 shell 子进程接收用户的指令。
我们可以通过 ps 命令查看主机上的这些进程信息(输出的列表中,第 2 列表示进程的 PID,第 3 列表示进程的 PPID,也就是父进程的 PID)
[shanke ~] # ps -ef | grep -v 'grep' | egrep 'zsh|ssh'
root 1287 1 0 Jul17 ? 00:00:02 /usr/sbin/sshd
root 16636 1287 0 18:10 ? 00:00:00 sshd: root@pts/0
root 16643 16636 0 18:10 pts/0 00:00:00 -zsh
root 16791 1287 0 18:10 ? 00:00:00 sshd: root@pts/1
root 16793 16791 0 18:10 pts/1 00:00:00 -zsh
root 16912 1287 0 18:10 ? 00:00:00 sshd: root@pts/2
root 16914 16912 0 18:10 pts/2 00:00:00 -zsh
可以看到所有的 shell 进程(我的电脑上用的 zsh 就是具体的一个 shell 进程实现,后面可能用 bash 那查看到的就是 bash 了无所谓,都是 shell 的实现)都分别有一个 sshd 的父进程,而所有的 sshd 进程都有一个共同的 sshd 父进程。这和我们刚刚画的图是一样的。
最终这个 sshd 进程也有一个父进程,是神秘的 1 号进程,我们在这里先留个悬念。

熟悉而又陌生的 shell

你仅仅用了两个 shell 进程,就让这两个用户都以为自己掌控了这台主机,你可真是个天才呢。
那 shell 进程是什么呢?你可能会说它不就是个黑窗口么。
没错,shell 就是用户与操作系统之间的接口,用于解释用户命令并执行相关的程序。
shell 进程是一个通用的说法,具体有最早的老祖宗 sh,以及现在最常用的 bash,还有定制化能力较强的 zsh 等。
通过 SHELL 变量可以查看你当前会话的默认 shell 是什么,通过 ps -p $ 可以查看当前正在使用的 shell 是什么。
[shanke ~] # echo $SHELL
/bin/zsh
[shanke ~] # ps -p $
PID TTY TIME CMD
16643 pts/0 00:00:00 zsh
通过 PS1 变量可以查看并修改命令提示符前面的内容。
[shanke ~] # echo $PS1
[%n@%m %1~] %#
[shanke ~] # PS1="[hehehehehehe]: "
[hehehehehehe]: PS1='[%m %1~] %#'
[shanke ~] #
通过这些我们可以直观感受到,shell 其实也是个普通的程序而已,只不过它是系统启动后第一个和用户直接打交道的进程,并拥有直接执行其他进程的功能,所以看起来比较特殊罢了。
说了这么多,不如直接看一下 shell 进程的源码。这里我选择了比较简单的 xv6 源码中的 sh 实现,它精简地把所有 shell 程序都遵循的核心逻辑写出来了。
int main(void) {
static char buf[100];
// 读取命令
while(getcmd(buf, sizeof(buf)) >= 0){
// 创建新进程
if(fork() == 0)
// 执行命令
runcmd(parsecmd(buf));
// 等待进程退出
wait();
}
}
void runcmd(struct cmd *cmd) {
...
struct execcmd ecmd = (struct execcmd*)cmd;
...
// 执行(替换为)用户传入的命令程序
exec(ecmd->argv[0], ecmd->argv);
...
}
没错,shell 程序就是个死循环,它永远不会自己退出,除非我们手动终止了这个 shell 进程。
在死循环里面,就是持续不断地读取(getcmd)用户输入的命令(比如说 ls),创建一个新的进程(fork),在新进程里执行(exec)这个命令,最后等待(wait)进程退出,再次进入读取下一条命令的循环中。
这里的 fork + exec 是经典的 Linux 创建新进程并执行指定程序的方式,其中的细节你可以先不用管。
好了,shell 的原理现在就很清晰了,我们回到刚刚的进程树下看看会发生什么变化。
此时你的两个用户分别在自己的 shell 里执行自己的命令,每执行一个命令就会创建一个新的子进程。那么进程树就会如下图所示。
现在我们来总结一下:在你的主机上有一堆有着父子关系的进程们,其中有一个 sshd 进程监听着用户的远程登录。每当有用户连接过来时就创建一个 shell 子进程和用户交互,shell 不断读取用户的命令并创建出新的进程。
可以说 Linux 上所有运行着的东西,都可以总结成这样一张进程树的图。

神秘的一号进程

既然 Linux 上所有运行着的东西其实就是一堆进程,那最开始是由谁来一步一步创造出这么多进程的呢?也就是说所有进程的初始发动机是什么?
还记得刚刚我们说的神秘的 1 号进程吗?我们尝试从它的身上寻找答案,通过 ps 命令看看主机上的所有进程信息。
你执行出来的效果可能是这样的
[shanke ~] # ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Jul17 ? 00:01:01 /usr/lib/systemd/systemd
也可能是这样的
(base) ➜ ~ ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 012下午 ?? 13:36.77 /sbin/launchd
前者是在我的一台 Linux 云主机上,后者是在我的 mac 电脑上。
我们以 Linux 为例,如果你继续查看所有的进程信息,你会发现这样一个规律。不论是哪个进程,它的父进程(PPID)一定要么是 1 号进程,要么它父进程的父进程 ... 最终也是 1 号进程(这里先不考虑 kthreadd 这个 2 号内核进程和它的子进程)。
这个 1 号进程就是 Linux 上的第一个进程,由最早的 Linux 版本中的 init 进程逐渐进化到现在的 systemd 进程,但本质上就还是第一个启动的一个普普通通的进程而已。
systemd 进程的细节比较多,但简单说其实就是扫描几个指定目录下的特定后缀名的文件,然后解析出里面要执行的程序,把它执行起来。Linux 上最初的一批由 systemd 启动的进程信息,都写在这些目录下。
比如刚刚我们看到的 sshd 进程,就写在了 /lib/systemd/system/sshd.service 这个文件中,可以看到这个文件中有一个 ExecStart 变量,后面就写着如何启动 sshd 服务。
[shanke ~] # cat /lib/systemd/system/sshd.service
[Unit]
Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.service
Wants=sshd-keygen.service
[Service]
Type=notify
EnvironmentFile=/etc/sysconfig/sshd
ExecStart=/usr/sbin/sshd -D $OPTIONS
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=42s
[Install]
WantedBy=multi-user.target
所以说 Linux 上原本就只有 systemd 这一个进程,只不过这个进程的作用就是创造出超级多的子进程,这些子进程中功能比较复杂的可能又会创造出自己的子进程。
这样一来 Linux 上就充斥着各种各样的进程,整个系统就被 systemd 给盘活了。
好了,现在我们把整个 Linux 的进程发动机找到了,我们再把图补充一下。
现在我们有底气了,整个 Linux 就是一堆运行着的进程,并且它们之间以树的关系绑定在一起。那么之后实现的任何功能,归根结底也逃不开这个最底层的逻辑体系。

文件系统好像也是这样

除了这个动态的进程树之外,还有个相对静态的文件系统,也是以树的形式组织起来的。这个就很符合我们的直觉了,甚至 Linux 上有个 tree 命令可以直接以树状结构来查看。
[shanke ~] # tree / -L 2
/
├── bin
├── boot
├── data
├── dev
│ ├── cpu
│ ├── mem
│ ├── random
│ └── zero
├── etc
│ ├── hosts
├── home
├── lib
├── lib64
├── mnt
├── opt
├── proc
│ ├── 1
│ ├── cgroups
│ ├── meminfo
│ ├── mounts
│ ├── sys
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var
这个 / 就表示根目录,是所有目录结构的起点位置,通常是通过读取某块硬盘里的数据,按照指定的文件系统格式(比如 ext4)解析,然后形成一个树状的目录结构。
而这个 / 根目录,就类似进程中的 1 号进程,是一切的起点。
你可能会说,我们不是要讲容器么,怎么一直在讲这些看似没什么关联的东西?而且这些不都是非常符合直觉的常识么。
别急,最后你会发现,容器被拆解之后的最深层,其实就是玩这些简单的不能再简单的功能而已。

奸商行为被发现

现在我们回到最初的需求,你正在用一台机器卖个两个人同时使用,并做到尽可能让他们以为自己独占了整个机器。那么现在做到了么?
自信点,你做到了!你通过这样的小把戏,的的确确把两个人骗了好长时间。
直到有一天,有一个用户通过 shell 命令在根目录上创建了一个文件,随便起了个名字。
[shanke ~] # cd /
[shanke /] # touch hehehe
[shanke /] # tree -L 1
/
├── bin
├── boot
├── data
...
├── hehehe
根目录下赫然出现了个 hehehe 文件。
有一天另一个用户无意中登录了自己的 shell,查看了根目录的文件,发现了居然有其他人也在用这台机器,于是找你算账。
这可怎么办呢?这时你开始思考,如何才能让他们都以为自己独占了整个机器呢?
我们下讲再一起来探索。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《手把手带你写个最精简的 docker》
立即购买
登录 后留言

精选留言

由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论
大纲
固定大纲
从一个奸商开始做起
熟悉而又陌生的 shell
神秘的一号进程
文件系统好像也是这样
奸商行为被发现
显示
设置
留言
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部
文章页面操作
MAC
windows
作用
esc
esc
退出沉浸式阅读
shift + f
f11
进入/退出沉浸式
command + ⬆️
home
滚动到页面顶部
command + ⬇️
end
滚动到页面底部
⬅️ (仅针对订阅)
⬅️ (仅针对订阅)
上一篇
➡️ (仅针对订阅)
➡️ (仅针对订阅)
下一篇
command + j
page up
向下滚动一屏
command + k
page down
向上滚动一屏
p
p
音频播放/暂停
j
j
向下滚动一点
k
k
向上滚动一点
空格
空格
向下滚动一屏
播放器操作
MAC
windows
作用
esc
esc
退出全屏
⬅️
⬅️
快退
➡️
➡️
快进
空格
空格
视频播放/暂停(视频全屏时生效)