Linuxカーネル

Linuxカーネル

Linuxカーネルについて書きたいと思う。

マルチタスク

LinuxマルチタスクのOS

まず、LinuxカーネルマルチタスクなOSのカーネルである。
マルチタスクとは、「複数のプログラムを同時に並列実行できる」ということ。

プロセスとスレッド

ここで、プログラムにはプロセスとスレッドがあることが重要である。
プロセスは、それぞれに個別の独立したメモリ領域が与えられる。
それに対して、スレッドはプロセスの中に存在する並列処理であり、
同じメモリ領域を参照することができる。
このため、マルチスレッド環境では、排他制御を行う「ロック」が肝心となる。
ロックを適切に行うことで、スレッドセーフな関数を作らなければならない。
プロセスにおいても、プロセス間通信(IPC)と呼ばれる機構を使うことで、
メモリを共有したり、簡単な排他制御を行う(セマフォ)ことが可能となっている。

コンテキスト切り替え

プログラムがもしあったとして、それをマルチタスクで実行するためにどうするか。
プログラムは、実際、機械語でCPU命令とメモリアドレスが記述されているため、
単純に考えれば、CPUとメモリがそれぞれのプログラムに必要となる。
しかしながら、マルチタスクのOSでは、
プログラムの命令を、とても小さな時間で切り替えながら実行することで、
CPUの仮想化を実現する。
つまり、プログラムの実行しているレジスタの内容や、プログラムカウンタなどを、
task_structと呼ばれる構造体に退避させて、
プログラムを「安全に停止させる」ことができればいい。
この上で、プログラムを次々に停止・再実行する。
これを「コンテキスト切り替え」と呼ぶ。

仮想アドレス空間

また、メモリ領域については、
カーネルがハードウェアのMMUという機構を上手く使うことで、
プログラムが実際のメモリ領域(論理アドレスやリニアアドレスと呼ぶ)にアクセスした段階で、
カーネルがそれを物理アドレスに翻訳する。
このようにすることで、カーネルはメモリ領域を全てのプログラムに「個別に与える」ことができる。
このような仕組みを「仮想アドレス空間」と呼ぶ。

ページアドレッシング

Linuxはメモリアドレスをページと呼ばれる小さな単位で管理している。
Linuxでは効率化と使用メモリ領域の削減のために、
三段階のページアドレッシングを採用しており、
大きなものから順に、
ページグローバルディレクトリがあり、
その中にページミドルディレクトリがあり、
その中にページテーブルがあり、
ページテーブルにおいて実際のページ領域をマッピングする。

割り込み

カーネルは、IOデバイスにアクセスする時点で、
IOデバイスの処理の終了を待機するが、
IOデバイスはプログラムの命令よりも動作が遅いので、
いちいちIOデバイス処理を待っていたのでは効率が悪い。
そのため、カーネルはIOデバイス処理を待つ間に、
必要とあれば別の処理を行う。
そして、IOデバイスの処理が終わった段階で、
カーネルに対して「割り込み」を行う。
割り込まれた時、カーネルはIOデバイスの処理の完了を知り、
その後の命令を実行する。

デバイスドライバ

実際のところ、カーネルはデバイスコントローラの詳細を
デバイスドライバという小さなソフトウェアモジュールによって知っている。
カーネルにおいて、実際のデバイスの詳細を知っているのは、
デバイスドライバだけである。
バイスには、キャラクタ型デバイスとブロック型デバイスがある。
キャラクタ型デバイスは、その時その時の状況を、
バイト列(文字列)によって知らせるようなデバイスであり、
システムコールのストリームを通じて操作できる。
ブロック型デバイスは、内部にファイルシステムが存在するデバイスであり、
ファイル処理APIを通じてアクセスする。
キャラクタ型デバイスは、ランダムアクセス(好きな時に好きな場所のデータを知ること)ができないが、
ブロック型デバイスは、ランダムアクセスができる。

ファイルシステム

