🪅

Rubyのputsとpをシステムコールまで深掘りしてみる

に公開

はじめに

Rubyでデバッグや表示系で使われる'puts'と'p'、例えば下記のケースではhelloが返ってきますが、内部的にはどの様になっているのでしょうか?
今回は'puts'と'p'の深掘りを簡単に行います。

$ puts "hello"
hello
$ p "hello"
"hello"

結果としては両関数とも、writeシステムコールが呼ばれていましたが、その呼ばれ方が異なることがわかりました。
C言語で書かれているRubyですが、printfは使用されずに標準出力機能が実装されていることがわかりました。

マシンスペック

MacBook Air M2 arm64
Docker上でUbutnu24.04を立ち上げて実施

Macから手軽にubuntu仮想環境に入る方法

# Dockerfileを作成してビルド
docker build -t ruby-syscall - <<'EOF'
FROM ubuntu:24.04
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ruby-full gcc strace linux-tools-common && \
    rm -rf /var/lib/apt/lists/*
WORKDIR /ws
EOF

# コンテナに入る
echo 'puts "hello from puts"; p "hello from p"' > test.rb && \
docker run --privileged -it --rm -v "$PWD/test.rb":/ws/test.rb ruby-syscall

事前知識

システムコール

本記事ではシステムコールを取り扱います。システムコールとは、OSの機能を呼び出すための機構です。我々が普段使用している高級言語で標準出力に文字を出したり、ファイルのI/Oを制御する際にコードの深いところではシステムコールが呼び出されています。
簡単なイメージとしては下記のように表され、アプリケーションコードはカーネル空間にはアクセスせずに、システムコールを使用してOS, HWとやりとりをします。
厳密には言葉の定義が異なるかもしれませんが、インターフェースのような役割を持っています。

検証

straceで見てみる

単純にstraceで今回使用されているシステムコールとその回数を見てみます。

strace -e trace=write ruby test.rb 2>&1 | tee syscall.log | cat

write(1, "hello from puts\n\"hello from p\"\n", 31hello from puts
"hello from p"
) = 31
+++ exited with 0 +++

syscallのwrite(ARM64)

ここで、引数でwriteをトレースの対象として指定しました。ここの戻り値に'write(...)'とありますが、これはシステムコールのwriteを呼び出していることを示しています。
writeの実体はARM64であればksys_writeあたりでしょうか。
下記のように引数をとります。

引数 解説
fildes open()をコールして得られたファイル記述子。これは整数値である。それぞれ、標準入力、標準出力、標準エラーを表す値として、0、1、2を指定することもできる。
buf filedesが指すファイルに書き込まれる内容の文字配列を指す。
nbyte filedesが指すファイルに文字配列bufから書き込むバイト数を指定する。

https://en.wikipedia.org/wiki/Write_(system_call)

今回の戻り値は、標準出力にputspで入力した文字列が31バイトとして出力されることがわかります。

ソースコードを確認する

検証では、writeが呼ばれていることがわかりましたが、実際にはどうなのかをGitHubのrubyのソースコードから解析します。

puts

定義自体はこのファイルのL15792にありました。実体はこちらにありました。中身ではrb_io_writeが呼ばれており、rb_write_internalも呼ばれています。その先はinternal_write_funcとなり、writeシステムコールを呼んでいます。

p

定義自体はこちらにありました。実体はrb_f_pで、その中にrb_p_writeio_writevが呼ばれio_fwritev...となりinternal_writev_funcから、do_write_retryでwriteシステムコール呼ばれています。

C言語のprintfを使用していない

rubyはC言語で書かれていますが、C言語の標準出力を担うprintfが使用されておらず、writeシステムコールが呼ばれている作りとなっていました。
今回は説明を割愛しますが、printfも同様にwriteシステムコールが使用されています。

まとめ

今回はrubyの'puts'と'p'の中身を覗いてみました。それぞれは同様に標準出力で使用しますがcallされる時の経路が異なることがわかりました。
また、最終的にはwriteシステムコールが呼ばれていることがわかりました。

Discussion