すべてのプログラミング言語(少なくとも私が使用する言語)では、ファイルを開いてから読み取りまたは書き込みを行う必要があります。
しかし、このオープン操作は実際に何をしますか?
典型的な機能のマニュアルページは、実際には「読み取り/書き込みのためにファイルを開く」以外のことを教えてくれません:
http://www.cplusplus.com/reference/cstdio/fopen/
https://docs.python.org/3/library/functions.html#open
明らかに、この関数を使用することで、ファイルへのアクセスを容易にする何らかのオブジェクトの作成を含むことがわかります。
これを置く別の方法は、open
関数を実装する場合、Linuxで何をする必要があるかということです。
ほとんどすべての高級言語では、ファイルを開く関数は、対応するカーネルシステムコールのラッパーです。他の派手なこともできますが、現代のオペレーティングシステムでは、ファイルを開くときは常にカーネルを通過する必要があります。
これが、fopen
ライブラリ関数の引数、またはPythonのopen
がopen(2)
システムコールの引数によく似ている理由です。
ファイルを開くことに加えて、これらの関数は通常、結果的に読み取り/書き込み操作で使用されるバッファーを設定します。このバッファの目的は、Nバイトを読み取りたいときはいつでも、基礎となるシステムコールの呼び出しがより少ない値を返すかどうかに関係なく、対応するライブラリ呼び出しがNバイトを返すようにすることです。
私は実際に自分の機能を実装することに興味はありません。必要に応じて、「言語を超えて」地獄がどうなっているのかを理解するだけです。
Unixライクなオペレーティングシステムでは、open
の呼び出しに成功すると、ユーザープロセスのコンテキストでは単なる整数である「ファイル記述子」が返されます。その結果、この記述子は、開かれたファイルとやり取りするすべての呼び出しに渡され、close
を呼び出した後、記述子は無効になります。
open
の呼び出しは、さまざまなチェックが行われる検証ポイントのように機能することに注意することが重要です。すべての条件が満たされていない場合、記述子の代わりに-1
を返すことで呼び出しが失敗し、エラーの種類がerrno
に示されます。重要なチェックは次のとおりです。
カーネルのコンテキストでは、プロセスのファイル記述子と物理的に開かれたファイルの間に何らかのマッピングが必要です。記述子にマップされる内部データ構造には、ブロックベースのデバイスを扱うさらに別のバッファー、または現在の読み取り/書き込み位置を指す内部ポインターが含まれる場合があります。
このガイドではopen()
システムコールの簡易版 をご覧になることをお勧めします。次のコードスニペットを使用します。これは、ファイルを開いたときに舞台裏で行われることを表しています。
0 int sys_open(const char *filename, int flags, int mode) {
1 char *tmp = getname(filename);
2 int fd = get_unused_fd();
3 struct file *f = filp_open(tmp, flags, mode);
4 fd_install(fd, f);
5 putname(tmp);
6 return fd;
7 }
簡単に説明すると、このコードの機能は次のとおりです。
filp_open
関数には実装があります
struct file *filp_open(const char *filename, int flags, int mode) {
struct nameidata nd;
open_namei(filename, flags, mode, &nd);
return dentry_open(nd.dentry, nd.mnt, flags);
}
次の2つのことを行います。
struct file
を作成して返します。この構造体は、前述したオープンファイルのリストのエントリになります。返された構造体をプロセスの開いているファイルのリストに保存(「インストール」)します。
read()
、write()
、close()
などのファイル操作関数に渡すことができます。これらのそれぞれはカーネルに制御を渡し、カーネルはファイル記述子を使用してプロセスのリスト内の対応するファイルポインターを検索し、そのファイルポインターの情報を使用して実際に読み取り、書き込み、またはクローズを実行できます。野心的であれば、この単純化された例を、Linuxカーネルのopen()
システムコールの実装、 do_sys_open()
と呼ばれる関数と比較できます。類似点を見つけるのに問題はないはずです。
もちろん、これはopen()
を呼び出したときに起こることの「最上位層」にすぎません-より正確には、ファイルを開くプロセスで呼び出されるカーネルコードの最高レベルの部分です。高度なプログラミング言語では、この上に追加のレイヤーが追加される場合があります。下位レベルでは多くのことが行われます。 (説明のために Ruslan および pjc5 に感謝します。)おおまかに、上から下へ:
open_namei()
およびdentry_open()
は、カーネルの一部でもあるファイルシステムコードを呼び出して、ファイルおよびディレクトリのメタデータとコンテンツにアクセスします。 filesystem はディスクから生のバイトを読み取り、それらのバイトパターンをファイルとディレクトリのツリーとして解釈します。/dev/sda
などを使用してブロックデバイスレイヤーから生データにアクセスできます。)これは キャッシングのため多少不正確 である可能性もあります。 :-Pしかし、真剣に、私が省いた多くの詳細があります-このプロセス全体がどのように機能するかを記述する複数の本を書くことができます(私ではありません)。しかし、それはあなたにアイデアを与えるはずです。
あなたが話したいファイルシステムやオペレーティングシステムは私で大丈夫です。いいね!
ZX Spectrumでは、LOAD
コマンドを初期化すると、システムがタイトループに入り、Audio In行が読み取られます。
データの開始は一定のトーンで示され、その後に長い/短いパルスのシーケンスが続きます。短いパルスはバイナリ0
用で、長いパルスはバイナリ1
用です( https:// en.wikipedia.org/wiki/ZX_Spectrum_software )。タイトロードループは、バイト(8ビット)が一杯になるまでビットを収集し、これをメモリに保存し、メモリポインタを増やし、ループしてさらにビットをスキャンします。
通常、ローダーが最初に読み込むのは、短い固定フォーマットheaderで、少なくとも予想されるバイト数と、場合によっては追加情報を示しますファイル名、ファイルタイプ、ロードアドレスとして。この短いヘッダーを読み取った後、プログラムはデータのメインバルクのロードを続行するか、ロードルーチンを終了してユーザーに適切なメッセージを表示するかを決定できます。
ファイルの終了状態は、予想されるバイト数(ソフトウェアに固定されたバイト数、またはヘッダーに示されているような可変数)を受信することで認識できます。一定の時間、ロードループが予想される周波数範囲でパルスを受信しなかった場合、エラーがスローされました。
この回答の背景
説明されている手順では、通常のオーディオテープからデータをロードします。したがって、オーディオ入力をスキャンする必要があります(テープレコーダに標準プラグで接続されています)。 LOAD
コマンドは技術的にはopen
ファイルと同じですが、物理的にファイルをロードするに結び付けられています。これは、テープレコーダーがコンピューターによって制御されておらず、ファイルを(正常に)開くことはできませんが、ロードできないためです。
「タイトループ」とは、(1)CPU、Z80-A(メモリが提供される場合)が3.5 MHzであり、(2)Spectrumに内部クロックがないためです!つまり、すべてのT-states(命令時間)のカウントを正確に保持する必要がありました。シングル。命令。そのループ内で、正確なビープ音のタイミングを維持するためだけに。
幸いなことに、その低いCPU速度には、1枚の用紙のサイクル数を計算できるという明確な利点があり、したがって、実際にかかる時間を計算できます。
ファイルを開いたときの動作はオペレーティングシステムによって異なります。以下に、ファイルを開いたときに何が起こるかを示し、詳細に興味がある場合はソースコードを確認できるので、Linuxで何が起こるかを説明します。この回答が長くなりすぎるため、権限については説明しません。
Linuxでは、すべてのファイルは inode と呼ばれる構造によって認識されます。各構造には一意の番号があり、すべてのファイルは1つのiノード番号のみを取得します。この構造は、ファイルのメタデータ(ファイルサイズ、ファイル許可、タイムスタンプ、ディスクブロックへのポインターなど)を保存しますが、実際のファイル名自体は保存しません。各ファイル(およびディレクトリ)には、ファイル名エントリと検索用のiノード番号が含まれています。関連する権限があると仮定して、ファイルを開くと、ファイル名に関連付けられた一意のiノード番号を使用してファイル記述子が作成されます。多くのプロセス/アプリケーションが同じファイルを指すことができるため、inodeには、ファイルへのリンクの総数を保持するリンクフィールドがあります。ファイルがディレクトリに存在する場合、そのリンクカウントは1です。ハードリンクがある場合、そのリンクカウントは2になり、ファイルがプロセスによって開かれる場合、リンクカウントは1ずつ増加します。
簿記、ほとんど。これには、「ファイルが存在しますか?」などのさまざまなチェックが含まれます。 「書き込み用にこのファイルを開く権限がありますか?」.
しかし、それはすべてカーネルのものです-独自のおもちゃのOSを実装している場合を除き、掘り下げることはあまりありません(もし楽しんでいるなら、それは素晴らしい学習体験です)。もちろん、ファイルを開いているときに受け取る可能性のあるすべてのエラーコードを学習して、適切に処理できるようにする必要があります。
コードレベルで最も重要な部分は、開いているファイルにhandleを提供することです。これは、ファイルに対して行う他のすべての操作に使用します。この任意のハンドルの代わりにファイル名を使用できませんでしたか?確かに-しかし、ハンドルを使用するといくつかの利点が得られます。
read
します。ハンドルを使用してファイルの特定の「オープン」を識別することにより、同じファイルに対して複数の同時ハンドルを持ち、それぞれが独自の場所から読み取ることができます。ある意味では、ハンドルはファイルへの移動可能なウィンドウとして機能します(そして非常に便利な非同期I/O要求を発行する方法)。他にもできることはいくつかあります(たとえば、プロセス間でハンドルを共有して物理チャネルを使用するwithoutを使用します。UNIXシステムでは、ファイルはデバイスやその他のさまざまな仮想チャネルにも使用されます。したがって、これは厳密に必要というわけではありません)、しかし、それらはopen
操作自体に実際には結びついていないので、私はそれを掘り下げるつもりはありません。
実際には空想を読むためにオープンするときの核心でneeds起こります。必要なのは、ファイルが存在することを確認し、アプリケーションにそれを読み取るための十分な特権があり、ファイルに読み取りコマンドを発行できるハンドルを作成することだけです。
実際の読み取り値がディスパッチされるのは、これらのコマンドです。
OSは、多くの場合、読み取り操作を開始してハンドルに関連付けられたバッファーをいっぱいにすることにより、読み取りを開始します。その後、実際に読み取りを行うと、ディスクIOで待機するのではなく、バッファの内容をすぐに返すことができます。
書き込み用に新しいファイルを開くには、OSが新しい(現在空の)ファイルのディレクトリにエントリを追加する必要があります。そして再び、書き込みコマンドを発行できるハンドルが作成されます。
基本的に、openを呼び出すにはファイルを見つけてから、必要なものをすべて記録して、後でI/O操作で再び見つけられるようにする必要があります。それは非常にあいまいですが、すぐに考えられるすべてのオペレーティングシステムに当てはまります。詳細はプラットフォームごとに異なります。すでに多くの回答があり、現代のデスクトップオペレーティングシステムについて説明しています。 CP/Mで少しプログラミングを行ったので、CP/Mでどのように動作するかについての知識を提供します(MS-DOSはおそらく同じように動作しますが、セキュリティ上の理由から、通常はこのように行われません) )。
CP/Mには、FCBと呼ばれるものがあります(Cについて述べたように、構造体と呼ぶことができます。実際には、さまざまなフィールドを含むRAMの35バイトの連続した領域です)。 FCBには、ファイル名とディスクドライブを識別する(4ビット)整数を書き込むフィールドがあります。次に、カーネルのOpen Fileを呼び出すときに、CPUのレジスタの1つに配置することにより、この構造体へのポインタを渡します。しばらくすると、オペレーティングシステムが戻り、構造体がわずかに変更されます。このファイルに対して行うI/Oが何であれ、この構造体へのポインタをシステムコールに渡します。
CP/MはこのFCBで何をしますか?特定のフィールドを独自に使用するために予約し、これらを使用してファイルを追跡するため、プログラム内からこれらのフィールドに触れないでください。 Open File操作は、FCBにあるものと同じ名前のファイルをディスクの先頭にあるテーブルで検索します(ワイルドカード文字「?」は任意の文字に一致します)。ファイルが見つかった場合、ディスク上のファイルの物理的な場所などの情報をFCBにコピーします。これにより、後続のI/O呼び出しが最終的にこれらの場所をディスクドライバーに渡すBIOSを呼び出します。このレベルでは、詳細は異なります。