Linuxシステムプログラミング(1): ファイルI/O

参考文献

1.UNIXの基礎知識

1.1 UNIX アーキテクチャ (下図を参照)

  • 厳密には、オペレーティングシステムは、コンピュータのハードウェア資源を制御し、プログラムの実行環境を提供するソフトウェアと定義でき、比較的小規模で環境の中核に位置するため、通常はカーネルと呼ばれます
    • カーネルとのインターフェースはシステムコールと呼ばれます(下図の斜線部分)
    • パブリック関数ライブラリはシステムコールインタフェース上に構築されており、アプリケーションはパブリック関数ライブラリまたはシステムコールのどちらかを使用できます。
    • シェルは、他のアプリケーションを実行するためのインターフェイスを提供する特別なアプリケーションです。

ここに画像の説明を挿入します

1.2 ファイルとディレクトリ

1.2.1 ファイルシステム

  • UNIX ファイル システムは、ディレクトリとファイルの階層構造です。すべての開始点は、ルートと呼ばれるディレクトリです。このディレクトリの名前は、文字「/」です。
  • ディレクトリは、ディレクトリ エントリを含むファイルです。論理的には、各ディレクトリ エントリには、ファイル名そのファイルの属性を説明する情報が 含まれていると考えることができます。
    • ファイル属性とは、ファイルの種類(通常のファイルであるかディレクトリであるかなど)、ファイル サイズ、ファイルの所有者、ファイルのアクセス許可(他のユーザーがファイルにアクセスできるかどうか)、ファイルの最終変更時刻などを指します。
    • stat 関数と fstat 関数は、すべてのファイル属性を含む情報構造を返します。

1.2.2 ファイル名

  • ディレクトリ内のそれぞれの名前はファイル名 (ファイル名) と呼ばれます。
    • ファイル名に使用できる文字は、スラッシュ (/) とヌル文字の 2 文字だけです。
    • スラッシュはパス名を構成するファイル名を区切るために使用され、ヌル文字はパス名の終了に使用されます。
  • 移植性を高めるため、POSIX.1 ではファイル名を文字セット (a~z、A~Z)、数字 (0~9)、ピリオド (.)、ダッシュ (-)、およびアンダースコア (_) に制限することを推奨しています。
  • 新しいディレクトリを作成すると、. (ドットと呼ばれる) と... (ドットと呼ばれる) という 2 つのファイル名が自動的に作成されます。
    • ドットは現在のディレクトリを指し、ドットは親ディレクトリを指します。
    • ルート ディレクトリの最上位では、ドット ドットはドットと同じです

1.2.3 パス名

  • スラッシュで区切られた 1 つ以上のファイル名のシーケンス (スラッシュで始めることもできます) がパス名 (pathmamme) になります。
    • スラッシュで始まるパス名は絶対パス名であり、それ以外の場合は 相対パス名 と呼ばれます。相対パス名は、現在のディレクトリからの相対パス名を指します。
    • ファイルシステムのルート名 (/) は、ファイル名を含まない特別な絶対パス名です。

1.2.4 作業ディレクトリ

  • 各プロセスには作業ディレクトリがあり、現在の作業ディレクトリとも呼ばれます。すべての相対パス名は作業ディレクトリから解釈されます。プロセスは chdir 関数を使用して作業ディレクトリを変更できます。
  • 相対パス名 doc/memo/joe は、現在の作業ディレクトリの doc ディレクトリ内のメモ ディレクトリ内のファイル (またはディレクトリ) joe を参照します。
    • パス名から、doc と memo は両方ともディレクトリであることがわかりますが、joe がファイルであるかディレクトリであるかは区別できません。
  • パス名/urs/lib/lint は絶対パス名で、ルート ディレクトリの usr ディレクトリの lib ディレクトリにあるファイル (またはディレクトリ) lint を参照します。

1.3 入力と出力

1.3.1 ファイル記述子

  • ファイル記述子は通常、特定のプロセスによってアクセスされているファイルを識別するためにカーネルが使用する小さな非負の整数ですカーネルが既存のファイルを開くか、新しいファイルを作成すると、ファイル記述子が返されます。

1.3.2 標準入力、標準出力、標準エラー

  • 新しいプログラムが実行されるたびに、すべてのシェルはそのプログラムの 3 つのファイル記述子、つまり標準入力、標準出力、および標準エラーを開きます。

1.3.3 バッファなし I/O

  • open、read、write、lseek、および close 関数は、バッファなし I/O を提供します。これらの関数はファイル記述子を使用します。

1.3.4 标准 I/O

  • 標準 I/O 関数は、これらのバッファなし I/O 関数にバッファ付きインターフェイスを提供します。最もよく知られている標準 I/O 関数は printf です。

1.4 手順とプロセス

1.4.1 手順

  • プログラムは、ディスク上のディレクトリに保存されている実行可能ファイルですカーネルは exec 関数を使用してプログラムをメモリに読み込み、プログラムを実行します。

1.4.2 プロセスとプロセス ID

  • プログラムの実行インスタンスはプロセスと呼ばれ、一部のオペレーティング システムではタスクを使用して実行中のプログラムを表します。
  • UNIX システムでは、各プロセスがプロセス ID と呼ばれる一意の数値識別子を持っていることが保証されます。プロセス ID は常に負ではない整数です。

1.4.3 プロセス制御

  • プロセス制御には、fork、exec、waitpid の 3 つの主な関数があります (exec 関数には 7 つのバリエーションがありますが、これらは総称して exec 関数と呼ばれることがよくあります)。

1.4.4 スレッドとスレッド ID

  • 通常、プロセスには制御スレッドが 1 つだけあります。つまり、特定の時間に実行される一連の機械命令ですいくつかの問題は、制御の複数のスレッドがその異なる部分で動作している場合に、はるかに簡単に解決できます。さらに、複数の制御スレッドは、マルチプロセッサ システムの並列機能を最大限に活用することもできます。
  • プロセス内のすべてのスレッドは、同じアドレス空間、ファイル記述子、スタック、およびプロセス関連の属性を共有しますスレッドは同じストレージ領域にアクセスできるため、不一致を避けるために共有データへのアクセスを同期する必要があります。
  • プロセスと同様に、スレッドも ID によって識別されますただし、スレッドは、それが属するプロセス内でのみ機能します。あるプロセスのスレッド ID は、別のプロセスでは意味を持ちません。プロセス内の特定のスレッドで作業する場合、スレッドの ID を使用してそのスレッドを参照できます。

