Rust 製 OS、 Redox OS に UNIX ドメインソケットを実装した
はじめに
お久しぶりです。isan です。
5月から Rust 製 OS である Redox OS の Summer of Code に参加させていただいています。
先日、そのプログラム中のプロジェクトの1つである UNIX ドメインソケット(UDS) の初期実装が終わりましたので、記事にさせていただきます。(この記事は公式ブログで公開されている RSoC 2025: Implementing Unix Domain Sockets の日本語版です。)
この記事の目的
この記事は、私が Summer of Code で行った UNIX ドメインソケット(UDS) 実装の経験を共有するとともに、Rust 製 OS である Redox OS への興味を持っていただくことを目的としています。
この記事では、次の流れで説明していきます。
- Redox OS とは
- UNIX ドメインソケットとは
- UNIX ドメインソケットを Redox OS の独自デザイン "Schemes and Resources" に適応させる
- Redox OS 上でのFD( ファイルディスクリプタ ) の受け渡し
sendmsg
とrecvmsg
の実装
Redox OS とは
まず、Redox OS について説明します。
Redox OS はオープンソースの Rust 製 OS です。
Redox OS は主に次のような特徴を持ちます。(公式サイトより https://www.redox-os.org/)
これらだけでなく、ネットワーク機能やエディタが存在していたりします。
また、システムコールが少ないです。(現在 35 個)(kernel の syscall ハンドラ より)
現在は Wayland の移植作業も行われており、来る 0.11 リリースの優先事項として捉えられているようです。そこで、今回 Wayland 移植の主要な TODO の一つである UDS の実装の一部を解決させていただきました。
UNIX ドメインソケット(UDS)とは
UNIX ドメインソケット (UDS) について説明します。
UDS とは、同じコンピュータ上のプロセス同士が通信するための手段の一つです。ソケットと聞くとネットワークソケットを思い浮かべるかもしれませんが、少し異なりソケットのアドレスに IP アドレスとポートを使用する代わりに、ファイルシステムのファイルを接続のアドレスとして利用します。
これにより、ネットワークスタックを経由する必要がないため、コンピュータ内部での通信に特化した高速なプロセス間通信が可能になります。
わかりやすいところだと Docker でしょうか?
エラーが起きた時に次のような表示がされることがありますよね、これは Docker が通信に UDS を使用している証です。
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock:
調べてみたところ、この docker.sock
は Docker API のエントリポイントとし利用されているようです。Docker cli はこのソケットを利用し、コマンドを実行しているようです。
シェルで次のコマンドを打ってみてください。
ls /var/run
docker.sock
を含め、いくつかのサービスが .sock
ファイルを使用し、UDS を利用していることがわかると思います。ぜひ、どのサービスがなんのために UDS を利用しているか調べてみてください。
少し UDS のことがわかったでしょうか?
これからは、Redox OS における UDS の実装について話していきます。
まず、UDS を Redox OS にどのように適応させたかを紹介します。
その後、UDS の最も重要な機能ひとつである FD( ファイルディスクリプタ ) の受け渡しについて深く掘り下げます。
そして、それを利用する sendmsg
関数と recvmsg
関数がどのように実装されているかを説明します。
また、その中で Redox OS の独自デザイン "Schemes and Resouces" や便利な SYS_CALL
インタフェースについても触れていきます。
では、始めましょう!
UNIX ドメインソケットを Redox OS に適応させる
ここでは、Redox の "Schemes and Resources" を紹介し、私が UDS をどのようにそれに適応させたか説明していきます。
"Schemes and Resouces"
"An essential design choice made for Redox is to refer to resources using scheme-rooted paths."
-- Schemes and Resources より
Redox OS には 「スキームとリソース」 という独自のデザインがあります。
スキーム
スキームは主に次のものを表します。
• リソースの種類
• リソースへのアクセス方法の印
• デーモンやドライバが提供する名前付きサービス
例えば、ファイルシステムを提供している RedoxFS は file
スキームを提供しており、ネットワーク機能を提供している smolned は ip
や tcp
、udp
スキームを提供しています。
リソース
リソースは、なんらかの「もの」です。物理デバイスであることも、ファイルやソケットであることもあります。
"/scheme/[scheme-name]/[resource-name]" というフォーマットの Scheme-rooted Path でアクセスされます。
Scheme-rooted Path の例:
- file: "/home/user/file.txt" -> "/scheme/file/home/user/file.txt"
- tcp: "127.0.0.1:8080" -> "/scheme/tcp/127.0.0.1/8080"
ちなみにシステムコールが少ない理由の一つがこれです。
スキームはシステムコールをどう解釈するかを変えることができます。
open の例:
file
:open("/scheme/file/home/user/file.txt", O_CREAT)
-> ファイルシステムが "/home/user/file.txt" にリンクしたファイルを開くtcp
:open("/scheme/tcp/127.0.0.1/8080", O_CREAT)
-> tcp が "127.0.0.1:8080" にバインドされたソケットを開くdup の例:
file
:dup(fd, b"")
-> FD を複製するtcp
:dup(fd, b"listen")
->accept
の動作をし、コネクションを受け入れたら、新しくソケットを発行する
uds
スキームを追加する
ipcd に 私は新しい uds
スキームを実装し、それを ipcd(プロセス間通信デーモン)内で処理することにしました。これは tcp
や udp
スキームの設計と似ており、uds
という新しい UDS を処理するための特別なサービスです。
これは、次のような利点があります。
- 明確さ: 通信ロジックを専用のデーモン(ipcd)内に閉じ込めることができます。
- 保守性: RedoxFS は優れたファイルシステムであることに集中でき、ipcd は優れた IPC を提供することに集中できます。
- 一貫性:
tcp
やudp
と同じパターンを踏襲し、開発者にとって一貫性のある予測可能なアーキテクチャを構築できます。
しかし、この実装だとソケットのパーミッションチェックが行えなくなってしまうため、 UDS の
bind
とconnect
処理のみ RedoxFS との統合を行っています。
libc のbind
とconnect
は、ソケットの名前にファイルシステムのパスを使用します。
libc::bind
内部の操作
ファイルシステムのパスを受け取り、ソケットへの名前付けとファイルシステム上でのソケットファイル作成を行います。このソケットファイルはソケットの FD とマッピングされ、RedoxFS 内にそのマッピングが保持されます。libc::connect
内部の操作
ファイルシステムのパスを受け取り、まず対象ソケットファイルの FD を開きます。この操作の際に RedoxFS によるパーミッションチェックが行われます。この FD を使用して、ソケットの実際のconnect
処理に使用することができるワンタイムトークンを発行します。最終的に、このワンタイムトークンを引数に接続元ソケットの FD を使用してconnect
を呼び出すことで、実際のconnect
処理が行われます。
FD( ファイルディスクリプタ ) 受け渡しの仕組み
UDSを使うと、プロセスはソケットを介して FD を送信できます。これにより、ファイルや他のソケットのようなリソースを共有できます。FD 受け渡しは、Wayland プロトコルなど、多くのアプリケーションで使用される強力な機能です。
ここでは、ロギングデーモンを例に FD 受け渡しについて説明し、Redox でどのように実現されるかを紹介します。
例: ロギングデーモン
2種類のプロセスを持つロギングシステムを想像してみましょう。
- ロギングデーモン(特権): このプロセスは高い権限で実行されます。システム上でメインのログファイル(例:
/var/log/app.log
)を開くことを許可された唯一のプロセスです。 - ワーカプロセス(非特権): これらは権限が制限された通常のアプリケーションプロセスです。ログを書き込むことはできますが、ログファイルを読み取ったり、
/var/log/
内の他のファイルにアクセスしたりすることはできません。
FD 受け渡しがないと、ログファイルを全ユーザ書き込み可能にしなければならないかもしれません。
しかし、それは大きなセキュリティリスクです。
FD受け渡しを使えば、安全に解決することができます。
- 特権を持つロギングデーモンが起動し、
/var/log/app.log
を開いて、そのFDを取得します。 - ログを書き込む必要があるワーカプロセスが、UDS を介してロギングデーモンに接続します。
- デーモンは FD 受け渡しを使い、ログファイルの FD をソケット経由でワーカに送信します。
- ワーカプロセスは新しい FD を受け取ります。この新しい FD は元のファイルパスへのアクセス権を与えませんが、同じ開かれたファイルを指します。
- これで、ワーカはこの受け取った FD を使ってログファイルに書き込むことができますが、
/var/log/app.log
やそのディレクトリ内の他のファイルを直接開く権限は依然としてありません。
Redox で FD 受け渡しを実現する方法
Redox では、FD 受け渡しは sendfd
システムコールと、dup
システムコールの特別な使い方(名前付き dup
による "recvfd
"操作)によって実現されます。
FD の目的は、使用するために開かれたリソースへの参照として機能することです。
FD は、カーネル内で維持されるプロセスのファイルディスクリプタテーブルへのインデックスであり、ターゲットリソースを識別するファイルディスクリプションを指します。
FD はそれを所有するプロセスのコンテキストでのみ意味をなすため、プロセス間で単に FD 番号を送信するだけでは、リソースへの参照を渡すという目標は達成できません。
カーネルがプロセス間で参照を移動させる処理に関与する必要があります。
その仕組みを以下に示します。
- 送信側プロセスは、ソケットと送信する FD を指定して
sendfd
システムコールを呼び出します。 - カーネルはシステムコールを受け取り、送信側プロセスの
FD
テーブルからそのFD
を削除します。 - カーネルはソケットを管理するスキームにリクエストを送信します。
- スキームはそのリクエストを処理し、リクエスト ID を使ってカーネルから対応する FD を取得します。
- 受信側プロセスは、"recvfd" 引数を指定してソケットに対して
dup
を呼び出します。 - スキームは、"recvfd" が指定されていることから FD 受け取り命令であることを認識し、FD を返却します。それと同時に、すでに開かれている FD を返却していることをカーネルに報告します。
- 受信側プロセスは、元の FD と同じ開かれたファイルを指す新しい FD をカーネルから取得します。
Rust のプログラムで書くと次のようになります。
// 送信側プロセス
let log_file_fd = open("/var/log/app.log", O_WRONLY | O_CREAT | O_APPEND);
let fd_to_send = dup(log_file_fd, b"");
syscall::sendfd(socket, fd_to_send);
// 受信側プロセス
let received_fd = syscall::dup(socket, b"recvfd");
// これで受信側プロセスは `received_fd` を使ってログファイルに書き込める
syscall::write(received_fd, b"Log message from worker\n");
syscall::close(received_fd);
この方法により、受信側プロセスは元のファイルパスを知る必要なく FD にアクセスできます。
余談ですが、UDS の
bind
操作は主にsendfd
を使用して行われています。
libc::bind
は内部でソケットファイルを作成するディレクトリを開き、ディレクトリの FD を取得します。その後ソケットの FD を複製し、その FD をディレクトリの FD を介してsendfd
を呼び出すことで、 RedoxFS に送っています。
RedoxFS は受けとった FD からファイル名やファイルモードを取得し、ソケットファイルを作成します。そしてソケットファイルの node と受け取った FD を内部でマッピングします。
sendmsg
と recvmsg
の SYS_CALL
インタフェースによる実装
前のセクションでは、FD 受け渡しの概念と sendfd
メカニズムを紹介しました。 ここでは、この種の高度な UDS 機能に不可欠な標準 C ライブラリ関数 sendmsg
と recvmsg
、そしてその実装についてさらに深く掘り下げます。ここでは Redox の SYS_CALL
インタフェースも紹介します。
sendmsg
と recvmsg
とは
Redox の標準 C ライブラリ(libc)の実装は "relibc" と呼ばれ、Linux と同じ API を提供する sendmsg
や recvmsg
のような関数が含まれています。
しかし、これらの関数の実装は、Redox の内部 API を利用する方法で行われています。
sendmsg
および recvmsg
関数を使用すると、プロセスはデータペイロードと制御情報(補助データ)の両方を含む構造化されたメッセージを交換できます。これらは以下の標準 C 構造体を使用します。
// https://man7.org/linux/man-pages/man2/send.2.html
struct msghdr {
void *msg_name; /* Optional address */
socklen_t msg_namelen; /* Size of address */
struct iovec *msg_iov; /* Scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* Ancillary data, see below */
size_t msg_controllen; /* Ancillary data buffer size */
int msg_flags; /* Flags (unused) */
};
// https://man7.org/linux/man-pages/man3/cmsg.3.html
struct cmsghdr {
size_t cmsg_len; /* Data byte count, including header
(type is socklen_t in POSIX) */
int cmsg_level; /* Originating protocol */
int cmsg_type; /* Protocol-specific type */
/* followed by
unsigned char cmsg_data[]; */
};
ここで重要なのは msg_control
フィールドです。これはファイルディスクリプタ(SCM_RIGHTS
)やプロセス資格情報のような補助データを運ぶために使用されます。
SYS_CALL
インタフェース
Redox OS は少数のシステムコールを持ちます。スキームに情報を送信したり、スキームから情報を受信したりするにはいくつかの方法があり、read
と write
システムコールはそのうちの 2 つです。
write
システムコールはスキームに情報を送信します。uds
スキームの場合、通常のsyscall::write(fd, data)
は、スキームによって管理されるソケットにデータペイロードを送信します。
read
システムコールはスキームから情報を受信します。uds
スキームの場合、通常のsyscall::read(fd, buf)
は、スキームによって管理されるソケットからデータペイロードを受信します。
スキームと通信するデータの種類が 1 つしかない場合は、通常の read
と write
呼び出しで十分です。
しかし、常にそうとは限りません。
sendmsg
と recvmsg
では、メッセージペイロードだけでなく補助データも処理する必要があるため、状況はより複雑になります。
この目的のために read
と write
を使用することもできますが、スキームはデータがメッセージペイロードなのか補助データなのかを区別できません。
これを解決するために、「名前付きdup
」アプローチを使用できます。特別なパス引数(例:"ancillary"
)を指定してdupを呼び出すと、この特別な用途のために指定されたソケットの新しい FD が発行されます。その後、この新しい FD に書き込みまたは読み取りを行うと、スキームはその操作が補助データ用であることを認識できます。
// 補助データの読み書き用に新しいfdを発行する
let special_fd = syscall::dup(socket, b"ancillary");
// ソケットに補助データを書き込む
syscall::write(special_fd, ancillary_data);
// ソケットから補助データを読み取る
syscall::read(special_fd, buf);
syscall::close(special_fd);
この「名前付きdup」アプローチは機能しますが、非効率です。たった 1 つの補助データを送信するだけで、少なくとも 3 つの別々のシステムコールが必要になり、大きなオーバーヘッドが発生します。これは、Redox OS の開発者の中で dup-write/read-close
パターンとして有名なようです。
これを解決するために導入されたのが SYS_CALL
インタフェースです。
SYS_CALL
インタフェースの定義を以下に示します。
pub fn sys_call(
fd: usize,
payload: &mut [u8],
flags: CallFlags,
metadata: &[u64],
) -> Result<usize>;
SYS_CALL
を使用すると、ペイロードとメタデータを 1 回のシステムコールで送信できます。payload
はメインデータの送受信に使用されます。metadata
パラメータは、呼び出しの種類を表したり、ペイロードとは異なる情報を与えるなど、さまざまな情報に使用できます。
さらに、SYS_CALL
インタフェースは msghdr
構造体全体を処理するのに十分な柔軟性があります。簡単なプロトコルを定義することで、送信元アドレス、iovec
、および制御データを、uds
スキームが簡単に解析できる単一の効率的なシステムコールと使用することができます。
sendmsg
と recvmsg
の実装方法
sendmsg
と recvmsg
を実装するために、私はメッセージベース通信のための簡単なプロトコルを設計しました。これらすべてがどのように組み合わさるかを見るために、ファイルディスクリプタの受け渡しを含む sendmsg
と recvmsg
呼び出しの処理を追ってみましょう。
こちらがそのプロトコルのデータフォーマットです。
msg データフォーマット:
[name_len(usize)][name]
[payload_len(usize)][payload_data]
[ancillary_datas]
cmsg データフォーマット:
[level(i32)][type(i32)][data_len(usize)][data]
sendmsg
関数の実装はシリアライザとして機能します。呼び出されると、アプリケーションから提供された msghdr
構造体の処理を開始します。
- 補助データ(
msg_control
)を反復処理します。SCM_RIGHTS
タイプの制御メッセージを見つけると、そのメッセージにリストされている各ファイルディスクリプタに対して内部的にsyscall::sendfd
を呼び出します。これにより、FD がuds
スキームに送信され、ソケットの FD キューに入れられます。 - 次に、メッセージのそれぞれのバイトストリーム表現を作成します。
SCM_RIGHTS
データの場合、FD 自体をシリアライズするのではなく、sendfd
で送信したFD
の数をシリアライズします。 - 最後に、ペイロードデータ、構造化された補助データ(FD の数など )を含むシリアライズされたストリーム全体が、単一の
SYS_CALL
でuds
スキームに送信されます。(送信側は名前を指定することがないので名前をは書き込みません。)
逆に、recvmsg
関数はデシリアライザとして機能します。
- 最初に
uds
スキームに対してSYS_CALL
を行い、完全なメッセージストリームを受信します。 - このストリームを解析し、
msghdr
のiovec
バッファにペイロードデータを格納します。 - 補助データ部分を解析し、
SCM_RIGHTS
の FD 数を見つけると、スキームのキューで待機しているファイルディスクリプタの数がわかります。その後、内部的にsyscall::dup(socket, b"recvfd")
を指定された回数呼び出し、Redoxの FD 受信メカニズムを使用してファイルディスクリプタを取得します。
メッセージの送受信には1回のシステムコールしかかからず、これは「名前付きdup
」アプローチよりもとても効率的です。
relibc は Redox のファイルディスクリプタ送信メカニズムのすべての複雑さを標準 C ライブラリ内にカプセル化します。
これで、アプリケーション開発者は、基盤となる sendfd
、dup
、およびSYS_CALL
の仕組みを気にすることなく、Redox 上で sendmsg
と recvmsg
を使用できるようになりました。
余談ですが、
SYS_CALL
はbind
やconnect
でも利用されています。
元々、それらはdup-dup2-close
によって実装されていました。(以前の実装)これもSYS_CALL
で metadata を同時に送ることで解決できようになりました。
SYS_CALL
は、非効率なdup-[syscall]-close
パターンを排除し、複数のシステムコールをたった 1 つに置き換える強力な機能です。
最後に
いかがでしたでしょうか? この記事では、私が Rust 製 OS、Redox OSで UNIX ドメインソケット(UDS) を実装した経験を共有しました。今回の実装は、Redox OS が将来的に Wayland をサポートするための重要なマイルストーンです。この記事を通じて、Redox OS の設計思想の面白さとその魅力が伝われば嬉しいです。
UNIX ドメインソケットの足りない機能を Issue としてたてたので、ご興味があればぜひコントリビュートをしてみてください。Missing Unix socket features.
Redox OS における IPC やスキームの作り方に関してはこちらが参考になると思います。
https://gitlab.redox-os.org/redox-os/base/-/blob/main/ipcd/src/chan.rs?ref_type=heads
ファイルディスクリプタに関しては、dup
などのシステムコールを眺めてみるとわかりやすと思います。
https://gitlab.redox-os.org/redox-os/kernel/-/blob/master/src/syscall/fs.rs?ref_type=heads#L224
現在、私は次のステップとして sendfd
システムコールの拡張とファイルディスクリプタテーブルの拡張に取り組んでいます。
現在の sendfd
が持つ、
- 一度に1つの FD しか送信できない非効率性
- FD をテーブルから削除する以外の選択肢がない点
といった課題を解決し、より柔軟でパワフルな機能を目指しています。
このプロジェクトが完了した際には、その成果をまた記事としてお届けする予定ですので、どうぞご期待ください。それでは、また次の記事でお会いしましょう!
Discussion