Linuxカーネルでは、モノリシックカーネルであるため、
ファイルシステム処理がカーネルに統合されている。
ブロック型ファイルシステムext2ext4が標準的に使われるが、
B-Tree型のファイルシステムであるReiserFS, XFS, JFSなども使うことができる。
また、次世代の(おそらくZFSを超える)ファイルシステムであるBtrfsも開発されている。
LinuxカーネルではVFSの仕組みにより、
ファイルシステムが変わってもシステムコールAPIは変わらない。
どのファイルシステムにも、同じようにプログラムからアクセスできる。
また、ジャーナリングに対応したファイルシステムでは、
一度マシンを強制終了しても、ファイルやデータが消え去ることがない。
また、内部的にはi-nodeと呼ばれるインデックス処理を行っている。
全てのファイルにはi-nodeから参照できる。

ネットワーク

Linuxカーネルはネットワークをサポートしており、
BSDソケットインターフェースからネットワーク通信ができる。
BSDソケットは、互いに2つのソケットが繋がると有効になり、
片方に書き込むと片方から読み込むことができる。
TCPUDPもサポートされている。
TCPでは、再送制御などの多くの機能がある。
UDPには、そうした機能がない代わり、ビデオや音声を送ったりする時に、
誤りを無視して高速に通信することができる。
BSDソケットは多くの場合サーバー・クライアントシステムであり、
片方のサーバが待ち受けをし、そのサーバに対してクライアントが接続する。

入出力とシステムコール

また、Linuxでは端末を用いた入出力が可能である。
端末のデバイスファイルは、多くの場合/dev/tty1が第一の仮想コンソールである。
Linuxにおいては、標準入力、標準出力、標準エラー出力
多くの場合プロセスに与えられるが、
このほかにも、openで開いたファイルやソケットなどにも文字列を入出力できる。
システムコールについては、open(), close(), write(), read()が、
ファイルに対しての読み書きとして使える。
ファイルディスクリプタと呼ばれる整数が、それぞれのストリームの識別に使われる。
しかしながら、多くの場合、システムコールを使うよりも、
C言語のstdioというライブラリを使った方が高速であり、また便利である。
システムコールに比べて、stdioはバッファリングを行うため、
何度も同じデータにアクセスする時の速度が高速になる。
また、システムコールのread()やwrite()は固定長の読み書きを行うが、
stdioは文字単位や行単位の読み書きが可能であり、
printfのようなフォーマット出力と呼ばれる便利な仕組みが存在する。
stdioではファイルディスクリプタを生に触るのではなく、
FILE型のポインタと呼ばれるラッパーを使う。

シグナル

プログラムを強制終了するためには、シグナルを使う。
シグナルにはいくらかの種類があるが、
カーネルがシグナルを処理する際のデフォルトの挙動は、
「無視する」「強制終了する」「コアダンプを吐く」の3種類。
しかしながら、シグナルはプロセス側で処理を書き換えることができる。
これを「シグナルを捕捉する」あるいは「トラップする」と言う。
シグナルを上手くプロセス側で捕捉することで、
デフォルトで無視されるシグナルも役に立つのである。

カーネルシステムコールを待ち受けるだけ

このようなカーネルをどのように開発するか、という問題だが、
Linuxカーネルは、基本的にシステムコールを待ち受けるだけの
イベント駆動なOSである。
よって、Linuxカーネルは自分からは何もしない。
何かの要求があった時にだけ、働くように作られている。

書籍「ふつうのLinuxプログラミング」より

仮想CPUと仮想メモリという「幻想」

Linuxでは複数のプロセスを並列して実行することができる。これは、「プロセスにCPUやメモリがそれぞれのためにたくさんあるかのように幻想を見せる」ことで成り立っている。
素のコンピュータにはCPUとメモリは1つずつしかない。
だが、非常に短い時間単位で実行するプロセスを切り替えることで、プロセスはCPUが自分専用であるかのように見える。
それぞれに配分される時間のことを「タイムスライス」と呼び、いつタイムスライスを与えるか管理する機構を「スケジューラ」と呼ぶ。
プロセスにはタイムスタイスを与える優先度が決まっており、niceコマンドなどで変えることができる。CPUに余裕がない時はタイムスライスをもらえない。また、read()などでハードディスクなどから読み込む中のプロセスは、データが届くまで動作する必要が無いため、効率を考えてタイムスライスを与えないようになっている。
ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆しました。)

アドレス空間とページ

