数行のコードのLinuxコンテナ

KVMに関する前回の記事の続きとして、新しい翻訳を公開し、busyboxDockerイメージの実行例を使用してコンテナーがどのように機能するかを理解します。


コンテナに関するこの記事は、KVMに関する前回の記事の続きです独自の小さなコンテナでbusyboxDockerイメージを実行することにより、コンテナがどのように機能するかを正確に示したいと思います。



仮想マシンとは異なり、コンテナは非常にあいまいであいまいです。通常、コンテナと呼ばれるのは、ホストオペレーティングシステム内の分離された環境で一緒に出荷して実行できる、必要なすべての依存関係を備えたスタンドアロンのコードパッケージです。これが仮想マシンの説明だと思われる場合は、さらに深く掘り下げて、コンテナがどのように実装されているかを見てみましょう。



BusyBox Docker



私たちの主な目標は、Dockerの通常のbusyboxイメージを実行することですが、Dockerは使用しません。Dockerは、イメージのファイルシステムとしてbtrfs使用します。イメージをダウンロードして、ディレクトリに解凍してみましょう。



mkdir rootfs
docker export $(docker create busybox) | tar -C rootfs -xvf -


これで、busyboxイメージファイルシステムがrootfsフォルダーに解凍されましたもちろん、。/ rootfs / bin / sh実行して動作するシェルを取得することはできますが、プロセス、ファイル、またはネットワークインターフェイスのリストを見ると、OS全体にアクセスできることがわかります。



それでは、隔離された環境を作成してみましょう。



クローン



子プロセスがアクセスできるものを制御したいのでfork(2)の代わりにclone(2)を使用しますクローンはほぼ同じことを行いますが、(ホストと)共有するリソースを示すフラグを渡すことができます。 次のフラグが許可されます。







  • CLONE_NEWNET-分離されたネットワークデバイス
  • CLONE_NEWUTS-ホストとドメイン名(UNIXタイムシェアリングシステム)
  • CLONE_NEWIPC -IPCオブジェクト
  • CLONE_NEWPID-プロセス識別子(PID)
  • CLONE_NEWNS-マウントポイント(ファイルシステム)
  • CLONE_NEWUSER-ユーザーとグループ。


私たちの実験では、プロセス、IPC、ネットワーク、およびファイルシステムを分離しようとします。それでは始めましょう:



static char child_stack[1024 * 1024];

int child_main(void *arg) {
  printf("Hello from child! PID=%d\n", getpid());
  return 0;
}

int main(int argc, char *argv[]) {
  int flags =
      CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;
  int pid = clone(child_main, child_stack + sizeof(child_stack),
                  flags | SIGCHLD, argv + 1);
  if (pid < 0) {
    fprintf(stderr, "clone failed: %d\n", errno);
    return 1;
  }
  waitpid(pid, NULL, 0);
  return 0;
}


コードはスーパーユーザー権限で実行する必要があります。そうしないと、クローン作成が失敗します。



この実験では興味深い結果が得られます。子のPIDは1です。initプロセスには通常PID1があることをよく知っています。ただし、この場合、子プロセスは独自の分離されたプロセスリストを取得し、最初のプロセスになります。



ワーキングシェル



新しい環境を簡単に習得できるように、子プロセスでシェルを開始しましょう。dockerrunのような任意のコマンドを実行してみましょう