1.5 エラー処理

  • UNIX システム関数でエラーが発生すると、通常は負の値が返され、整数変数 errno には特定の情報を含む値が設定されます。一部の関数は、エラー時に負の値を返す代わりに、別の規則を使用します。たとえば、オブジェクトへのポインタを返すほとんどの関数は、エラーが発生すると null ポインタを返します。
  • POSIX.1 および ISO C では、errno を変更可能な整数の左辺値に展開されるシンボルとして定義しています。
    • エラー番号を含む整数、またはエラー番号へのポインタを返す関数を指定できます。
  • スレッドをサポートする環境では、複数のスレッドがプロセス アドレス空間を共有し、各スレッドには独自のローカル errno があり、あるスレッドが別のスレッドに干渉するのを防ぎます。
  • errno については 2 つのルールに注意する必要があります。
    • まず、エラーが発生しない場合、その値はルーチンによってクリアされません。したがって、関数の値は、戻り値がエラーを示している場合にのみチェックされます。
    • 2 番目: errno 値を 0 に設定する関数はなく、<errno.h> で定義されているすべての定数は 0 ではありません。

1.6 ユーザーの識別

1.6.1 ユーザーID

  • パスワード ファイル エントリのユーザー ID は、システムに対してそれぞれの異なるユーザーを識別する数値です。システム管理者は、ユーザーのログイン名を決定すると同時に、ユーザーのユーザー ID を決定します。ユーザーは自分のユーザー ID を変更できません。通常、各ユーザーは一意のユーザー ID を持っています。
  • ユーザー ID 0 のユーザーは、ルート ユーザー (root) またはスーパーユーザー (superuser) ですパスワードファイルには通常、ログイン名がrootであるログイン項目があり、このユーザーの権限をスーパーユーザー権限と呼びます。特定のオペレーティング システム機能は、システムを自由に制御できるスーパー ユーザーのみが使用できます。

1.6.2 グループID

  • パスワード ファイル エントリには、数値であるユーザーのグループ ID も含まれます。グループ ID は、ユーザーのログイン名を指定するときにシステム管理者によっても割り当てられます。一般に、パスワード ファイルには同じグループ ID を持つ複数のログインが存在しますグループは、ユーザーをプロジェクトまたは部門にグループ化するために使用されます。このメカニズムにより、同じグループのメンバー間でリソースを共有できます。
  • グループ ファイルは、グループ名を数値のグループ ID にマップします。通常、グループ ファイルは /etc/group です。
  • ファイル システムは、ディスク上のファイルごとに、ファイルの所有者のユーザー ID とグループ ID を保存します。これら 2 つの値を保存するには 4 バイトだけが必要です (それぞれが 2 バイトの整数値として保存されていると仮定します)権限の検証中、文字列の比較は整数の比較よりも時間がかかります。
  • ただし、ユーザーにとっては数字よりも名前を使用する方が便利なので、パスワード ファイルにはログイン名とユーザー ID の間のマッピング関係が含まれ、グループ ファイルにはグループ名とグループ D の間のマッピング関係が含まれます。

1.7 信号

  • シグナルは、何かが起こったことをプロセスに通知するために使用されますたとえば、プロセスが除算演算を実行し、その除数が 0 の場合、SIGEPE (浮動小数点例外) という名前のシグナルがプロセスに送信されます。プロセスにはシグナルを処理する次の 3 つの方法があります。

    • (1)信号を無視します一部の信号は、0 による除算やプロセス アドレス空間外のストレージ ユニットへのアクセスなどのハードウェア例外を示します。これらの例外の結果は不確実であるため、この処理方法は推奨されません。
    • (2)システムのデフォルトの方法に従って処理します除数が 0 の場合、システムのデフォルトの方法はプロセスを終了することです。
    • (3)シグナルの発生時に呼び出される関数 (シグナルのキャッチと呼ばれる) を提供します独自に作成した関数を提供することで、シグナルがいつ生成されたかを知り、それを希望の方法で処理できます。
  • シグナルはさまざまな状況で発生する可能性があります。端末のキーボードで信号を生成するには 2 つの方法があります

    • 現在実行中のプロセスを中断するために使用される割り込みキー (通常は Delete キーまたは Crl+C) と終了キー (通常は Ctrl+\) です。
    • kill 関数を呼び出しますあるプロセスからこの関数を呼び出すと、別のプロセスにシグナルが送信されます。もちろん、これにはいくつかの制限があります。プロセスにシグナルを送信するときは、そのプロセスの所有者またはスーパーユーザーである必要があります。

1.8 時間値

  • UNIX システムでは 2 つの異なる時刻値が使用されています
    • (1)カレンダー時間この値は、協定世界時 (UTC) (初期のマニュアルでは UTC をグリニッジ標準時と呼んでいます) の 1970 年 1 月 1 日の 00:00:00 の特定の時刻から経過した累積秒数です。これらの時間値は、ファイルの最終変更時間などを記録するために使用できます。
      • この時間値を保存するには、システムの基本データ型 time_t が使用されます。
    • (2)処理時間CPU 時間とも呼ばれ、プロセスによって使用される CPU リソースを測定します。処理時間はクロック単位で測定されます。かつては 1 秒を 50、60、または 100 クロック刻みとして捉えていました。
      • システムの基本データ型 Clock_t には、この時刻値が格納されます。
  • プロセスの実行時間を測定する場合、UNIX システムはプロセスの 3 つのプロセス時間値を維持します。
    • 時刻
      • クロック時間は実時間とも呼ばれ、プロセスが実行される合計時間であり、その値はシステム内で同時に実行されるプロセスの数に関係します。
    • ユーザーのCPU時間
      • ユーザー CPU 時間は、ユーザー命令の実行に費やした時間です。
    • システムCPU時間
      • システム CPU 時間は、プロセスがカーネル プログラムを実行するのにかかる時間です。
      • ユーザー CPU 時間とシステム CPU 時間の合計は、CPU 時間と呼ばれることがよくあります。

1.9 システムコールとライブラリ関数

  • システムコールとは何ですか?

    • オペレーティング システムによって実装され、外部アプリケーションに提供されるアプリケーション プログラミング インターフェイス (API) は、アプリケーションとシステム間のデータ対話のブリッジです。
    • すべてのオペレーティング システムは、プログラムがカーネルにサービスを要求するためのさまざまなサービスのエントリ ポイントを提供します。UNIX 実装のさまざまなバージョンでは、明確に定義された限られた数のエントリ ポイントがカーネルに直接提供されており、これらのエントリ ポイントはシステム コールと呼ばれます。
  • 汎用ライブラリ関数は 1 つ以上のカーネル システム コールを呼び出すことがありますが、それらはカーネルへのエントリ ポイントではありません。

    • たとえば、printf 関数は write システム コールを呼び出して文字列を出力します。
    • ただし、関数 strcpy (文字列のコピー) と atoi (ASCII を整数に変換) はカーネル システム コールを使用しません。
  • システム コールとライブラリ関数はどちらも C 関数の形式をとり、アプリケーションにサービスを提供します。

    • ライブラリ関数は置き換えることができますが、システムコールは通常置き換えることができません
    • 通常、システム コールは最小限のインターフェイスを提供しますが、ライブラリ関数は通常、より複雑な機能を提供します。
  • C標準ライブラリ関数とシステム関数・呼び出し関係:画面に「hello」を出力する例

    • システム コールは、システム関数 (マニュアル ページの関数) の浅いカプセル化に相当します。

