TCP ソケットと `SO_REUSEPORT` オプションに関する問題を解決するために Linux カーネル v5.14 から取り込まれる予定のパッチセットについて 2 回に分けて解説します。 - https://lore.kernel.org/bpf/20210612123224.12525-1-kuniyu@amazon.co.jp/ - https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=1f26622b791b6a1b346d1dfd9d04450e20af0f41 Part 1 では `SO_REUSEPORT` オプション、カーネルの挙動と問題点、パッチセットの効果について解説し、 Part 2 ではカーネルの実装と修正方法、追加した eBPF の機能について解説します。 ## SO_REUSEPORT とは 従来、 Linux では一つの IP アドレス・ポート番号の組に対する TCP コネクションは一つのソケットのみで待ち受けることができました。トラフィックの多いサーバーでは単一のソケットに対する `accept()` がボトルネックになるため、 [v3.9 から SO_REUSEPORT というオプションが導入されました](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=c617f398edd4db2b8567a28e899a88f8f574798d)。 `SO_REUSEPORT` を有効した複数のソケットは同一の IP アドレス・ポート番号への通信を待ち受けることが可能なため、複数のプロセスから異なるソケットに対して `accept()` を呼び出せるようになり、ボトルネックが解消されました。 まずは簡単な Python のスクリプトを実行して `SO_REUSEPORT` の効果を確認してみましょう。 ### クイズ 1 以下のスクリプト (`quiz1.py`) を実行した時、何が出力されるでしょうか。少しだけスクロールを止めて考えてみてください。 ```python3:quiz1.py import socket ADDR = '127.0.0.1' PORT = 8000 def get_server(): s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) s.bind((ADDR, PORT)) s.listen(32) return s def get_client(): c = socket.socket() c.connect((ADDR, PORT)) return c def main(): server_1 = get_server() server_2 = get_server() client = get_client() client.send(b'Hello World') server_2.settimeout(3) child = server_2.accept()[0] print(child.recv(1024)) if __name__ == '__main__': main() ``` #### 正解 何度かスクリプトを実行すると、おおよそ同じ頻度で 2 つの出力が確認できます。 `client` からのコネクションを `server_2` から `accept()` できた場合は以下のように表示され、 ``` $ python3 quiz1.py b'Hello World' ``` `accept()` できない場合はタイムアウトが発生して以下のように出力されます。 ``` $ python3 quiz1.py Traceback (most recent call last): File "quiz1.py", line 30, in <module> main() File "quiz1.py", line 26, in main child = server_2.accept()[0] File "/usr/lib64/python3.7/socket.py", line 212, in accept fd, addr = self._accept() socket.timeout: timed out ``` ### クイズ 2 では以下のスクリプト (`quiz2.py`) を実行した時、何が出力されるでしょうか。今回は `server_2` を作成する前に `client` が `connect()` を呼び出しており、 `server_2` の作成後に `server_1` が `close()` されている点に注意してください。 ```python3:quiz2.py import socket ADDR = '127.0.0.1' PORT = 8000 def get_server(): s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) s.bind((ADDR, PORT)) s.listen(32) return s def get_client(): c = socket.socket() c.connect((ADDR, PORT)) return c def main(): server_1 = get_server() client = get_client() client.send(b'Hello World') server_2 = get_server() server_1.close() server_2.settimeout(3) child = server_2.accept()[0] print(child.recv(1024)) if __name__ == '__main__': main() ``` #### 正解 今度は何度実行しても必ずタイムアウトが発生します。 ``` $ python3 quiz2.py Traceback (most recent call last): File "quiz2.py", line 33, in <module> main() File "quiz2.py", line 29, in main child = server_2.accept()[0] File "/usr/lib64/python3.7/socket.py", line 212, in accept fd, addr = self._accept() socket.timeout: timed out ``` #### 不正解...? 決して以下のように出力されることはありません。 ``` $ python3 quiz2.py b'Hello World' ``` ### クイズ 3 最後のクイズです。以下のスクリプト (`quiz3.py`) を実行した際、何が出力されるでしょうか。 `drop_ack(True)` は 3-way handshake (3WHS) の最後の ACK パケットのみを破棄します。したがってクライアントの視点では 3WHS が即座に完了するものの、サーバーの視点では 3WHS を完了する前に `server_1` が `close()` される点に注意してください。 ```python3:quiz3.py import socket import subprocess ADDR = '127.0.0.1' PORT = 80 def get_server(): s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) s.bind((ADDR, PORT)) s.listen(32) return s def get_client(): c = socket.socket() c.connect((ADDR, PORT)) return c def drop_ack(add=True): subprocess.run( 'iptables -{} INPUT -d {} -p tcp --dport {} ' '--tcp-flags SYN,ACK ACK -j DROP'.format( 'A' if add else 'D', ADDR, PORT ).split(" ") ) def main(): server_1 = get_server() drop_ack(True) client = get_client() client.send(b'Hello World') server_2 = get_server() server_1.close() drop_ack(False) server_2.settimeout(3) child = server_2.accept()[0] print(child.recv(1024)) if __name__ == '__main__': main() ``` #### 正解 今回も必ずタイムアウトが発生します。 ``` $ sudo python3 quiz3.py Traceback (most recent call last): File "quiz3.py", line 48, in <module> main() File "quiz3.py", line 44, in main child = server_2.accept()[0] File "/usr/lib64/python3.7/socket.py", line 212, in accept fd, addr = self._accept() socket.timeout: timed out ``` #### 不正解...? クイズ 2 と同じく、このように出力されることはありません。 ``` $ sudo python3 quiz3.py b'Hello World' ``` ## カーネルの挙動と問題点 クイズ 1 では `accept()` に成功・失敗する 2 通りの結果が得られました。これよりカーネルは `SO_REUSEPORT` が有効なソケットに対してコネクションをランダムに割り当てることが分かります。 `SO_REUSEPORT` が有効なソケットは `listen()` した時点でコネクションを割り当てる対象になり、 `close()` (または `shutdown()`) するとその対象から外れます。このおかげで単純にプロセスを増減させれば、簡単にサーバーをスケールさせることができます。また設定変更などが生じた際は、新しいプロセスを起動して古いプロセスを停止することで、簡単にリロードを行えます。 その一方、[オプション導入当時の LWN.net の記事](https://lwn.net/Articles/542629/)からも分かるように、コネクションを特定のソケットに割り当てる挙動によって問題が発生することが知られています。この問題は 2 つに分類でき、クイズ 2 と 3 でそれぞれを簡単にシミュレートしています。そしてそのいずれもがサーバーのリロードは実際は単純に古いソケットを `close()` するだけでは上手くいかないことを示しています。 ### accept() できるコネクション クイズ 2 では `connect()` を呼び出した時点でサーバーのソケットは `server_1` のみが存在するため、カーネルはコネクションを `server_1` に割り当てます。しかし、 `server_1` を `close()` すると RST が送信されてこのコネクションは破棄されてしまいます。 ``` $ sudo tcpdump -i lo -nn -t ... IP 127.0.0.1.55042 > 127.0.0.1.8000: Flags [S], seq 2468297767, win 65495, options [mss 65495,sackOK,TS val 2200745635 ecr 0,nop,wscale 7], length 0 IP 127.0.0.1.8000 > 127.0.0.1.55042: Flags [S.], seq 2618403070, ack 2468297768, win 65483, options [mss 65495,sackOK,TS val 2200745635 ecr 2200745635,nop,wscale 7], length 0 IP 127.0.0.1.55042 > 127.0.0.1.8000: Flags [.], ack 1, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 0 IP 127.0.0.1.55042 > 127.0.0.1.8000: Flags [P.], seq 1:12, ack 1, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 11 IP 127.0.0.1.8000 > 127.0.0.1.55042: Flags [.], ack 12, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 0 IP 127.0.0.1.8000 > 127.0.0.1.55042: Flags [R.], seq 1, ack 12, win 512, options [nop,nop,TS val 2200745635 ecr 2200745635], length 0 ``` ある程度ソケットプログラミングに馴染みのあるユーザーであればこの点を不思議に思うことはないかもしれません。詳しくは Part 2 で説明しますが、カーネルは 3WHS が完了したコネクションをサーバーのソケットの accept キューに入れます。そのソケットを `close()` すると、それに伴って accept キューも破棄されるため、 accept キューに入ったコネクションも同様に破棄されます。 この問題を回避するには、サーバーのソケットを `close()` する前に、失敗するまで `accept()` を繰り返し実行する必要があります。いわゆる Connection Draining です。 ### accept() できないコネクション クイズ 3 でも `connect()` を呼び出した時点ではサーバーのソケットは `server_1` のみが存在し、最終的には `server_1` の `close()` によって RST が送信されます。クイズ 2 と違うのは 3WHS が完了する前に `server_1` が `close()` される点です。 ``` $ sudo tcpdump -i lo -nn -t ... IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [S], seq 2794889430, win 65495, options [mss 65495,sackOK,TS val 2200808677 ecr 0,nop,wscale 7], length 0 IP 127.0.0.1.8000 > 127.0.0.1.55044: Flags [S.], seq 3423506731, ack 2794889431, win 65483, options [mss 65495,sackOK,TS val 2200808677 ecr 2200808677,nop,wscale 7], length 0 IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [.], ack 1, win 512, options [nop,nop,TS val 2200808677 ecr 2200808677], length 0 IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [P.], seq 1:12, ack 1, win 512, options [nop,nop,TS val 2200808677 ecr 2200808677], length 11 IP 127.0.0.1.55044 > 127.0.0.1.8000: Flags [P.], seq 1:12, ack 1, win 512, options [nop,nop,TS val 2200808882 ecr 2200808677], length 11 IP 127.0.0.1.8000 > 127.0.0.1.55044: Flags [R], seq 3423506732, win 0, length 0 ``` クイズ 2 から `close()` の前に `accept()` を繰り返し実行する必要があると学びました。しかし、実際に試してみると上手くいかないことがわかるでしょう。 `server_1` を `close()` していた時点では 3WHS が完了しておらず、 accept キューにコネクションがないためです。 これより、カーネルは SYN を受信した時点でコネクションを特定のソケットに割り当てていることが分かります。基本的にこの挙動はユーザースペースで認知できないため、 3WHS を完了していないコネクションはユーザーが知らないうちに破棄される可能性があります。 [v4.6 で追加された eBPF の機能](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=fd1914b2901badd942f008ce57bf4a938d29fde4)を利用すればこの問題は回避できます。これも詳しくは Part 2 で扱いますが、 eBPF のプログラムを `SO_REUSEPORT` が有効なソケットにアタッチすると、コネクションを割り当てるソケットを選択できるようになります。逆に eBPF で特定のソケットにコネクションを割り当てないようにすることもできます。したがって、以下のように eBPF を利用して Connection Draining を実装することでこの問題を回避できます。 - eBPF でコネクションを割り当てる際にカウンタをインクリメントする - eBPF でコネクションを割り当てないようにする - カウンタの数まで `accept()` を繰り返す、または、全てのタイマーが切れるまで `accept()` を繰り返す ## 解決策 クイズ 2 と 3 ではいずれも `server_1` を `close()` する時点で `server_2` が存在します。同じ IP アドレスとポート番号へのコネクションを扱える `server_2` であれば破棄されるコネクションを `accept()` できるはずです。つまり、 `server_1` に一度割り当てられたコネクションを `server_2` に移植できれば問題は解決します。 同様の議論は過去にも[メーリングリスト](https://lore.kernel.org/netdev/1443313848-751-1-git-send-email-tolga.ceylan@gmail.com/)で行われていました。[ちょうどこの時期に eBPF で Connection Draining が可能になった](https://lore.kernel.org/netdev/1450492683.8474.123.camel@edumazet-glaptop2.roam.corp.google.com/)こともあり、 [TCP](https://lore.kernel.org/netdev/1458830744.10868.72.camel@edumazet-glaptop3.roam.corp.google.com/) と [eBPF](https://lore.kernel.org/netdev/20160325162114.GA72479@ast-mbp.thefacebook.com/) で改修の難しさが割りに合わないため、 [eBPF による Connection Draining が推奨されていました](https://lore.kernel.org/netdev/1458925242.6473.41.camel@edumazet-glaptop3.roam.corp.google.com/)。 ただし Connection Draining にも欠点があり、セキュリティなどの理由から即座にプロセスを置き換えたい場合に問題になります。例えば Connection Draining した HTTP リクエストが WebSocket に Upgrade した場合、終了したいプロセスが想定以上に長く動作し、結局プロセスを強制終了してコネクションを破棄する必要が出てくるかもしれません。もし Connection Draining を行わずに新しいプロセスに移植できていれば、このコネクションは破棄されなかったかもしれません。コネクションを移植したところで同じ問題が起きる可能性はありますが、 Connection Draining で破棄されるコネクションを減らすことはできるでしょう。 ### net.ipv4.tcp_migrate_req さて、いよいよパッチセットのお話です。 v5.14 からは新しい sysctl ノブ `net.ipv4.tcp_migrate_req` が追加されます。これを有効にするとカーネルが本来コネクションを破棄していたタイミングで、同じ IP アドレスとポート番号への通信を待ち受けるソケットをランダムに選択してコネクションを移植します。つまり、クイズ 2 と 3 で*真に*正しい結果を取得できるようになります。 ``` $ sudo sysctl -w net.ipv4.tcp_migrate_req=1 net.ipv4.tcp_migrate_req = 1 $ python3 quiz2.py b'Hello World' $ sudo python3 quiz3.py b'Hello World' ``` 一見素晴らしい機能ですが、もちろん注意すべき点があります。それが `net.ipv4.tcp_migrate_req` のデフォルト値を `0` にした理由です。 最終的に `accept()` で取得するソケット (クイズ中の `child`) で使用できる機能は、コネクションが最初に割り当てられたサーバーのソケットに依存するため、そこに差異が生じると思わぬエラーが発生するかもしれません。例えばサーバーのソケットに `setsockopt()` で `TCP_SAVE_SYN` を設定すると、割り当てられたコネクションの SYN のデータをカーネルが内部的に保持します。保持されたデータは `accept()` したソケットに対して `getsockopt()` で `TCP_SAVED_SYN` を指定することで取得できます。もし新しいプロセスでのみ `TCP_SAVE_SYN` が有効になっていた場合、古いプロセスから移植されてきたコネクションに対して `TCP_SAVED_SYN` でデータの取得を試みるも失敗するでしょう。 このように移植元と移植先のソケットで `setsockopt()` レベルの差異がある場合や、そもそも違うプロセス (nginx と squid など) を同じポートで動作させて eBPF でコネクションを振り分けている場合などは、[別途追加した eBPF の機能](https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=d5e4ddaeb6ab2c3c7fbb7b247a6d34bb0b18d87e)で移植先を選択するようにしてください。詳しくは Part 2 で扱います。 ## その他の回避策 最後に Connection Draining 以外の二つの回避策を紹介します。 1 つ目は Unix ドメインソケットで `SCM_RIGHTS` を使用する fd passing です。サーバーのソケットをプロセス間で引き継げばそもそも `close()` を呼び出すことはありません。ただし[ユースケース次第でそのロジックは複雑になる](https://lore.kernel.org/bpf/20210428012734.cbzie3ihf6fbx5kp@kafai-mbp.dhcp.thefacebook.com/)でしょう。 2 つ目が最も簡単な方法です。お気づきかも知れませんが、この問題の発生頻度はそこまで高くない上に、問題が発生する時間もとても短いです。 RST を受信するとクライアント側で `connect()` が失敗しますが、リトライで容易に問題は回避できます。実際、クライアント側が堅牢に実装されていれば私はこの問題に気づくことはなかったでしょう。 ## おわりに Part 1 では eBPF が必要となる特殊なユースケースを除いて、ユーザースペースで知っておくべき内容は概ね解説できたと思います。実際に使ってみてもしバグを発見した場合は `netdev@vger.kernel.org` と `bpf@vger.kernel.org` を CC に入れて私にメールを送って頂ければ幸いです。
Linux カーネルを開発する際、コードを書き換える度に、ビルド、インストール、再起動、テスト、というのはなかなか面倒です。この手間を出来るだけ省くために、私は [QEMU](https://www.qemu.org/) を利用しています。この記事では私がカーネル開発で使用する環境を構築する方法について紹介します。 ## 環境 普段は EC2 上で開発を行っています。特にディストリビューションにこだわりは無いのでなんとなく Amazon Linux 2 を選んでいます。 OS: Amazon Linux 2 ## 手順 ホームディレクトリ (~/) 配下で作業を行なっているので、必要に応じて書き換えてください。 ``` $ WORKDIR=~ ``` ### 1. カーネルのダウンロード git でカーネルのソースコードをダウンロードします。各サブシステムの最新のカーネルは [git.kernel.org](https://git.kernel.org/) からダウンロードできます。私はネットワークサブシステムの開発を行うために、 [netdev の net-next](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/) を選びました。特に決まったものがなければ [linux-next](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/) を選ぶと良いでしょう。 ``` $ cd ${WORKDIR} $ sudo yum install -y git $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git ``` ### 2. カーネルのビルド QEMU で x86_64 をエミュレートするので、 x86_64 向けのデフォルトの設定を `.config` に書き込んでビルドしています。 `make menuconfig` で設定を書き換えることも可能ですが、その場合は `ncurses-devel` をインストールする必要があります。 ``` $ sudo yum install -y gcc flex bison elfutils-libelf-devel openssl-devel $ cd net-next $ make x86_64_defconfig $ make -j $(nproc) ``` ### 3. Buildroot のダウンロード QEMU でカーネルを起動する際、ルートファイルシステムが必要になります。基本的なコマンド群を含むルートファイルシステムを簡単に作成できるので、 [Buildroot](https://buildroot.org/) を使用します。 ``` $ cd ${WORKDIR} $ git clone git://git.buildroot.net/buildroot ``` ### 4. Buildroot のビルド ビルドするコマンドは以下の通りです。 `make menuconfig` でアーキテクチャや必要なコマンドを選択します。ビルドにはそこそこ時間がかかるので洗濯機を回して、 YouTube で期間限定で公開されている好きなアーティストの[ライブ映像](https://www.youtube.com/watch?v=YU-RGLuC-Ho)を流しながら、たまったお皿洗いでもすると良いかもしれません。 ``` $ sudo yum install -y ncurses-devel gcc-c++ patch perl-Data-Dumper perl-ExtUtils-MakeMaker perl-Thread-Queue $ cd buildroot $ make menuconfig $ make -j $(nproc) ``` QEMU で x86_64 をエミュレートするので、アーキテクチャは x86_64 を選択します。読み書き出来ればファイルシステムはなんでもいいと思いますが、昔から Ubuntu をよく使っていたので ext4 を選びました。 Btrfs の開発をしたい方は btrfs を選びましょう。 - Target options - Target Architecture - x86_64 を選択 - Filesystem images - ext2/3/4 root filesystem にチェックを入れる - ext2/3/4 variant - ext4 を選択 複数セッションでコマンドを実行したい場面は良くあると思うので、 openssh は選択した方が良いでしょう。その他には curl, iptables, nginx, tcpdump, python3 を選択しました。 - Toolchain - C library - glibc を選択 - Install glibc utilities にチェックを入れる - Target packages - Interpreter languages and scripting - python3 にチェックを入れる - core python3 modules - ssl にチェックを入れる - Libraries - Crypto - openssl support - ssl library - openssl を選択 - openssl - openssl binary にチェックを入れる - openssl additional engines にチェックを入れる - Networking - libcurl にチェックを入れる - curl binary にチェックを入れる - SSL/TLS library to use - OpenSSL を選択 - Networking applications - iptables にチェックを入れる - nginx にチェックを入れる - ngx_http_ssl_module にチェックを入れる - openssh にチェックを入れる - tcpdump にチェックを入れる ### 5. DHCP の設定 外部向けの通信をするためには IP アドレスの設定が必要になります。 QEMU でカーネルを起動する時に `-netdev` オプションで特に何も指定しない場合、 QEMU のビルトイン DHCP サーバーを利用できます。([参考](https://wiki.archlinux.org/index.php/QEMU#User-mode_networking)) Buildroot で作成したルートファイルシステム (`${WORKDIR}/buildroot/output/images/rootfs.ext4`) 内の、 `/etc/network/interfaces` に DHCP の設定があります。 ``` # interface file auto-generated by buildroot auto lo iface lo inet loopback ``` デフォルトではループバックインターフェイス (lo) のみ設定されており、このままでは外部宛の通信ができないので、 eth0 に IP アドレスが割り当てられるように以下の 2 行を書き加える必要があります。 ``` auto eth0 iface eth0 inet dhcp ``` 以下のコマンドで Buildroot で作成したルートファイルシステムをマウントして設定を追記します。 ``` $ sudo mkdir /mnt/buildroot $ sudo mount -o loop ${WORKDIR}/buildroot/output/images/rootfs.ext4 /mnt/buildroot $ echo -e '\nauto eth0\niface eth0 inet dhcp' | sudo tee -a /mnt/buildroot/etc/network/interfaces ``` ### 6. SSH の設定 Buildroot ではデフォルトユーザーが root で、パスワードも設定されていません。手元の OS であればちゃんと別のユーザーで公開鍵認証の設定をするところですが、 QEMU 上のテスト環境なのでパスワードなしで root にログインできるようにします。そのために `/etc/ssh/sshd_config` で以下の設定に書き換えます。 ``` PermitRootLogin yes PermitEmptyPasswords yes ``` 既にマウントしているファイルシステム内の `/etc/ssh/sshd_config` を書き換えます。(かっこつけて sed コマンドを使用していますが、初めは emacs で書き換えてました。) ``` $ sudo sed -i -e 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' \ -e 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' \ /mnt/buildroot/etc/ssh/sshd_config $ sudo umount /mnt/buildroot ``` ### 7. QEMU でカーネルを起動 QEMU をインストールして、ちょっと長いコマンドでカーネルを起動します。カーネルのツリーや、ファイルシステムに応じて少しコマンドが変わるので、適宜変更してください。このコマンドではホストの 10022 番ポートを QEMU で起動したカーネルの 22 番ポートへフォワードするようになっています。 ``` $ sudo yum install -y qemu $ qemu-system-x86_64 -boot c -m 2048M \ -kernel ${WORKDIR}/net-next/arch/x86/boot/bzImage \ -hda ${WORKDIR}/buildroot/output/images/rootfs.ext4 \ -append "root=/dev/sda rw console=ttyS0,115200 nokaslr" \ -serial stdio -display none \ -nic user,hostfwd=tcp::10022-:22 ``` ユーザー名は root でパスワードなしでログインできます。 ``` Welcome to Buildroot buildroot login: root # uname -r 5.8.0-rc1 ``` 別のターミナルを開いて SSH でもログインできます。 ``` $ ssh root@localhost -p 10022 ... # curl -kI https://kuniyu.jp HTTP/1.1 200 OK ... ``` 終了するには QEMU を起動したターミナルで Ctrl+C を押してください。 ``` # (Ctrl+C を押す) qemu-system-x86_64: terminating on signal 2 ``` ### 8. Alias の設定 QEMU のコマンドは当然覚えられませんし、毎回コピペして実行するのも面倒です。なのでエイリアスを登録して短いコマンドで実行出来るようにしています。私は `net-next` コマンドでカーネルを起動できるようにしています。 ``` $ echo "alias net-next='qemu-system-x86_64 -boot c -m 2048M -kernel ${WORKDIR}/net-next/arch/x86/boot/bzImage -hda ${WORKDIR}/buildroot/output/images/rootfs.ext4 -append \"root=/dev/sda rw console=ttyS0,115200 nokaslr\" -serial stdio -display none -nic user,hostfwd=tcp::10022-:22'" >> ~/.bashrc $ source ~/.bashrc ``` ## おわりに カーネル開発を始めた半年ちょっと前、コードを書き換えてはビルドして、、、と行なっていたのですが、 GRUB の設定を間違えたり、間違ったコードを書いて EC2 がお釈迦になることが多々ありました。自分と同じようにカーネルに興味を持った方が同じ悲しみを味合わないよう、少しでも参考になれば幸いです。