これは、Linuxのプロセス間通信(IPC)に関するシリーズの最初の記事です。このシリーズでは、Cのコード例を使用して、次のIPCメカニズムを明確にします。
- 共有ファイル
- 共有メモリ(セマフォ付き)
- パイプ(名前付きおよび名前なし)
- メッセージキュー
- ソケット
- 信号
この記事では、共有ファイルと共有メモリというこれらのメカニズムの最初の2つに進む前に、いくつかのコアコンセプトを確認します。
プロセス は実行中のプログラムであり、各プロセスには独自のアドレススペースがあり、プロセスがアクセスできるメモリ位置で構成されています。プロセスには1つ以上のスレッドがあります 実行可能命令のシーケンスである実行のシーケンス:シングルスレッド プロセスにはスレッドが1つしかないのに対し、マルチスレッド プロセスには複数のスレッドがあります。プロセス内のスレッドは、さまざまなリソース、特にアドレス空間を共有します。したがって、プロセス内のスレッドは共有メモリを介して直接通信できますが、一部の最新言語(Goなど)では、スレッドセーフチャネルの使用など、より統制のとれたアプローチが推奨されています。ここで興味深いのは、デフォルトでは、さまざまなプロセスがしないことです。 共有メモリ。
プロセスを起動して通信する方法はさまざまですが、次の例では2つの方法が主流です。
- あるプロセスを開始するために端末が使用され、別のプロセスを開始するために別の端末が使用される可能性があります。
- システム機能フォーク あるプロセス(親)内で呼び出され、別のプロセス(子)を生成します。
最初の例は、ターミナルアプローチを採用しています。コード例は、私のWebサイトのZIPファイルで入手できます。
プログラマーは、プログラムでのファイルの使用を悩ませている多くの落とし穴(存在しないファイル、ファイルのアクセス許可の誤りなど)を含め、ファイルアクセスに精通しています。それにもかかわらず、共有ファイルは最も基本的なIPCメカニズムである可能性があります。 1つのプロセス(プロデューサー )ファイルの作成と書き込み、および別のプロセス(コンシューマー )この同じファイルから読み取ります:
writes +-----------+ reads
producer-------->| disk file |<-------consumer
+-----------+
このIPCメカニズムを使用する際の明らかな課題は、競合状態 発生する可能性があります。プロデューサーとコンシューマーがまったく同時にファイルにアクセスする可能性があるため、結果が不確定になります。競合状態を回避するには、書き込み間の競合を防ぐ方法でファイルをロックする必要があります。 読み取りかどうかに関係なく、操作およびその他の操作 または書き込み 。標準システムライブラリのロッキングAPIは、次のように要約できます。
- プロデューサーは、ファイルに書き込む前に、ファイルの排他ロックを取得する必要があります。 独占 ロックは最大で1つのプロセスで保持できます。これにより、ロックが解除されるまで他のプロセスがファイルにアクセスできないため、競合状態が除外されます。
- コンシューマーは、ファイルから読み取る前に、少なくともファイルの共有ロックを取得する必要があります。複数のリーダー 共有を保持できます 同時にロックしますが、ライターはありません 単一のリーダーでもファイルにアクセスできます 共有ロックを保持します。
共有ロックは効率を高めます。 1つのプロセスがファイルを読み取っているだけで、その内容を変更していない場合、他のプロセスが同じことを行うのを妨げる理由はありません。ただし、書き込みには明らかにファイルへの排他的アクセスが必要です。
標準のI/Oライブラリには、 fcntlという名前のユーティリティ関数が含まれています。 これは、ファイルの排他ロックと共有ロックの両方を検査および操作するために使用できます。この関数は、ファイル記述子を介して機能します 、プロセス内でファイルを識別する非負の整数値。 (異なるプロセスの異なるファイル記述子は、同じ物理ファイルを識別する場合があります。)ファイルロックのために、Linuxはライブラリ関数 flock を提供します 、これは fcntlの薄いラッパーです 。最初の例では、 fcntlを使用しています APIの詳細を公開する関数。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FileName "data.dat"
#define DataString "Now is the winter of our discontent\nMade glorious summer by this sun of York\n"
void report_and_exit(const char* msg) {
perror(msg);
exit(-1); /* EXIT_FAILURE */
}
int main() {
struct flock lock;
lock.l_type = F_WRLCK; /* read/write (exclusive versus shared) lock */
lock.l_whence = SEEK_SET; /* base for seek offsets */
lock.l_start = 0; /* 1st byte in file */
lock.l_len = 0; /* 0 here means 'until EOF' */
lock.l_pid = getpid(); /* process id */
int fd; /* file descriptor to identify a file within a process */
if ((fd = open(FileName, O_RDWR | O_CREAT, 0666)) < 0) /* -1 signals an error */
report_and_exit("open failed...");
if (fcntl(fd, F_SETLK, &lock) < 0) /** F_SETLK doesn't block, F_SETLKW does **/
report_and_exit("fcntl failed to get lock...");
else {
write(fd, DataString, strlen(DataString)); /* populate data file */
fprintf(stderr, "Process %d has written to data file...\n", lock.l_pid);
}
/* Now release the lock explicitly. */
lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &lock) < 0)
report_and_exit("explicit unlocking failed...");
close(fd); /* close the file: would unlock if needed */
return 0; /* terminating the process would unlock as well */
}
プロデューサーの主な手順 上記のプログラムは次のように要約できます。
- プログラムは、タイプ struct flockの変数を宣言します 、これはロックを表し、構造体の5つのフィールドを初期化します。最初の初期化:
lock.l_type = F_WRLCK; /* exclusive lock */
ロックを排他的にします(読み取り/書き込み )共有ではなく(読み取り専用 ) ロック。 プロデューサーの場合 ロックを取得すると、プロデューサーまで、他のプロセスはファイルの書き込みまたは読み取りを行うことができなくなります。 fcntl を適切に呼び出して、明示的にロックを解除します。 または、ファイルを閉じることによって暗黙的に。 (プロセスが終了すると、開いているファイルはすべて自動的に閉じられ、ロックが解除されます。)
- 次に、プログラムは残りのフィールドを初期化します。主な効果は、全体 ファイルはロックされます。ただし、ロックAPIでは、指定されたバイトのみをロックできます。たとえば、ファイルに複数のテキストレコードが含まれている場合、1つのレコード(またはレコードの一部)をロックし、残りをロック解除したままにすることができます。
- fcntlへの最初の呼び出し :
if (fcntl(fd, F_SETLK, &lock) < 0)
呼び出しが成功したかどうかを確認しながら、ファイルを排他的にロックしようとします。一般的に、 fcntl 関数は-1を返します (したがって、ゼロ未満)失敗を示します。 2番目の引数F_SETLK fcntlへの呼び出しを意味します しない ブロック:関数はすぐに戻り、ロックを許可するか、失敗を示します。フラグがF_SETLKWの場合 ( W 最後に待つ )が代わりに使用され、 fcntlへの呼び出し ロックを取得できるようになるまでブロックします。 fcntlへの呼び出しで 、最初の引数 fd はファイル記述子であり、2番目の引数は実行するアクションを指定します(この場合、 F_SETLK ロックを設定するため)、3番目の引数はロック構造のアドレスです(この場合、&lock 。
- プロデューサーの場合 ロックを取得すると、プログラムは2つのテキストレコードをファイルに書き込みます。
- ファイルに書き込んだ後、プロデューサー ロック構造のl_typeを変更します ロック解除へのフィールド 値:
lock.l_type = F_UNLCK;
fcntlを呼び出します ロック解除操作を実行します。プログラムは、ファイルを閉じて終了することで終了します。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FileName "data.dat"
void report_and_exit(const char* msg) {
perror(msg);
exit(-1); /* EXIT_FAILURE */
}
int main() {
struct flock lock;
lock.l_type = F_WRLCK; /* read/write (exclusive) lock */
lock.l_whence = SEEK_SET; /* base for seek offsets */
lock.l_start = 0; /* 1st byte in file */
lock.l_len = 0; /* 0 here means 'until EOF' */
lock.l_pid = getpid(); /* process id */
int fd; /* file descriptor to identify a file within a process */
if ((fd = open(FileName, O_RDONLY)) < 0) /* -1 signals an error */
report_and_exit("open to read failed...");
/* If the file is write-locked, we can't continue. */
fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */
if (lock.l_type != F_UNLCK)
report_and_exit("file is still write locked...");
lock.l_type = F_RDLCK; /* prevents any writing during the reading */
if (fcntl(fd, F_SETLK, &lock) < 0)
report_and_exit("can't get a read-only lock...");
/* Read the bytes (they happen to be ASCII codes) one at a time. */
int c; /* buffer for read bytes */
while (read(fd, &c, 1) > 0) /* 0 signals EOF */
write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */
/* Release the lock explicitly. */
lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &lock) < 0)
report_and_exit("explicit unlocking failed...");
close(fd);
return 0;
}
消費者 プログラムは、ロッキングAPIの機能を強調するために必要以上に複雑です。特に、消費者 プログラムは、最初にファイルが排他的にロックされているかどうかを確認してから、共有ロックを取得しようとします。関連するコードは次のとおりです。
lock.l_type = F_WRLCK;
...
fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */
if (lock.l_type != F_UNLCK)
report_and_exit("file is still write locked...");
F_GETLK fcntlで指定された操作 呼び出しはロック(この場合は F_WRLCK として指定された排他的ロック)をチェックします 上記の最初のステートメントで。指定されたロックが存在しない場合は、 fcntl 呼び出しにより、ロックタイプフィールドが自動的に F_UNLCKに変更されます この事実を示すために。ファイルが排他的にロックされている場合、コンシューマー 終了します。 (プログラムのより堅牢なバージョンには、コンシューマーが含まれている場合があります スリープ 少し試してみてください。)
ファイルが現在ロックされていない場合は、コンシューマー 共有を取得しようとします(読み取り専用 )ロック( F_RDLCK )。プログラムを短くするには、 F_GETLK fcntlに電話する F_RDLCK が原因で、削除される可能性があります 読み取り/書き込みの場合、呼び出しは失敗します ロックはすでに他のプロセスによって保持されていました。 読み取り専用であることを思い出してください ロックは、他のプロセスがファイルに書き込むことを防ぎますが、他のプロセスがファイルから読み取ることを許可します。つまり、共有 ロックは複数のプロセスで保持できます。共有ロックを取得した後、コンシューマー プログラムは、ファイルから一度に1バイトずつ読み取り、バイトを標準出力に出力し、ロックを解除してファイルを閉じ、終了します。
これは、%を使用して同じ端末から起動された2つのプログラムからの出力です。 コマンドラインプロンプトとして:
% ./producer
Process 29255 has written to data file...
% ./consumer
Now is the winter of our discontent
Made glorious summer by this sun of York
この最初のコード例では、IPCを介して共有されるデータはテキストです。シェイクスピアの演劇からの2行リチャード3世 。それでも、共有ファイルのコンテンツは大量の任意のバイト(デジタル化された映画など)である可能性があり、ファイル共有は非常に柔軟なIPCメカニズムになります。欠点は、アクセスに読み取りまたは書き込みが含まれるかどうかに関係なく、ファイルアクセスが比較的遅いことです。いつものように、プログラミングにはトレードオフが伴います。次の例では、共有ファイルではなく共有メモリを介したIPCの利点があり、それに応じてパフォーマンスが向上します。
Linuxシステムは、共有メモリ用に2つの別個のAPIを提供します。レガシーSystemVAPIと最新のPOSIXAPIです。ただし、これらのAPIを単一のアプリケーションに混在させることはできません。 POSIXアプローチの欠点は、機能がまだ開発中であり、インストールされているカーネルバージョンに依存していることです。これは、コードの移植性に影響を与えます。たとえば、POSIX APIは、デフォルトで、共有メモリをメモリマップトファイルとして実装します。 :共有メモリセグメントの場合、システムはバッキングファイルを維持します 対応する内容で。 POSIXでの共有メモリは、バッキングファイルなしで構成できますが、これは移植性に影響を与える可能性があります。私の例では、POSIX APIとバッキングファイルを使用しています。これは、メモリアクセス(速度)とファイルストレージ(永続性)の利点を組み合わせたものです。
共有メモリの例には、 memwriterという名前の2つのプログラムがあります。 およびmemreader 、セマフォを使用します 共有メモリへのアクセスを調整します。共有メモリがライターと一緒に登場するときはいつでも 、マルチプロセッシングであろうとマルチスレッドであろうと、メモリベースの競合状態のリスクも同様です。したがって、セマフォは共有メモリへのアクセスを調整(同期)するために使用されます。
memwriter プログラムは、最初に独自の端末で開始する必要があります。 memreader その後、プログラムを独自の端末で(12秒以内に)開始できます。 memreaderからの出力 は:
This is the way the world ends...
その他のLinuxリソース
- Linuxコマンドのチートシート
- 高度なLinuxコマンドのチートシート
- 無料のオンラインコース:RHELの技術概要
- Linuxネットワーキングのチートシート
- SELinuxチートシート
- Linuxの一般的なコマンドのチートシート
- Linuxコンテナとは何ですか?
- 最新のLinux記事
各ソースファイルの上部には、コンパイル中に含まれるリンクフラグを説明するドキュメントがあります。
セマフォが同期メカニズムとしてどのように機能するかを確認することから始めましょう。一般的なセマフォは、カウントセマフォとも呼ばれます。 、インクリメント可能な値(通常はゼロに初期化されます)があるため。自転車をレンタルする店を考えてみましょう。数百台の自転車が在庫にあり、店員がレンタルを行うために使用するプログラムがあります。自転車をレンタルするたびに、セマフォが1つ増えます。自転車が返却されると、セマフォが1つ減ります。レンタルは、値が100に達するまで継続できますが、少なくとも1台の自転車が返却されるまで停止する必要があります。これにより、セマフォが99に減少します。
バイナリセマフォ は、0と1の2つの値のみを必要とする特殊なケースです。この状況では、セマフォは mutexとして機能します。 :相互排除構造。共有メモリの例では、ミューテックスとしてセマフォを使用しています。セマフォの値が0の場合、 memwriter 単独で共有メモリにアクセスできます。書き込み後、このプロセスはセマフォの値をインクリメントし、それによって memreaderを許可します。 共有メモリを読み取ります。
/** Compilation: gcc -o memwriter memwriter.c -lrt -lpthread **/
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
#include "shmem.h"
void report_and_exit(const char* msg) {
perror(msg);
exit(-1);
}
int main() {
int fd = shm_open(BackingFile, /* name from smem.h */
O_RDWR | O_CREAT, /* read/write, create if needed */
AccessPerms); /* access permissions (0644) */
if (fd < 0) report_and_exit("Can't open shared mem segment...");
ftruncate(fd, ByteSize); /* get the bytes */
caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
ByteSize, /* how many bytes */
PROT_READ | PROT_WRITE, /* access protections */
MAP_SHARED, /* mapping visible to other processes */
fd, /* file descriptor */
0); /* offset: start at 1st byte */
if ((caddr_t) -1 == memptr) report_and_exit("Can't get segment...");
fprintf(stderr, "shared mem address: %p [0..%d]\n", memptr, ByteSize - 1);
fprintf(stderr, "backing file: /dev/shm%s\n", BackingFile );
/* semaphore code to lock the shared mem */
sem_t* semptr = sem_open(SemaphoreName, /* name */
O_CREAT, /* create the semaphore */
AccessPerms, /* protection perms */
0); /* initial value */
if (semptr == (void*) -1) report_and_exit("sem_open");
strcpy(memptr, MemContents); /* copy some ASCII bytes to the segment */
/* increment the semaphore so that memreader can read */
if (sem_post(semptr) < 0) report_and_exit("sem_post");
sleep(12); /* give reader a chance */
/* clean up */
munmap(memptr, ByteSize); /* unmap the storage */
close(fd);
sem_close(semptr);
shm_unlink(BackingFile); /* unlink from the backing file */
return 0;
}
memwriterの概要は次のとおりです。 およびmemreader プログラムは共有メモリを介して通信します:
- memwriter 上に示したプログラムは、 shm_openを呼び出します システムが共有メモリと調整するバッキングファイルのファイル記述子を取得する関数。この時点では、メモリは割り当てられていません。誤解を招くような名前の関数ftruncateへの後続の呼び出し :
ftruncate(fd, ByteSize); /* get the bytes */
ByteSizeを割り当てます この場合、バイト、適度な512バイト。 memwriter およびmemreader プログラムは共有メモリにのみアクセスし、バッキングファイルにはアクセスしません。システムは、共有メモリとバッキングファイルの同期を担当します。
- memwriter 次に、 mmapを呼び出します function:
caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
ByteSize, /* how many bytes */
PROT_READ | PROT_WRITE, /* access protections */
MAP_SHARED, /* mapping visible to other processes */
fd, /* file descriptor */
0); /* offset: start at 1st byte */共有メモリへのポインタを取得します。 ( memreader 同様の呼び出しを行います。)ポインタタイプ caddr_t cで始まります callocの場合 、動的に割り当てられたストレージをゼロに初期化するシステム関数。 memwriter memptrを使用します 後の書き込み用 ライブラリstrcpyを使用した操作 (文字列コピー)関数。
- この時点で、 memwriter 書き込みの準備はできていますが、最初にセマフォを作成して、共有メモリへの排他的アクセスを確保します。 memwriter の場合、競合状態が発生します memreader 読んでいた。 sem_openへの呼び出しの場合 成功:
sem_t* semptr = sem_open(SemaphoreName, /* name */
O_CREAT, /* create the semaphore */
AccessPerms, /* protection perms */
0); /* initial value */その後、書き込みを続行できます。 SemaphoreName (空でない一意の名前ならどれでもかまいません)両方の memwriterのセマフォを識別します およびmemreader 。初期値のゼロは、セマフォの作成者、この場合は memwriterを示します。 、この場合、書き込みに進む権利 操作。
- 書いた後、 memwriter セマフォ値を1にインクリメントします:
if (sem_post(semptr) < 0) ..
sem_postへの呼び出しで 働き。セマフォをインクリメントすると、ミューテックスロックが解放され、 memreaderが有効になります。 読み取りを実行する 手術。念のため、 memwriter また、 memwriterから共有メモリのマップを解除します アドレス空間:
munmap(memptr, ByteSize); /* unmap the storage *
これにより、 memwriterが禁止されます 共有メモリへのさらなるアクセスから。
/** Compilation: gcc -o memreader memreader.c -lrt -lpthread **/
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
#include "shmem.h"
void report_and_exit(const char* msg) {
perror(msg);
exit(-1);
}
int main() {
int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /* empty to begin */
if (fd < 0) report_and_exit("Can't get file descriptor...");
/* get a pointer to memory */
caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
ByteSize, /* how many bytes */
PROT_READ | PROT_WRITE, /* access protections */
MAP_SHARED, /* mapping visible to other processes */
fd, /* file descriptor */
0); /* offset: start at 1st byte */
if ((caddr_t) -1 == memptr) report_and_exit("Can't access segment...");
/* create a semaphore for mutual exclusion */
sem_t* semptr = sem_open(SemaphoreName, /* name */
O_CREAT, /* create the semaphore */
AccessPerms, /* protection perms */
0); /* initial value */
if (semptr == (void*) -1) report_and_exit("sem_open");
/* use semaphore as a mutex (lock) by waiting for writer to increment it */
if (!sem_wait(semptr)) { /* wait until semaphore != 0 */
int i;
for (i = 0; i < strlen(MemContents); i++)
write(STDOUT_FILENO, memptr + i, 1); /* one byte at a time */
sem_post(semptr);
}
/* cleanup */
munmap(memptr, ByteSize);
close(fd);
sem_close(semptr);
unlink(BackingFile);
return 0;
}
両方のmemwriter およびmemreader プログラム、主な関心のある共有メモリ関数は shm_open およびmmap :成功すると、最初の呼び出しはバッキングファイルのファイル記述子を返し、2番目の呼び出しはそれを使用して共有メモリセグメントへのポインタを取得します。 shm_openへの呼び出し memwriter を除いて、2つのプログラムで類似しています。 プログラムは共有メモリを作成しますが、 memreader この作成済みのメモリにのみアクセスします:
int fd = shm_open(BackingFile, O_RDWR | O_CREAT, AccessPerms); /* memwriter */
int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /* memreader */
ファイル記述子が手元にある場合、 mmapの呼び出し 同じです:
caddr_t memptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
mmapの最初の引数 NULL 、これは、システムが仮想アドレス空間のどこにメモリを割り当てるかを決定することを意味します。代わりにアドレスを指定することは可能です(ただし注意が必要です)。 MAP_SHARED フラグは、割り当てられたメモリがプロセス間で共有可能であることを示し、最後の引数(この場合はゼロ)は、共有メモリのオフセットが最初のバイトであることを意味します。 サイズ 引数は割り当てられるバイト数(この場合は512)を指定し、保護引数は共有メモリの書き込みと読み取りが可能であることを示します。
memwriter プログラムは正常に実行され、システムはバッキングファイルを作成して維持します。私のシステムでは、ファイルは / dev / shm / shMemEx 、 shMemEx 私の名前として(ヘッダーファイル shmem.h で指定) )共有ストレージ用。 memwriterの現在のバージョン およびmemreader プログラム、ステートメント:
shm_unlink(BackingFile); /* removes backing file */
バッキングファイルを削除します。 リンク解除の場合 ステートメントを省略すると、プログラムの終了後もバッキングファイルが保持されます。
memreader 、 memwriterのように 、 sem_open の呼び出しで、その名前を介してセマフォにアクセスします 。しかし、 memreader その後、 memwriter まで待機状態になります 初期値が0のセマフォをインクリメントします:
if (!sem_wait(semptr)) { /* wait until semaphore != 0 */
待機が終了したら、 memreader 共有メモリからASCIIバイトを読み取り、クリーンアップして終了します。
共有メモリAPIには、共有メモリセグメントとバッキングファイルを同期するための操作が明示的に含まれています。これらの操作は、混乱を減らし、メモリ共有とセマフォのコードに焦点を合わせ続けるために、例から省略されています。
memwriter およびmemreader セマフォコードが削除された場合でも、プログラムは競合状態を引き起こさずに実行される可能性があります: memwriter 共有メモリセグメントを作成し、すぐに書き込みます。 memreader これが作成されるまで、共有メモリにアクセスすることさえできません。ただし、ベストプラクティスでは、書き込みのたびに共有メモリアクセスを同期する必要があります。 操作が混在しており、セマフォAPIは、コード例で強調表示されるほど重要です。
共有ファイルと共有メモリの例は、プロセスが共有ストレージを介して通信する方法を示しています。 、一方の場合はファイル、もう一方の場合はメモリセグメント。両方のアプローチのAPIは比較的簡単です。これらのアプローチには共通の欠点がありますか?最近のアプリケーションは、ストリーミングデータを処理することが多く、実際、非常に大きなデータストリームを処理します。共有ファイルと共有メモリのどちらのアプローチも、大量のデータストリームには適していません。あるタイプまたは別のタイプのチャネルの方が適しています。したがって、パート2では、チャネルとメッセージキューを紹介します。ここでも、Cのコード例を示します。
[Linuxでのプロセス間通信の完全ガイドをダウンロード]