ここに画像の説明を挿入します

2. UNIX の標準と実装

2.1 UNIXの標準化

2.1.1 IOS C

  • ISO C 標準は現在、ISO/TEC の C プログラミング言語国際標準ワーキング グループによって維持および開発されており、このワーキング グループは ISO/IEC JTC1/SC22/WG14 (略して WG14) と呼ばれています。ISO C 標準の目的は、UNIX システムだけでなく、多数の異なるオペレーティング システムに C プログラムの移植性を提供することです。
  • ISO C 標準で定義されたヘッダー ファイル

ここに画像の説明を挿入します

2.1.2 IEEE POSIX.1

  • POSIX.1 は、もともと IEEE (電気電子技術者協会) によって開発された標準ファミリーです。POSIX.1 は、ポータブル オペレーティング システム インターフェイスを指します当初は IEEE 標準 1003.1-1988 (オペレーティング システム インターフェイス) のみを参照していましたが、後に拡張され、シェルやユーティリティなど、1003 とマークされた多くの標準およびドラフト標準 (1003.2、このチュートリアルでは 1003.1 を使用します) が含まれるようになりました。
    • 1003.1 標準では実装ではなくインターフェイスを指定しているため、システム コールとライブラリ関数は区別されず、標準内のすべてのルーチンは関数と呼ばれます。
  • POSIX.1 標準で定義されている必須ヘッダー ファイル

ここに画像の説明を挿入します

2.2 UNIX システムの実装

2.2.1 4.4 BSD

  • BSD (Berkeley Sofware Distribution) は、カリフォルニア大学バークレー校のコンピューター システム研究グループによって開発および配布されました。4.2BSD は 1983 年にリリースされ、4.3BSD は 1986 年にリリースされ、4.4BSD は 1994 年にリリースされました。

2.2.2 FreeBSD

  • FreeBSD は 4.4BSD-Lite オペレーティング システムに基づいています。カリフォルニア大学バークレー校のコンピュータ システム研究グループが UNIX オペレーティング システムの BSD バージョンの研究開発作業を終了することを決定し、386BSD プロジェクトが長い間無視された後、FreeBSD プロジェクトが設立されました。 BSD シリーズにこだわり続けます。

2.2.3 リナックス

  • Linux は 1991 年に MNIX の代替として Linus Torvalds によって開発されました。
  • Linux は、UNIX に似た豊富なプログラミング環境を提供するオペレーティング システムであり、GNU Public License の指導のもとで無料で使用できます。

2.2.4 Mac OS X

  • Mac OS X は、以前のバージョンとはまったく異なるテクノロジーを使用しています。そのコア オペレーティング システムは「Darwin」と呼ばれ、Mach カーネル、FreeBSD オペレーティング システム、およびオブジェクト指向フレームワークおよびその他のカーネル拡張機能を備えたドライバーの組み合わせに基づいています。

2.2.5 ソラリス

  • Solaris は、Sun Microsystems (現 Oracle) によって開発された UNIX のバージョンです。

2.3 基本的なシステムデータ型

  • 特定の実装関連のデータ型は、ヘッダー ファイル <sys/types.h> で定義されており、基本システム データ型と呼ばれます。

  • よく使用されるいくつかの基本的なシステム データ型

ここに画像の説明を挿入します

3. 文件 I/O

3.1 はじめに

  • 利用可能なファイル I/O 機能: ファイルを開く (オープン)、ファイルの読み取り (読み取り)、ファイルの書き込み (書き込み) など。
  • UNIX システムのほとんどのファイル I/O は、open、read、write、lseek、close の 5 つの関数のみを必要とします。

この章で説明する関数は、(標準 I/O 関数とは対照的に)アンバッファーI/O と呼ばれることがよくあります。

  • バッファなしとは、読み取りと書き込みのそれぞれがカーネル内のシステム コールを呼び出すことを意味します。
  • これらのバッファなし I/O 関数は ISO C の一部ではありませんが、POSIX1 の一部です。

3.2 ファイル記述子

  • カーネルにとって、開いているすべてのファイルはファイル記述子によって参照されます。

    • ファイル記述子は負ではない整数です
    • 既存のファイルを開くとき、または新しいファイルを作成するときに、カーネルはファイル記述子をプロセスに返します。
    • ファイルの読み取りまたは書き込み時には、open または create によって返されたファイル記述子を使用してファイルを識別し、それを読み取りまたは書き込みのパラメーターとして渡します。
  • 慣例により、UNIX システム シェル

    • ファイル記述子 0 はプロセスの標準入力に関連付けられています
    • ファイル記述子 1 はプロセスの標準出力に関連付けられています
    • ファイル記述子 2 はプロセスの標準エラーに関連付けられています
  • POSIX.1 準拠のアプリケーションでは、マジックナンバー 0、1、2 は標準化されていますが、可読性を向上させるために記号定数 STDIN_FILENO、STDOUT_FILENO、および STDERR_FILENO に置き換える必要があります。これらの定数はヘッダー ファイル <unistd.h> で定義されます。

ファイル記述子はファイル構造へのポインタ
ですPCB プロセス制御ブロック: 本質的には構造体であり、そのメンバーはファイル記述子テーブルです

ここに画像の説明を挿入します

3.3 関数 open および openat (ファイルを開くまたは作成する)

3.3.1 関数オープンとパラメータ分析でのオープン

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>  // 定义 flags 参数

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // 仅当创建新文件时才使用第三个参数,表明文件权限

int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
  • pathname: 開くか作成するファイルのパス名
  • flags: この関数の複数のオプションを記述するために使用されます。次の 1 つ以上の定数を使用して「OR」演算を実行し、flags パラメータを形成します。
    • O_RDONLY (読み取り専用にオープン)、O_WRONLY (書き込み専用にオープン)、O_RDWR (読み取りおよび書き込み専用にオープン)、O_EXEC (実行専用にオープン)、O_SEARCH (検索専用にオープン、ディレクトリに使用)
    • O_APPEND (書き込むたびにファイルの末尾に追加)
    • O_CREAT (このファイルが存在しない場合は作成し、3 番目のパラメータ mode と一緒に使用します)
    • O_EXCL (O_CREATも指定され、ファイルがすでに存在する場合はエラーが発生します)
    • O_NONBLOCK (このファイルを開く操作と後続の I/O 操作にノンブロッキング モードを設定します)
    • O_TRUNC (このファイルが存在し、書き込み専用または読み書き可能で正常に開かれた場合、その長さを 0 に切り捨てます)
  • 関数の戻り値
    • 成功した場合は、ファイル記述子を返します。
    • エラーが発生した場合は-1が返されます
  • dirfd パラメータは、open 関数と openat 関数を区別します。3 つの可能性があります。
    • path パラメータに絶対パス名を指定した場合、dirfd パラメータは無視され、openat 関数は open 関数と同等になります。
    • path パラメータは相対パス名を指定し、dirfd パラメータはファイル システム内の相対パス名の開始アドレスを指定します。dirfd パラメータは、相対パス名が存在するディレクトリを開くことによって取得されます。
    • path パラメータは相対パス名を指定し、dirfd パラメータには特別な値 AT_FDCWD が設定されます。この場合、パス名は現在の作業ディレクトリ内で取得され、openat 関数の動作は open 関数と同様です。
  • openat 関数は、POSIX.1 の最新バージョンの新関数の 1 つであり、2 つの問題を解決することを目的としています。
    • まず、スレッドが現在の作業ディレクトリを開くだけでなく、相対パス名を使用してディレクトリ内のファイルを開くことができるようにします。
      • 同じプロセス内のすべてのスレッドは同じ現在の作業ディレクトリを共有するため、同じプロセスの複数の異なるスレッドを異なるディレクトリで同時に動作させることは困難です。
    • 2 番目に、チェックから使用までの時間 (TOCTTOU) エラーを回避できます。
      • TOCTTOU バグの基本的な考え方は、ファイルベースの関数呼び出しが 2 つあり、2 番目の呼び出しが最初の呼び出しの結果に依存する場合、プログラムは脆弱であるということです2 つの呼び出しはアトミックな操作ではないため、2 つの関数呼び出しの間でファイルが変更された可能性があり、これにより最初の呼び出しの結果が無効になり、プログラムの最終結果が不正確になります。