メモリはアクセスするために住所である「アドレス」を必要とする。そのため、0番地から始まる「メモリ」をカーネルは準備する。
メモリをそれぞれのプロセスのために個別に提供するために、メモリにアクセスした時点(プロセスから見えるこのメモリのアドレスを論理アドレスと呼ぶ)でカーネルとCPUにいったん渡して、実際のアドレス(実際のアドレスを物理アドレスと呼ぶ)に翻訳する仕掛けを作り出す。この仕掛けによって確保されるメモリの全体を「アドレス空間」と呼ぶ。
プロセスごとにアドレスを用意すると、他のプロセスに不用意にアクセスすることができなくなり、安全性も向上する。暴走しても、他のプロセスに(基本的に)影響しないため、そのプロセスだけを殺せばいい。
アドレス空間は、4KB/8KBの小さな「ページ」に分割されて管理され、ページ単位で物理アドレス論理アドレスを管理し、アドレス空間全体は連続したアドレス空間ではなく、ページごとのばらばらな位置に対応付けられる。
物理アドレスに対応していないページがあってもよく、必要となった時点で物理アドレスに割り当てられる(あるいは、アクセスを禁止する)。アクセスの禁止されたページにプロセスがアクセスすると、カーネルはプロセスにシグナルSIGSEGVを送ってそれを知らせる。これがsegmentation fault(セグメンテーション・フォールト、あるいはセグメンテーション違反)。カーネルは意図的に論理アドレスの最初の数ページをアクセス禁止にして物理アドレスを割り当てないでおくが、それはNULLポインタのアクセスを確実に検出するためであり、LinuxでNULLポインタを参照すると確実にsegmentation faultが起きる。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆・編集して引用しました。)

仮想メモリ機構の応用

仮想メモリ機構には次のようにさまざまな応用がある。

  • ページング
    • ページングは、ハードディスクを物理メモリの代わりに使う機構。
  • メモリマップトファイル
    • メモリマップトファイルは、ファイルをメモリとしてアクセスできるようにする。
  • 共有メモリ
    • 共有メモリは、特定範囲の物理メモリを複数プロセスで共有する機構。

(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆・編集して引用しました。)

アドレス空間の構造

アドレス空間は一様に平たいわけではなく、用途によって区分されている。

  • テキスト領域
    • 機械語のプログラムのこと。
    • 「テキストファイル」のテキストとはまったく関係ない。
  • データ領域
  • BSS領域
    • グローバル変数や関数内のスタティック変数のうち、初期化が必要ないものが置かれる領域。
    • 実行ファイルではこの領域はサイズだけが記録されている。
  • ヒープ領域
    • ヒープ領域は、malloc()が管理する領域。
    • 実行時に拡大・縮小する可能性がある。
  • スタック領域
    • スタック領域は、関数呼び出しに関連したデータを置く。
    • 例えば関数の引数やローカル変数などが置かれる。名前のとおりスタックとして使われる。

(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆・編集して引用しました。)
基本的に、プロセスを実行すると、そのプロセスに対してテキスト領域、ヒープ領域、スタック領域が割り当てられることを知っておけば良い。

アドレス空間を覗く

プロセスファイルシステムを使うと、これらの領域が実際のプロセスにどう配置されているのか見ることができる。
プロセスIDがnのプロセスのメモリ配置を見たければ、/proc/n/mapsをcatする。

$ cat /proc/24856/maps
08048000-08049000 r-xp 00000000 03:07 1612780   /usr/local/bin/testprog
08049000-0804a000 rw-p 00000000 03:07 1612780   /usr/local/bin/testprog
40000000-40013000 r-xp 00000000 03:05 15687     /lib/ld-2.2.5.so
40013000-40014000 rw-p 00013000 03:05 15687     /lib/ld-2.2.5.so
4001a000-40131000 r-xp 00000000 03:05 15688     /lib/libc-2.2.5.so
40131000-40137000 rw-p 00116000 03:05 15688     /lib/libc-2.2.5.so
40137000-4013b000 rw-p 00000000 00:00 0
bfffe000-c0000000 rwxp fffff000 00:00 0

