web-dev-qa-db-ja.com

ファイルを開くことは実際に何をしますか?

すべてのプログラミング言語(少なくとも私が使用する言語)では、ファイルを開いてから読み取りまたは書き込みを行う必要があります。

しかし、このオープン操作は実際に何をしますか?

典型的な機能のマニュアルページは、実際には「読み取り/書き込みのためにファイルを開く」以外のことを教えてくれません:

http://www.cplusplus.com/reference/cstdio/fopen/

https://docs.python.org/3/library/functions.html#open

明らかに、この関数を使用することで、ファイルへのアクセスを容易にする何らかのオブジェクトの作成を含むことがわかります。

これを置く別の方法は、open関数を実装する場合、Linuxで何をする必要があるかということです。

257
jramm

ほとんどすべての高級言語では、ファイルを開く関数は、対応するカーネルシステムコールのラッパーです。他の派手なこともできますが、現代のオペレーティングシステムでは、ファイルを開くときは常にカーネルを通過する必要があります。

これが、fopenライブラリ関数の引数、またはPythonのopenopen(2)システムコールの引数によく似ている理由です。

ファイルを開くことに加えて、これらの関数は通常、結果的に読み取り/書き込み操作で使用されるバッファーを設定します。このバッファの目的は、Nバイトを読み取りたいときはいつでも、基礎となるシステムコールの呼び出しがより少ない値を返すかどうかに関係なく、対応するライブラリ呼び出しがNバイトを返すようにすることです。

私は実際に自分の機能を実装することに興味はありません。必要に応じて、「言語を超えて」地獄がどうなっているのかを理解するだけです。

Unixライクなオペレーティングシステムでは、openの呼び出しに成功すると、ユーザープロセスのコンテキストでは単なる整数である「ファイル記述子」が返されます。その結果、この記述子は、開かれたファイルとやり取りするすべての呼び出しに渡され、closeを呼び出した後、記述子は無効になります。

openの呼び出しは、さまざまなチェックが行われる検証ポイントのように機能することに注意することが重要です。すべての条件が満たされていない場合、記述子の代わりに-1を返すことで呼び出しが失敗し、エラーの種類がerrnoに示されます。重要なチェックは次のとおりです。

  • ファイルが存在するかどうか。
  • 呼び出しプロセスが、指定されたモードでこのファイルを開く特権を持っているかどうか。これは、ファイル許可、所有者ID、およびグループIDを呼び出しプロセスのそれぞれのIDに一致させることによって決定されます。

カーネルのコンテキストでは、プロセスのファイル記述子と物理的に開かれたファイルの間に何らかのマッピングが必要です。記述子にマップされる内部データ構造には、ブロックベースのデバイスを扱うさらに別のバッファー、または現在の読み取り/書き込み位置を指す内部ポインターが含まれる場合があります。

181

このガイドでは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  }

簡単に説明すると、このコードの機能は次のとおりです。

  1. カーネル制御メモリのブロックを割り当て、ユーザー制御メモリからファイル名をコピーします。
  2. 未使用のファイル記述子を選択します。これは、現在開いているファイルの拡張可能なリストへの整数インデックスと考えることができます。各プロセスには独自のこのようなリストがありますが、カーネルによって維持されます。コードから直接アクセスすることはできません。リスト内のエントリには、iノード番号、プロセス許可、オープンフラグなど、基盤となるファイルシステムがディスクからバイトを取り出すために使用する情報が含まれています。
  3. 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つのことを行います。

    1. ファイルシステムを使用して、渡されたファイル名またはパスに対応するiノード(または、より一般的には、ファイルシステムが使用する内部識別子の種類)を検索します。
    2. Iノードに関する重要な情報を含むstruct fileを作成して返します。この構造体は、前述したオープンファイルのリストのエントリになります。
  4. 返された構造体をプロセスの開いているファイルのリストに保存(「インストール」)します。

  5. カーネル制御メモリの割り当てられたブロックを解放します。
  6. ファイル記述子を返します。これは、read()write()close()などのファイル操作関数に渡すことができます。これらのそれぞれはカーネルに制御を渡し、カーネルはファイル記述子を使用してプロセスのリスト内の対応するファイルポインターを検索し、そのファイルポインターの情報を使用して実際に読み取り、書き込み、またはクローズを実行できます。