3.3.2 ファイル名とパス名の切り詰め

  • POSIX.1 では、定数 _POSIX_NO_TRUNC は、長すぎるファイル名またはパス名を切り詰めるか、エラーを返すかを決定しますファイル システムのタイプに応じて、この値は異なる場合があります。fpathconf または pathconf を使用して、ディレクトリがサポートする動作の種類を問い合わせることができます。長すぎるファイル名を切り詰めるか、エラーを返すか?
  • _POSIX_NO_TRUNC が有効な場合、パス名全体が PATH_MAX を超えるか、パス名内のいずれかのファイル名が NAME_MAX を超えると、エラーが返され、errno が ENAMETOOLONG に設定されます。

3.4 関数クローズ(開いているファイルを閉じる)

#include <unistd.h>

int close(int fd);
  • 関数の戻り値

    • 成功した場合は 0 を返します
    • エラーが発生した場合は-1が返されます
  • ファイルを閉じると、プロセスがファイルに対して保持していたすべてのレコード ロックも解除されます。

  • プロセスが終了すると、カーネルは開いているすべてのファイルを自動的に閉じます。多くのプログラムは、close でファイルを明示的に閉じることなく、この機能を利用します。

3.5 関数create(新規ファイルの作成)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int creat(const char *pathname, mode_t mode);
  • 関数の戻り値

    • 成功した場合は、書き込み専用に開かれたファイル記述子を返します。
    • エラーが発生した場合は-1が返されます
  • この関数は次と同等です

    open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
    

creat の欠点の 1 つは、作成されたファイルが書き込み専用として開かれることです新しいバージョンの open が提供される前は、一時ファイルを作成し、そのファイルに書き込み、その後ファイルから読み取る場合は、creat、close、open を呼び出す必要がありました。これで、上記の方法でオープン実装を呼び出すことができます

3.3-3.5 の場合

ケース1

// open.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    fd = open("./AUTHORS.txt", O_RDONLY);
    printf("fd = %d\n", fd);
    
    close(fd);	
    
    return 0;
}
$ gcc open.c -o open

$ ./open
# 输出如下,表示文件存在并正确打开
fd = 3

ケース2

// open2.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT, 0644); // rw-r--r--
    printf("fd = %d\n", fd);

    close(fd);

    return 0;
}
$ gcc open2.c -o open2

$ ./open2
fd = 3

$ ll 
# 创建了一个新文件 AUTHORS.cp,且文件权限对应于 0644
-rw-r--r-- 1 yue yue    0 9月  10 22:19 AUTHORS.cp

ケース3

// open3.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    // 如果文件存在,以只读方式打开并且截断为 0
    // 如果文件不存在,则把这个文件创建出来并指定权限为 0644
    fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // rw-r--r--
    printf("fd = %d\n", fd);

    close(fd);

    return 0;
}
$ gcc open3.c -o open3

$ ./open3
# 输出如下,表示文件存在并正确打开
fd = 3

$ ll 
# 首先在 AUTHORS.cp 文件中输入内容,然后经过 O_TRUNC 截断后为 0
-rw-r--r-- 1 yue yue    0 9月  10 22:19 AUTHORS.cp

ケース4

  • ファイルを作成するときにファイルのアクセス許可モードを指定します。この許可は umask にも影響されます。結論は
    • ファイル権限 = モード & ~umask
$ umask
0002 # 表明默认创建文件权限为 ~umask = 775(第一个 0 表示八进制)
// open4.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    fd = open("./AUTHORS.cp2", O_RDONLY | O_CREAT | O_TRUNC, 0777); // rwxrwxrwx
    printf("fd = %d\n", fd);

    close(fd);

    return 0;
}
$ gcc open4.c -o open4

$ ./open4
fd = 3

$ ll 
# 创建了一个新文件 AUTHORS.cp2,且文件权限为 mode & ~umask = 775(rwxrwxr-x)
-rwxrwxr-x 1 yue yue    0 9月  10 22:38 AUTHORS.cp2*

事例5

  • オープン関数でよくあるエラー
    • 開いているファイルが存在しません
    // open5.c
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd;
    
        fd = open("./AUTHORS.cp4", O_RDONLY);
        printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
    
        close(fd);
    
        return 0;
    }
    
    $ gcc open5.c -o open5
    
    $ ./open5
    fd = -1, errno = 2 : No such file or directory
    
    • 読み取り専用ファイルを書き込みモードで開きます (ファイルを開くための対応する権限はありません)。
    // open6.c
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd;
    
        fd = open("./AUTHORS.cp3", O_WRONLY); // AUTHORS.cp3 文件权限为只读
        printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
    
        close(fd);
    
        return 0;
    }
    
    $ gcc open6.c -o open6
    
    $ ./open6
    fd = -1, errno = 13 : Permission denied
    
    • 書き込み専用にディレクトリを開く
    $ mkdir mydir # 首先创建一个目录
    
    // open7.c
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd;
    
        fd = open("mydir", O_WRONLY);
        printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
    
        close(fd);
    
        return 0;
    }
    
    $ gcc open7.c -o open7
    
    $ ./open7
    fd = -1, errno = 21 : Is a directory
    