一番左は、論理アドレスの範囲(16進数表記)。
一番右は、mmap()でメモリにマップされているファイル名。testprogがこのとき使ったプログラム名で、libc-2.2.5.soはプログラムが使っているライブラリの名前。testprogはダイナミックリンクで作成されているので、実行時にライブラリが必要。
左から二番目は、ファイルのパーミッションと良く似た形式のメモリ領域の属性(「読み・書き・実行」の許可)。最後の「p」はそのプロセスにprivateな領域、つまりそのプロセスにしかアクセスできないことを表す。上記の出力例ではすべて「p」だが、「s」になる場合もある。sはshared(共有されている)のsで、共有メモリ機構によって共有されている場合に表示される。
属性がr-xpになっていて対応するファイルがある領域はテキスト。rw-pで対応するファイルがある領域はBSS。属性がrwxpで、対応するファイルがない領域はスタック。最後に、残った下から2つ目の部分はヒープ。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆・編集して引用しました。)

バイスファイルとストリーム

バイスファイルとプロセスは、文字などのバイト列であるストリームをやりとりする。UNIXでは、バイト列の流れとして考えられるものであれば、どんなものでも基本的にストリームに繋ぐことができるようになっていることが多い。
ストリームとは、バイト列の流れ道のこと。ストリームは、主に、プロセスとファイルシステムの間で、標準入出力やファイルの間でやりとりを行うための流れ道となる。だが、デバイスについても、バイト列であると考えられるならストリームに繋ぐことができる。たとえば、マウスやキーボードなどのデバイスも、バイト列の流れであると考えられる。デバイスファイルは、こうしたストリームを繋げるための「とっかかり」として用意されている。
バイスファイルの種類には、キャラクタ型デバイスファイルとブロック型デバイスファイルがある。これは、好きな時に好きなところにアクセスできるかどうかの違いである。ハードディスクはブロックデバイスで、プリンタやモデムはキャラクタデバイスである。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆・編集して引用しました。)

バイスファイル(/dev)

/devにはデバイスファイルが置かれる。
UNIXではあらかじめあらゆるデバイスのデバイスファイルをここに作っておくのが伝統的なやり方だが、Linux 2.4以降ではシステムに存在するデバイスのデバイスファイルだけをその都度作成するdevfsという仕組みが導入された。
だが、devfsにはUSBのような動的デバイスとの相性が良くないという問題があり、Linux 2.6以降ではudevという仕組みが使われている。devfsはカーネルの一部として実装されているが、udevはカーネルの外側で実装されている。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆・編集して引用しました。)

プロセスファイルシステム(/proc)

プロセスファイルシステムは、プロセスをファイルシステム上に表現する仕組み。
プロセスIDが1のプロセスの情報を見たければ、ディレクトリ/proc/1を見る。
もともとはデバッガのために作られた仕組みだったが、現在はpsなどのプロセス関係のコマンドもプロセスファイルシステムを全面的に使って実装している。
また、最初はプロセスだけが現れていたプロセスファイルシステムだが、いつからかカーネルの情報をリアルタイムに出力する仕組みとして/procが使われるようになってきた。
そして、プロセスファイルシステムは情報を得るだけではなく、カーネルに何かの設定を指定するために使うことも出来る。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆・編集して引用しました。)

書籍「詳解 Linuxカーネル」より

プロセスの実装

詳解 Linuxカーネル 第2版」を参考に執筆・編集して引用しました。
カーネルがプロセスを管理するために、各プロセスはプロセスディスクリプタによって表される。プロセスディスクリプタには、プロセスの現在の状態における情報が含まれている。
カーネルは、プロセスの実行を停止する際に、その時点でのCPUプロセッサのレジスタの内容を、プロセスディスクリプタの中に退避する。
レジスタには以下のようなものがある。
・プログラムカウンタ(PC)レジスタとスタックポインタ(SP)レジスタ
・汎用レジスタ
浮動小数レジスタ
・プロセッサ制御レジスタ(プロセッサステータスワード)。CPUの状態の情報がある。
・メモリ管理レジスタ。プロセスによって使われるRAMの管理に使われる。
カーネルはプログラムの実行の再開を決めると、停止しておいたCPUレジスタを復旧するために、対応するプロセスディスクリプタのメンバ情報を使用する。プログラムカウンタとして退避された値は、最後に実行された命令の次に続く命令を指している。よって、プロセスは停止した位置から実行を再開する。
UNIXカーネルはリエントラントであり、同時に複数のプロセスをカーネルモードで実行することがある。(後日注記:リエントラントとは「再入可能」という意味で、あるプログラムやサブルーチンの実行をしている時に同じプログラムやサブルーチンを実行できるという意味。リエントラント(再入可能)とは - IT用語辞典 e-Wordsを参照のこと。)
それぞれのプロセスは、固有なアドレス空間において実行される。ユーザモードで実行中のプロセスは、固有のスタックとデータとコード領域を参照する。カーネルモードで実行するプロセスは、カーネルのデータとコード領域を参照し、もうひとつのスタックを用いる。
UNIXのシグナルは、システムの事象をプロセスに通知する仕組みを提供する。