野心的であれば、この単純化された例を、Linuxカーネルのopen()システムコールの実装、 do_sys_open() と呼ばれる関数と比較できます。類似点を見つけるのに問題はないはずです。


もちろん、これはopen()を呼び出したときに起こることの「最上位層」にすぎません-より正確には、ファイルを開くプロセスで呼び出されるカーネルコードの最高レベルの部分です。高度なプログラミング言語では、この上に追加のレイヤーが追加される場合があります。下位レベルでは多くのことが行われます。 (説明のために Ruslan および pjc5 に感謝します。)おおまかに、上から下へ:

  • open_namei()およびdentry_open()は、カーネルの一部でもあるファイルシステムコードを呼び出して、ファイルおよびディレクトリのメタデータとコンテンツにアクセスします。 filesystem はディスクから生のバイトを読み取り、それらのバイトパターンをファイルとディレクトリのツリーとして解釈します。
  • ファイルシステムは、カーネルの一部である ブロックデバイスレイヤー を使用して、ドライブからこれらの生のバイトを取得します。 (面白い事実:Linuxでは、/dev/sdaなどを使用してブロックデバイスレイヤーから生データにアクセスできます。)
  • ブロックデバイス層は、カーネルコードでもあるストレージデバイスドライバーを呼び出して、「セクターXの読み取り」などの中レベルの命令からマシンコード内の個々の 入出力命令 に変換します。ストレージデバイスドライバーには、 IDE(S)ATASCSIFirewire 、およびドライブが使用できるさまざまな通信規格に対応しています。 (命名は混乱していることに注意してください。)
  • I/O命令は、プロセッサチップとマザーボードコントローラの組み込み機能を使用して、物理ドライブに向かうワイヤで電気信号を送受信します。これはハードウェアであり、ソフトウェアではありません。
  • ワイヤのもう一方の端では、ディスクのファームウェア(埋め込み制御コード)が電気信号を解釈してプラッタを回転させ、ヘッドを移動(HDD)するか、フラッシュROMセル(SSD)などを読み取りますそのタイプのストレージデバイス上のデータにアクセスするために必要です。

これは キャッシングのため多少不正確 である可能性もあります。 :-Pしかし、真剣に、私が省いた多くの詳細があります-このプロセス全体がどのように機能するかを記述する複数の本を書くことができます(私ではありません)。しかし、それはあなたにアイデアを与えるはずです。

80
David Z

あなたが話したいファイルシステムやオペレーティングシステムは私で大丈夫です。いいね!


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枚の用紙のサイクル数を計算できるという明確な利点があり、したがって、実際にかかる時間を計算できます。

68
usr2564301

ファイルを開いたときの動作はオペレーティングシステムによって異なります。以下に、ファイルを開いたときに何が起こるかを示し、詳細に興味がある場合はソースコードを確認できるので、Linuxで何が起こるかを説明します。この回答が長くなりすぎるため、権限については説明しません。

Linuxでは、すべてのファイルは inode と呼ばれる構造によって認識されます。各構造には一意の番号があり、すべてのファイルは1つのiノード番号のみを取得します。この構造は、ファイルのメタデータ(ファイルサイズ、ファイル許可、タイムスタンプ、ディスクブロックへのポインターなど)を保存しますが、実際のファイル名自体は保存しません。各ファイル(およびディレクトリ)には、ファイル名エントリと検索用のiノード番号が含まれています。関連する権限があると仮定して、ファイルを開くと、ファイル名に関連付けられた一意のiノード番号を使用してファイル記述子が作成されます。多くのプロセス/アプリケーションが同じファイルを指すことができるため、inodeには、ファイルへのリンクの総数を保持するリンクフィールドがあります。ファイルがディレクトリに存在する場合、そのリンクカウントは1です。ハードリンクがある場合、そのリンクカウントは2になり、ファイルがプロセスによって開かれる場合、リンクカウントは1ずつ増加します。

