コース内容#
IPC—— プロセス間通信
—— 共有メモリ ——#
- 父親が子供を持つ前に共有するデータの位置を約束する
- 関連インターフェース:shm*
shmget#
System V タイプの共有メモリセグメントを割り当てる [セグメントメモリ]
[PS] System V の通信方式は、まだ使用されている;その起動方式はすでに廃止されている
- man shmget
- プロトタイプ
- 戻り値の型:int
- 引数の型:key_t,size_t,int
- ipc→プロセス間通信
- 説明
- 渡された key と返された id は関連付けられている
- 新しい共有メモリセグメントのサイズは、整数ページサイズに対応する [切り上げ]
- 共有メモリを作成するにはフラグが必要:IPC_CREAT
- IPC_CREAT がない場合、ユーザーが key に対応するセグメントへのアクセス権を確認する
- 注意:少なくとも 9 ビットで権限を明示する必要がある、例えば 0600、前の 0 は欠かせない
- 戻り値
- 成功した場合、有効なメモリ識別子 [id] を返す;エラーの場合、-1 を返し errno を設定する
- key を使って id を取得した後、どのように対応するアドレスを見つけるのか?shmat 👇
shmat、shmdt#
共有メモリ操作 [attach,アタッチ;detach,デタッチ]
- man shmat
- プロトタイプ
- shmat の戻り値は:void *
- 説明
- shmat
- id で指定された共有メモリセグメントをアタッチして呼び出しプロセスのアドレス空間に追加する
- 各プロセスは、自分が独立した連続メモリを使用していると考える [仮想メモリ技術]
- shmaddr はアタッチアドレスを明示する:
- NULL:システムが自動的にアタッチ [一般的]
- そうでなければ、自動的に切り下げられたアドレスにアタッチ [shmflg で SHM_RND が定義されている]または手動でページ整列されたアドレスを保証する
- 成功した呼び出しは共有メモリの構造体 shmid_ds を更新する
- shm_nattach:アタッチ数。共有メモリに対応する実際の物理空間は複数のプロセスにアタッチされる可能性がある
- shmdt
- shmat の逆操作
- 操作する必要があるのは現在アタッチされているアドレスである
- shmat
- 戻り値
- 成功した場合、shmat はアドレスを返し、shmdt は 0 を返す
- エラーの場合、-1 を返し、errno を設定する
- id はシステム全体で一意であり、同じ id は必ず同じ物理メモリに対応する
- 【注意】同じ id が shmat で返されるアドレスは異なる、これは異なるプロセスで独立したアドレス空間として表現される [仮想メモリの概念]
- さて、shmget で key から id を取得し、shmat で id からメモリアドレスを取得するが、【key】はどのように得られるのか?👇
ftok#
パス名とプロジェクト識別子を System V IPC の key に変換する
- man ftok
- プロトタイプ
- 変換を完了するにはパス名と int 型の変数が必要
- 説明
- ファイルは存在し、アクセス可能である必要がある
- proj_id は少なくとも 8 ビットの有効ビットを持ち、0 であってはいけない
- 同じファイル名と proj_id は同じ戻り値に対応する
- したがって、固定の入力パラメータに対して、返される key は固定である
- 戻り値
- 成功した場合、key を返す;失敗した場合、-1 を返し errno を設定する
shmctl#
共有メモリの制御
- man shmctl
- プロトタイプ
- cmd:コマンド、int 型。予測できるのは大文字のマクロ定義のいくつか
- 説明
- shmid_ds 構造体の詳細情報を見ることができる
- 一部のコマンド
- IPC_STAT:shmid_ds 構造体の情報を buf にコピーする [読み取り権限が必要]
- IPC_SET:shmid_id 構造体を変更する
- IPC_RMID:セグメントが削除されることをマークする
- 共有メモリセグメントがアタッチされていない場合にのみ、セグメントは実際に削除される
- buf 変数は必要なく、NULL に設定するだけでよい
- [PS] 本当に削除されたかどうかを確認する必要がある、さもなければ残留物が残る可能性がある [戻り値で確認できる]
- IPC_INFO [Linux 特有、互換性のため、できるだけ使用を避ける]
- 戻り値
- 一般的に:0、成功 [INFO、STAT を除く];-1、エラー
—— スレッド関連 ——#
スレッドライブラリから [pthread]
ミューテックス、条件変数
[PS] マルチスレッド、高並列の場合、共有メモリを使用する際はミューテックスを考慮する必要がある
pthread_mutex_*#
【ミューテックス】操作 mutex
- man pthread_mutex_init など
- lock:lock を使用する際、すでにロックされているかどうかを判断し、ロックされている場合は保留されるまで待機する [ブロックが発生する可能性のある場所]
- init:動的初期化方式で、属性変数を使用する必要がある
- 属性インターフェース:pthread_mutexattr_*
- init:初期化
- 初期化関数では、attr は出力パラメータで、戻り値は 0
- setpshared:プロセス間共有を設定する
- pshared 変数は int 型で、実際にはフラグである
- 0:プロセス間プライベート;1:プロセス間共有 → それぞれマクロ PTHREAD_PROCESS_PRIVATE、PTHREAD_PROCESS_SHARED に対応
- init:初期化
- ミューテックスの基本操作:属性変数を作成 👉 ミューテックスを初期化 👉 ロック / アンロック操作、詳細はコードデモを参照 —— 共有メモリ [ミューテックス]
pthread_cond_*#
【条件変数】条件を制御する
- man pthread_cond_init など
- 構造は mutex に似ている
- 条件変数は同期デバイスの一種で、スレッドがプロセッサリソースを解放して待機し、特定の条件が満たされるまで待機することを許可する
- 基本操作:条件信号を送信し、条件信号を待機する
- ⭐必ずミューテックス mutex に関連付ける必要がある、競合条件を回避するため:あるスレッドが条件を準備する前に、別のスレッドがすでに信号を送信している可能性があるため、メッセージが失われることがある
- init:属性変数を使用して初期化できる [ただし、実際には Linux スレッド実装では属性変数は無視されている]
- signal:待機中のスレッドの中の1つだけを再開する;待機中のスレッドがいない場合は何も起こらない
- broadcast:すべての待機中のスレッドを再開する
- wait
- ① mutex をアンロックし、条件変数がアクティブになるのを待つ [真の原子操作]
- ② wait が戻る前に [スレッドが再開する前 / 信号を受け取る前]、mutex を再びロックする
- [PS]
- wait を呼び出すとき、ミューテックスはロックされている必要がある
- 待機中は CPU を消費しない
- ❗ 図の黄色い枠の部分
- wait で mutex をアンロックして待機する間 [原子操作]、他のスレッドは信号を送信できない
- ❓ この時点で mutex はすでにアンロックされているため、この原子操作にはミューテックスのようなものが必要かもしれない
- ❓ wait が先に待機の準備をしてから ->mutex をアンロックするのが正常なロジックであり、上でアンロックしてから原子操作を行うのは、先にアンロックすることに何か意味があるのか
コードデモ#
共有メモリ [ロックなし]#
5 つのプロセスが 1~10000 の累積和をデモする
- ❗ ここでは子プロセスが親プロセスから継承した共有メモリアドレス share_memory を直接使用している [仮想アドレス]
- 見ると、異なるプロセスの同じ仮想アドレスはすべて同じ共有メモリを指している [物理メモリ]
- 子プロセスが継承した shmat と id を通じて共有アドレスを取得すると、子プロセス内の新しい仮想アドレスに対応するが、同じく同じ共有メモリを指しており、元の継承された共有アドレス share_memory も同様に使用可能
- ❗ 共有メモリを使用した後、実際に削除する方法
- shmdt を使用してすべてのプロセスでアタッチされた共有メモリをデタッチする [実験によると、このステップがなくても、プロセス終了時に自動的にデタッチされる]
- その後、shmctl を使用して IPC_RMID と組み合わせて共有メモリセグメントを削除する [shmid を削除]
- [PS]
- 各プロセスは 1 つのアタッチをカウントし、アタッチが 0 になると、shmctl は実際にメモリセグメントを削除する
- shmget の IPC_EXCL フラグを削除して、すでに存在するかどうかを確認しないこともできるが、根本的な方法ではない
- 上記の shmctl 操作を行わない場合
- 最初の実行は問題なく、共有メモリセグメントが正常に作成されたことを示す
- しかし、2 回目の実行ではファイルがすでに存在すると表示される
- 前回の実行後、共有メモリは自動的に削除されず、2 回目の実行でも同じ key を使用して共有メモリを作成している [毎回同じ key に対応する shmid は異なる]
- [探索プロセス]
- ipcs:IPC 関連のリソース情報を表示する
- 削除されていない共有メモリ、メッセージキュー、セマフォを見ることができる
- それらの key、id、権限 perms も見ることができる
- それらの nattch がすべて 0 であることも確認でき、すでにアタッチされていないことを示す
- ipcrm:IPC リソースを削除する
- パラメータの使用は --help で確認する
- key または id を使用してリソースを削除できる
- 手動でリソースを削除してからプログラムを実行する:
ipcs | grep [owner] | awk '{print $2}' | xargs ipcrm -m && ./a.out
- ipcs:IPC 関連のリソース情報を表示する
- ❗ エラーの本質的な原因 [個人的理解]
- 1~10000 の正しい累積和は、50005000 であるべきである
- 複数のプロセスが同時に操作する共有メモリ [または] 1 つのプロセスの読み書き操作が完全に完了する前に、別のプロセスが新しい操作を開始した場合。例えば:
- 1 つのプロセスが now++ を実行し、メモリに書き込むとき、別のプロセスが新しい now を取得して再び ++ し、加算された now と古い sum を加算する
- つまり、now++ と sum 加算の 2 つの操作は原子操作ではなくなる
- しかし、一般的に CPU の速度が非常に速いため、ほとんどの場合原子操作が行われる [now++ と sum 加算のメモリへの書き込み]、エラーが発生する可能性は非常に低い
- マルチコアは単一コアよりも計算エラーが発生しやすい、なぜなら 1 つのプロセッサは同時に 1 つのプロセスしか実行できないから
- [PS]
- 権限を設定する際、0600 の最初の 0 は 8 進数を使用することを示す
- usleep (100):プロセスを 100ms 休止させ、1 つのプロセスが 1 回計算するごとにブロックし、他のプロセスが続行できるようにする、単に分業を示すため
- ヘッダーファイルは man マニュアルに従って自分で追加する
共有メモリ [ミューテックス]#
メモリ内でより効率的なロック、ファイルロックに似ている 例に行くの思想
- ミューテックスを作成:属性変数を作成👉ミューテックスを初期化
- プロセス間でミューテックスを使用するための 2 つの条件
- ① ミューテックス変数を共有メモリに置き、各プロセスに共有して、各プロセスを制御する
- ② 親プロセスでミューテックスを作成する際、作成された属性変数をプロセス共有に設定する [デフォルトではスレッド間でのみ機能する]
- ただし、現在のいくつかのカーネルではこの操作を行わなくても動作する可能性があるが、互換性のために設定することをお勧めする ❗
- [PS]
- 累積和を計算した後、ロックを解除することを忘れないでください
- システムで最も遅い操作は IO 操作であるため、printf の前にロックを解除し、ミューテックスを早く開放することで、より効率的になる
- fflush を使用してバッファを手動でフラッシュし、出力の混乱を避ける
共有メモリ [条件変数]#
各プロセスが 100 回ずつ計算する
- cond 条件変数の初期化は、mutex ミューテックスに似ている
- Linux スレッドの実装では、実際には attr 属性変数は必要ない;mutex も同様
- 単一コアマシンの場合、条件信号を送信する前に少し usleep する必要があり、子プロセスが wait の準備をするのを保証する。さもなければ、
- 親プロセスは優先的に実行され、信号を送信するが、この時点で子プロセスが実行される順番ではないため、信号を見逃すことになる
- 子プロセス間でも同様で、信号を送信する前に他の子プロセスが wait 状態に達するのを先に実行する必要がある [実際には usleep でも保証できない]
- ❗ 注意
- wait の前にロックされた mutex が必要
- 各操作の後にロック解除 + 信号送信操作を忘れないでください [100 回の計算 / 10000 の累積完了]
- 信号送信操作の順序には 2 つの方法があります:
- ① ロック —— 信号送信 —— ロック解除
- 欠点:待機スレッドは信号を受け取るためにカーネルから再開される [→ユーザーモード]、しかしロック可能な mutex がないため、残念ながら再び [ユーザーモードから] カーネル空間に戻るまで待機する
- 2 回のコンテキストスイッチ [カーネルモードとユーザーモード間] が性能を損なう。
- 利点:
- スレッドの優先度を保証する [❓]
- さらに、Linux スレッドの実装では、cond_wait キューと mutex_lock キューがあるため、ユーザーモードに戻ることはなく、したがって性能の損失はない
- 欠点:待機スレッドは信号を受け取るためにカーネルから再開される [→ユーザーモード]、しかしロック可能な mutex がないため、残念ながら再び [ユーザーモードから] カーネル空間に戻るまで待機する
- ② ロック —— ロック解除 —— 信号送信 [この記事ではこの方法を採用]
- 利点:cond_wait キュー内のスレッドがロック可能な mutex を使用できることを保証する
- 欠点:低優先度のスレッドが mutex を取得する可能性がある
- 参考:条件変数 pthread_cond_signal、pthread_cond_wait——CSDN
- ① ロック —— 信号送信 —— ロック解除
- 実装効果
- 各プロセスが 100 回ずつ計算する
- [PS] usleep であってもメッセージの欠落を完全に回避することはできず、信号の発信を記録するために共有メモリ内の変数を使用することができる;さらに、偽の再開が発生する可能性もある
- 総合的な解決方法は、偽の再開 && メッセージの欠落を参照 ——CSDN
if (!count) { // ->メッセージの欠落 [まだwait状態でない]
pthread_mutex_lock(&lock);
while (condition_is_false) { // ->偽の再開 [同時に複数が再開]
pthread_cond_wait(&cond, &lock);
}
//...
pthread_mutex_unlock(&lock);
}
簡易チャットルーム#
- chat.h
- ユーザー名 + メッセージ + 共有メモリの標準装備
- 1.server.c
- ロジックは前のコードデモと基本的に似ている;後のクリア操作に注意、主にデータの安全のため
- 2.client.c
- 41 行目の while ループの役割に注目:クライアントがロックを奪うのを防ぎ、サーバーが signal を受け取れないようにする
- 隠れたリスクが存在する:あるクライアントがロックを取得できず、ブロックが発生する
- [注意] gcc で 2.client.c をコンパイルする際、必ず - lpthread を追加すること;さもなければエラーは出ないが、実行時にサーバーが信号を受け取れない
- 効果デモ
- 左がサーバー、右が 2 つのクライアント
追加知識点#
- 親子関係のあるプロセス:親子間、兄弟間
考察点#
- プロセス間で計算を奪い合う VS. 各プロセスが 100 回ずつ計算する、どちらの方法がより効率的か?
- 後者の方が効率的で、極端に考えると、1 つのプロセスがすべてを計算する方がより効率的である
- これは CPU のスループットの問題に関係しており、このような累積は CPU 集約型であり、IO 集約型ではない
- 4 高度なプロセス管理 ——プロセッサ制約型と IO 制約型プロセスを参照
Tips#
- IPC 関連のリソース情報を表示するコマンド:ipcs
- ユーザースレッドの場合、1 つのプロセス内の 1 つのスレッドがクラッシュすると、プロセス内のすべてのスレッドがクラッシュする
- 映画の推薦:《ダンス・ウィズ・ウルブズ》 1990
- 異なる文化を体験し、多くの静かで美しいシーンがある
- https://pan.baidu.com/s/1hqxmTAYyd3iCOyGdCTPmYw
- 提取コード:yqed