プロセス切り替え

「詳解 Linuxカーネル 第2版」を参考に執筆・編集して引用しました。
プロセスの実行を管理するために、カーネルは実行プロセスの休止や再開を行う必要がある。このような処理のことをプロセス切り替え、タスク切り替え、コンテキスト切り替えと呼ぶ。
各プロセスは自身のアドレス空間を持つことができるが、CPUレジスタはすべてのプロセスで共有されている。そのためカーネルはプロセスの実行を再開する前に、共有しているレジスタの値をプロセスが休止した瞬間の値に戻す必要がある。
プロセスの実行を再開する前にレジスタに戻す必要のある一連のデータのことをハードウェアコンテキストと呼ぶ。ハードウェアコンテキストはプロセスの実行に必要なすべての情報が格納されているプロセス実行コンテキストのサブセット。Linuxではプロセスのハードウェアコンテキストの一部はプロセスディスクリプタ内に、残りはカーネルモードスタックに格納されている。
ここではローカル変数prevは切り替えられるプロセスのプロセスディスクリプタを、nextはその代わりに実行されようとしているプロセスのプロセスディスクリプタを指しているものとする。これによりプロセス切り替えのことを、「prevのハードウェアコンテキストを退避してnextのハードウェアコンテキストで置き換える動作」と定義することができる。プロセス切り替えは頻繁に発生するため、ハードウェアコンテキストの退避や復旧にかかる時間を最小限に抑えることは重要。
プロセス切り替えは、明確な一か所、schedule()関数にて起きる。本質的に、すべてのプロセス切り替えは以下の2つの処理からなる。
1.ページグローバルディレクトリを切り替え、新しいアドレス空間を有効にする。
2.カーネルモードスタックとハードウェアコンテキストを切り替える。プロセスが実行するために必要な情報すべてである(CPUレジスタも含む)。
prevは切り替え元プロセスのディスクリプタを指し、nextは次に実行するプロセスのディスクリプタを指す。prevとnextはschedule()関数内のローカル変数。
プロセス切り替えの2つ目の処理は、switch_toマクロが実行する。switch_toマクロは、もっともハードウェア依存なカーネル処理の1つ。
switch_toマクロは、prev、next、lastの3引数を取る。schedule()関数の、実際のマクロ呼び出しは以下のようになる。

switch_to(prev, next, last);

prex引数とnext引数の役割が、単にローカル変数prevとnextの代替であることは理解できる。最後のlast引数についてだが、実は、どのプロセス切り替えにおいても、それに関与するプロセスは2つではなく3つである。
カーネルがプロセスAからプロセスBに切り替えようとしているとする。shcedule()関数では、prevはプロセスAのディスクリプタを指し、nextはプロセスBのディスクリプタを指す。switch_toマクロがプロセスAを停止した瞬間、プロセスAの処理の流れは止まる。
後になり、カーネルがプロセスAを再実行しようとしたとき、他のプロセスCから切り替わらなければならない。そのとき、prevがプロセスCを指し、nextがプロセスAを指している、別のswitch_toマクロが実行される。プロセスAが実行の流れを再開したとき、プロセスは元のカーネルスタックを見つけ、prev変数がプロセスAのディスクリプタを指し、next変数がBのディスクリプタを指す。プロセス切り替え処理の後半はプロセスAの上で動作し、カーネルはプロセスCへの参照を失ってしまう。
swicth_toマクロのlast引数は、prev変数にプロセスCのディスクリプタのアドレスを再設定するために利用する。この仕組みは、関数呼び出し時に使わないレジスタを利用して実現している。先頭のprev引数はCPUレジスタに対応しており、マクロが実行を始めたときのprev変数の値を読み込む。マクロが終了するとき、last変数に割り当てられた同じレジスタの内容を、prev変数に書き戻す。しかし、CPUレジスタはプロセス切り替えの前後で変化しない。prevにプロセスCのディスクリプタを受け取り、スケジューラはプロセスCが他のCPU上で動作できないか調べる。