3.6 関数 lseek (開いているファイルのオフセットを明示的に設定)

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
  • 開いている各ファイルには、それに関連付けられた「現在のファイル オフセット」があり、これは通常、ファイルの先頭から数えたバイト数を示す負ではない数値です。

  • lseek の l は長整数型を表します

  • 関数の戻り値

    • 成功した場合は、新しいファイルのオフセットを返します
    • エラーが発生した場合は-1が返されます
  • システムのデフォルトでは、ファイルが開かれるとき、O_APPEND オプションが指定されていない限り、このオフセットは 0 に設定されます。

  • パラメータ オフセットの解釈は、パラメータの値に関係します。

    • wherece が SEEK_SET の場合、ファイルのオフセットは、ファイルの先頭からのオフセット バイトに設定されます。
      • SEEK_SET(0) 絶対オフセット
    • wherece が SEEK_CUR の場合、ファイルのオフセットは、現在の値にオフセットを加えた値に設定されます。オフセットは正または負の値を指定できます。
      • SEEK_CUR(1) 現在位置を基準としたオフセット
    • wherece が SEEK_END の場合、ファイルのオフセットは、ファイルの長さにオフセットを加えたものに設定されます。オフセットは正または負の値を指定できます。
      • SEEK_END (2) ファイルの末尾を基準としたオフセット
  • lseek はカーネル内の現在のファイル オフセットを記録するだけであり、I/O 操作は発生しません。このオフセットは次の読み取りまたは書き込み操作に使用されます。

  • ファイル オフセットは、ファイルの現在の長さよりも大きくすることができます。その場合、ファイルへの次回の書き込みによりファイルが長くなり、ファイルに穴が作成されますが、これは許可されます。ファイル内に存在するが書き込まれていないバイトは 0 として読み取られます。

ケース1

  • ファイルの読み取りと書き込みでは同じオフセット位置が使用されます
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    
    int main(void) {
          
          
        int fd, n;
        char msg[] = "It's a test for lseek\n";
        char ch;
    
        fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);
        if (fd < 0) {
          
          
            perror("open lseek.txt error");
            exit(1);
        }
    
        // 使用 fd 对打开的文件进行写操作,读写位置位于文件结尾处
        write(fd, msg, strlen(msg));
        // 若注释下行代码,由于文件写完之后未关闭,读、写指针在文件末尾,所以不调节指针,直接读取不到内容
        lseek(fd, 0, SEEK_SET); // 修改文件读写指针位置,位于文件开头
    
        while ((n = read(fd, &ch, 1))) {
          
          
            if (n < 0) {
          
          
                perror("read error");
                exit(1);
            } 
            write(STDOUT_FILENO, &ch, n);  // 将文件内容按字节读出,写出到屏幕
        }
    
        close(fd);
    
        return 0;
    }
    

ケース2

  • lseekを使用してファイルサイズを取得します
    // lseek_size.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd = open(argv[1], O_RDWR);
        if (fd == -1) {
          
          
            perror("open error");
            exit(1);
        }
    
        int length = lseek(fd, 0, SEEK_END);
        printf("file size: %d\n", length);
    
        close(fd);
    
        return 0;
    }
    
    $ gcc lseek_size.c -o lseek_size
    $ ./lseek_size fcntl.c  # fcntl.c 文件大小为 678
    678
    

ケース3

  • lseekを使用してファイルサイズを拡張する
    • ファイル サイズを実際に拡張するには、IO 操作を発生させる必要があります。
    // 修改案例 2 中下行代码(扩展 111 大小)
    // 这样并不能真正扩展,使用 cat 命令查看文件大小未变化
    int length = lseek(fd, 111, SEEK_END);
    
    // 在 printf 函数下行写如下代码(引起 IO 操作)
    write(fd, "\0", 1); // 结果便是在扩展的文件尾部追加文件空洞
    
  • truncate 機能を使用してファイルを直接拡張できます
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    
    int main(int argc, char*argv[]) {
          
          
        int ret = truncate("dict.cp", 250);
        printf("ret = %d\n", ret);
    
        return 0;
    }
    

lseek によって読み取られるファイル サイズは、常にファイル ヘッダーを基準にしています。lseek を使用してファイル サイズを読み取ると、実際には、読み取りポインターと書き込みポインターの最初の位置と最後の位置の間のオフセットの差が使用されます。新しく開かれたファイルの場合、読み取りポインターと書き込みポインターの最初の位置はファイルの先頭になります。これを使用してファイルサイズを拡張する場合は、IO が発生するため、少なくとも 1 文字を書き込む必要があります。

3.7 関数読み取り(開いているファイルからデータを読み取る)

#include <unistd.h>

// ssize_t 表示带符号整型;void* 表示通用指针
// 参数1:文件描述符;参数2:存数据的缓冲区;参数3:缓冲区大小
ssize_t read(int fd, void *buf, size_t count);
  • 関数の戻り値
    • 読み込みに成功した場合は読み取ったバイト数が返され、ファイルの終端に達した場合は0が返されます。
    • エラーが発生した場合は-1が返されます
    • -1 が返され、errno = EAGIN または EWOULDBLOCK の場合は、読み取りが失敗したのではなく、読み取りが非ブロッキング方式でデバイス ファイル/ネットワーク ファイルを読み取り、ファイルにデータがないことを意味します。
  • 実際に読み取られたバイト数が、要求された読み取りバイト数よりも少ないさまざまな状況が考えられます。
    • 1. 通常のファイルを読み込む場合、必要なバイト数を読み込む前にファイルの終端に到達します。
      • たとえば、ファイルの終わりに達するまであと 30 バイトがあり、100 バイトを読み取る必要がある場合、read は 30 を返します。次回 read が呼び出されるとき、0 (ファイルの終わり) が返されます。
    • 2. 端末デバイスから読み取る場合、通常は一度に最大 1 行が読み取られます。
    • 3. ネットワークから読み取る場合、ネットワークのバッファリング メカニズムにより、戻り値が読み取りに必要なバイト数よりも少なくなる場合があります。
    • 4. パイプまたは FIFO から読み取るときに、パイプに含まれるバイト数が必要なバイト数に満たない場合、読み取りは使用可能な実際のバイト数のみを返します。
    • 5. 一部のレコード指向のデバイス (テープなど) から読み取る場合、一度に返されるレコードは最大 1 つです
    • 6. 信号により割り込みが発生し、データの一部が読み出された場合

3.8 関数書き込み(開いているファイルにデータを書き込む)

#include <unistd.h>

// 参数1:文件描述符;参数2:待写出数据的缓冲区;参数3:数据大小
ssize_t write(int fd, const void *buf, size_t count);
  • 関数の戻り値

    • 書き込みが成功すると、書き込まれたバイト数が返されます(通常、戻り値はパラメータのカウント値と同じですが、それ以外の場合はエラーが発生します)。
    • エラーが発生した場合は-1が返されます
  • 書き込みエラーの一般的な理由は、ディスクがいっぱいであるか、特定のプロセスのファイル長制限を超えていることです。

  • 通常のファイルの場合、書き込みはファイルの現在のオフセットから開始されます。ファイルを開くときに O_APPEND オプションが指定されている場合、各書き込み操作の前に、ファイル オフセットがファイルの現在の末尾に設定されます。書き込みが成功すると、ファイル オフセットは実際に書き込まれたバイト数だけ増分されます。