16
Jaco

簿記、ほとんど。これには、「ファイルが存在しますか?」などのさまざまなチェックが含まれます。 「書き込み用にこのファイルを開く権限がありますか?」.

しかし、それはすべてカーネルのものです-独自のおもちゃのOSを実装している場合を除き、掘り下げることはあまりありません(もし楽しんでいるなら、それは素晴らしい学習体験です)。もちろん、ファイルを開いているときに受け取る可能性のあるすべてのエラーコードを学習して、適切に処理できるようにする必要があります。

コードレベルで最も重要な部分は、開いているファイルにhandleを提供することです。これは、ファイルに対して行う他のすべての操作に使用します。この任意のハンドルの代わりにファイル名を使用できませんでしたか?確かに-しかし、ハンドルを使用するといくつかの利点が得られます。

  • システムは、現在開いているすべてのファイルを追跡し、それらが削除されないようにすることができます(たとえば)。
  • 最新のOSはハンドルを中心に構築されています-ハンドルでできる便利なことがたくさんあり、すべての異なる種類のハンドルはほぼ同じように動作します。たとえば、Windowsファイルハンドルで非同期I/O操作が完了すると、ハンドルが通知されます。これにより、通知されるまでハンドルをブロックしたり、操作を完全に非同期で完了したりできます。ファイルハンドルで待機することは、スレッドハンドル(たとえば、スレッドの終了時に通知される)、プロセスハンドル(再び、プロセスの終了時に通知される)、またはソケット(非同期操作が完了するとき)で待機するのとまったく同じです。同様に重要なのは、ハンドルがそれぞれのプロセスによって所有されているため、プロセスが予期せず終了した場合(またはアプリケーションの記述が不十分な場合)、OSは解放できるハンドルを認識します。
  • ほとんどの操作は位置指定です-ファイルの最後の位置からreadします。ハンドルを使用してファイルの特定の「オープン」を識別することにより、同じファイルに対して複数の同時ハンドルを持ち、それぞれが独自の場所から読み取ることができます。ある意味では、ハンドルはファイルへの移動可能なウィンドウとして機能します(そして非常に便利な非同期I/O要求を発行する方法)。
  • ハンドルはmuchファイル名よりも小さいです。ハンドルは通常、ポインターのサイズで、通常は4または8バイトです。一方、ファイル名は数百バイトになる場合があります。
  • ハンドルを使用すると、OSはアプリケーションを開いていてもOSをmoveできます。ハンドルはまだ有効で、ファイル名が変更されていても同じファイルを指します。

他にもできることはいくつかあります(たとえば、プロセス間でハンドルを共有して物理チャネルを使用するwithoutを使用します。UNIXシステムでは、ファイルはデバイスやその他のさまざまな仮想チャネルにも使用されます。したがって、これは厳密に必要というわけではありません)、しかし、それらはopen操作自体に実際には結びついていないので、私はそれを掘り下げるつもりはありません。

11
Luaan

実際には空想を読むためにオープンするときの核心でneeds起こります。必要なのは、ファイルが存在することを確認し、アプリケーションにそれを読み取るための十分な特権があり、ファイルに読み取りコマンドを発行できるハンドルを作成することだけです。

実際の読み取り値がディスパッチされるのは、これらのコマンドです。

OSは、多くの場合、読み取り操作を開始してハンドルに関連付けられたバッファーをいっぱいにすることにより、読み取りを開始します。その後、実際に読み取りを行うと、ディスクIOで待機するのではなく、バッファの内容をすぐに返すことができます。

書き込み用に新しいファイルを開くには、OSが新しい(現在空の)ファイルのディレクトリにエントリを追加する必要があります。そして再び、書き込みコマンドを発行できるハンドルが作成されます。

7
ratchet freak

基本的に、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を呼び出します。このレベルでは、詳細は異なります。

5
Wilson