🌌

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( ファイルディスクリプタ ) の受け渡し
  • sendmsgrecvmsg の実装

Redox OS とは

まず、Redox OS について説明します。
Redox OS はオープンソースの Rust 製 OS です。
Redox OS は主に次のような特徴を持ちます。(公式サイトより https://www.redox-os.org/)

  • マイクロカーネル設計(プロセスマネージャやドライバがユーザスペースで実行)
  • UNIX ライク
  • Rust 製の libc も開発(relibc)
  • GUI を持つ(Orbital)

これらだけでなく、ネットワーク機能やエディタが存在していたりします。
また、システムコールが少ないです。(現在 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 は iptcpudp スキームを提供しています。

リソース
リソースは、なんらかの「もの」です。物理デバイスであることも、ファイルやソケットであることもあります。
"/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 の動作をし、コネクションを受け入れたら、新しくソケットを発行する

ipcd に uds スキームを追加する

私は新しい uds スキームを実装し、それを ipcd(プロセス間通信デーモン)内で処理することにしました。これは tcpudp スキームの設計と似ており、uds という新しい UDS を処理するための特別なサービスです。

これは、次のような利点があります。

  • 明確さ: 通信ロジックを専用のデーモン(ipcd)内に閉じ込めることができます。
  • 保守性: RedoxFS は優れたファイルシステムであることに集中でき、ipcd は優れた IPC を提供することに集中できます。
  • 一貫性: tcpudp と同じパターンを踏襲し、開発者にとって一貫性のある予測可能なアーキテクチャを構築できます。

しかし、この実装だとソケットのパーミッションチェックが行えなくなってしまうため、 UDS の bindconnect 処理のみ RedoxFS との統合を行っています。
libc の bindconnect は、ソケットの名前にファイルシステムのパスを使用します。

  • 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 番号を送信するだけでは、リソースへの参照を渡すという目標は達成できません。
カーネルがプロセス間で参照を移動させる処理に関与する必要があります。

その仕組みを以下に示します。

  1. 送信側プロセスは、ソケットと送信する FD を指定して sendfd システムコールを呼び出します。
  2. カーネルはシステムコールを受け取り、送信側プロセスの FD テーブルからその FD を削除します。
  3. カーネルはソケットを管理するスキームにリクエストを送信します。
  4. スキームはそのリクエストを処理し、リクエスト ID を使ってカーネルから対応する FD を取得します。
  5. 受信側プロセスは、"recvfd" 引数を指定してソケットに対して dup を呼び出します。
  6. スキームは、"recvfd" が指定されていることから FD 受け取り命令であることを認識し、FD を返却します。それと同時に、すでに開かれている FD を返却していることをカーネルに報告します。
  7. 受信側プロセスは、元の 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 を内部でマッピングします。

sendmsgrecvmsgSYS_CALL インタフェースによる実装

前のセクションでは、FD 受け渡しの概念と sendfd メカニズムを紹介しました。 ここでは、この種の高度な UDS 機能に不可欠な標準 C ライブラリ関数 sendmsgrecvmsg 、そしてその実装についてさらに深く掘り下げます。ここでは Redox の SYS_CALL インタフェースも紹介します。

sendmsgrecvmsg とは

Redox の標準 C ライブラリ(libc)の実装は "relibc" と呼ばれ、Linux と同じ API を提供する sendmsgrecvmsg のような関数が含まれています。
しかし、これらの関数の実装は、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 は少数のシステムコールを持ちます。スキームに情報を送信したり、スキームから情報を受信したりするにはいくつかの方法があり、readwrite システムコールはそのうちの 2 つです。

write システムコールはスキームに情報を送信します。uds スキームの場合、通常のsyscall::write(fd, data) は、スキームによって管理されるソケットにデータペイロードを送信します。
read システムコールはスキームから情報を受信します。uds スキームの場合、通常のsyscall::read(fd, buf) は、スキームによって管理されるソケットからデータペイロードを受信します。

スキームと通信するデータの種類が 1 つしかない場合は、通常の readwrite 呼び出しで十分です。
しかし、常にそうとは限りません。
sendmsgrecvmsg では、メッセージペイロードだけでなく補助データも処理する必要があるため、状況はより複雑になります。
この目的のために readwrite を使用することもできますが、スキームはデータがメッセージペイロードなのか補助データなのかを区別できません。

これを解決するために、「名前付き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 スキームが簡単に解析できる単一の効率的なシステムコールと使用することができます。

sendmsgrecvmsg の実装方法

sendmsgrecvmsg を実装するために、私はメッセージベース通信のための簡単なプロトコルを設計しました。これらすべてがどのように組み合わさるかを見るために、ファイルディスクリプタの受け渡しを含む sendmsgrecvmsg 呼び出しの処理を追ってみましょう。
こちらがそのプロトコルのデータフォーマットです。

msg データフォーマット:
[name_len(usize)][name]
[payload_len(usize)][payload_data]
[ancillary_datas]

cmsg データフォーマット:
[level(i32)][type(i32)][data_len(usize)][data]

sendmsg 関数の実装はシリアライザとして機能します。呼び出されると、アプリケーションから提供された msghdr 構造体の処理を開始します。

  1. 補助データ(msg_control)を反復処理します。SCM_RIGHTS タイプの制御メッセージを見つけると、そのメッセージにリストされている各ファイルディスクリプタに対して内部的にsyscall::sendfd を呼び出します。これにより、FD が uds スキームに送信され、ソケットの FD キューに入れられます。
  2. 次に、メッセージのそれぞれのバイトストリーム表現を作成します。SCM_RIGHTS データの場合、FD 自体をシリアライズするのではなく、sendfd で送信した FD の数をシリアライズします。
  3. 最後に、ペイロードデータ、構造化された補助データ(FD の数など )を含むシリアライズされたストリーム全体が、単一の SYS_CALLuds スキームに送信されます。(送信側は名前を指定することがないので名前をは書き込みません。)

逆に、recvmsg 関数はデシリアライザとして機能します。

  1. 最初に uds スキームに対して SYS_CALL を行い、完全なメッセージストリームを受信します。
  2. このストリームを解析し、msghdriovec バッファにペイロードデータを格納します。
  3. 補助データ部分を解析し、SCM_RIGHTS の FD 数を見つけると、スキームのキューで待機しているファイルディスクリプタの数がわかります。その後、内部的に syscall::dup(socket, b"recvfd") を指定された回数呼び出し、Redoxの FD 受信メカニズムを使用してファイルディスクリプタを取得します。

メッセージの送受信には1回のシステムコールしかかからず、これは「名前付きdup」アプローチよりもとても効率的です。

relibc は Redox のファイルディスクリプタ送信メカニズムのすべての複雑さを標準 C ライブラリ内にカプセル化します。
これで、アプリケーション開発者は、基盤となる sendfddup、およびSYS_CALL の仕組みを気にすることなく、Redox 上で sendmsgrecvmsg を使用できるようになりました。

余談ですが、SYS_CALLbindconnect でも利用されています。
元々、それらは 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