記事ディレクトリ
- プロセス間通信とは
- 匿名パイプ
- 名前付きパイプ
-
システム V 共有メモリ
-
システム V メッセージキュー
-
信号量
1. プロセス間通信とは何ですか?
まず、プロセスは独立したページ テーブル、PCB、仮想アドレス空間を使用して独立して実行されるため、親プロセスと子プロセス間のデータは相互に補完し、干渉します。これにより、プロセス間通信がより困難になります。オペレーティング システムが独立するように設計されているためです。
プロセス間通信の本質: 異なるプロセスは同じリソース (メモリ空間) を参照する必要があります。
プロセス間通信の目的:
- データ転送: あるプロセスはそのデータを別のプロセスに送信する必要があります。
- リソース共有: 複数のプロセス間で同じリソースを共有します。
- 通知イベント: プロセスは、別のプロセスまたはプロセスのグループにメッセージを送信して、特定のイベントが発生したことを通知する必要があります (プロセスの終了時に親プロセスに通知するなど)。
- プロセス制御: 一部のプロセスは、別のプロセス (デバッグ プロセスなど) の実行を完全に制御することを望んでいますが、この時点で、制御プロセスは、別のプロセスのすべてのトラップと例外をインターセプトし、そのステータスの変化を時間内に把握できることを望んでいます。
プロセス間通信の必要性:
- 単一プロセスは同時実行機能を使用できないため、複数のプロセス間のコラボレーションを実現できません。
- 例: データの送信、同期実行フロー、メッセージ通知など。
- したがって、プロセス間通信は目的ではなく、複数のプロセス間の連携を実現するための手段です。
プロセス通信の技術的背景:
- プロセスが独立しているためです。仮想アドレス空間とページ テーブルは、プロセス操作 (プロセス カーネル データ構造、プロセス コードとデータ) の独立性を保証します。
- 通信費が比較的高い
プロセス間通信の本質的な理解:
- プロセス間通信の前提条件は、異なるプロセスが同じ「メモリ」(特定の構造) を参照する必要があることです。
- 同じ「メモリ」はどのプロセスにも属さないため、共有することが重視される必要があります。
プロセス間通信の分類:プロセス間通信方式の規格
- Linux はネイティブで --- パイプを提供します
- 匿名パイプ
- 名前付きパイプ
- スタンドアロン通信---複数プロセス---System V IPC
- System V メッセージキュー
- System V 共有メモリ
- System V セマフォ
- ネットワーク通信---マルチスレッド---POSIX IPC
- メッセージキュー
- 共有メモリ
- 信号量
- ミューテックス
- 条件変数
- 読み書きロック
1. パイプライン
パイプラインとは何ですか?
パイプ、英語ではパイプです。これは、Linux コマンド ラインを学習するときに紹介する非常に重要な概念です。その発明者は、UNIX の初期のシェルの発明者でもある Douglas McElroy です。シェルを発明した後、システム操作でコマンドを実行するときに、あるプログラムの出力を別のプログラムに転送して処理する必要があることがよくあることに気付きました。この操作は、入出力のリダイレクトとファイルの追加を使用して実現できます。パイプは本質的にファイルであり、前のプロセスは書き込み用にファイルを開き、後続のプロセスは読み取り用にファイルを開きます。このようにして、前に書いて後で読むことでコミュニケーションが成立します。実際、パイプラインの設計も UNIX の「すべてがファイルである」設計原則に従っており、本質的にはファイルです。Linux システムは、パイプラインをファイル システムに直接実装し、VFS を使用してアプリケーションの操作インターフェイスを提供します。
- 一方向のコミュニケーションのみ
- すべてのリソースはパイプライン内で送信されます
2. パイプラインの原理
パイプライン通信の背後には、パイプラインを介したプロセス間のプロセス通信があります
パイプは、Unix におけるプロセス間通信の最も古い形式です。あるプロセスから別のプロセスへのデータ フローを「パイプライン」と呼びます。ここの真ん中のデータリソースはどのプロセスにも属していません
注: ここでは、who コマンドを使用して現在のクラウド サーバーのログイン ユーザーを表示し (1 行に 1 ユーザーが表示されます)、wc -l は現在の行数をカウントするために使用されます。
現在のプロセスが子プロセスを作成する場合、ファイル記述子テーブルを子プロセスにコピーする必要がありますか? また、テーブル内で指定されている多数のファイルを子プロセスにコピーする必要がありますか?
ここでのファイル記述子はプロセスとファイルの関係を表し、現在のプロセスがどのファイルが開かれているかを確認できることを示します。これを子プロセスにコピーする必要があります。ただし、表で示されているファイルはプロセスとは何の関係もないため、コピーする必要はありません。この親子プロセス内の構造体 files_struct は同じであるため、そこに保存されているファイル ポインタはすべて同じファイルを指します。(ここで、親子プロセスが表示画面に出力するときに、ファイル No. 1 に出力することを説明できます)。ここで、父と息子が同じ公開ファイルを閲覧したことがわかります。これがパイプラインです!
ここでは、fork() を使用して親子プロセスを作成し、親プロセスは書き込みのみ、子プロセスは読み取りのみができるようにします。次に、ここで、親プロセスの読み取り機能をオフにして書き込みを保持し、次に、親プロセスの読み取り機能をオフにする必要があります。子プロセスの書き込み関数をオフにして読み取りを保持する関数。そうすれば、親プロセスと子プロセスが同じリソースを参照できるようになります。
知らせ:
- Linux ではすべてがファイルであるため、パイプもファイルです。ファイルはカーネルに属するため、すべてのプロセス間通信はカーネル レベルに属します。
- プロセス間通信パイプラインはメモリ上に存在する必要があるため、純粋なメモリ通信方式ですが、このときにディスクにデータを書き込む必要があると通信効率が低下し、ここでのデータは一時的なデータであることが多いです。データをディスクに書き込みません (データの永続性とも呼ばれます)
2.匿名パイプ
#include <unistd.h>機能: 名前のないパイプを作成する原型: int pipe(int fd[2]);パラメータ
- fd: ファイル記述子の配列。fd[0]は読み取り終了を表し、fd[1]は書き込み終了を表します。
- fd[2] は、パイプラインが正常に作成されたかどうかを確認するための出力パラメーターとして使用されます。
- 戻り値: 成功した場合は 0 が返され、失敗した場合はエラー コードが返されます。
1.フォークを使用してパイプラインの原則を共有する
子プロセスがフォークを介して継承できるようにすること、つまり血縁関係のあるプロセスが通信できるようにすることは、親子プロセスでよく使用され、異なるプロセスで同じリソースを確認できるようになります。
ここで注意すべき点は、pipefd[0] が読み取り側を表し、pipefd[1] が書き込み側を表すことです。
テストコード:
#include <iostream>
#include <unistd.h>
#include <assert.h>
int main()
{
//1.创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
//由于在Debug条件下assert是有效的,但是在Release版本下是无效的
assert(n!=-1);//强制检查管道是否创建成功
(void)n;//这里是说明在Release版本下n被使用过
std::cout<<"pipefd[0]"<<pipefd[0]<<std::endl;//3
std::cout<<"pipefd[1]"<<pipefd[1]<<std::endl;//4
return 0;
}
ここで、パイプラインが正常に作成されたことがわかります。
2.ファイル記述子の観点から-パイプラインを深く理解する
コード例:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
int main()
{
//1.创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
//由于在Debug条件下assert是有效的,但是在Release版本下是无效的
assert(n!=-1);//强制检查管道是否创建成功
(void)n;//这里是说明在Release版本下n被使用过
//如果实在Debug版本下就不打印
#ifdef DEBUG
std::cout<<"pipefd[0]"<<pipefd[0]<<std::endl;//3
std::cout<<"pipefd[1]"<<pipefd[1]<<std::endl;//4
#endif
//创建子进程
pid_t id=fork();
assert(id!=-1);
if(id==0)
{
//子进程
//构建单向通信信道
//子进程读取,关闭子进程的写端
close(pipefd[1]);
char buffer[1024];
while(true)
{
//从pipefd[0]中进行读取,读取到缓冲区中
ssize_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
//表示读取成功
if(s>0)
{
buffer[s]=0;
std::cout<<"child get a message ["<<getpid()<<"] Father#"<<buffer<<std::endl;
}
}
//最后关闭子进程的读端,这里也可以不需要关闭,最终会由OS管理
//close(pipefd[0]);
exit(0);
}
//父进程
//构建单向通信的信道
//父进程进行写入,关闭相应的读端
close(pipefd[0]);
std::string message="我是父进程,我正在给你发消息";
int count=0;
char send_buffer[1024];
while(true)
{
//构建一个变化的字符串
//将printf的内容格式化到字符串中去
snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
//进行写入操作
//这里是需要写入到管道文件中所以不需要+1,因为'\0'写入到文件中没有意义
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
}
pid_t ret=waitpid(id,nullptr,0);//非阻塞方式等待
assert(ret>0);
(void)ret;
close(pipefd[1]);
return 0;
}
ここで、父プロセスと子プロセスの両方がバッファとしてバッファを使用していることがわかりますが、なぜグローバル バッファとして定義できないのでしょうか。
コピーオンライトが発生するためです。
3.カーネルの観点から-パイプラインの本質
4 パイプラインの概要
- 1. パイプはプロセス間通信の手段であり、パイプは血のつながったプロセス間のプロセス間通信に使用され、父と子の通信によく使われます。
- 2. パイプラインは、プロセス間のコラボレーションを可能にすることでアクセス制御を提供します。
- 3. パイプラインが提供するのはストリーム指向の通信サービスです --- バイトストリーム指向 (対応するプロトコルが必要)
- 4. パイプはファイルに基づいており、ファイルのライフ サイクルはプロセスに従い、パイプのライフ サイクルはプロセスに従います。
- 5. パイプは一方向通信であり、半二重通信の特殊なケースです。
上記のコード例では、親プロセスは 1 秒ごとにメッセージを送信しますが、子プロセスは読み取り時間制限を設定していませんが、子プロセスは親プロセスの書き込みに従って読み取り可能であり、その後、子プロセスは一定時間スリープします。親プロセスの 1 秒。主に、親プロセスがリソースの準備完了を書き込むのを待機します。
パイプはファイルであり、モニターもファイルであるため、父と息子が同時にモニターに書き込む場合、待ち時間などはありません。なぜなら、以前にモニターに印刷したときは、常にファイルに印刷していたからです。ずらして監視します。この状況はアクセス制御の欠如と呼ばれます。次に、子プロセスが親プロセスの書き込みを待つ方法をアクセス制御と呼びます。
知らせ:
- パイプ内のデータがいっぱいになると、書き込み側は、読み取り側がデータの読み取りを完了するまで待ってから書き込む必要があります。
- パイプが空の場合、読み取り側は書き込み側が書き込むのを待ちます。
検証コード:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
int main()
{
//1.创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
//由于在Debug条件下assert是有效的,但是在Release版本下是无效的
assert(n!=-1);//强制检查管道是否创建成功
(void)n;//这里是说明在Release版本下n被使用过
//如果实在Debug版本下就不打印
#ifdef DEBUG
std::cout<<"pipefd[0]"<<pipefd[0]<<std::endl;//3
std::cout<<"pipefd[1]"<<pipefd[1]<<std::endl;//4
#endif
//创建子进程
pid_t id=fork();
assert(id!=-1);
if(id==0)
{
//子进程
//构建单向通信信道
//子进程读取,关闭子进程的写端
close(pipefd[1]);
char buffer[1024];
while(true)
{
//从pipefd[0]中进行读取,读取到缓冲区中
ssize_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
//表示读取成功
if(s>0)
{
sleep(20);
buffer[s]=0;
std::cout<<"child get a message ["<<getpid()<<"] Father#"<<buffer<<std::endl;
}
}
//最后关闭子进程的读端,这里也可以不需要关闭,最终会由OS管理
//close(pipefd[0]);
exit(0);
}
//父进程
//构建单向通信的信道
//父进程进行写入,关闭相应的读端
close(pipefd[0]);
std::string message="我是父进程,我正在给你发消息";
int count=0;
char send_buffer[1024];
while(true)
{
//构建一个变化的字符串
//将printf的内容格式化到字符串中去
snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
//进行写入操作
//这里是需要写入到管道文件中所以不需要+1,因为'\0'写入到文件中没有意义
write(pipefd[1],send_buffer,strlen(send_buffer));
std::cout<<count<<std::endl;
}
pid_t ret=waitpid(id,nullptr,0);//非阻塞方式等待
assert(ret>0);
(void)ret;
close(pipefd[1]);
return 0;
}
上の図から、バッファーがいっぱいになると書き込みを続けることができず、コードのサブプロセスによってのみゆっくりと読み取ることができることがわかります。
次に、書き込み側の fd がクローズしていないことを確認します データがあれば読み出し、データがなければ待ちます 書き込み側の fd はクローズしています 読み取り側は read が 0 を返しますファイルの終わりが読み取られたことを確認します(このとき、バッファは内容を読み取った後に終了できます)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
int main()
{
//1.创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
//由于在Debug条件下assert是有效的,但是在Release版本下是无效的
assert(n!=-1);//强制检查管道是否创建成功
(void)n;//这里是说明在Release版本下n被使用过
//如果实在Debug版本下就不打印
#ifdef DEBUG
std::cout<<"pipefd[0]"<<pipefd[0]<<std::endl;//3
std::cout<<"pipefd[1]"<<pipefd[1]<<std::endl;//4
#endif
//创建子进程
pid_t id=fork();
assert(id!=-1);
if(id==0)
{
//子进程
//构建单向通信信道
//子进程读取,关闭子进程的写端
close(pipefd[1]);
char buffer[1024];
while(true)
{
//从pipefd[0]中进行读取,读取到缓冲区中
ssize_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
//表示读取成功
if(s>0)
{
buffer[s]=0;
std::cout<<"child get a message ["<<getpid()<<"] Father#"<<buffer<<std::endl;
}
else if(s==0)
{
std::cout<<"writer quit(father),me quit"<<std::endl;
break;
}
}
//最后关闭子进程的读端,这里也可以不需要关闭,最终会由OS管理
//close(pipefd[0]);
exit(0);
}
//父进程
//构建单向通信的信道
//父进程进行写入,关闭相应的读端
close(pipefd[0]);
std::string message="我是父进程,我正在给你发消息";
int count=0;
char send_buffer[1024];
while(true)
{
//构建一个变化的字符串
//将printf的内容格式化到字符串中去
snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
//进行写入操作
//这里是需要写入到管道文件中所以不需要+1,因为'\0'写入到文件中没有意义
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
std::cout<<count<<std::endl;
if(count==5)
{
std::cout<<"writer quit(father)"<<std::endl;
break;
}
}
close(pipefd[1]);
pid_t ret=waitpid(id,nullptr,0);//非阻塞方式等待
assert(ret>0);
(void)ret;
return 0;
}
一般に、カーネルは同期し、パイプライン操作のための相互排他的なパイプラインは半二重であり、データは一方向にのみ流れることができます。2 者間の通信が必要な場合は、2 つのパイプラインを確立する必要があります。
半二重通信: 通信側はデータの送信または受信を行いますが、データの送信と受信を同時に行うことはできません。
全二重通信: データは受信だけでなく送信も可能です。
a. 書き込みは速く、読み込みは遅く、書き込みがいっぱいになるとそれ以上書き込みできなくなります。
b. 書き込みは遅く読み出しは速い パイプ内にデータがない場合、読み出し側は待たなければなりません。
c. ファイルの終わりが読み取られたことを示す、0 を書き込み、読み取ります。
d. 読み取りをオフにして書き込みを続行すると、OS は書き込みプロセスを終了します。
5. プロセスプール
プロセスプールの作成
親プロセスと各子プロセスのパイプラインを確立し、固定サイズのコマンドコード(4KB)で子プロセスに指示を送ります。
メイクファイル
ProcessPool:ProcessPool.cc
g++ -o $@ $^ -std=c++11 -DEBUG
.PHONY:clean
clean:
rm -f ProcessPool
ありがとう。hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <functional>
#include <vector>
#include <unordered_map>
typedef std::function<void()> func;
std::vector<func> callbacks;
std::unordered_map<int,std::string> desc;
void readMySQL()
{
std::cout<<"sub process[ "<<getpid()<<"] 执行访问数据库的任务"<<std::endl;
}
void execulerUrl()
{
std::cout<<"sub process[ "<<getpid()<<"] 执行url解析"<<std::endl;
}
void cal()
{
std::cout<<"sub process[ "<<getpid()<<"] 执行加密任务"<<std::endl;
}
void save()
{
std::cout<<"sub process[ "<<getpid()<<"] 执行数据持久化任务"<<std::endl;
}
void load()
{
desc.insert({callbacks.size(),"readMySQL:读取数据库"});
callbacks.push_back(readMySQL);
desc.insert({callbacks.size(),"execulerUrl:进行url解析"});
callbacks.push_back(execulerUrl);
desc.insert({callbacks.size(),"cal:进行加密计算"});
callbacks.push_back(cal);
desc.insert({callbacks.size(),"save:进行数据持久化"});
callbacks.push_back(save);
}
void showHandler()
{
for(const auto & iter:desc)
{
std::cout<<iter.first<<"\t"<<iter.second<<std::endl;
}
}
int handlerSize()
{
return callbacks.size();
}
1. 手動タスクディスパッチメソッド ProcessPool.cc:
#include <iostream>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <cstdlib>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
#define PROCESS_NUM 5
int waitCommand(int waitFd,bool &quit)
{
uint32_t command=0;
ssize_t s=read(waitFd,&command,sizeof(command));
if(s==0)
{
quit=true;
return -1;
}
assert(s==sizeof(uint32_t));
return command;
}
void sendAndWakeup(pid_t who,int fd,uint32_t command)
{
write(fd,&command,sizeof(command));
std::cout<<"main process call process "<<who<<"execute "<<desc[command]<<"through "<<fd<<std::endl;
}
int main()
{
load();
std::vector<std::pair<pid_t,int>> slots;
//创建多个进程
for(int i=0;i<PROCESS_NUM;i++)
{
//创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
assert(n==0);
(void)n;
pid_t id=fork();
assert(id!=-1);
//子进程进行读取
if(id==0)
{
//child
//关闭写端
close(pipefd[1]);
while(true)
{
bool quit=false;
int command=waitCommand(pipefd[0],quit);//如果不发就阻塞
//执行对应的命令
if(quit) break;
if(command>=0&&command<handlerSize())
{
callbacks[command]();
}
else
{
std::cout<<"非法command"<<std::endl;
}
}
exit(1);
}
//father
//进行写入,关闭读端
close(pipefd[0]);
slots.push_back(std::pair<pid_t,int>(id,pipefd[1]));
}
//开始任务
srand((unsigned long)time(nullptr)^getpid()^222222222222222L);
while(true)
{
int select;
int command;
std::cout<<"################################################"<<std::endl;
std::cout<<"# 1.show functions #"<<std::endl;
std::cout<<"# 2.send command #"<<std::endl;
std::cout<<"################################################"<<std::endl;
std::cout<<"Please Select>";
std::cin>>select;
if(select==1) showHandler();
else if(select==2)
{
std::cout<<"Enter Your Command> ";
std::cin>>command;
//选择进程
int choice=rand()%slots.size();
//布置任务
sendAndWakeup(slots[choice].first,slots[choice].second,command);
}
}
//关闭fd,结束所有进程,所有的子进程都会退出
for(const auto &slot:slots)
{
close(slot.second);
}
//回收所有的子进程信息
for(const auto &slot:slots)
{
//等待全部子进程
waitpid(slot.first,nullptr,0);
}
return 0;
}
2. タスクを自動的にディスパッチする ProcessPool.cc
#include <iostream>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <cstdlib>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
#define PROCESS_NUM 5
int waitCommand(int waitFd,bool &quit)
{
uint32_t command=0;
ssize_t s=read(waitFd,&command,sizeof(command));
if(s==0)
{
quit=true;
return -1;
}
assert(s==sizeof(uint32_t));
return command;
}
void sendAndWakeup(pid_t who,int fd,uint32_t command)
{
write(fd,&command,sizeof(command));
std::cout<<"main process call process "<<who<<"execute "<<desc[command]<<"through "<<fd<<std::endl;
}
int main()
{
load();
std::vector<std::pair<pid_t,int>> slots;
//创建多个进程
for(int i=0;i<PROCESS_NUM;i++)
{
//创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
assert(n==0);
(void)n;
pid_t id=fork();
assert(id!=-1);
//子进程进行读取
if(id==0)
{
//child
//关闭写端
close(pipefd[1]);
while(true)
{
bool quit=false;
int command=waitCommand(pipefd[0],quit);//如果不发就阻塞
//执行对应的命令
if(quit) break;
if(command>=0&&command<handlerSize())
{
callbacks[command]();
}
else
{
std::cout<<"非法command"<<std::endl;
}
}
exit(1);
}
//father
//进行写入,关闭读端
close(pipefd[0]);
slots.push_back(std::pair<pid_t,int>(id,pipefd[1]));
}
//开始任务
srand((unsigned long)time(nullptr)^getpid()^222222222222222L);
while(true)
{
int command=rand()%handlerSize();
int choice=rand()%slots.size();
sendAndWakeup(slots[choice].first,slots[choice].second,command);
sleep(1);
}
//关闭fd,结束所有进程,所有的子进程都会退出
for(const auto &slot:slots)
{
close(slot.second);
}
//回收所有的子进程信息
for(const auto &slot:slots)
{
//等待全部子进程
waitpid(slot.first,nullptr,0);
}
return 0;
}
6. パイプラインの読み取りおよび書き込みルール
読み込むデータがない場合
- O_NONBLOCK 無効: 読み取り呼び出しはブロックされます。つまり、データが到着するまでプロセスは実行を一時停止します。
- O_NONBLOCK 有効: 読み取り呼び出しは -1 を返し、errno 値は EAGAIN です。
パイプがいっぱいになったとき
- O_NONBLOCK 無効: プロセスがデータを読み取るまで、書き込み呼び出しはブロックされます。
- O_NONBLOCK 有効: 呼び出しは -1 を返し、errno 値は EAGAIN です
- すべてのパイプの書き込み端に対応するファイル記述子が閉じている場合、読み取りは 0 を返します。
- すべてのパイプ リーダーに対応するファイル記述子が閉じられている場合、書き込み操作によりシグナル SIGPIPE が生成され、書き込みプロセスが終了する可能性があります。
- 書き込まれるデータの量が PIPE_BUF 以下の場合、Linux は書き込みのアトミック性を保証します。
- 書き込まれるデータの量が PIPE_BUF よりも大きい場合、Linux は書き込みのアトミック性を保証しなくなります。
7.パイプラインの特性
- これは、共通の祖先を持つプロセス (関連関係のあるプロセス) 間の通信にのみ使用できます。通常、パイプはプロセスによって作成され、その後プロセスが fork を呼び出し、そのパイプは親プロセスと子プロセスの間で使用できます。 。
- Pipeline はストリーミング サービスを提供します
- 一般に、プロセスが終了するとパイプが解放されるため、パイプのライフサイクルはプロセスに応じて変化します。
- 一般に、カーネルはパイプライン操作を同期し、相互に排除します。
- パイプラインは半二重であり、データは一方向にのみ流れることができるため、2 者間の通信が必要な場合は 2 つのパイプラインを確立する必要があります。
3. 名前付きパイプ
- パイプ アプリケーションの制限の 1 つは、共通の祖先を持つ (相互に関連する) プロセス間でのみ通信できることです。
- 無関係なプロセス間でデータを交換したい場合は、名前付きパイプと呼ばれることが多い FIFO ファイルを使用してそのジョブを実行できます。
- 名前付きパイプは特別な種類のファイルです
名前付きパイプ: 通信の本質は、パイプを確立する前に 2 つの異なるプロセスが同じリソースを参照する必要があることです。パイプファイルをディスク上に作成できます。パイプ ファイルを開くことはできますが、メモリ データはディスクにフラッシュされません。パイプライン ファイルはシステム パスに存在し、パスは一意です。両方のプロセスは、パイプ ファイルのパスを通じて同じリソースを参照できます。匿名パイプとの違いは、ファイルがメモリ上にファイルを作成するだけであり、親子プロセスによってアクセスされることです。名前付きパイプのパイプ ファイルもメモリ上のファイルですが、マッピングとファイル ディレクトリが存在します。ディスク上にあります。
名前付きパイプは、次のコマンドを使用してコマンド ラインから作成できます。
mkfifo ファイル名
名前付きパイプはプログラムからも作成できます。関連する関数は次のとおりです。
int mkfifo(const char *ファイル名,mode_t モード);
パイプ ファイルにデータを書き込んだ後、ファイルはブロックされます。、この時点で読む必要があります。
この問題は read を使用することで解決できます。
次にサイクル数の読み取りを確認します。
この時点で、一方の端末が書き込みを行っており、もう一方の端末が読み取りを行っていることがわかります。
name_pipeのリンクを解除することでパイプファイルを削除することも可能です。
1. 名前付きパイプを使用してサーバーとクライアントの通信を実装する
メイクファイル
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
ログ.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
//定义日志的几种状态
const std::string msg[]={
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message,int level)
{
//时间,日志信息
std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<<msg[level]<<"|"<<message;
return std::cout;
}
#endif
通信.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include "log.hpp"
//设置管道文件的权限
#define MODE 0666
//缓冲区大小
#define SIZE 128
//管道文件的路径
std::string ipcPath="./fifo.ipc";
#endif
client.cc
#include "comm.hpp"
int main()
{
//获取管道文件,以写的方式打开
int fd=open(ipcPath.c_str(),O_WRONLY);
if(fd<0)
{
perror("open");
exit(1);
}
//ipc过程
//创建需要发送的字符串
std::string buffer;
while(true)
{
std::cout<<"Please Enter Message Line:> ";
//将字符串放入buffer中
std::getline(std::cin,buffer);
//将buffer写入管道文件中
write(fd,buffer.c_str(),buffer.size());
}
//关闭文件
close(fd);
return 0;
}
サーバー.cc
#include "comm.hpp"
static void getMessage(int fd)
{
char buffer[SIZE];
while(true)
{
memset(buffer,'\0',sizeof buffer);
ssize_t s=read(fd,buffer,sizeof(buffer)-1);
if(s>0)
{
std::cout<<"["<<getpid()<<"]"<<"client say>"<<buffer<<std::endl;
}
else if(s==0)
{
//end of file
std::cerr<<"["<<getpid()<<"]"<<"read end of file,client quit,server quit too!"<<std::endl;
break;
}
else{
//read error
perror("read");
break;
}
}
}
int main()
{
//1.创建管道文件
if(mkfifo(ipcPath.c_str(),MODE)<0)
{
perror("mkfifo");
exit(1);
}
//写日志
Log("创建管道文件成功",Debug)<<"step 1"<<std::endl;
//2.正常的文件操作
int fd=open(ipcPath.c_str(),O_RDONLY);
if(fd<0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功",Debug)<<"step 2"<<std::endl;
//这里创建三个进程来读取
int nums=3;
for(int i=0;i<nums;i++)
{
pid_t id=fork();
if(id==0)
{
//通信
getMessage(fd);
exit(2);
}
}
//回收子进程
for(int i=0;i<nums;i++)
{
waitpid(-1,nullptr,0);//等待任意进程退出
}
//3.编写正常的通信代码
//创建缓冲区
char buffer[SIZE];
while(true)
{
//将缓冲区清空
memset(buffer,'\0',sizeof(buffer));
//将管道中的内容读取到缓冲区中
ssize_t s=read(fd,buffer,sizeof(buffer)-1);
//如果读取成功的话
if(s>0)
{
std::cout<<"["<<getpid()<<"]"<<"client say>"<<buffer<<std::endl;
}
else if(s==0)//如果读取到文件结尾的位置
{
//end of file
std::cerr<<"["<<getpid()<<"]"<<"read end of file,client quit,server quit too!"<<std::endl;
break;
}
else{
//read error
perror("read");
break;
}
}
//4.关闭文件
close(fd);
Log("关闭管道文件成功",Debug)<<"step 3"<<std::endl;
unlink(ipcPath.c_str());//通信完成,删除文件
Log("删除管道文件成功",Debug)<<"step 4"<<std::endl;
return 0;
}
ここでは、3 つのプロセスがクライアントから送信された情報をめぐって競合しており、情報を取得した人が情報を送信することがわかります。
2. 匿名パイプと名前付きパイプの違い
- 匿名パイプは、pipe 関数によって作成され、開かれます。
- 名前付きパイプは mkfififo 関数によって作成され、open で開かれます。
- FIFO (名前付きパイプ) とパイプ (匿名パイプ) の唯一の違いは、作成方法とオープン方法ですが、作業が完了すると、セマンティクスは同じになります。
3. 名前付きパイプの開始ルール
現在のオープン操作が読み取りのために FIFO をオープンすることである場合
- O_NONBLOCK 無効: 対応するプロセスが書き込み用に FIFO を開くまでブロックします。
- O_NONBLOCK 有効: 成功をすぐに返します
現在のオープン操作が書き込み用に FIFO をオープンすることである場合
- O_NONBLOCK 無効: 対応するプロセスが読み取り用に FIFO を開くまでブロックします。
- O_NONBLOCK 有効: 失敗をすぐに返します。エラー コードは ENXIO です。
4. System V共有メモリ
System V IPC によって提供される通信方法は 3 つあります。
- システム V 共有メモリ
- システム V メッセージキュー
- システム V セマフォ
System V 共有メモリと System V メッセージ キューはデータを送信するように設計されており、System V セマフォはプロセス間の同期と相互排他を行うように設計されています。
1.system V 共有メモリ
1.共有メモリのデータ構造
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
2. 共有メモリの作成
共有メモリはプロセスに属さず、オペレーティング システムに属します。共有メモリのプロバイダーはオペレーティング システムです。
プロセスの各ペアが通信に共有メモリを必要とする場合、オペレーティング システムは最初にそれを記述し、次にそれを管理するためにそれを編成する必要があります。共有メモリ = 共有メモリ ブロック + 対応する共有メモリ カーネル データ構造
1. 共有メモリ機能
1.shmget関数
- 機能: 共有メモリの作成に使用されます。
- プロトタイプ
- int shmget(key_t キー、size_t サイズ、int shmflg);
- パラメータ
- key: この共有メモリセグメントの名前
- サイズ: 共有メモリのサイズ
- shmflg: 9 つのパーミッションフラグで構成されており、用途はファイル作成時のモードフラグと同じです。
- 戻り値: 成功した場合は共有メモリセグメントの識別コードである非負の整数を返し、失敗した場合は -1 を返します。
shmget 関数の機能は共有メモリの作成と取得です。ここで、shmflg は IPC_CREAT または IPC_EXCL (IPC_CREAT は 0) であることに注意してください。
IPC_CREAT: 共有メモリを作成する際、既にシステムの最下層に存在する場合は直接取得して返し、存在しない場合は作成して返します。
IPC_EXCL: IPC_EXCL を単独で使用するのは意味がありません。IPC_EXCL と IPC_CREAT を組み合わせて使用する必要があります。最下層が存在しない場合は作成して返します。最下層が存在する場合はエラーを返します。返品が成功した場合、それは新品のシュミドである必要があります。
戻り値 ファイルの fd に似た、共有メモリのユーザーレベルの表現。
キー値: キー値はオペレーティング システム内で一意にすることができます。サーバーとクライアントは同じキー値を使用します。キー値が同じであれば、キー値が何であっても問題ありません。これにより、通信プロセスは私が作成した共有メモリを参照できるようになります。同じアルゴリズム ルールを使用することで、一意のキー値を形成するだけで十分です。キーの作成時にのみ使用されます。ほとんどの場合、ユーザーは共有メモリにアクセスするときに shmid を使用します。ftok関数を使用してキー値を取得します。
2.ftok関数
- 関数: キー値を返すために使用されます。
- プロトタイプ
key_t ftok(const char *パス名, int proj_id);
- パラメータ
- pathname: 指定されたファイル名 (ファイルは存在し、アクセス可能である必要があります)
- proj_id: サブシリアル番号。int ですが、8 ビットのみが使用されます (0 ~ 255)
- 戻り値: 実行が成功した場合は key_t 値が返され、それ以外の場合は -1 が返されます。
ここでは、パス (パス名) とプロジェクト ID を渡す必要があります。ftok 関数は、対応するアルゴリズムに従ってパスとプロジェクト ID を組み合わせて一意の値を形成します。ただし、ここで作成したキー値が下位にも存在する可能性があるため、ここでのftokが正常に作成されない可能性があります。ここでは、クライアントとサーバーによって作成されたキーの値が同じである必要があります。
メイクファイル
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
通信.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include "log.hpp"
#define PATH_NAME "/home/hsj"
#define PROJ_ID 0x66
#endif
サーバー.cc
#include "comm.hpp"
int main()
{
//1.创建公共的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
Log("create key done",Debug)<<"Server say:"<<k<<std::endl;
}
client.cc
#include "comm.hpp"
int main()
{
key_t k=ftok(PATH_NAME,PROJ_ID);
Log("create key done",Debug)<<"Client say:"<<k<<std::endl;
return 0;
}
Linux の ipcs コマンドを使用して、プロセス間通信デバイスに関する情報を表示します。
ipcs コマンドを単独で使用すると、デフォルトでメッセージキュー、共有メモリ、セマフォに関する通信情報が一覧表示されます。特定のものを表示したい場合は、対応するオプションを追加する必要があります。
- -q: メッセージキュー関連情報をリストします。
- -m: 共有メモリ関連情報をリストします。
- -s: セマフォ関連情報の一覧表示
ipcs コマンドによって出力される情報の各列の意味は次のとおりです。
ここで注意してください。key はカーネル レベルで共有メモリの一意性を保証する方法であり、shmid はユーザー レベルで共有メモリの一意性を保証する方法です。
共有メモリを解放するにはどうすればよいですか?
方法 1:
ipcrm -m shmid コマンドを使用して、指定した ID の共有メモリ リソースを解放できます。
System V IPC リソースのライフサイクルはカーネルによって異なります。再起動しない限り常に存在します
1. 手動削除
2.コードの削除
shmctl を使用して共有メモリをオフにすることもできます。
方法 2:
プログラムを使用して共有メモリ リソースを解放できます。
shmctl を使用して解放します。
3.shmctl関数
関数プロトタイプ:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 機能: 共有メモリの制御に使用されます。
- プロトタイプ
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- パラメータ
- shmid: shmget によって返される共有メモリ識別コード
- cmd: 実行されるアクション (可能な値は 3 つあります)
- buf: 共有メモリのモードステータスとアクセス許可を保持するデータ構造体を指します。
- 戻り値: 成功した場合は 0、失敗した場合は -1
- 最初のパラメータ shmid は、制御される共有メモリのユーザーレベルの識別子を表します。
- 2 番目のパラメーター cmd は、特定の制御アクションを表します。
- 3 番目のパラメータ buf は、制御された共有メモリのデータ構造を取得または設定するために使用されます。
このうち、shmctl 関数の第 2 パラメータ cmd としてよく使用されるのは、次の 3 つのオプションです。
テストコード:
#include "comm.hpp"
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID); //获取key值
if (key < 0)
{
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
if (shm < 0)
{
perror("shmget");
return 2;
}
printf("key: %x\n", key); //打印key值
printf("shm: %d\n", shm); //打印句柄
sleep(2);
shmctl(shm, IPC_RMID, NULL); //释放共享内存
sleep(2);
return 0;
}
ここでは監視スクリプトが使用されています: while :; do ipcs -m ; sleep 1;done
Attach は関連付けを意味し、detach は関連付けなしを意味し、n は番号を意味し、attach は関連付けを意味し、共有メモリに関連付けられているプロセスの数を示します。
4.shmat関数
- 機能: 共有メモリセグメントをプロセスアドレス空間に接続します。
- プロトタイプ
- void *shmat(int shmid, const void *shmaddr, int shmflg);
- パラメータ
- shmid: 共有メモリ識別子
- shmaddr: 接続アドレスを指定します
- shmflg: 可能な 2 つの値は SHM_RND と SHM_RDONLY です
- 戻り値: 成功した場合は共有メモリの最初のセクションを指すポインタが返され、失敗した場合は -1 が返されます。
shmat のパラメータのうち、shmaddr 共有メモリの仮想アドレスは、オペレーティング システムがそれを埋めることを示すために使用されます。shmflg はマウント方法を選択し、オペレーティング システムがそれを埋めることを示すために 0 を使用します。
5.shmdt関数
- 機能: 現在のプロセスから共有メモリセグメントを切り離します。
- プロトタイプ
- int shmdt(const void *shmaddr);
- パラメータ
- shmaddr: shmat によって返されるポインター
- 戻り値: 成功した場合は 0、失敗した場合は -1
- 注: 現在のプロセスから共有メモリ セグメントを切り離すことは、共有メモリ セグメントを削除することを意味しません。
作成された共有メモリをプログラムから切り離すことにより、shmaddr が共有メモリの仮想アドレスになります。
メイクファイル
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
ログ.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
//定义日志的几种状态
const std::string msg[]={
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message,int level)
{
//时间,日志信息
std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<<msg[level]<<"|"<<message;
return std::cout;
}
#endif
通信.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include "log.hpp"
//定义项目的路径,为了key生成
#define PATH_NAME "/home/hsj"
//定义项目的id,这里可以随意取
#define PROJ_ID 0x66
//共享内存的大小,最好是以页(page:4096)的整数倍
#define SHM_SIZE 4096
#endif
client.cc
#include "comm.hpp"
int main()
{
//创建我们上述的key值,用于我们两个进程都能相同的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果key值创建失败了,也就是我们的底层已经有了这一个key值,它会返回-1
if(k<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(1);
}
//如果创建成功,就将其写入日志文件中
Log("create key done",Debug)<<"Client say:"<<k<<std::endl;
//获取共享内存
//shmid(key值,需要开辟的共享内存大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREAT)
int shmid=shmget(k,SHM_SIZE,0);
//如果我们共享内存开辟失败,就返回错误信息,并且退出
if(shmid<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(2);
}
Log("create key success",Debug)<<"Client say:"<<k<<std::endl;
sleep(10);
//shmat(打开的共享内存地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg系统也会帮我们填写)
char* shmaddr=(char*)shmat(shmid,nullptr,0);
if(shmaddr==nullptr)
{
Log("attach shm failed",Error)<<"Client say:"<<k<<std::endl;
exit(3);
}
Log("attach shm success",Debug)<<"Client say:"<<k<<std::endl;
sleep(10);
//去关联
//将共享内存段与当前进程脱离
int n=shmdt(shmaddr);
//如果脱离失败了,就打印日志
assert(n!=-1);
(void)n;
Log("detach shm success",Debug)<<"Client say:"<<k<<std::endl;
sleep(10);
//client不需要释放共享内存资源,由server端释放即可
return 0;
}
サーバー.cc
#include "comm.hpp"
//将k转换成十六进制输出
std::string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer,sizeof buffer,"0x%x",k);
return buffer;
}
int main()
{
//1.创建公共的key值
//这里创建的key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端一样的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果此时创建失败了,则断言
assert(k!=-1);
(void)k;
Log("create key done",Debug)<<"Server say:"<<TransToHex(k)<<std::endl;
//2.创建共享内存,建议要创建一个全新的共享内存
//0666操作权限
int shmid=shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
//如果创建失败了就写日志,并且退出
if(shmid==-1)
{
perror("shmget");
exit(1);
}
Log("create shm done",Debug)<<"shmid:"<<shmid<<std::endl;
sleep(10);
//3.将指定的共享内存挂载到自己的地址空间
char* shmaddr=(char*)shmat(shmid,nullptr,0);
Log("attach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
sleep(10);
//这里是进行通信的逻辑代码
//4.将指定的共享内存,从自己的地址空间去关联
int n=shmdt(shmaddr);
assert(n!=-1);
(void)n;
Log("detach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
sleep(10);
//最后删除共享内存,IPC_RMID
n=shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
(void)n;
Log("del shm done",Debug)<<"shmid:"<<shmid<<std::endl;
return 0;
}
まず監視スクリプトを起動して共有メモリを表示し、次にサーバーを起動し、次にクライアントを起動します。ここでは、サーバーが起動して最初に共有メモリを開き、次に共有メモリを独自のアドレス空間に接続することがわかります。10 秒待った後、関連付けをキャンセルし、10 秒待った後、サーバーは共有メモリ領域を解放します。クライアントは主に、サーバーの起動後に共有メモリを自身のアドレス空間に接続し、10 秒間待機した後、関連付けを解除します。ここで、nattach が 0 から 1 に変化することがわかります。つまり、サーバーが独自のアドレス空間を関連付けた後、クライアントも独自のアドレス空間を関連付けるため、2 に変化します。その後、サーバーが終了したため 1 に変更され、その後、クライアントも終了したため 0 に変更されました。結局、サーバー側で共有メモリが解放されたため消えてしまいました。
スタック間の共有領域はカーネルのものですか、それともユーザーのものですか?
領域のこの部分はユーザー空間に属します。つまり、システムコールをする必要がなく、直接アクセスすることができ、双方のプロセスが通信したい場合には、メモリレベルで直接読み書きすることができます。
パイプと FIFO が読み取りと書き込みを通じて通信する必要があるのはなぜですか?
読み取りと書き込みはシステム コール インターフェイスなので、すべてシステム コールです。呼び出すパイプは本質的にファイルであり、ファイルはカーネル内の特定のデータ構造であり、オペレーティング システムによって維持されます。スタック間の共有メモリはユーザー空間に属します。上記のコードは主に、両方のプロセスが同じリソースを参照できるようにします。
3. プロセス間通信
サーバー.cc
#include "comm.hpp"
//将k转换成十六进制输出
std::string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer,sizeof buffer,"0x%x",k);
return buffer;
}
int main()
{
//1.创建公共的key值
//这里创建的key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端一样的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果此时创建失败了,则断言
assert(k!=-1);
(void)k;
Log("create key done",Debug)<<"Server say:"<<TransToHex(k)<<std::endl;
//2.创建共享内存,建议要创建一个全新的共享内存
//0666操作权限
int shmid=shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
//如果创建失败了就写日志,并且退出
if(shmid==-1)
{
perror("shmget");
exit(1);
}
Log("create shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//3.将指定的共享内存挂载到自己的地址空间
char* shmaddr=(char*)shmat(shmid,nullptr,0);
Log("attach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//这里是进行通信的逻辑代码
char buffer[SHM_SIZE];
while(true)
{
//读到什么就打印什么
printf("%s\n",shmaddr);
if(strcmp(shmaddr,"quit")==0) break;
sleep(1);
}
strcmp(shmaddr,"quit");
//4.将指定的共享内存,从自己的地址空间去关联
int n=shmdt(shmaddr);
assert(n!=-1);
(void)n;
Log("detach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//最后删除共享内存,IPC_RMID
n=shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
(void)n;
Log("del shm done",Debug)<<"shmid:"<<shmid<<std::endl;
return 0;
}
client.cc
#include "comm.hpp"
int main()
{
Log("child pid is:",Debug)<<getpid()<<std::endl;
//创建我们上述的key值,用于我们两个进程都能相同的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果key值创建失败了,也就是我们的底层已经有了这一个key值,它会返回-1
if(k<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(1);
}
//如果创建成功,就将其写入日志文件中
Log("create key done",Debug)<<"Client say:"<<k<<std::endl;
//获取共享内存
//shmid(key值,需要开辟的共享内存大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREAT)
int shmid=shmget(k,SHM_SIZE,0);
//如果我们共享内存开辟失败,就返回错误信息,并且退出
if(shmid<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(2);
}
Log("create key success",Debug)<<"Client say:"<<k<<std::endl;
//shmat(打开的共享内存地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg系统也会帮我们填写)
char* shmaddr=(char*)shmat(shmid,nullptr,0);
if(shmaddr==nullptr)
{
Log("attach shm failed",Error)<<"Client say:"<<k<<std::endl;
exit(3);
}
Log("attach shm success",Debug)<<"Client say:"<<k<<std::endl;
//进程间通信逻辑
//client将共享内存看做一个char类型的buffer
char a='a';
for(;a<='c';a++)
{
//我们每一次都向shmaddr[共享内存的起始地址]写入
snprintf(shmaddr,SHM_SIZE-1,"hello server,我是其他进程,我的pid,%d,inc:%c\n",getpid(),a);
sleep(5);
}
//向共享内存中拷贝一个quit
strcpy(shmaddr,"quit");
//去关联
//将共享内存段与当前进程脱离
int n=shmdt(shmaddr);
//如果脱离失败了,就打印日志
assert(n!=-1);
(void)n;
Log("detach shm success",Debug)<<"Client say:"<<k<<std::endl;
//client不需要释放共享内存资源,由server端释放即可
return 0;
}
結論 1 : 通信する双方が共有メモリを使用している限り、一方はデータを共有メモリに直接書き込み、もう一方はそのデータをすぐに見ることができます。したがって、共有メモリはすべてのプロセス間通信 (IPC) の中で最も高速です。
共有メモリはあまり多くのコピーを必要としないため、操作データをオペレーティング システムに引き渡す必要がありません。
ここでコピーが 2 つだけになるのは、プロセス A でキーボードから入力されたデータが直接共有メモリに配置され、プロセス B が共有メモリ内のデータを直接モニタに出力するためです。
パイプの場合:
- 1. キーボードから独自に定義したバッファへの最初のコピーです。
- 2. 自己定義バッファがパイプ ファイルに 2 回目にコピーされます。
- 3. パイプ ファイルからユーザー層バッファーへの 3 番目のコピー
- 4. ユーザー層バッファから表示画面への印刷は 4 部目です。
1. 手動入力、通信版サーバー出力
client.cc
#include "comm.hpp"
int main()
{
Log("child pid is:",Debug)<<getpid()<<std::endl;
//创建我们上述的key值,用于我们两个进程都能相同的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果key值创建失败了,也就是我们的底层已经有了这一个key值,它会返回-1
if(k<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(1);
}
//如果创建成功,就将其写入日志文件中
Log("create key done",Debug)<<"Client say:"<<k<<std::endl;
//获取共享内存
//shmid(key值,需要开辟的共享内存大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREAT)
int shmid=shmget(k,SHM_SIZE,0);
//如果我们共享内存开辟失败,就返回错误信息,并且退出
if(shmid<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(2);
}
Log("create key success",Debug)<<"Client say:"<<k<<std::endl;
//shmat(打开的共享内存地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg系统也会帮我们填写)
char* shmaddr=(char*)shmat(shmid,nullptr,0);
if(shmaddr==nullptr)
{
Log("attach shm failed",Error)<<"Client say:"<<k<<std::endl;
exit(3);
}
Log("attach shm success",Debug)<<"Client say:"<<k<<std::endl;
//进程间通信逻辑
while(true)
{
//向0号文件中读取数据,也就是从标准输入读取数据直接放入我们的缓冲区中,大小为SHM_SIZE
ssize_t s=read(0,shmaddr,SHM_SIZE-1);
//如果我们数据读取成功了
if(s>0)
{
shmaddr[s-1]=0;
if(strcmp(shmaddr,"quit")==0) break;
}
}
//向共享内存中拷贝一个quit
strcpy(shmaddr,"quit");
//去关联
//将共享内存段与当前进程脱离
int n=shmdt(shmaddr);
//如果脱离失败了,就打印日志
assert(n!=-1);
(void)n;
Log("detach shm success",Debug)<<"Client say:"<<k<<std::endl;
//client不需要释放共享内存资源,由server端释放即可
return 0;
}
サーバー.cc
#include "comm.hpp"
//将k转换成十六进制输出
std::string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer,sizeof buffer,"0x%x",k);
return buffer;
}
int main()
{
//1.创建公共的key值
//这里创建的key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端一样的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果此时创建失败了,则断言
assert(k!=-1);
(void)k;
Log("create key done",Debug)<<"Server say:"<<TransToHex(k)<<std::endl;
//2.创建共享内存,建议要创建一个全新的共享内存
//0666操作权限
int shmid=shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
//如果创建失败了就写日志,并且退出
if(shmid==-1)
{
perror("shmget");
exit(1);
}
Log("create shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//3.将指定的共享内存挂载到自己的地址空间
char* shmaddr=(char*)shmat(shmid,nullptr,0);
Log("attach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//这里是进行通信的逻辑代码
char buffer[SHM_SIZE];
while(true)
{
//读到什么就打印什么
printf("%s\n",shmaddr);
if(strcmp(shmaddr,"quit")==0) break;
sleep(1);
}
strcmp(shmaddr,"quit");
//4.将指定的共享内存,从自己的地址空间去关联
int n=shmdt(shmaddr);
assert(n!=-1);
(void)n;
Log("detach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//最后删除共享内存,IPC_RMID
n=shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
(void)n;
Log("del shm done",Debug)<<"shmid:"<<shmid<<std::endl;
return 0;
}
結論 2: 共有メモリにはアクセス制御がありません
共有メモリは、メモリに高速にアクセスするための操作メカニズムを提供するため、共有メモリにはアクセス制御がありません。共有メモリにデータがあるかどうかに関係なく、サーバーは読み取りを続けます。読み取り側と書き込み側ですら、相手が存在するかどうかはわかりません。内容が無いだけでは読まれません。これにより、同時実行の問題が発生します。
制御せずにサーバーに情報を送信し続けると、サーバーが読み取る情報が不完全になる可能性があり、データの不整合が発生します。
2. パイプによる共有メモリのアクセス制御
メイクファイル
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
ログ.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
//定义日志的几种状态
const std::string msg[]={
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message,int level)
{
//时间,日志信息
std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<<msg[level]<<"|"<<message;
return std::cout;
}
#endif
通信.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include "log.hpp"
//定义项目的路径,为了key生成
#define PATH_NAME "/home/hsj"
//定义项目的id,这里可以随意取
#define PROJ_ID 0x66
//共享内存的大小,最好是以页(page:4096)的整数倍
#define SHM_SIZE 4096
//创建一个管道文件名
#define FIFO_NAME "./fifo"
//创建一个类,用来创建管道
class Init
{
public:
Init()
{
//将权限掩码设置为0
umask(0);
//创建我们的管道,分别传入管道的地址,还有我们的读写的权限
int n=mkfifo(FIFO_NAME,0666);
//判断我们的管道是否创建成功
assert(n==0);
(void)n;
Log("create fifo success",Notice)<<"\n";
}
~Init()
{
//将管道进行删除
unlink(FIFO_NAME);
Log("remove fifo success",Notice)<<"\n";
}
};
//定义读取和写入模式
#define READ O_RDONLY
#define WRITE O_WRONLY
//封装接口,打开我们的文件
int OpenFIFO(std::string pathname,int flags)
{
//要打开的文件的路径还有打开文件的模式
int fd=open(pathname.c_str(),flags);
//判断是否打开成功
assert(fd>=0);
return fd;
}
//让进程进行等待
void Wait(int fd)
{
Log("等待中....",Notice)<<"\n";
//将此时的tmp写入到管道中
uint32_t tmp=0;
//将数据从fd管道中读取到我们的tmp中,读取4个字节的大小
ssize_t s=read(fd,&tmp,sizeof(uint32_t));
assert(s==sizeof(uint32_t));
(void)s;
}
//唤醒另外一个进程
void Signal(int fd)
{
uint32_t tmp=1;
//将我们的1写入我们管道中
ssize_t s=write(fd,&tmp,sizeof(uint32_t));
assert(s==sizeof(uint32_t));
(void)s;
Log("唤醒中....",Notice)<<"\n";
}
//关闭我们的管道文件
void CloseFifo(int fd)
{
close(fd);
}
client.cc
#include "comm.hpp"
int main()
{
Log("child pid is:",Debug)<<getpid()<<std::endl;
//创建我们上述的key值,用于我们两个进程都能相同的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果key值创建失败了,也就是我们的底层已经有了这一个key值,它会返回-1
if(k<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(1);
}
//如果创建成功,就将其写入日志文件中
Log("create key done",Debug)<<"Client say:"<<k<<std::endl;
//获取共享内存
//shmid(key值,需要开辟的共享内存大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREAT)
int shmid=shmget(k,SHM_SIZE,0);
//如果我们共享内存开辟失败,就返回错误信息,并且退出
if(shmid<0)
{
Log("create key failed",Error)<<"Client say:"<<k<<std::endl;
exit(2);
}
Log("create key success",Debug)<<"Client say:"<<k<<std::endl;
//shmat(打开的共享内存地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg系统也会帮我们填写)
char* shmaddr=(char*)shmat(shmid,nullptr,0);
if(shmaddr==nullptr)
{
Log("attach shm failed",Error)<<"Client say:"<<k<<std::endl;
exit(3);
}
Log("attach shm success",Debug)<<"Client say:"<<k<<std::endl;
int fd=OpenFIFO(FIFO_NAME,WRITE);
//进程间通信逻辑
while(true)
{
//向0号文件中读取数据,也就是从标准输入读取数据直接放入我们的缓冲区中,大小为SHM_SIZE
ssize_t s=read(0,shmaddr,SHM_SIZE-1);
//如果我们数据读取成功了
if(s>0)
{
shmaddr[s-1]=0;
//客户端写入成功之后,唤醒服务端
Signal(fd);
if(strcmp(shmaddr,"quit")==0) break;
}
}
CloseFifo(fd);
//去关联
//将共享内存段与当前进程脱离
int n=shmdt(shmaddr);
//如果脱离失败了,就打印日志
assert(n!=-1);
(void)n;
Log("detach shm success",Debug)<<"Client say:"<<k<<std::endl;
//client不需要释放共享内存资源,由server端释放即可
return 0;
}
サーバー.cc
#include "comm.hpp"
Init init;
//将k转换成十六进制输出
std::string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer,sizeof buffer,"0x%x",k);
return buffer;
}
int main()
{
//1.创建公共的key值
//这里创建的key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端一样的key值
key_t k=ftok(PATH_NAME,PROJ_ID);
//如果此时创建失败了,则断言
assert(k!=-1);
(void)k;
Log("create key done",Debug)<<"Server say:"<<TransToHex(k)<<std::endl;
//2.创建共享内存,建议要创建一个全新的共享内存
//0666操作权限
int shmid=shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
//如果创建失败了就写日志,并且退出
if(shmid==-1)
{
perror("shmget");
exit(1);
}
Log("create shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//3.将指定的共享内存挂载到自己的地址空间
char* shmaddr=(char*)shmat(shmid,nullptr,0);
Log("attach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
int fd=OpenFIFO(FIFO_NAME,READ);
//这里是进行通信的逻辑代码
char buffer[SHM_SIZE];
while(true)
{
//等待客户端唤醒
Wait(fd);
//读到什么就打印什么
printf("%s\n",shmaddr);
if(strcmp(shmaddr,"quit")==0) break;
}
//4.将指定的共享内存,从自己的地址空间去关联
int n=shmdt(shmaddr);
assert(n!=-1);
(void)n;
Log("detach shm done",Debug)<<"shmid:"<<shmid<<std::endl;
//最后删除共享内存,IPC_RMID
n=shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
(void)n;
Log("del shm done",Debug)<<"shmid:"<<shmid<<std::endl;
CloseFifo(fd);
return 0;
}
上記のコードはパイプの特性を利用しています。パイプにコンテンツがない場合、読み取りプロセスはブロックされます。パイプがいっぱいの場合、パイプの書き込み側もブロックされます。
5. System V メッセージキュー
1. メッセージキューの基本原理
メッセージ キューは、実際にはシステム内にキューを作成します。キューの各メンバーはデータ ブロックです。これらのデータ ブロックは、タイプと情報で構成されます。相互に通信する 2 つのプロセスは、何らかの方法で同じメッセージを参照します。メッセージ キュー。これら 2 つのプロセスが相互にデータを送信するときは、両方ともデータ ブロックをメッセージ キューの末尾に追加します。これら 2 つのプロセスがデータ ブロックを取得するときは、次に示すように、両方ともメッセージ キューの先頭からデータ ブロックをフェッチします。
- メッセージ キュー内の特定のデータ ブロックを誰が誰に送信するかは、データ ブロックのタイプによって異なります。
总结:
- メッセージ キューは、あるプロセスから別のプロセスにデータのチャンクを送信する方法を提供します。
- 各データ ブロックにはタイプがあるとみなされ、受信プロセスによって受信されたデータ ブロックは異なるタイプ値を持つことができます。
- 共有メモリと同様に、メッセージ キュー リソースも自分で削除する必要があります。そうしないと、システム V IPC リソースのライフ サイクルがカーネルに依存するため、メッセージ キュー リソースは自動的にクリアされません。
2. メッセージキューのデータ構造
もちろん、システム内に多数のメッセージ キューが存在する可能性もあり、システムはメッセージ キューに関連するカーネル データ構造も維持する必要があります。
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
メッセージ キュー データ構造の最初のメンバーが同じ型の構造変数msg_perm
であることがわかります。構造は次のように定義されています。shm_perm
ipc_perm
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
msqid_ds
データ構造とメッセージ キューの構造ipc_perm
は、それぞれ /usr/include/linux/msg.h と /usr/include/linux/ipc.h で定義されます。
3. メッセージキュー(msgget)の作成
メッセージキューを作成するには、msgget関数を使用する必要がありますが、msgget関数の関数プロトタイプは次のとおりです。
int msgget(key_t key, int msgflg);
例証します:
- メッセージ キューを作成するには、ftok 関数を使用してキー値を生成する必要もあります。このキー値は、msgget 関数の最初のパラメータとして使用されます。
- msgget 関数の 2 番目のパラメータは、共有メモリを作成するときに使用される shmget 関数の 3 番目のパラメータと同じです。
- メッセージ キューが正常に作成されると、msgget 関数は有効なメッセージ キュー識別子 (ユーザー層識別子) を返します。
4. メッセージキューの解放(msgctl)
メッセージキューを解放するには、msgctl関数を使用する必要がありますが、msgctl関数の関数プロトタイプは次のとおりです。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
例証します:
msgctl 関数のパラメータは、msgctl 関数の 3 番目のパラメータがメッセージ キューの関連データ構造で渡されることを除き、共有メモリを解放するときに使用される shmctl 関数の 3 つのパラメータと同じです。
5. メッセージキュー送信データメソッド(msgsnd)
メッセージキューにデータを送信するには、msgsnd関数を使用する必要がありますが、msgsnd関数の関数プロトタイプは次のとおりです。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgsnd関数のパラメータの説明:
- 最初のパラメータ msqid は、メッセージ キューのユーザー レベルの識別子を表します。
- 2 番目のパラメータ msgp は、送信されるデータ ブロックを表します。
- 3 番目のパラメータ msgsz は、送信されるデータ ブロックのサイズを示します。
- 4 番目のパラメータ msgflg は、データ ブロックの送信方法を示します。通常、デフォルトは 0 です。
msgsnd 関数の 2 番目のパラメーターは、次の構造でなければなりません。
<span style="color:#273849"><span style="background-color:#ffffff"><span style="color:#657b83"><span style="background-color:#fdf6e3"><code class="language-c"><span style="color:#859900">struct</span> <span style="color:#b58900">msgbuf</span><span style="color:#586e75">{</span>
<span style="color:#859900">long</span> mtype<span style="color:#586e75">;</span> <span style="color:#93a1a1">/* message type, must be > 0 */</span>
<span style="color:#859900">char</span> mtext<span style="color:#586e75">[</span><span style="color:#268bd2">1</span><span style="color:#586e75">]</span><span style="color:#586e75">;</span> <span style="color:#93a1a1">/* message data */</span>
<span style="color:#93a1a1">//该结构当中的第二个成员mtext即为待发送的信息,当我们定义该结构时,mtext的大小可以自己指定</span>
<span style="color:#586e75">}</span><span style="color:#586e75">;</span></code></span></span></span></span>
msgsnd関数の戻り値の説明:
- msgsnd は正常に呼び出され、0 を返します。
- msgsnd 呼び出しが失敗し、-1 が返されました。
6. メッセージキューのデータ取得方法(msgrcv)
メッセージ キューからデータを取得するには、msgrcv 関数を使用する必要があります。msgrcv 関数の関数プロトタイプは次のとおりです。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
パラメータ:
- 最初のパラメータ msqid は、メッセージ キューのユーザー レベルの識別子を表します。
- 2 番目のパラメーター msgp は、取得されたデータ ブロックを表し、出力パラメーターです。
- 3 番目のパラメータ msgsz は、取得するデータ ブロックのサイズを示します。
- 4 番目のパラメータ msgtyp は、受信するデータ ブロックのタイプを示します。
戻り値:
- msgsnd は正常に呼び出され、mtext 配列で取得された実際のバイト数を返します。
- msgsnd 呼び出しが失敗し、-1 が返されました。
6. セマフォ
1. セマフォの基本概念
- プロセスは共有リソースを必要とし、一部のリソースは相互に排他的に使用する必要があるため、プロセス間でこれらのリソースの使用が競合します。この関係をプロセス相互排他と呼びます。
- システム内の特定のリソースは、一度に 1 つのプロセスによってのみ使用が許可されており、そのようなリソースはクリティカル リソースまたは相互排他的リソースと呼ばれます。
- プロセス内で重要なリソースが関与するプログラム セグメントは、クリティカル セクションと呼ばれます。
- IPC リソースは削除する必要があります。削除しないと、システム V IPC のライフサイクルがカーネルに依存するため、自動的に削除されません。
共有メモリの理解に基づいて、次のようになります。
- 1. プロセス間通信を可能にするには、まず異なるプロセスが同じリソースを参照できるようにします。
- 2. 以前の通信方法は基本的に、最初に 1 つの問題を解決することに焦点を当てており、異なるプロセスが共有メモリなどの同じリソースを参照できるようにしていましたが、タイミングの問題も発生し、データの不整合が発生しました。
クリティカル リソース:複数のプロセス (実行フロー) から見える共通のリソースは、クリティカル リソースと呼ばれます。
クリティカル セクション:プロセスのコードをクリティカル セクションと呼び、重要なリソースにアクセスします。
したがって、保護なしで同じリソース (重要なリソース) にアクセスすることが主な理由で、複数の実行フローが相互に実行すると干渉します。非クリティカルセクションでは、複数の実行フローは互いに影響しません。クリティカル セクションをより適切に保護するために、いつでも複数の実行フローのうち 1 つのプロセスだけがクリティカル セクションに入ることができます。これを相互排他と呼びます。(つまり、私が訪問を終えるまで待ってから訪問する必要があります)
相互排除の本質はシリアル化ですが、シリアル化するとマルチスレッドの効率が低下します。
例えば:
私たちが映画を見に映画館に行く場合、映画館には VIP 上映スペースが 1 つしかありません。私たちは皆、そこにいたいと思っています。ただし、この場所を占有できるのは、いつでも 1 人だけです。私が座っているときは邪魔することはできません。私がこの席に座ると、私自身のクリティカルセクションのコードを実行していると言われます。これは相互排除です。しかし、実際に映画館に何百もの座席がある場合、誰がどこに座るかという問題を解決するには、一般に、映画に行く前に全員がチケットを購入する必要があります。
ここの映画は私たちの重要なリソースです。私たちはそれぞれ映画を見たいと思っています。つまり、それぞれが自分のクリティカル セクション コードを実行したいと考えています。映画を見るには、座席 (上映ホールのリソース) が必要です。そうすれば、この座席は本当にあなたのものになります。一人で座りますか? この位置では、この席はあなたのものですか? あまり!複数の人が同じ場所を争うことを避けるために、最初にチケットを購入します。このスポットはショー全体を通してあなたのものです。つまり、チケットを購入している限り、この座席を使用する権利があるということです。ここでのチケット購入の本質は座席予約の仕組みです。
これらのプロセスがクリティカル セクション リソースの異なる部分にアクセスする必要がある場合、これらのプロセスがクリティカル セクションの異なる部分にアクセスする限り、同時実行が保証されます。これは、上記の例と同様に、200 人が映画を見ている場合、各人が異なる位置にある場合、この 200 人は同時に映画を見ることができます。各プロセスが重要なリソースに入り、重要なリソースの一部にアクセスできるようにする必要がありますが、ユーザーが直接映画館に行って座席を占有することができないように、プロセスに重要なリソースを直接使用させることはできません。つまり、最初にセマフォを申請する必要があります (つまり、最初にチケットを購入する必要があります!)
セマフォの本質は、 int count =n; に似たカウンターです (正確ではありません) クリティカル セクションに入ろうとするすべてのプロセスは、セマフォを申請する必要があります。
セマフォ申請の要点:
セマフォをカウンターさせましょう --
セマフォのアプリケーションが成功する限り、当社の重要なリソースはお客様が必要とするリソースを確実に予約します。
セマフォの申請の本質は、実際には重要なリソースの予約メカニズムです。
セマフォの申請 -> 重要なリソースへのアクセス -> セマフォの解放
セマフォを解放すると、セマフォ++が
セマフォを表すために整数を使用することはできません。親子プロセスであっても、n--が発生するとコピーオンライトが発生し、同時に一つのnに対して動作することができないため、親子プロセスのnは一人につき一つですが、そして同時にそれに影響を与える方法はありません。
複数のプロセス (共有メモリ内の整数 n) が同じグローバル変数を参照できるようにし、全員がセマフォ n を申請するとします。これも不可能です。クライアントの場合、最初に n-- 、共有メモリ n++ にデータを書き込みます。サーバーの場合も、最初に n-- 、共有メモリを読み取り、次に n++ を実行しますが、変数に対しても n-- を実行します。時には物事がうまくいかないこともあります。
コンピュータでは、データ操作である n-- が実行されます。コンピュータの CPU には計算能力があり、データは位置 n に配置され、n はメモリ内にあります。つまり、計算は CPU 内で行われ、変数はメモリ内に存在する必要があります。次に、CPU が命令を実行すると、
- 1. まず、メモリ内のデータを CPU 内のレジスタにロードする必要があります (読み取り命令)
- 2.n--(命令の分析と実行)
- 3. CPUによって変更されたnをメモリに書き戻す(結果を書き戻す)
実行フローはいつでも実行中に切り替えることができます。上記の実行操作には 3 つのステップがあり、いつでも切り替えることができます。すべての実行フローで共有されるレジスターのセットは 1 つだけですが、レジスター内のデータは各実行フローに属し、その実行フローのコンテキストになります。プロセスを切り替える場合には、コンテキスト保護とコンテキスト回復が必要です。ここでの n が 5 であると仮定します。クライアントの n が CPU に入ると、n は 5 になります。つまり、上記の最初のステップまで実行すると、クライアント プロセスがスイッチアウトされ、クライアントのデータが切り替わります。対応する端末と実行するステップ (n=5、第 2 ステップ) が記録されます。この場合、レジスタの n は 2 のままで、プロセス クライアントの n は 5 です。次に、サーバー プロセスが CPU 上で実行され、n が 4 に変更されます。クライアントがリロードするとき、クライアントは戻ってきたときに元のプロセス コンテキスト情報を復元する必要があります。つまり、セマフォを 5 に直接リセットし、-- 操作を実行してセマフォを 4 に変更します。これは、現時点ではセマフォがクリティカル セクションに入ったプロセスの数を正確に表すことができなくなっていることを意味します。タイミングの問題により、n には中間状態が存在し、データの不整合が生じる可能性があります。ここで n を安全ではないと呼びます。n-- 操作にアセンブリ行が 1 つしかない場合、その操作はアトミックです。(上記の n-- はアトミックではありません)
アトミック性:実行しないか、実行するかのどちらかです。中間状態は存在しません。これをアトミック性と呼びます。
セマフォカウンター
- セマフォの申請-》カウンター---》P操作-》アトミックである必要があります
- セマフォを解放する-》Counter++-》V操作-》アトミックである必要があります
要約:
- セマフォは重要なリソースの予約メカニズムです。
- セマフォ自体も重要なリソースであり、そのアトミック性を維持する必要があるため、すべてのプロセスに同じセマフォを認識させたい場合は、プロセス間通信が必要です。(同じリソースを参照)
2. セマフォのデータ構造
関連するカーネル データ構造も、システム内のセマフォに対して維持されます。
セマフォのデータ構造は次のとおりです。
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
セマフォ データ構造の最初のメンバーもipc_perm
型構造変数であり、ipc_perm
構造は次のように定義されます。
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
msqid_ds
データ構造と共有メモリの構造はipc_perm
、それぞれ /usr/include/linux/sem.h と /usr/include/linux/ipc.h に定義されています。
3. セマフォの作成(semget)
セマフォ セットを作成するには、semget 関数を使用する必要があります。semget 関数の関数プロトタイプは次のとおりです。
int semget(key_t key, int nsems, int semflg);
説明する:
- セマフォ セットを作成するには、ftok 関数を使用してキー値を生成する必要もあります。このキー値は semget 関数の最初のパラメータとして使用されます。
- semget 関数の 2 番目のパラメータ nsems は、作成されたセマフォの数を示します。
- semget関数の第3パラメータは、共有メモリ作成時に使用するshmget関数の第3パラメータと同じです。
- セマフォ セットが正常に作成されると、semget 関数は有効なセマフォ セット識別子 (ユーザー層識別子) を返します。
4. セマフォの解放(semctl)
セマフォセットを削除するには、semctl関数を使用する必要があります。semctl関数の関数プロトタイプは次のとおりです。
int semctl(int semid, int semnum, int cmd, ...);
5. セマフォ操作(semop)
セマフォセットを操作するには semop 関数を使用する必要がありますが、semop 関数の関数プロトタイプは次のとおりです。
int semop(int semid, struct sembuf *sops, unsigned nsops);