Cで作るコンテナもどき #2


はじめに

この記事は、FUNアドベントカレンダー2020の22日目の記事になります。

昨日はともかさんの学内限定初心者に優しいハッカソンFunLocksを運営した話でした。
今年はpart1とpart2それぞれ記事を書いているのですが、2つともともかさんの次になります。なんででしょうね。不思議。

環境

  • debian buster
  • kernel 4.19.0

さくらのクラウド上で実行しています。

今回で扱う範囲

namespace,chroot,cgroupを実装します。
今回、cgroupではcpuの制御しか行いません。

namespaceの切り分け

コンテナを作成するために、namespaceの切り分けを行う必要があります。これに対応したシステムコールはunshareとなります。
似たような機能を持つシステムコールにcloneがあります。
せっかくですので、ここでunshareとcloneの違いを簡単にまとめてからどちらを使うか考えましょう。

unshare

現在のプロセスに対して名前空間共有の制御を行う。

引数

  • int flags
    与えられたflagに対応した資源を共有しません。 つまり、何も指定しないとすべて共有されるってことかな。
返り値 : int

成功時0, 失敗時-1

clone

子プロセスの作成と同時に名前空間共有制御を行う。
fork + unshareが混ざったイメージ

引数
  • unsigned long flags
    与えられたflagに対応した資源を共有します
  • void *child_stack
    割愛
  • void *ptid
    割愛
  • void *ctid
    割愛
  • struct pt_regs *regs
    割愛

なんかいっぱいありますね。
unshareとは違い子プロセスも作成するので、子プロセスの使用するメモリやその他諸々が必要になります。(めんどくさくて調べていないので詳しくは各自で)

返り値 : long

子プロセスのスレッドidが返されます。失敗時-1、子プロセスは作成されません。

結局どっち使うの?

すこし長くなってしまいましたが、今回はunshareを使用したいと思います。
理由としては以下となります。

  • 引数と返り値がわかりやすい。
  • cloneの説明にスレッドに使うことが多いみたいなこと書いてある。

実装してみる  

#define _GNU_SOURCE
#include<sched.h>
#include<unistd.h>
#include<stdio.h>
#include<errno.h>

int main(){
        const unsigned int UNSHARE_FLAGS = ( CLONE_FILES | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWPID);
        if (unshare(UNSHARE_FLAGS) < 0){
                perror("unshare");
        }
        printf("ok\n");
        return 0;
}

で名前空間を分離することができるとおもいます。
unshareの実行しかしておらず、エラーが出なかったことだけしか確認していないので、この段階ではちゃんと動いたかわかりません。

flagの説明は割愛します。UNSHAREに書いてあるので各自見てみてください。

rootの変更

namespaceを切り分けたあとはrootを変更してあげます。
システムコールのchrootを使います。

chroot

ルートディレクトリの変更。
引数で指定したpathが以降で'/'として扱われます。

引数

  • const char* path
    変更先のパス

返り値

成功時0, エラーの場合-1

実装してみる

#include<unistd.h>
#include<errno.h>
#include<stdio.h>

int main(){
        char *argv[3];
        argv[0] = "/";
        argv[1] = NULL;

        if( chroot("./test") < 0 ){
                perror("chroot");
                return 1;
        }
        if ( chdir("./test") < 0 ){
                perror("chdir");
                return 1;
        }
        if ( execve("/bin/ls", argv, NULL) < 0){
                perror("execve");
                return 1;
        }
        printf("ok\n");
        return 0;
}

testというディレクトリを作成し、そこにchrootを行います。
その後/bin/lsを実行するコードを書きました。
testは空のディレクトリなので、もちろん、/bin/lsなんてないのでうまくいきません。
これでchrootがうまく機能しているというのが確認できます。(賢い確認方法ではない)
ちなみに、ホストマシンのものではないdebianのrootディレクトリを用意して、chrootした場合は正常に動きました。

リソースの制限

コンテナとして成り立たせるためにはリソースの制限を行う必要があります。例えば、メモリの使用量だとか、CPU利用の制限とか、いろいろです。  
リソースの制限に関しては、システムコールがあるわけではなく、cgroupというLinuxの機能があります。
cgroupはv1とv2がありますが、今回はv2を使用します。
cgroup v2の細かい説明や仕様については長くなるのでここでは説明しません。
興味あるかたはぜひこちらをご覧になってください。

cgroupを用いたリソースの制限

cgroupを使えるようにするには以下の手順を踏む必要があります。
1. cgroupfsのマウント
2. cgroupの作成(ディレクトリの作成)
3. cgroup.procsへ制御するプロセスIDの書き込み
4. サブシステムの登
5. サブシステムへの制限

実装してみる

今回僕の環境では/sys/fs/cgroupにすでにマウントされていたので行いません。


#include<sys/mount.h>
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>
#include<stdio.h>

int main(){
        //make cgroup
        if( access("/sys/fs/cgroup/container", F_OK) < 0){
                if( mkdir("/sys/fs/cgroup/container", 0644) < 0){
                        perror("mkdir");
                        return -1;
                }
        }
        //set pid
        int fd;
        fd = open("/sys/fs/cgroup/container/cgroup.procs", O_WRONLY);
        if( fd < 0 ){
                perror("cgroup open");
                return -1;
        }
        int _pid = getpid();
        char buff[6];
        snprintf(buff, 6 , "%d", _pid);
        write(fd, buff, 6);
        close(fd);

        //set subsystem
        fd = open("/sys/fs/cgroup/container/cgroup.subtree_control", O_WRONLY);
        if( fd < 0 ){
                perror("subsystem open");
                return -1;
        }
        write(fd, "+cpu", 5);
        close(fd);

        //set cpu max
        fd = open("/sys/fs/cgroup/container/cpu.max", O_WRONLY);
        if( fd < 0 ){
                perror("cpu open");
                return -1;
        }
        write(fd, "10000", 6);
        close(fd);
        while(1){
                fd = open("/dev/null", O_WRONLY);
                write(fd,"hello world\n", 12);
                close(fd);
        }
}

現在のプロセスに対して、CPU最大10%までの制限をかけて、"hello world"を/dev/nullに無限に出力するものです。
(yesコマンドみたいな?)
これを実行した状態でプロセスのCPU使用率を確認してみましょう。

おおよそ10%に固定されていることがわかります。
多分上手く動いてますね。

補足

        //set cpu max
        fd = open("/sys/fs/cgroup/container/cpu.max", O_WRONLY);
        if( fd < 0 ){
                perror("cpu open");
                return -1;
        }
        write(fd, "10000", 6);
        close(fd);c

writeで"10000"とありますが、これはCPU時間のことです。今回、CPU時間の最大が100000なので1/10の"10000"となっています。
これでCPU使用率の設定ができます。

おわり

今回はnamespace,chroot,cgroupを実装しました。
コードの質問や、挙動が違う部分、間違ってる部分あればTwitterまでお願いします。
次回はいつになるかわかりませんが、capabilityを実装しようと思います。

参考にしたところ

Cで作るコンテナもどき #1
LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術
Man page of UNSHARE
Man page of CLONE
Man page of CHROOT