UNIXからLinuxまでのI/O抽象化を理解する
ファイルシステム、special file、file descriptor、event loopを一つの流れで整理する
Copyright © 2026 LWP 山中 一弘
本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。
記事要約
UNIXのI/O抽象化を考えるとき、最初に混乱しやすいのは「ファイルシステムはブロックデバイスの上にあるのに、そのブロックデバイスが /dev/rk0 のようにファイルシステム上の名前で見える」という点です。素直に読むと、ファイルシステムを読むためにファイルシステムが必要であるかのように見えます。
この記事では、この見かけ上の循環を、UNIX V6のブートストラップ順序、special file、major/minor番号、pathname lookup、実I/Oの分離という観点から整理します。そのうえで、古典UNIXの「everything is a file」が、BSD socket以後の「file descriptorによる統一」へ移り、さらにLinuxでeventfd、timerfd、signalfd、pidfd、epoll、io_uringのような非ファイル的対象までfdとして扱う方向へ広がった流れを説明します。
中心にあるのは、「すべてが通常ファイルである」という話ではありません。名前で対象に到達し、権限を確認し、種別に応じて操作をdispatchし、必要な場合はfdという能力ハンドルとして受け渡す、というOSの抽象化の流れです。
本記事の対象とゴール
想定読者
UNIXやLinuxのI/Oを、断片的な用語ではなく一つの設計の流れとして理解したい方。
/dev/null、/dev/rk0、ブロックデバイス、inode、file descriptorの関係で混乱したことがある方。
select、poll、epoll、eventfd、io_uringなどが、なぜファイルディスクリプタと関係するのかを整理したい方。
「everything is a file」という言葉を、歴史的にも実装的にも少し正確に理解したい方。
本記事で得られること
UNIX V6で、ファイルシステムとブロックデバイスが循環していない理由を説明できるようになります。
/dev/rk0 のようなspecial fileが、通常ファイルではなくデバイスへの名前付き入口であることを理解できます。
UNIXからLinuxにかけて、抽象化の中心がpathname/inodeからfile descriptorへ広がっていく流れを整理できます。
現代Linuxのevent loopや非同期I/Oを、古典UNIXの設計思想の延長と変化の両方から見られるようになります。
本記事で扱わないこと
UNIX V6ソースコードの全関数を逐語的に追うこと。
Linuxカーネル内部構造のバージョン別差分を網羅すること。
io_uring の具体的なプログラミング手順。
Plan 9、BSD、Linuxの設計優劣を比較して結論づけること。
先に結論
UNIX V6のファイルシステムとブロックデバイスの関係は、循環していません。カーネルは最初にrawなデバイスを認識し、root deviceを決め、その上にroot filesystemをmountします。/dev/rk0 のような名前は、その後でroot filesystem上のpathnameとして解決できるようになります。
/dev/rk0 は通常ファイルではありません。ファイルシステム上に名前はありますが、inodeの種別はblock special fileです。pathname lookupまではファイルシステムを使いますが、その後のread/writeは通常ファイルのデータブロックを読むのではなく、major/minor番号をもとにデバイスドライバへdispatchされます。
したがって、古典UNIXの「everything is a file」は、「すべてが通常ファイルである」という意味ではありません。より正確には、多くのI/O対象をpathname、inode、object type、operation dispatch、file descriptorという枠組みに接続する設計だと見るべきです。
BSD socket以後、この統一の中心は少し変わります。socketはpathnameから作られません。しかし、socketもfdとして扱えます。ここで抽象化の中心は、openできる名前から、すでに権限確認済みのカーネル内オブジェクトを指すfdへ移っていきます。現代Linuxでは、eventfd、timerfd、signalfd、pidfd、epoll fd、io_uring fdのように、「通常ファイルではないがfdとして待てる、渡せる、閉じられる」対象が増えています。
第1章(循環に見える理由)
この章では、最初の混乱を固定します。混乱を曖昧にしたままUNIXのI/Oを読むと、ファイルシステム、デバイス、inode、driverの役割が重なって見えてしまいます。
1.1 ファイルシステムはブロックデバイスの上にある
UNIX V6では、ファイルシステムはディスクなどのブロックデバイス上に構築されます。通常ファイルを読むとき、概念的には次の流れになります。
textユーザープロセス
read / write
ファイルシステム層
buffer cache
block device driver
disk
ファイルシステムは、ファイルのoffsetから論理ブロック番号を求め、inodeを見て実際のディスク上のブロック番号へ変換し、buffer cacheを経由してブロックデバイスを読みます。
ここで重要なのは、ファイルシステムが扱うのは「ファイル内の何番目のブロックか」であり、ブロックデバイス層が扱うのは「デバイス上の何番ブロックか」だという違いです。
1.2 ブロックデバイスが /dev/rk0 として見える
一方、ユーザー空間からは、ブロックデバイスが /dev/rk0 のような名前で見えます。この名前はファイルシステム上にあります。
そのため、次のような循環に見えます。
textファイルシステムを読むにはブロックデバイスが必要
ブロックデバイスを指定するには /dev/rk0 というファイルシステム上の名前が必要
しかし、これは時間の順序と役割の違いを混同した見方です。
1.3 成立順序が違う
UNIX V6では、最初から /dev/rk0 というpathnameを使ってroot filesystemを成立させているわけではありません。おおまかには次の順序です。
textカーネルがraw deviceを認識する
root deviceが決まる
root filesystemがmountされる
inode treeが読めるようになる
/devなどのpathnameが解決できるようになる
ユーザープロセスが/dev/rk0などをopenできるようになる
つまり、/dev/rk0 はroot filesystemが成立した後に、ユーザー空間から見える名前です。root filesystemを成立させるために、ユーザー空間の /dev/rk0 を先にopenしているわけではありません。
この順序を押さえると、見かけ上の循環は消えます。
第2章(ファイルシステム自身もブロックとして読まれる)
この章では、なぜ自己言及的に見えるのかをもう少し分解します。ファイルシステムは通常ファイルだけでなく、自分自身の管理情報もブロックとして読みます。
2.1 メタデータもディスク上のブロックである
ファイルシステムは、通常ファイルの中身だけを読んでいるわけではありません。次のような管理情報もディスク上にあります。
super block
inode block
directory block
indirect block
free list block
これらも、結局はディスク上のブロックです。したがって、ファイルシステムは自分自身の管理情報を読むときにも、bread や bwrite に相当するブロックI/Oを使います。
ここが自己言及的に見える第一の理由です。
2.2 ブロックI/O層はpathnameを知らない
ただし、ブロックI/O層はpathnameやdirectoryを理解していません。inodeの意味も、通常ファイルの構造も、ユーザーが指定したファイル名も知りません。
ブロックI/O層が受け取るのは、基本的には次の組み合わせです。
textdevice number + block number
この層は、指定されたデバイス上の指定されたブロックをbuffer cache経由で読み書きします。ファイルシステムが「このinodeを読むために、このブロックが必要だ」と判断する部分と、ブロックI/O層が「このデバイスのこのブロックを読む」部分は、役割が異なります。
2.3 ファイル内ブロックとデバイス上ブロックを分ける
通常ファイルを読む場合、ファイルシステムは次のような変換を行います。
textfile offset
logical block number
bmap
physical block number
bread
buffer cache
device driver
bmap に相当する処理は、ファイル内の論理ブロック番号を、デバイス上の物理ブロック番号へ対応づけます。この対応づけがファイルシステムの仕事です。
一方、物理ブロック番号が決まった後の読み書きは、ブロックデバイス側の仕事です。この分離があるため、ファイルシステムがメタデータを読むことは自己矛盾ではありません。
第3章(special fileはデバイスへの名前付き入口である)
この章では、/dev/rk0 の正体を整理します。/dev/rk0 はファイルシステム上の名前ですが、通常ファイルではありません。
3.1 inodeの種別が違う
UNIX V6では、inodeのmodeによって、そのinodeが何を表すかが決まります。代表的には、通常ファイル、directory、character special file、block special fileがあります。
ブロックデバイスを表すinodeでは、block special fileを示すビットが立っています。概念的には、次のような判定です。
c
i_mode & IFBLK
この判定により、カーネルはそのinodeを通常ファイルとして扱いません。
通常ファイルであれば、inode内のブロックアドレスをたどってファイルデータを読みます。しかしspecial fileであれば、そのinodeはファイルデータの場所ではなく、デバイスへの接続情報を持ちます。
3.2 major/minor番号でdriverへ接続する
通常ファイルのinodeは、データブロックへの参照を持ちます。
textregular file inode
data block addresses
disk blocks
一方、special fileのinodeは、データブロックへの参照ではなく、major/minor番号を持ちます。
textspecial file inode
major number
minor number
device driver
major numberは、どの種類のデバイスドライバを使うかを示します。minor numberは、そのドライバ配下のどの装置、区画、単位を使うかを示します。
したがって /dev/rk0 という名前は、ブロックデバイスそのものではありません。正確には、ブロックデバイスへ到達するための名前付き入口です。
3.3 pathname lookupと実I/Oは別である
/dev/rk0 をopenするとき、最初に行われるのは通常のpathname lookupです。
text/
dev
rk0
この段階では、directoryを読み、directory entryを探し、inodeを取得します。ここまではファイルシステムの仕事です。
しかし、取得したinodeがblock special fileであると分かると、通常ファイルとしてのread/writeには進みません。処理は次のように切り替わります。
textpathname lookup
inode
block special fileと判定
major/minorを取得
block device switch table
device driver
この意味で、「ファイルシステムがパスされる」という言い方は半分正しいです。正確には、pathname解決まではファイルシステムを使い、read/writeの実体は通常ファイルのファイルシステム処理ではなくデバイスドライバへ行く、ということです。
第4章(everything is a fileを正確に読む)
この章では、有名な言葉を少し正確に読み替えます。ここを曖昧にすると、UNIXの統一性と限界の両方を見誤ります。
4.1 すべてが通常ファイルではない
UNIXの有名な思想に、everything is a fileがあります。しかし、これは「すべてが通常ファイルである」という意味ではありません。
より正確には、多くのI/O対象が次の枠組みに接続される、ということです。
textpathname
inode
object type
operation dispatch
inodeは、単なるファイル内容への入口ではありません。対象の種別を判定し、適切な処理へ分岐させる接点でもあります。
4.2 ファイルシステムは名前空間でもある
この構造から見ると、ファイルシステムは単なるデータ保存場所ではありません。少なくとも次の役割を持っています。
名前空間管理
オブジェクトへの接続管理
権限管理
種別判定
dispatchの起点
/dev/rk0 は、この性質をよく示しています。名前はファイルシステム上にありますが、その先にあるのは通常ファイルのデータブロックではなく、デバイスドライバへの接続です。
4.3 pipeもファイル的に扱われる
古典UNIXでは、pipeもかなりファイル的に扱われます。ユーザーから見ると、pipeはpathnameを持たない一時的な通信路です。しかし、read/writeできる対象としてfdに接続されます。
概念的には次のように見られます。
textpipe()
file descriptor pair
pipe buffer
通常ファイル、デバイス、pipeを、かなり一貫したI/Oモデルの中に収めていたことが、UNIXの強さでした。
第5章(socketによって中心がfdへ移る)
この章では、古典UNIXの統一性がどこで変わったかを見ます。大きな転換点の一つがsocketです。
5.1 socketはpathnameから作られない
BSD socketは、通常次のように作られます。
c
fd = socket(AF_INET, SOCK_STREAM, 0);これは、次の形ではありません。
c
fd = open("/path/to/object", flags);つまり、socketはpathnameから生成されません。古典UNIX的な流れである、pathname、inode、fdという経路から外れます。
5.2 それでもfdには統合される
socketはpathname/inodeモデルから外れます。しかし、socketで得られる値はfdです。そのため、ユーザー空間からはread、write、close、select、pollなどの操作対象になります。
ここで、統一の形が変わります。
textopenによる統一
は一部崩れます。しかし、
textfdによる統一
は維持されます。
この時点で、UNIX系OSの抽象化の中心は、少しずつpathname/inodeからfdへ移っていきます。
5.3 fdは能力ハンドルとして読める
fdは単なる小さな整数ではありません。プロセスごとのfd tableを通じて、カーネル内の対象を指すハンドルです。
この観点では、fdは能力ハンドルのように読めます。open、socket、pipeなどで得られたfdは、すでに一定の権限確認や生成処理を通過した対象への入口です。
この理解を持つと、現代Linuxで多くの対象がfdとして表現される理由が見えやすくなります。
第6章(Linuxではfdが多様な対象を束ねる)
この章では、現代Linuxの方向を整理します。Linuxでは、fdは通常ファイルやsocketだけでなく、さまざまなカーネル内オブジェクトへの入口になっています。
6.1 fd tableからstruct fileへ
Linuxでは、ユーザープロセスが持つfdは、概念的にはプロセスごとのfd tableを通じてカーネル内の struct file に到達します。struct file は、対象オブジェクトと操作テーブルへの入口になります。
概念的には次のように整理できます。
textfile descriptor
fd table
struct filetarget object
file_operations
file_operations に相当する操作テーブルがあるため、read、write、poll、ioctl、mmap、releaseなどの操作を、対象の種類に応じてdispatchできます。
ここでも重要なのは、すべてが通常ファイルなのではなく、共通の操作入口に接続されるという点です。
6.2 eventfd、timerfd、signalfd、pidfd
現代Linuxでは、次のような対象もfdとして扱えます。
eventfd: イベント通知やカウンタをfdとして扱う。
timerfd: タイマーの満了をfdとして待てる。
signalfd: シグナル配送をfdから読めるイベントとして扱う。
pidfd: プロセスをPID番号そのものではなくfdとして参照する。
これらは通常ファイルではありません。永続的なファイル内容を保存するものでもありません。それでもfdであるため、closeでき、poll/epollで待て、別の仕組みに渡せます。
特にpidfdは、「PID番号は再利用される」という問題に対して、プロセスをより安全に参照するための方向を示しています。単なる番号ではなく、カーネルが管理する対象へのハンドルとして扱えるからです。
6.3 epoll fdとio_uring fd
epollもfdを返します。epoll fdは、ほかのfd群の準備状態を監視するためのfdです。
textepoll fd
監視対象fd群
ready event
event loop
これは、fdが「データを読む対象」だけでなく、「ほかのfdを監視する対象」にもなったことを示します。
io_uringも、従来のread/write中心の同期的な見方とは違い、送信用キューと完了キューを中心にI/Oを扱います。ここでは、fdは同期的なread/writeの対象であるだけでなく、非同期I/Oを組み立てる部品になります。
第7章(それでもファイルシステム名前空間は残る)
この章では、fd中心になった後も、なぜファイルシステム的な名前空間が残り続けるのかを整理します。
7.1 すべてをfilesystemに押し込めばよいわけではない
Plan 9のように、より徹底して「すべてをファイルシステム名前空間で扱う」方向もありました。しかし、主流の実用環境では、すべてをpathnameで扱う方向には進みませんでした。
理由はいくつもあります。既存のPOSIX資産、Linuxの普及、socket APIの定着、GUIやネットワークや非同期処理との相性などです。さらに、現代的なI/O対象には、pathnameよりfdやqueueのほうが自然なものがあります。
たとえば、一時的な接続、高頻度イベント、非同期完了、グラフ状の関係、プロセス参照などは、永続的なファイル名だけで表すより、生成済みハンドルとして扱うほうが自然です。
7.2 /proc、/sys、cgroupfs、debugfs、FUSE
一方で、Linuxはファイルシステム名前空間を捨てていません。むしろ、管理、観察、設定のためには、ファイルシステム的な見せ方を積極的に使っています。
代表例は次のとおりです。
/proc
/sys
cgroupfs
debugfs
FUSE
これらは、通常ファイルの保存場所というより、カーネル内の状態や設定を名前空間として見せる仕組みです。
7.3 管理はfilesystem、実行はfdやqueue
現代OSの流れは、単純に「filesystemの時代が終わった」という話ではありません。むしろ、役割が分かれてきたと見るほうが自然です。
管理、観察、設定、探索には、filesystem的な名前空間が強いです。人間が見て、たどって、権限を確認し、設定を変更するには、名前の階層は便利です。
一方、実行、通信、イベント待ち、非同期完了には、fd、event loop、queueのほうが向いています。ここでは、永続名よりも、生成済みの能力ハンドルや完了通知のほうが重要になります。
第8章(AIエージェント時代のnamed capability)
この章では、少し先の見方として、AIエージェント時代の抽象化を考えます。UNIXの歴史は、単に古いOSの話ではなく、「対象にどう名前を付け、どう権限を与え、どう操作するか」という現在の設計にもつながります。
8.1 名前だけでも、ハンドルだけでも足りない
人間にとっては、名前空間が重要です。/proc や /sys のように、名前でたどれることは、理解、管理、監査に向いています。
一方、プログラムやエージェントにとっては、権限確認済みのハンドルが重要です。すでに許可された対象だけを扱えること、対象が途中で別物にすり替わらないこと、イベントとして待てることが重要になります。
この両方を合わせると、これから重要になるのは、単なるファイル名でも単なるfdでもなく、名前付き能力のような設計です。
8.2 everything is a named capability
古典UNIXを単純化して言えば、everything is a fileでした。socket以後のUNIX/Linuxを単純化して言えば、everything is a file descriptorに近づきました。
AIエージェントや自動化の時代には、さらに次のように言えるかもしれません。
texteverything is a named capability
つまり、対象には人間が理解できる名前があり、同時にプログラムが安全に扱える能力ハンドルがある、という形です。
これはUNIX V6から直接そうなっていたという意味ではありません。むしろ、UNIX V6のspecial file、BSD socket、Linux fd群、疑似ファイルシステムを通じて見えてくる、抽象化の方向です。
まとめ
UNIX V6のファイルシステムとブロックデバイスの関係は、見かけほど循環していません。カーネルはraw deviceを認識し、その上にroot filesystemをmountし、その後で /dev/rk0 のようなpathnameが使えるようになります。
/dev/rk0 は通常ファイルではなく、block special fileです。pathname lookupまではファイルシステムを使いますが、read/writeの実体はmajor/minor番号をもとにデバイスドライバへdispatchされます。
この構造から、「everything is a file」は、すべてが通常ファイルであるという意味ではなく、多くのI/O対象を共通の入口と操作体系へ接続する設計だと理解できます。
socket以後、この統一の中心はpathname/inodeからfdへ広がりました。現代Linuxでは、通常ファイル、directory、device、pipe、socketだけでなく、eventfd、timerfd、signalfd、pidfd、epoll fd、io_uring fdのような対象もfdとして扱われます。
そして現在も、filesystem的な名前空間は残っています。人間が管理し、観察し、設定するには名前空間が便利です。一方、実行、通信、イベント、非同期完了にはfdやqueueが向いています。
UNIXからLinuxまでのI/O抽象化は、「ファイル」という一語で終わる話ではありません。名前、種別、権限、dispatch、fd、event loop、queueへと広がる、OSが対象をどう扱うかの歴史です。
出典メモ
元Markdown: C:\Users\hoehoe\Downloads\unix_linux_io_abstraction_expanded.md
記事化日: 2026年5月18日
本記事は、元Markdownの構成と主張をもとに、LWP公開記事向けに章立て、前提説明、用語整理、結論の前倒しを補って再構成したものです。