ブロッキングとノンブロッキング

  • ブロック: プロセスがブロッキング システム関数を呼び出すと、プロセスはスリープ状態になります。このとき、カーネルは、プロセスが待機しているイベント (ネットワーク上でのデータ パケットの受信など) が発生するまで、他のプロセスが実行されるようにスケジュールします。 ). 、または sleep の呼び出しで指定されたスリープ時間が経過した場合)、実行を継続することが可能です。スリープ状態の反対は実行状態です。Linux カーネルでは、実行状態のプロセスは 2 つの状況に分けられます。

    • 実行が予定されていますCPU はプロセスのコンテキスト内にあります。プログラム カウンタはプロセスの命令アドレスを格納します。汎用レジスタはプロセスの動作の中間結果を格納します。CPU はプロセスの命令を実行し、プロセスのアドレス空間を読み書きしています。プロセス。
    • 準備完了状態このプロセスはイベントの発生を待つ必要がなく、いつでも実行できますが、CPU は現在別のプロセスを実行しているため、プロセスはカーネルによってスケジュールされる準備完了キューで待機しています。
  • 通常のファイルの読み取りはブロックされません。何バイト読み取っても、限られた時間内に必ず読み取りが返されます。端末デバイスまたはネットワークからの読み取りは必ずしも当てはまりません。端末から入力されたデータに改行文字が含まれていない場合、端末デバイスを読み取るために read を呼び出すとブロックされます。ネットワーク上でデータ パケットが受信されない場合、read を呼び出します。ネットワークからの読み取りはブロックされますが、ブロックされるかどうかについては、どれくらいの時間がかかるのかも不明です。データが到着しない場合は、そこでブロックされたままになります。同様に、通常のファイルへの書き込みはブロックされませんが、端末デバイスまたはネットワークへの書き込みはブロックされません。

    • /dev/tty – 端末ファイル

ブロック読み取り端子

// block_readtty.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    
    
    char buf[10];
    int n;
    
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0){
    
    
        perror("read STDIN_FILENO");
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    
    return 0;
}
$ gcc block_readtty.c -o block
$ ./block  # 此时程序在阻塞等待输入,下面输入 hello 后回车即结束
hello
hello

ノンブロッキング読み取り端子

// nonblock_readtty.c
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"

int main(void) {
    
    
    char buf[10];
    int fd, n, i;
    
    // 设置 /dev/tty 非阻塞状态(默认为阻塞状态)
    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); 
    if(fd < 0) {
    
    
        perror("open /dev/tty");
        exit(1);
    }
    printf("open /dev/tty ok... %d\n", fd);

    for (i = 0; i < 5; i++) {
    
    
        n = read(fd, buf, 10);
        if (n > 0) {
    
      // 说明读到了东西
            break;
        }
        if (errno != EAGAIN) {
    
      
            perror("read /dev/tty");
            exit(1);
        } else {
    
    
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            sleep(2);
        }
    }

    if (i == 5) {
    
    
        write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    } else {
    
    
        write(STDOUT_FILENO, buf, n);
    }

    close(fd);

    return 0;
}
$ gcc block_readtty.c -o block
$ ./block  # 此时程序在阻塞等待输入,下面输入 hello 后回车即结束
hello
hello

3.9 I/O効率

  • 読み取り/書き込み機能を使用してファイルのコピーを実装する
// 将一个文件的内容复制到另一个文件中:通过打开两个文件,循环读取第一个文件的内容并写入到第二个文件中
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    
    
    char buf[1];  // 定义一个大小为 1 的字符数组,用于存储读取或写入的数据
    int n = 0;

    // 打开第一个参数所表示的文件,以只读方式打开
    int fd1 = open(argv[1], O_RDONLY);
    if (fd1 == -1) {
    
    
        perror("open argv1 error");
        exit(1);
    }

    // 打开第二个参数所表示的文件,以可读写方式打开,如果文件不存在则创建,如果文件存在则将其清空
    int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);
    if (fd2 == -1) {
    
    
        perror("open argv2 error");
        exit(1);
    }

    // 循环读取第一个文件的内容,每次最多读取 1024 字节
    // 将返回的实际读取字节数赋值给变量 n
    while ((n = read(fd1, buf, 1024)) != 0) {
    
    
        if (n < 0) {
    
    
            perror("read error");
            break;
        }
        // 将存储在 buf 数组中的数据写入文件描述符为 fd2 的文件
        write(fd2, buf, n);
    }

    close(fd1);
    close(fd2);

    return 0;
}
  • fputc/fgetc関数を使用してファイルコピーを実装する
// 使用了 C 标准库中的文件操作函数 fopen()、fgetc() 和 fputc() 来实现文件的读取和写入
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    
    
    FILE *fp, *fp_out;
    int n = 0;
    
    fp = fopen("hello.c", "r");
    if (fp == NULL) {
    
    
        perror("fopen error");
        exit(1);
    }

    fp_out = fopen("hello.cp", "w");
    if (fp_out == NULL) {
    
    
        perror("fopen error");
        exit(1);
    }

    // 判断是否读取到文件结束符 EOF
    while ((n = fgetc(fp)) != EOF) {
    
    
        fputc(n, fp_out);  // 将读取的字符写入输出文件
    }

    fclose(fp);
    fclose(fp_out);

    return 0;
}
  • 読み取り/書き込み: バイトを書き込むたびに、カーネル モードとユーザー モードを常に切り替えることになるため、非常に時間がかかります。
  • fgetc/fputc: 4096 バッファがあるため、バイトごとに書き込まれず、カーネルとユーザーの間の切り替えが少なくなります (バッファ出力メカニズムへの事前読み取り)。

システム関数は必ずしもライブラリ関数より高速であるとは限りません。ライブラリ関数が使用できる場合は、ライブラリ関数を使用してください。
標準 I/O 関数にはユーザー バッファが付属しています。システム コールにはユーザー レベルのバッファがありません。システム バッファは使用できます。

  • さまざまなバッファ長を使用した Linux での読み取り操作の時間結果
    • ほとんどのファイル システムは、パフォーマンスを向上させるために、ある種の先読みバッファリング テクノロジを使用します。シーケンシャル読み取りが検出されると、システムは、アプリケーションがデータを迅速に読み取るものと想定して、アプリケーションが必要とするよりも多くのデータを読み取ろうとします。先読みの効果は、以下の図で見ることができます。バッファ長が 32 バイトと小さい場合のクロック時間は、バッファ長が長い場合とほぼ同じです。

ここに画像の説明を挿入します