int child_main(void *arg) {
  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


/ bin / sh 引数を使用してアプリケーションを起動すると、コマンドを入力できる実際のシェルが開きます。この結果は、私たちが孤立について話したときに私たちがどれほど間違っていたかを証明しています。



# echo $$
1
# ps
  PID TTY          TIME CMD
 5998 pts/31   00:00:00 sudo
 5999 pts/31   00:00:00 main
 6001 pts/31   00:00:00 sh
 6004 pts/31   00:00:00 ps


ご覧のとおり、シェルプロセス自体のPIDは1ですが、実際には、メインOSの他のすべてのプロセスを表示してアクセスできます。その理由は、プロセスリストがprocfsから読み取られ、それがまだ継承されているためです。



したがって、procfsをアンマウントします



umount2("/proc", MNT_DETACH);




これで、procfsがマウントされていないため、シェルの起動時にpsmount、およびその他のコマンドが中断しますただし、これは親のprocfsリークよりも優れています。



Chroot



通常、chrootはルートディレクトリの作成に使用されますが、代替のpivot_rootを使用します。このシステム呼び出しは、現在のシステムルートをサブディレクトリに移動し、ルートに別のディレクトリを割り当てます。



int child_main(void *arg) {
  /* Unmount procfs */
  umount2("/proc", MNT_DETACH);
  /* Pivot root */
  mount("./rootfs", "./rootfs", "bind", MS_BIND | MS_REC, "");
  mkdir("./rootfs/oldrootfs", 0755);
  syscall(SYS_pivot_root, "./rootfs", "./rootfs/oldrootfs");
  chdir("/");
  umount2("/oldrootfs", MNT_DETACH);
  rmdir("/oldrootfs");
  /* Re-mount procfs */
  mount("proc", "/proc", "proc", 0, NULL);
  /* Run the process */
  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


tmpfs/ tmpにsysfs/ sys にマウントし、有効な/ devファイルシステムを作成することは理にかなっていますが、簡潔にするためにこの手順はスキップします。



これで、chrootを使用しているかのように、busyboxイメージのファイルのみが表示されます



/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var

/ # mount
/dev/sda2 on / type ext4 (rw,relatime,data=ordered)
proc on /proc type proc (rw,relatime)

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    4 root      0:00 ps

/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps ax


現時点では、コンテナはかなり孤立しているように見えますが、多分多すぎます。何もpingできず、ネットワークがまったく機能していないようです。



通信網



新しいネットワーク名前付けの作成はほんの始まりに過ぎませんでした。ネットワークインターフェイスを割り当て、パケットを正しく転送するように構成する必要があります。



br0インターフェイスがない場合は、手動で作成する必要があります(brctlはUbuntuのbridge-utilsパッケージの一部です)。



brctl addbr br0
ip addr add dev br0 172.16.0.100/24
ip link set br0 up
sudo iptables -A FORWARD -i wlp3s0  -o br0 -j ACCEPT
sudo iptables -A FORWARD -o wlp3s0 -i br0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/16 -j MASQUERADE


私の場合、wlp3s0がメインのWiFiネットワークインターフェイスであり、172.16.xxがコンテナのネットワークでした。



コンテナランチャーは、veth0とveth1のペアのインターフェイスを作成し、それらをbr0に関連付けて、コンテナ内にルーティングを設定する必要があります。main()



関数でクローンを作成する前に次のコマンドを実行します。



system("ip link add veth0 type veth peer name veth1");
system("ip link set veth0 up");
system("brctl addif br0 veth0");


clone()の呼び出しが終了したら、veth1を新しい子ネームスペースに追加します。



char ip_link_set[4096];
snprintf(ip_link_set, sizeof(ip_link_set) - 1, "ip link set veth1 netns %d",
         pid);
system(ip_link_set);


ここで、コンテナシェルでip linkを実行すると、ループバックインターフェイスとveth1 @xxxxインターフェイスが表示されます。しかし、ネットワークはまだ機能していません。コンテナに一意のホスト名を設定し、ルートを構成しましょう。



int child_main(void *arg) {

  ....

  sethostname("example", 7);
  system("ip link set veth1 up");

  char ip_addr_add[4096];
  snprintf(ip_addr_add, sizeof(ip_addr_add),
           "ip addr add 172.16.0.101/24 dev veth1");
  system(ip_addr_add);
  system("route add default gw 172.16.0.100 veth1");

  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


それがどのように見えるか見てみましょう:



/ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth1@if48: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether 72:0a:f0:91:d5:11 brd ff:ff:ff:ff:ff:ff

/ # hostname
example

/ # ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=57 time=27.161 ms
64 bytes from 1.1.1.1: seq=1 ttl=57 time=26.048 ms
64 bytes from 1.1.1.1: seq=2 ttl=57 time=26.980 ms
...


動作します!



結論



完全なソースコードはこちらから入手できますバグを見つけたり、提案があれば、コメントを残してください!



もちろん、Dockerはさらに多くのことを実行できます。しかし、Linuxカーネルに適したAPIがいくつあり、それらを使用してOSレベルの仮想化を実現するのがいかに簡単かは驚くべきことです。



あなたが記事を楽しんだことを望みます。あなたは上の著者のプロジェクトを見つけることができますGitHubのフォローTwitterにニュースに従ってください、などを経由してRSSフィード



All Articles