メモリ管理

「詳解 Linuxカーネル 第2版」を参考に執筆・編集して引用しました。
最近のUNIXは、仮想記憶と呼ばれる抽象概念を提供する。仮想記憶は、アプリケーションからのメモリ要求と、ハードウェアのメモリ管理管路(MMU)との間の、論理的なレイヤーとなる。
これにより、多くの利点がある。
・複数のプロセスを同時に実行出来る。
・物理メモリよりも多くのメモリをアプリケーションのために動作させることができる。
・部分的にしかコードがメモリ内に読み込まれていないプログラムのプロセスも実行出来る。
・利用可能な物理メモリの一部分だけにアクセスが許される。
・同一のライブラリやプログラムのイメージを共有できる。
・プログラムを、物理メモリのどこにでも再配置できる。
プログラマは、物理メモリの構成を配慮せず、マシンに依存しないコードを書ける。
仮想記憶サブシステムで大切なのは、「仮想アドレス空間」という概念である。プロセスは、物理的なメモリアドレスとは異なるメモリアドレスを指定して参照できる。プロセスが仮想アドレスを使用する時に、カーネルMMUが連携して、メモリの実際の物理的な位置を割り出す。
カーネルメモリアロケータ(KMA)は、システムのありこちから出されるメモリ要求を、満たすためのサブシステム。

Linuxのメモリ管理機構

メモリ管理機構は、ハードウェアのMMUと呼ばれる機器の仕組みを活用します。
以下は「詳解 Linuxカーネル 第2版」を参考に執筆・編集して引用しました。
CPUはセグメンテーション回路というハードウェアによって、論理アドレスをリニアアドレスに変換します。その後、ページング回路を用いて、リニアアドレスを物理アドレスに変換します。
80x86プロセッサにはセグメント機構が用意されていますが、Linuxはごく一部の機能しか利用していません。ハードウェアにはセグメント機構とページング機構が用意されており、セグメント機構とページング機能の両方にプロセスの物理アドレス空間を分割する機能があり、冗長だからです。Linuxではページング機構を利用しています。
ページング回路はリニアアドレスを物理アドレスに変換します。
効率上の理由から、リニアアドレスをページという一定のサイズの領域に分けています。

ファイルシステム関係のシステムコール

「詳解 Linuxカーネル 第2版」を編集して引用しました。

open()

プロセスがアクセスできるのはオープンされているファイルだけ。ファイルをオープンするには、

fd = open(path, flag, mode);

とする。pathはファイルパスを指定する。flagはオープンする方法(読み取り専用、書き込み専用、読み書き両用、追記など)を指定し、ファイルが存在しない場合に新しく作ることもできる。modeは新しく作成するファイルのアクセス権。
このシステムコールは、オープンファイルオブジェクトを作成し、ファイルディスクリプタと呼ばれる識別子を返す。オープンファイルオブジェクトには、以下が含まれる。
・ファイルを処理するためのデータ。たとえば、ファイルのデータがコピーされるカーネルのバッファメモリ領域や、次回の操作が行われるファイル中の現在位置を示すoffsetメンバ。ファイルポインタと呼ばれることもある。
・プロセスから呼び出すことのできる複数のカーネル関数へのポインタ。呼び出し可能な関数の集合は、flag引数の値による。
オープンファイルオブジェクトには、オープンされたファイルとプロセスとのやり取りや制御に関連するデータが含まれている。一方、ファイルディスクリプタとは、どのやり取りかを指し示すもの。そのため、複数のファイルディスクリプタが同一プロセスの内で同じオープンファイルオブジェクトを指すこともある。
複数のプロセスが同じファイルを同時にオープンすることもある。この時、ファイルシステムは、それぞれのプロセスに対して、別々のオープンファイルオブジェクトとともに、別々のファイルディスクリプタを割り当てる。このような場合、UNIXファイルシステムでは、複数のプロセスが同じファイルに対して行うI/O操作に関して、何ら同期処理の類いを提供しない。ただし、ファイルの全体あるいは一部に対してプロセス同士が同期をとれるように、flock()などのシステムコールを利用できるようになっている。
プロセスは新しいファイルを作成するために、creat()システムコールを呼び出すこともできる。creat()はopen()と全く同様に、カーネルによって処理される。