3.10 ファイル共有

  • UNIX システムは、異なるプロセス間で開いているファイルの共有をサポートします

  • カーネルは 3 つのデータ構造を使用して開いているファイルを表し、それらの間の関係によって、ファイル共有に関して 1 つのプロセスが別のプロセスに与える影響が決まります。

    • (1) 各プロセスはプロセス テーブルにレコード エントリを持ち、レコード エントリにはオープン ファイル記述子のテーブルが含まれており、これはベクトルとみなすことができ、各記述子が 1 つのエントリを占有します。各ファイル記述子に関連付けられているものは次のとおりです。
      • ファイル記述子フラグ
      • ファイルテーブルエントリへのポインタ
    • (2) カーネルは、開いているすべてのファイルのファイル テーブルを維持します。各ファイルエントリには次の内容が含まれます
      • ファイルステータスフラグ(読み取り、書き込み、追加、同期、ノンブロッキングなど)
      • 現在のファイルのオフセット
      • ファイルの v ノード テーブル エントリへのポインタ
    • (3) 開いている各ファイル (またはデバイス) は v ノード構造を持っています。v ノードには、ファイル タイプへのポインタと、ファイルに対してさまざまな操作を実行するための関数が含まれています。ほとんどのファイルでは、v ノードにはファイルの i ノード (インデックス ノード) も含まれます。この情報は、ファイルを開くときにディスクからメモリに読み取られるため、ファイルに関するすべての関連情報を常に利用できます。
  • オープンファイルカーネルデータ構造

ここに画像の説明を挿入します

ファイル記述子フラグとファイル状態フラグのスコープの違い: 前者はプロセスの 1 つの記述子にのみ適用され、後者は指定されたファイル テーブル エントリを指すプロセス内のすべての記述子に適用されます。

3.11 アトミック操作

一般に、アトミック操作とは、複数のステップで構成される操作を指します操作がアトミックに実行される場合は、すべてのステップが実行されるか、何も実行されないかのいずれかになります。すべてのステップのサブセットのみを実行することはできません。

3.11.1 ファイルに追加する

  • ファイルの末尾にデータを追加するプロセスを考えてみましょう。

    • 単一プロセスの場合、このプログラムは正常に動作しますが、複数のプロセスがこの方法を使用して同じファイルに同時にデータを追加すると、問題が発生します。
    if(lseek(fd, OL, 2) < 0)
        err_sys("lseek error");
    if(write(fd, buf, 100) != 100)
        err_sys("write error");
    
  • A と B という 2 つの独立したプロセスがあり、どちらも同じファイルに追加しているとします。各プロセスは、O_APPEND フラグを使用せずにファイルをオープンしました。

    • この時点で、各プロセスには独自のファイル エントリがありますが、v-node エントリを共有します。
    • プロセス A が lseek を呼び出し、これによりプロセス A のファイルの現在のオフセットが 1500 バイト (現在のファイルの最後) に設定されるとします。
    • 次に、カーネルはプロセスを切り替え、プロセス B は lseek を実行し、ファイルへの現在のオフセットを 1500 バイト (現在のファイルの末尾) に設定します。
    • 次に、B は write を呼び出します。これにより、B の現在のファイル オフセットが 1600 に増加します。ファイルの長さが増加したため、カーネルは v-node の現在のファイルの長さを 1600 に更新します。
    • その後、カーネルはプロセス切り替えを実行して、プロセス A の動作を再開します。A が書き込みを使用すると、現在のファイル オフセット (1500) からファイルへのデータの書き込みが開始され、プロセス B によってファイルに書き込まれたばかりのデータが上書きされます。

    問題は、2 つの別々の関数呼び出しを使用する「最初にファイルの末尾を見つけてから書き込み」という論理操作にあります。

    • 回避策: これら 2 つの操作を他のプロセスのアトミック操作にしますカーネルは関数呼び出しの間にプロセスを一時的に中断する可能性があるため、複数の関数呼び出しを必要とする操作はアトミックではありません。
    • UNIX システムでは、このような操作に対してアトミック メソッドが提供されており、ファイルを開くときに O_APPEND フラグを設定します。これを行うと、カーネルは各書き込み操作の前にプロセスの現在のオフセットをファイルの末尾に設定するため、各書き込み操作の前に lseek を呼び出す必要がなくなります。

3.11.2 関数 pred および pwrite

#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
  • pread関数の戻り値

    • 成功した場合は読み取ったバイト数が返され、ファイルの最後まで読み取った場合は0が返されます。
    • エラーが発生した場合は-1が返されます
  • pwrite関数の戻り値

    • 成功した場合は、書き込まれたバイト数を返します
    • エラーが発生した場合は-1が返されます
  • pred の呼び出しは、 lseek を呼び出してから read を呼び出すことと同じですが、 pred には、この連続呼び出しとは次の重要な違いがあります。

    • pread を呼び出す場合、その位置決めおよび読み取り操作を中断することはできません。
    • 現在のファイルのオフセットを更新しない

3.12 関数 dup および dup2 (既存のファイル記述子をコピー)

#include <unistd.h>

// dup 主要起一个保存副本的作用
int dup(int oldfd);
// dup2 = dupto 将 oldfd 复制给 newfd,返回 newfd
int dup2(int oldfd, int newfd);

// cmd: F_DUPFD
// 可变参数 3:
    // 被占用的,返回最小可用的
    // 未被占用的,返回 = 该值的文件描述符
int fcntl(int fd, int cmd, ...)
  • 関数の戻り値

    • 成功した場合は、新しいファイル記述子を返します。
    • エラーが発生した場合は-1が返されます
  • dup によって返される新しいファイル記述子は、現在利用可能なファイル記述子の中で最も少ない数でなければなりません。

  • dup2 の場合、newfd パラメータを使用して新しい記述子の値を指定できます。

    • newfd がすでに開いている場合は、最初にそれを閉じます
    • oldfd = newfd の場合、dup2 は閉じずに newfd を返します。
    • それ以外の場合、newfd の FD_CLOEXEC ファイル記述子フラグはクリアされ、プロセスが exec を呼び出すときに newfd がオープンされます。
  • 記述子をコピーするもう 1 つの方法は、fcntl 関数を使用することです。次の関数呼び出しは同等です。

    dup(oldfd);
    fcntl(oldfd, F_DUPFD, 0);
    
    // 以下情况并不完全等价
    // (1) dup2 是一个原子操作,而 close 和 fcnt1 包括两个函数调用
        // 有可能在 close 和 fcntl 之间调用了信号捕获函数,它可能修改文件描述符
    // (2) dup2 和 fcntl 有一些不同的 errno
    dup2(oldfd, newfd);
    
    close(newfd);
    fcntl(oldfd, F_DUPFD, newfd);
    

ダップケース

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

// argc 表示参数个数,argv[] 是参数列表
int main(int argc, char *argv[]) {
    
    
    // 只读方式打开 argv[1] 指定的文件
    int oldfd = open(argv[1], O_RDONLY);       // 012  --- 3

    // 创建一个新的文件描述符 newfd,并与 oldfd 指向同一文件,最后返回新的文件描述符
    int newfd = dup(oldfd);    // 4

    printf("newfd = %d\n", newfd);

	return 0;
}

