live555は単一スレッドで動作しながらも、多数のクライアント接続を並列に処理できるネットワークミドルウェアである。その柔軟なスケジューリング性能を支えるのが、ソケット記述子とコールバック処理を管理する独自の循環双方向リンクリスト実装である。本稿では、このデータ構造の内部設計とイベント駆動時の動作フローを技術的に解説する。
循環双方向リンクリストの概要
各ノードが前後のノードへのポインタを保持する双方向リンクリストにおいて、末尾ノードの次ノードが先頭を、先頭ノードの前ノードが末尾を指すように接続された構造を指す。live555では、この構造を用いてオープン済みのソケットとそのI/O状態監視情報を格納・管理している。ノードの登録は主に先頭追加(Head Insertion)方式で実行される。
核心クラスの設計
リストの管理は3つのクラスに分割されており、スケジューラー内部で密接に連携する。
1. ノード記述クラス(HandlerDescriptor)
ソケットの識別情報と実行コンテキストを格納するデータ構造体である。
class HandlerDescriptor {
public:
HandlerDescriptor(HandlerDescriptor* successor);
virtual ~HandlerDescriptor();
// 公開メタデータ
int fd; // ソケットファイル記述子
int eventMask; // 監視イベントマスク(読み取り可能/書き込み可能/例外)
TaskScheduler::IOHandlerProc* callbackFunc; // イベント発生時のハンドラ
void* contextPtr; // ハンドラへ渡すユーザーデータ
private:
friend class HandlerSet;
friend class HandlerIterator;
HandlerDescriptor* nextNode; // 次ノードポインタ
HandlerDescriptor* prevNode; // 前ノードポインタ
};
各ノードはファイル記述子、監視条件マスク、実行関数、およびその関数に渡すコンテキストポインタの4要素で構成される。
2. リスト管理クラス(HandlerSet)
リスト全体のライフサイクルと操作を制御するクラスである。
class HandlerSet {
public:
HandlerSet();
virtual ~HandlerSet();
// ノードの登録・更新
void registerIO(int fd, int mask, TaskScheduler::IOHandlerProc* func, void* ctx);
// ノードの解放
void unregisterIO(int fd);
// 記述子の入れ替え
void remapSocket(int oldFd, int newFd);
private:
HandlerDescriptor* locateNode(int fd);
friend class HandlerIterator;
HandlerDescriptor rootEntry; // ダミールートノード
};
登録、削除、検索、およびファイル記述子の更新機能を提供する。記述子が変更されても、コールバック関数やコンテキストデータはそのまま維持される設計となっている。
3. イテレータクラス(HandlerIterator)
リストを安全に走査するためのヘルパーである。
class HandlerIterator {
public:
HandlerIterator(HandlerSet& targetSet);
virtual ~HandlerIterator();
HandlerDescriptor* advance(); // 次のノードへ遷移
void reset(); // 先頭に戻す
private:
HandlerSet& managedSet;
HandlerDescriptor* currentPtr; // 現在指しているノード
};
advance() は内部的なポインタを進め、次のノードのアドレスを返す。走査開始地点に戻った場合は nullptr を返して終了を示す。
実行フローとイベント駆動
システム初期化時(スケジューラーインスタンス生成時)、ルートノードは自己参照する循環リストとして初期化される。
HandlerSet::HandlerSet() : rootEntry(&rootEntry) {
rootEntry.fd = -1; // 仮の記述子(実際には使用されない)
}
ソケットの作成後、監視対象の状態と処理関数をバインドしてリストに登録する。
// 登録処理の呼び出し例
environment().scheduler().bindIOHandling(
serverSocket,
READABLE | EXCEPTION,
onConnectionRequest,
this
);
void BasicScheduler::bindIOHandling(int fd, int mask, IOHandlerProc* proc, void* ctx) {
if (fd < 0) return;
// ... プラットフォーム依存の制限チェック ...
FD_CLR(static_cast<unsigned>(fd), &readSet);
FD_CLR(static_cast<unsigned>(fd), &writeSet);
FD_CLR(static_cast<unsigned>(fd), &exceptionSet);
if (mask == 0) {
registry.removeHandler(fd);
// 必要に応じて最大監視数を更新
} else {
registry.registerHandler(fd, mask, proc, ctx);
if (mask & READABLE) FD_SET(static_cast<unsigned>(fd), &readSet);
if (mask & WRITABLE) FD_SET(static_cast<unsigned>(fd), &writeSet);
if (mask & EXCEPTION) FD_SET(static_cast<unsigned>(fd), &exceptionSet);
}
}
メインループ内では、select() システムコールの返却結果に基づき、リストをイテレートして該当するハンドラを呼び出す。
HandlerDescriptor* node = nullptr;
while ((node = iterator.advance()) != nullptr) {
int currentFd = node->fd;
int matchedEvents = 0;
// select()の結果セットと内部監視セットとの比較
if (FD_ISSET(currentFd, &readyRead) && FD_ISSET(currentFd, &readSet))
matchedEvents |= READABLE;
if (FD_ISSET(currentFd, &readyWrite) && FD_ISSET(currentFd, &writeSet))
matchedEvents |= WRITABLE;
if (FD_ISSET(currentFd, &readyExcept) && FD_ISSET(currentFd, &exceptionSet))
matchedEvents |= EXCEPTION;
// マスク一致時にコールバック実行
if ((matchedEvents & node->eventMask) != 0 && node->callbackFunc != nullptr) {
lastProcessedFd = currentFd;
// コールバック内で再帰的なイベントループ呼び出しを防ぐための記録
(*node->callbackFunc)(node->contextPtr, matchedEvents);
break; // 1ステップ処理の終了
}
}
構造選定に関する考察
実際の運用フローを確認すると、リストの走査や登録・削除処理において「前向きポインタ」の実質的な利用頻度は低い。初期化コストやメンテナスルーラットの観点からは、単方向リンクリストでも要件を充足し得る。循環双方向構造を採用している背景には、将来の高度なスロット管理や並列スケジューリングへの拡張性を確保するため、という設計思想が窺える。
live555のイベントループにおけるデータ構造の役割と処理順序は、このようにソケット監視とコールバック実行を緊密に連携させることで構築されている。