lseek()

オープンされたファイルは、順序的(シーケンシャル)にもランダムにもアクセスできる。カーネルは次の読み書き操作が行われる現在位置を示すファイルポインタを、オープンファイルオブジェクト中に保持している。UNIXはファイルの順次アクセスを前提としており、read()とwrite()は必ず現在のファイルポインタの位置を参照する。現在位置を変更したい場合には、プログラムは明示的にlseek()システムコールを呼び出す必要がある。ファイルがオープンされると、カーネルはファイルポインタをファイル中の先頭の1バイト目(オフセット0)に設定する。
lseek()は以下のような引数が必要。

newoffset = lseek(fd, offset, whence);

fdはオープンされたファイルのファイルディスクリプタ。offsetはファイルポインタの新しい位置を計算するのに使用される符号付き整数値。
whenceは新しい位置の計算方法を指定し、
・offsetの値に数値0を足す(ファイル先頭からのオフセット)
・現在のファイルポインタにoffsetの値を加える
・末尾のバイトの位置にoffsetの値を加える(ファイル末尾からのオフセット)
がある。

read()

read()は以下のような引数が必要。

nread = read(fd, buf, count);

fdはオープンされたファイルのファイルディスクリプタ、bufはプロセスのアドレス空間にある、データ転送先のバッファのアドレス、countは読み取るバイト数を表す。
カーネルがread()システムコールを処理するには、オープンされているファイルのoffsetメンバの現在値から始まるcountバイトを、ファイルディスクリプタfdを持つファイルから読み取ろうとする。カーネルは、EOF、空のパイプなどの場合、countバイト数の読み取りに成功しないこともある。返されるnreadの値は、実際に読み取られたバイト数を示す。ファイルポインタも元の値にnreadを加えて更新される。

write()

write()の引数についても同様。

nwrite = write(fd, buf, count);

bufの内容をcount分だけファイルディスクリプタfdによって参照されるファイルへと書き込む。成功した場合、書き込まれたバイト数が返る。

close()

プロセスは、ファイルの内容にアクセスする必要がなくなると、以下のシステムコールを呼び出すことができる。

res = close(fd);

これにより、ファイルディスクリプタfdに関連するオープンファイルオブジェクトが解放される。プロセスが終了すると、その時点でまだオープンされていたファイル全てがカーネルによってクローズされる。

ファイルの名前を変えたり、ファイルを削除する際、プロセスはそのファイルをオープンする必要はない。実際、このような操作は、対象ファイルではなく1つまたは2つのディレクトリの中身に対する操作となる。
ファイルリンクの名前を変更するには以下のようにする。

res = rename(oldpath, newpath);

次のようなシステムコールは、ファイルのリンク数を1減らし、ディレクトリの内の対応するエントリを削除する。ファイルが削除されるのは、リンクカウントの値が0になった時だけ。

res = unlink(pathname);

その他の書籍より

コンテキスト切り替え

動いているプログラムのことをプロセスと呼ぶ。プロセスが動作するためのプロセス空間や、CPUレジスタの値などが、プロセス固有の「コンテキスト」として保持される。これらはtask_struct構造体(task_t型)で管理される。
CPUの中のレジスタ群の値などのコンテキストは、コンテキスト切り替えの際、一時的にtask_struct構造体やカーネルスタックに退避され、再び実行状態に戻るとそのコンテキストをCPU上に読み込む。
1.2 プロセスとは? - Linux Kernel Documents Wiki - Linux Kernel Documents - OSDN1.3 プロセス切り替え - Linux Kernel Documents Wiki - Linux Kernel Documents - OSDNを参考に執筆しました。)

開発には英語力が必要

Linuxカーネルの開発には英語力が必要である。
カーネル内部のコメントも、
メンテナンスされているドキュメントも、
全て英語で書かれている。
この英語が読めれば、きちんと開発できるようになる。