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

## はじめに
この記事は、[FUNアドベントカレンダー2020](https://adventar.org/calendars/5150)の22日目の記事になります。

昨日はともかさんの[学内限定初心者に優しいハッカソンFunLocksを運営した話](https://mokomoka.hateblo.jp/entry/fun-part-advent-calendar-2020)でした。
今年はpart1とpart2それぞれ記事を書いているのですが、2つともともかさんの次になります。なんででしょうね。不思議。

## 環境
- debian buster
- kernel 4.19.0

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

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

## namespaceの切り分け
コンテナを作成するために、namespaceの切り分けを行う必要があります。これに対応したシステムコールは[unshare](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/unshare.2.html
)となります。
似たような機能を持つシステムコールに[clone](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/clone.2.html)があります。 
せっかくですので、ここで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の説明にスレッドに使うことが多いみたいなこと書いてある。


### 実装してみる　　
```c
#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](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/unshare.2.html)に書いてあるので各自見てみてください。

##rootの変更
namespaceを切り分けたあとはrootを変更してあげます。
システムコールの[chroot](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/chroot.2.html)を使います。

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

#### 引数  

- const char* path  
変更先のパス

#### 返り値
成功時0, エラーの場合-1

### 実装してみる
```c
#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の細かい説明や仕様については長くなるのでここでは説明しません。
興味あるかたはぜひ[こちら](https://gihyo.jp/admin/serial/01/linux_containers/0037?page=1)をご覧になってください。

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

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

```c

#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使用率を確認してみましょう。
![screen-capture-_online-video-cutter.com_-_3_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179700/720f6c70-8e9c-53bd-6c86-64b3b652bf06.gif)
おおよそ10%に固定されていることがわかります。
多分上手く動いてますね。

#### 補足
```c
        //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](https://twitter.com/materialofmouse)までお願いします。
次回はいつになるかわかりませんが、capabilityを実装しようと思います。

## 参考にしたところ
[Cで作るコンテナもどき　#1](https://qiita.com/materialofmouse/items/213b6ca24a33b62f15a7)
[LXCで学ぶコンテナ入門 －軽量仮想化環境を実現する技術](https://gihyo.jp/admin/serial/01/linux_containers)
[Man page of UNSHARE](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/unshare.2.html)
[Man page of CLONE](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/clone.2.html)
[Man page of CHROOT](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/chroot.2.html)