dup2の場合

  • 既存のファイル記述子 fd1 を別のファイル記述子 fd2 にコピーし、fd2 を使用して fd1 が指すファイルを変更します。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <pthread.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd1 = open(argv[1], O_RDWR);       // 012  --- 3
        int fd2 = open(argv[2], O_RDWR);       // 0123 --- 4
    
        // fd2 指向 fd1
        int fdret = dup2(fd1, fd2);         // 返回 新文件描述符 fd2
        printf("fdret = %d\n", fdret);
    
        // 打开一个文件,读写指针默认在文件头:如果写入的文件是非空的,写入的内容默认从文件头部开始写,会覆盖原有内容
        int ret = write(fd2, "1234567", 7); // 写入 fd1 指向的文件
        printf("ret = %d\n", ret);
    
        // 将输出到 STDOUT 的内容重定向到文件里
        dup2(fd1, STDOUT_FILENO);           // 将屏幕输入,重定向给 fd1 所指向的文件
    
        printf("---------886\n");
    
    	return 0;
    }
    

fcntlケース

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    
    
    int fd1 = open(argv[1], O_RDWR);

    printf("fd1 = %d\n", fd1);

    // 参数 3:传入一个文件描述符 k,如果 k 没被占用,则直接用 k 复制 fd1 的内容。如果 k 被占用,则返回描述符表中最小可用描述符
    // 0 被占用,fcntl 使用文件描述符表中可用的最小文件描述符返回
    int newfd = fcntl(fd1, F_DUPFD, 0);
    printf("newfd = %d\n", newfd);

    // 7 未被占用,返回 = 该值的文件描述符
    int newfd2 = fcntl(fd1, F_DUPFD, 7);
    printf("newfd2 = %d\n", newfd2);

    int ret = write(newfd2, "YYYYYYY", 7);
    printf("ret = %d\n", ret);

    return 0;
}
$ gcc ls-R.c -o fcntl2
$ ./fcntl2 mycat.c
fd1 = 3
newfd = 4
newfd2 = 7
ret = 7

3.13 関数 sync、fsync、fdatasync

  • 従来の UNIX システム実装では、カーネル内にバッファ キャッシュまたはページ キャッシュがあり、ほとんどのディスク I/O はバッファを通じて実行されます。
    • データをファイルに書き込むとき、カーネルは通常、最初にデータをバッファにコピーし、それからキューに入れ、後でディスクに書き込みます。この方法は遅延書き込みと呼ばれます。
    • 通常、カーネルは、他のディスク ブロック データ用にバッファを再利用する必要がある場合、すべての遅延書き込みデータ ブロックをディスクに書き込みます。
    • ディスク上の実際のファイル システムとバッファ内のコンテンツの一貫性を確保するために、UNIX システムは 3 つの関数 (sync、fsync、および fdatasync) を提供します。
#include <unistd.h>

int fsync(int fd);
int fdatasync(int fd);

void sync(void);
  • 関数の戻り値
    • 成功した場合は 0 を返します
    • エラーが発生した場合は-1が返されます
  • sync は、変更されたすべてのブロック バッファを書き込みキューに入れてから戻ります。実際のディスク書き込み操作が終了するのを待ちません。
    • update と呼ばれるシステム デーモンは、同期関数を定期的に (通常は 30 秒ごとに) 呼び出します。これにより、カーネルのブロック バッファが定期的にフラッシュされるようになります。
  • fsync 関数は、ファイル記述子 fd で指定されたファイルに対してのみ機能し、ディスク書き込み操作が完了するまで待機してから戻ります。
    • fsync は、変更されたブロックを直ちにディスクに書き込む必要があるデータベースなどのアプリケーションで使用できます。
  • fdatasync 関数は fsync に似ていますが、ファイルのデータ部分にのみ影響します。
    • データに加えて、fsync はファイル属性も同期的に更新します

3.14 関数fcntl(開いているファイルの属性変更)

#include <unistd.h>
#include <fcntl.h>

// 参数 3 可以是整数或指向一个结构的指针
int fcntl(int fd, int cmd, ... /* int arg */ );
  • 関数の戻り値
    • 成功した場合はcmdに依存します
      • 既存の記述子をコピーします: F_DUPFD または F_DUPFD_CLOEXEC、新しいファイル記述子を返します。
      • ファイル記述子フラグを取得/設定します: F_GETFD または F_SETFD、対応するフラグを返します。
      • ファイルステータスフラグの取得/設定: F_GETFL または F_SETFL、対応するフラグを返します。
      • 非同期 I/O 所有権の取得/設定: F_GETOWN または F_SETOWN、正のプロセス ID または負のプロセス グループ ID を返します。
      • レコード ロックの取得/設定: F_GETLK、F_SETLK、または F_SETLKW
    • エラーが発生した場合は-1が返されます

場合

// 终端文件默认是阻塞读的,这里用 fcntl 将其更改为非阻塞读
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MSG_TRY "try again\n"

int main(void) {
    
    
    char buf[10];
    int flags, n;

    flags = fcntl(STDIN_FILENO, F_GETFL);
    if (flags == -1) {
    
    
        perror("fcntl error");
        exit(1);
    }
    flags |= O_NONBLOCK; // 与或操作,打开 flags
    int ret = fcntl(STDIN_FILENO, F_SETFL, flags);
    if (ret == -1) {
    
    
        perror("fcntl error");
        exit(1);
    }

tryagain:
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
    
    
        if (errno != EAGAIN) {
    
    
            perror("read /dev/tty");
            exit(1);
        }
        sleep(3);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        goto tryagain;
    }
    write(STDOUT_FILENO, buf, n);

    return 0;
}

3.15 関数 ioctl

#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);
  • 関数の戻り値
    • エラーが発生した場合は-1が返されます
    • 成功した場合は、他の値を返します
  • デバイスの I/O チャネルを管理し、デバイスの特性を制御します (主にデバイス ドライバーで使用されます)
  • 通常、ファイルの物理的特性を取得するために使用されます(この特性はファイルの種類ごとに異なる値を持ちます)

3.16 受信パラメータと送信パラメータ

#include <string.h>

char* strcpy(char* dest, const char* src);
char* strcpy(char* dest, const char* src, size_t n);
  • 受信パラメータ: src

    • 関数パラメータとしてのポインタ
    • 通常は const キーワードで変更されます
    • ポインタは有効な領域を指しており、読み取り操作は関数内で実行されます。
  • 送信パラメータ: dest

    • 関数パラメータとしてのポインタ
    • 関数呼び出しの前に、ポインタが指すスペースは無意味である可能性がありますが、有効である必要があります。
    • 関数内で書き込み操作を行う
    • 関数呼び出しが完了した後、関数の戻り値として機能します
#include <string.h>

char* strtok(char* str, const char* delim);
char* strtok_r(char* str, const char* delim, char** saveptr);
  • パラメーターの受け渡しと受け渡し: saveptr
    • 関数パラメータとしてのポインタ
    • 関数呼び出しの前に、ポインタが指すスペースが実際の意味を持ちます。
    • 関数内では、最初に読み取り、次に書き込みます
    • 関数呼び出しが完了した後、関数の戻り値として機能します

おすすめ

転載: blog.csdn.net/qq_42994487/article/details/132842199