服务器 \ linux \ 一个仅有1KB大小的Docker容器

一个仅有1KB大小的Docker容器

总点击20
简介:不,这不是打错字,也不是玩笑。我创建了一个Docker容器,该容器包含一个Unix可执行文件,没有其他依赖关系,磁盘空间占用不足1KB。容器中没有其他文件,甚至没有libc。

不,这不是打错字,也不是玩笑。我创建了一个Docker容器,该容器包含一个Unix可执行文件,没有其他依赖关系,磁盘空间占用不足1KB。容器中没有其他文件,甚至没有libc。

这就是证明。

为什么?

在解释如何实现这个容器之前,应该先解释下为什么要这么做。 caddy-docker(这是我写的另外一个工具,在这里有详细的说明)将传入的请求根据其标签路由到其他运行的容器中去。我需要用caddy-docker作为特定主机的反向代理,要实现这个最简单的方法就是启动一个容器,这个容器唯一的目的就是包含两个特殊的标签,并且容器在停止之前不应该做任何事情。就在那个时候我想到了这个绝妙透顶的主意。

什么?

我立即开始研究这个应用程序,并因为其不寻常的目的而命名为“hang”。 Go可以轻松生成没有依赖关系的可执行文件,允许Docker容器从scratch继承。唯一的缺点是,Go可执行文件相当的大,大小通常会超过8MB。

这绝对不行。

我认为,编写一个这样的C程序很容易:为SIGTERM注册一个信号处理程序,并在收到这个信号的时候退出。不幸的是,这意味着我需要使用libc,这样,容器很快会变得与Go可执行文件差不多大小。这根本就没有任何优势。

汇编?

是的,生成一个没有依赖关系的小型可执行文件的最快方法是用汇编编写。我更喜欢Intel风格的语法,所以,NASM是不二选择。

系统调用

曾几何时,在x86架构出现的早些年间,系统调用看起来是这样的:

mov eax,0x01

mov ebx,0x00

int 0x80

第一行指定要执行哪个系统调用 - sys_exit。第二行指定返回值(0)。第三行产生一个内核后续会处理的中断。

x86操作系统后来转为使用sysenter/sysret,而x86_64则引入了一个新的操作码:syscall。与上面的例子类似,rax寄存器用于指定要调用的特定系统调用。上面的示例可以在x86_64程序集中进行重写,如下所示:

mov rax,0x3c

mov rdi,0x00

syscall

请注意,sys_exit的系统调用号在x86_64上是不同的。

信号处理程序

在C中注册信号处理程序很普通:

#include <signal.h>

void handler(int param) {}

int main() {

struct sigaction sa;

sa.sa_handler = handler;

sigaction(SIGTERM,&sa,0);

return 0;

}

不幸的是,C标准库隐瞒了以下这几件事情:

标志SA_RESTORER添加到了sa.sa_flags上sa.sa_restorer成员上设置了一个特殊的函数

我们不能直接将C代码转换成汇编,因为sigaction的结构与sys_rt_sigaction所期望的不一致。以下是NASM中内核结构的样子:

struc sigaction

.sa_handler resq 1

.sa_flags resq 1

.sa_restorer resq 1

.sa_mask resq 1

endstruc

每个成员的大小为8字节。

设置信号处理程序

首先,我们必须在.bss段中为该结构体分配空间:

section .bss

act resb sigaction_size

请注意,sigaction_size是汇编程序为我们创建的特殊值 - 它等于sigaction的大小(以字节为单位)。然后可以在.text段中初始化该结构体,如下所示:

section .text

global _start

lea rax,[handler]

mov [act + sigaction.sa_handler],rax

mov [act + sigaction.sa_flags],dword 0x04000000 ; SA_RESTORER

lea rax,[restorer]

mov [act + sigaction.sa_restorer],rax

handler和restorer这两个标签我们稍后会提到。现在我们可以调用sys_rt_sigaction这个系统调用了:

mov rax,0x0d ; sys_rt_sigaction

mov rdi,0x0f ; SIGTERM

lea rsi,[act]

mov rdx,0x00

mov r10,0x08

syscall

处理信号

下一步是等待SIGTERM信号的到来。 sys_pause这个系统调用可以用下面这种方式轻松地实现:

mov rax,0x22 ; sys_pause

syscall

处理程序本身很普通,它没有做任何事情:

handler:

ret

恢复器(restorer)也很简单,虽然它需要调用sys_rt_sigreturn系统调用:

restorer:

mov rax,0x0f ; sys_rt_sigreturn

syscall

构建

需要两个命令来构建应用程序。假定源文件名为hang.asm,则命令是:

nasm -f elf64 hang.asm

ld -s -o hang hang.o

这将产生一个名为hang的可执行文件,它很小:

$ stat hang

File: hang

Size: 736

是的,它只有736字节。

Dockerfile相当简单,只需要两个命令:

FROM scratch

ADD hang /usr/bin/hang

ENTRYPOINT ["/usr/bin/hang"]

测试

我们来看看容器是否能工作:

$ docker build -t nathanosman/hang .

$ docker run -d --name hang nathanosman/hang

此时,容器应该保持运行状态:

$ docker ps -a

CONTAINER ID IMAGE COMMAND STATUS

f1861f628ea8 nathanosman/hang "/usr/bin/hang" Up 3 seconds

当执行docker stop时,应该立即停止:

$ docker stop hang

hang

有用!我们来确认以下容器的大小是否和可执行文件的大小一致:

$ docker images

REPOSITORY TAG CREATED SIZE

nathanosman/hang latest 2 minutes ago 736B

是的!一个非常小的容器!

意见反馈 常见问题 官方微信 返回顶部