[Linux 開発 - マルチプロセス プログラミング]

[Linux 開発 - マルチプロセス プログラミング]

  • 以前、 Linux ネットワーク プログラミングのためのソケット プログラミングをいくつか整理しましたが、実際のサーバーサイドを実装したい場合は、ソケットだけでは十分ではありません。また、実際の Web サービスを構築するために何が必要かを知る必要があります。
  • プロセスは、オペレーティング システムによってスケジュールされ割り当てられる最小単位です

序文

最初のクライアントから 100 番目のクライアントまで順番にサービスを提供するサーバーを構築できます。もちろん、最初のクライアントはサーバー側に対して不満を抱くことはありませんが、クライアントあたりの平均サービス時間が 0.5 秒である場合、100 番目のクライアントはサーバー側に対してかなりの不満を抱くことになります。

1. 2種類のサーバー

本当に顧客のことを考えているのであれば、顧客満足度の平均基準を高める必要があります。以下のタイプのサーバーをお持ちであれば、満足できるはずです。? ?
「最初の接続要求の処理時間は 0 秒、50 回目の接続要求の処理時間は 50 秒、100 回目の接続要求の処理時間は 100 秒です。ただし、それが受け入れられる限り、サービスにかかる時間はわずかです」 1秒。"

上位リクエストの数が片手で数えられるのであれば、クライアントは間違いなくサーバーに満足するでしょう。しかし、この数値を超えるとすぐにクライアントは苦情を言い始めます。次のような形でサービスを提供すると良いでしょう。
「すべての接続リクエストの処理時間は 1 秒を超えることはありませんが、平均サービス時間は 2 ~ 3 秒です。」

2. コンカレントサーバーの実装方法:

サービス時間を延長することができたとしても、平均満足度を高めるためには、同時にリクエストを開始するすべてのクライアントにサービスを提供できるようにサーバー側を改善する必要があります。
また、CPU の演算時間よりもネットワークプログラムのデータ通信時間の方が大きな割合を占めるため、複数のクライアントにサービスを提供することは CPU を効率的に利用する方法となります。次に、複数のクライアントに同時にサービスを提供する同時サーバー側サービスについて説明します。以下に、代表的なサーバー側同時実装モデルとメソッドを示します。

  • 1マルチプロセスサーバー:複数のプロセスを作成してサービスを提供します。
  • 2多重化サーバ:I/Oオブジェクトを束ねて一元管理することでサービスを提供します。
  • 3マルチスレッドサーバー:クライアントと同じ数のスレッドを生成してサービスを提供します。

ここでは、マルチプロセス サーバーに焦点を当てます。

1. 理解と応用

1. プロセスの理解

  • プロセス: Plants vs. Zombies などの「メモリ空間を占有する実行中のプログラム」。複数の Plants vs. Zombies ゲーム プログラムが同時に実行されると、対応する数のプロセスが生成され、対応する数のメモリ空間が生成されます。のプロセスも占有されます。
  • オペレーティング システムの観点から見ると、プロセスはプログラム フローの基本単位であり、複数のプロセスが作成されると、オペレーティング システムは同時に実行されます。プログラムの実行中に複数のプロセスが生成されることがあります次に作成するマルチプロセスサーバもその代表的なサーバの一つです。サーバー側を作成する前に、まずプログラムを通じてプロセスを作成する方法を理解します。

2. CPUコア数とプロセス数

  • 2 つの演算デバイスを備えた CPU はデュアルコア CPU と呼ばれ、4 つの演算デバイスを備えた CPU はクアッドコア CPU と呼ばれます。言い換えれば、CPU には複数のコンピューティング デバイス (コア) が含まれる場合があります。コアの数は、同時に実行できるプロセスの数と同じです。逆に、プロセス数がコア数を超えると、プロセスは CPU リソースを時分割で使用します。しかし、CPU は非常に高速に動作するため、すべてのプロセスが同時に実行されているように感じられます。もちろん、コアの数が多ければ多いほど、この感覚はより顕著になります。

3. プロセスID

  • すべてのプロセスには、作成方法に関係なく、オペレーティング システムから ID が割り当てられます。この ID は「プロセス ID」と呼ばれ、その値は 2 より大きい整数です。1 は、オペレーティング システムの起動後の最初のプロセスに割り当てられる (オペレーティング システムを支援するために使用される) ため、ユーザー プロセスは ID 値 1 を取得できません
    コマンドを使用して、ps au現在実行中のすべてのプロセスを表示できます。このコマンドは PID (プロセス ID) もリストできることに注意することが重要です。a および u パラメータを指定すると、すべてのプロセスの詳細を一覧表示できます。

4. プロセスの作成

  • プロセスの作成方法はいくつかありますが、ここではマルチプロセスサーバーを作成するためのfork関数を紹介します。
#include <unistd.h>
//→成功时返回进程 ID,失败时返回-1。
pid_t fork(void);
  • fork 関数は、呼び出し元プロセスのコピーを作成します。つまり、まったく別のプログラムからプロセスを作成するのではなく、fork 関数を呼び出す実行中のプロセスをコピーします。さらに、両方のプロセスは、fork 関数の呼び出しに続いて (正確には、fork 関数が戻った後) ステートメントを実行します。ただし、同じメモリ空間は同じ処理でコピーされるため、以降のプログラムフローはfork関数の戻り値に基づいて区別する必要があります。つまり、フォーク関数の次のような特徴を利用してプログラムの実行フローを区別します。
    • 親プロセス: fork 関数は子プロセス ID を返します。
    • 子プロセス: fork 関数は 0 を返します。
    • ここで、「親プロセス」とは元のプロセス、つまりfork関数を呼び出す主体を指し、「子プロセス」(子プロセス)とはfork関数を呼び出した親プロセスによってコピーされるプロセスを指します。

5. fork 関数を呼び出した後のプログラム実行プロセス:

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

  • pid が 0 の場合、子プロセスが開始されたことを意味します。0 より大きい場合、親プロセスを意味します。0 未満の場合、失敗を意味します。fork 関数を呼び出した後、親プロセスと子プロセス
    は完全に独立したメモリ構造。

2. ゾンビプロセス

1. 定義

ファイル操作では、ファイルを閉じることは、ファイルを開くことと同じくらい重要です。同様に、プロセスの破棄もプロセスの作成と同様に重要ですプロセスの破壊が真剣に考慮されないと、プロセスはゾンビプロセスとなり、すべての人を悩ませることになります。

プロセスの世界でも同じことが当てはまります。プロセスは、作業が完了した後 (メイン関数でプログラムを実行した後) に破棄される必要がありますが、場合によっては、これらのプロセスがゾンビ プロセスとなり、システム内の重要なリソースを占有してしまうことがありますこの状態のプロセスは「ゾンビプロセス」と呼ばれ、システムに負担をかける原因の一つとなっています。

2. ゾンビプロセスの原因と解決策

  • まず、次の 2 つの例を使用して、fork 関数の呼び出しによって生成された子プロセスを終了する方法を示します。
    1 パラメータを渡して、exit 関数を呼び出します。
    2. main関数内でreturn文を実行し、値を返します。
    exit 関数に渡されるパラメータ値と main 関数の return ステートメントによって返される値は、オペレーティング システムに渡されますオペレーティングシステムは、これらの値が子プロセスを生成した親プロセスに渡されるまで、子プロセスを破棄しません。この状態のプロセスはゾンビプロセスです。言い換えれば、子プロセスをゾンビ プロセスに変えるのはオペレーティング システムです
  • 解決策:オペレーティング システムは、親プロセスがリクエスト (関数呼び出し) をアクティブに開始した場合にのみ、この値を渡します。つまり、親プロセスが子プロセスの終了ステータス値を積極的に要求しない場合、オペレーティング システムは常にそれを保存し、子プロセスを長期間ゾンビ プロセス状態に保ちます。つまり、親には子供を引き取る責任があるのです。
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    
    
	pid_t pid=fork();
	
	if(pid==0)     // if Child Process
	{
    
    
		puts("Hi I'am a child process");
	}
	else
	{
    
    
		printf("Child Process ID: %d \n", pid);
		sleep(30);     // Sleep 30 sec.
	}

	if(pid==0)
		puts("End child process");
	else
		puts("End parent process");
	return 0;
}

結果は次のようになります。Z 状態のプロセスはゾンビ プロセスです。S: スリープ中、R: 実行中、Z ゾンビ
ここに画像の説明を挿入します

3. 信号処理

1. 定義

  • プロセスの作成と破棄時には、親プロセスが子プロセスと同じくらいビジーであることが多いことを上で学びました。子プロセスをいつ終了するかはわかりません。したがって、子プロセスが終了するのを待つために waitpid 関数を延々と呼び出すことはできません。これには、応答するための信号処理が必要です

  • 子プロセスの終了を認識する主体はオペレーティングシステムであるため、親プロセスが動作中であることをオペレーティングシステムが伝えることができれば、効率的なプログラムを構築することができます。

  • シグナル処理: 特定のイベントが発生したときにオペレーティング システムによってプロセスに送信されるメッセージ。また、メッセージに応答して、メッセージに関連するカスタム操作を実行するプロセス。

2.信号機能

  • プロセスは、子プロセスが終了したことを検出すると、オペレーティング システムに特定の関数を呼び出すように要求します。リクエストはシグナル関数の呼び出しによって完了します (シグナルはシグナル登録関数と呼ばれます)。
//→为了在产生信号时调用,返回之前注册的函数指针。
/*
函数名∶signal
参数∶int signo, void(* func)(int)
返回类型∶参数为int型,返回void型函数指针。
*/
#include<signal.h>
void(*signal(int signo, void(*func)(int))(int);

//等价于下面的内容:
typedef void(*signal_handler)(int);
signal_handler signal(int signo,signal_handler func);

  • 上記関数を呼び出す際、第 1 パラメータは特殊ケース情報、第 2 パラメータは特殊ケースで呼び出される関数のアドレス値 (ポインタ) になります。最初のパラメータで表される状況が発生すると、2 番目のパラメータで指定された関数が呼び出されます。以下に、シグナル関数に登録できるいくつかの特殊なケースと対応する定数を示します。
    • SIGALRM : アラーム機能を呼び出して登録する時間になりました。
    • SIGINT : CTRL+C を入力します。
    • SIGCHLD : 子プロセスが終了します。(英語は子供です)

3.呼び出し方法

シグナル関数を呼び出すステートメントを作成して、次のリクエスト
1 を完了します。「子プロセスが終了すると、mychild 関数が呼び出されます。」
代码:signal(SIGCHLD, mychild);

  • このとき、mychild関数のパラメータはint、戻り値の型はvoidにする必要があります。シグナル関数の 2 番目のパラメーターに対応します。さらに、定数 SIGCHLD は子プロセスの終了を示し、シグナル関数の最初のパラメーターになる必要があります。

2. 「アラーム機能で登録する時間が来ました。タイムアウト関数を呼び出してください。」
3. 「CTRL+C を入力したときにキーコントロール関数を呼び出してください。」

  • これら 2 つの状況を表す定数はそれぞれ SIGALRM と SIGINT であるため、シグナル関数は次のように呼び出されます。
    2. シグナル(SIGALRM、タイムアウト);
    3. シグナル(SIGINT、キーコントロール);

以上が信号登録処理である。シグナルが登録された後、登録シグナルが発生すると (登録が発生したとき)、オペレーティング システムはシグナルに対応する関数を呼び出します。

#include<unistd.h>
//→返回0或以秒为单位的距SIGALRM信号发生所剩时间。
unsigned int alarm(unsigned int seconds);

この関数を呼び出して正の整数パラメータを渡すと、対応する時間 (秒単位) の後に SIGALRM 信号が生成されます。この関数に 0 を渡すと、SIGALRM 信号の前回の予約がキャンセルされます。本関数でシグナルを予約した後、シグナルに対応するシグナル処理関数が指定されていない場合は、何も処理せずに(シグナル関数を呼び出して)処理を終了します。

4. 例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 定义信号处理函数timeout,返回值为void
void timeout(int sig)
{
    
    
	if(sig==SIGALRM)
		puts("Time out!");
//为了每隔2秒重复产生SIGALRM信号,在信号处理器中调用alarm函数
	alarm(2);	
}
// 定义信号处理函数keycontrol,返回值为void
void keycontrol(int sig)
{
    
    
	if(sig==SIGINT)
		puts("CTRL+C pressed");
}

int main(int argc, char *argv[])
{
    
    
	int i;
//注册SIGALRM、SIGINT信号及相应处理器
	signal(SIGALRM, timeout);
	signal(SIGINT, keycontrol);
//预约2秒后发生SIGALRM信号
	alarm(2);

	for(i=0; i<3; i++)
	{
    
    
		puts("wait...");
		sleep(100);
	}
	return 0;
}
  • 上記の for ループ:
    信号の生成と信号プロセッサの実行を表示し、毎回 100 秒の待機時間を設けるために、合計 3 回、スリープ関数が
    ループ内で呼び出されます。つまり、プログラムはさらに 300 秒と約 5 分後に終了することになり、 // 長い時間ですが、実際の実行には 10 秒もかかりません。

どうしてこれなの?明らかに300秒です。

  • 理由: 「シグナルが発生すると、スリープ関数の呼び出しによってブロック状態になったプロセスが目覚めます。」 呼び出し関数の主体は確かにオペレーティング システムですが、プロセスが停止しているときに関数を呼び出すことはできません
    。睡眠状態にある。したがって、シグナルが生成されると、シグナルハンドラーを呼び出すために、スリープ関数の呼び出しによってブロック状態になったプロセスが目覚めます。さらに、プロセスが一度起動されると、再びスリープ状態になることはありません。これは、スリープ関数で指定された時間がまだ達していない場合にも当てはまります。そのためプログラムは10秒もかからずに終了しますが、CTRL+Cを連続で入力すると1秒もかからない場合もあります。

4. マルチタスクに基づく同時サーバー

1. 原則

(空间与时间的平衡,以时间换取空间,还是以空间换取时间)
クライアントがアクセス要求を行うたびに、受け入れフェーズのサーバーはフォークして、クライアントのアクセス要求を処理する子プロセスを作成します。同時に、親プロセスは受け入れフェーズに戻り、新しいクライアントを待ち続けます。リクエスト問題は、複数のユーザーが同時にオンラインになっている場合、ボトルネックによりメモリが急激に増加することです。

2. マルチタスク同時サーバー インスタンス

//-----------------------------------------------------多任务并发服务器(进程)---------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> //Linux标准数据类型
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char* message);
//------------------------------------服务端-----------------------

void hand_childProc(int sig)
{
    
    
    pid_t pid;
    int status = 0;
    waitpid(-1, &status, WNOHANG);//-1:回收僵尸进程,WNOHANG:非挂起方式,立马返回status状态
    printf("%s(%d):%s removed sub proc:%d\r\n", __FILE__, __LINE__, __FUNCTION__, pid);
}
//服务器
void ps_moretask_server()
{
    
    
    
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = hand_childProc;
    sigaction(SIGCHLD, &act, 0);//当发现有SIGCHLD信号时进入到子进程函数。处理任务和进程回收(防止僵尸进程)

    int serv_sock;
    struct sockaddr_in server_adr, client_adr;
    memset(&server_adr, 0, sizeof(server_adr));
    server_adr.sin_family = AF_INET;
    server_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_adr.sin_port = htons(9527);
    serv_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (bind(serv_sock, (sockaddr*)&server_adr, sizeof(server_adr)) == -1)
        error_handling("ps_moretask server bind error");
    if (listen(serv_sock, 5) == -1)
    {
    
    
        error_handling("ps_moretask server listen error");
    }
    int count = 0;
    char buffer[1024];
    while (true)
    {
    
    
        socklen_t size = sizeof(client_adr);
        int client_sock = accept(serv_sock, (sockaddr*)&client_adr, &size);
        if (client_sock < 0) {
    
    
            error_handling("ps_moretask server accept error");
            close(serv_sock);
            return;
        }

        pid_t pid = fork();//会复制客户端和服务端的socket
        if (pid == 0)
        {
    
    
            close(serv_sock);//子进程关闭服务端的socket,因为子进程为了处理客户端的任务

            ssize_t len = 0;
            while ((len = read(client_sock, buffer, sizeof(buffer))) > 0)
            {
    
    
                len = write(client_sock, buffer, strlen(buffer));
                if (len != (ssize_t)strlen(buffer)) {
    
    
                    //error_handling("write message failed!");
                    std::cout << "ps_moretask server write message failed!\n";

                    close(serv_sock);
                    return;
                }

                std::cout << "ps_moretask server read & write success!, buffer:" << buffer << "__len:" << len << std::endl;

                memset(buffer, 0, len);//清理

                close(client_sock);
                return;
            }

        }
        else if (pid < 0)
        {
    
    
            close(client_sock);
            error_handling("ps_moretask server accept fork error");
            break;
        }

       

        close(client_sock);//服务端关闭的时候,客户端会自动关闭
    }

    close(serv_sock);
}

//------------------------------------客户端-----------------------
void ps_moretask_client()
{
    
    
    int client = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servaddr.sin_port = htons(9527);
    int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));
    if (ret == -1) {
    
    
        std::cout << "ps_moretask client connect failed!\n";
        close(client);
        return;
    }
    std::cout << "ps_moretask client connect server is success!\n";

    char buffer[256] = "hello ps_moretask server, I am client!";
    while (1)
    {
    
    
        //fputs("Input message(Q to quit):", stdout);//提示语句,输入Q结束
        //fgets(buffer, sizeof(buffer), stdin);//对文件的标准输入流操作 读取buffer的256字节
        //if (strcmp(buffer, "q\n") == 0 || (strcmp(buffer, "Q\n") == 0)) {
    
    
        //    break;
        //}

        size_t len = strlen(buffer);
        size_t send_len = 0;

        //当数据量很大时,并不能一次把所有数据全部发送完,因此需要分包发送
        while (send_len < len)
        {
    
    
            ssize_t ret = write(client, buffer + send_len, len - send_len);//send_len 记录分包的标记
            if (ret <= 0) {
    
    //连接出了问题
                fputs("may be connect newwork failed,make client write failed!\n", stdout);
                close(client);
                return;
            }
            send_len += (size_t)ret;

            std::cout << "ps_moretask client write success, msg:" << buffer << std::endl;

        }
        memset(buffer, 0, sizeof(buffer));

        //当数据量很大时,并不能一次把所有数据全部读取完,因此需要分包读取
        size_t read_len = 0;
        while (read_len < len)
        {
    
    
            size_t ret = read(client, buffer + read_len, len - read_len);
            if (ret <= 0) {
    
    //连接出了问题
                fputs("may be connect newwork failed, make client read failed!\n", stdout);
                close(client);
                return;
            }
            read_len += (size_t)ret;
        }
        std::cout << "from server:" << buffer << std::endl;
    };
    sleep(2);//延时2秒关闭客户端
    close(client);
    std::cout << "ps_moretask client done!" << std::endl;
}

//------------------------------------调用函数-----------------------
void ps_moretask_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        printf("%s(%d):%s wait ps_moretask server invoking!\r\n", __FILE__, __LINE__, __FUNCTION__);

        sleep(1);
        for (int i = 0; i < 5; i++)
        {
    
    
            pid = fork();
            if (pid > 0) {
    
    
                continue;
            }
            else if (pid == 0)
            {
    
    
                //子进程启动客户端
                ps_moretask_client();
                break;//到子进程终止,避免指数级创建进程 n的2次方。
            }
        }
    }
    else if (pid == 0) {
    
    
        //启动服务端
        ps_moretask_server();
    }
}

5. プロセス間通信 (IPC)

  • IPC (Inter Process Communication):プロセス間通信カーネルによって提供されるバッファを介したデータ交換のメカニズムプロセス間通信とは、2 つの異なるプロセス間でデータを交換できることを意味します。これを実現するには、オペレーティング システムが両方のプロセスが同時にアクセスできるメモリ空間を提供する必要があります。

  • 以下のプロセスAとプロセスBの間の例は、プロセス間通信ルールです。

    • 「パンを1個持っていれば、変数パンの値は1になります。パンを食べると、パンの価値は0に戻ります。したがって、変数パンの値によって私のステータスが判断できます。」つまり、プロセス A プロセス B は変数パンを通じてその状態を通知され、プロセス B は変数ブレッドを通じてプロセス A の発言を聞きます。
  • 2 つのプロセスが同時にアクセスできるメモリ空間がある限り、この空間を介してデータを交換できますただし、プロセスは完全に独立したメモリ構造を持っています。fork 関数によって作成された子プロセスであっても、親プロセスとメモリ空間を共有しませんしたがって、プロセス間通信は他の特殊な方法でのみ行うことができます。

  • このプロセスでは、子プロセスは親プロセスのメモリをコピーしますが、親プロセスは子プロセスのメモリをコピーしないため、子プロセスの一部の操作は親プロセスには認識されません。

1. 匿名パイプ - PIPE

亲族管道,处理两个不相干的进程时会有问题

  • パイプはプロセスに属するリソースではありませんが、ソケットと同様にオペレーティング システムに属します(つまり、フォーク関数のコピー オブジェクトではありません)。したがって、2 つのプロセスは、オペレーティング システムによって提供されるメモリ空間を介して通信します。
  • 実際にはパイプラインには多くの種類があります。
    • 最も一般的に使用されるのは、おそらくシェル内の「|」です。これは実際にはパイプ文字であり、前の式の出力を取得し、次の式を入力として導入します。たとえば、ssh 関連のプロセスを表示するには、通常「ps aux|grep ssh」を使用します。
    • 一般的に使用されるプロセス間通信パイプは 2 つあり、1 つはパイプパイプ (ファミリー パイプとも呼ばれます)です
    • これに対応するのがfifo パイプで、パブリック パイプとも呼ばれます
#include <unistd.h>
//→成功时返回 0,失败时返回-1。
int pipe(int filedes[2]);
/*
Filedes[0] 通过管道接收数据时使用的文件描述符,即管道出口。
Fledes[1] 通过管道传输数据时使用的文件描述符,即管道入口。
*/
  • 双方向パイプライン:
    ここに画像の説明を挿入します
//进程通信——双管道(PIPE)
void ps_pipe_double_func()
{
    
    
    int fds_server[2] = {
    
     -1, -1 };
    int fds_client[2] = {
    
     -1, -1 };

    pipe(fds_server);//父进程创建管道
    pipe(fds_client);
    
    pid_t pid = fork();

    if (pid == 0)
    {
    
    
        char buffer[64] = "client send by child process!\n";
        char readBuf[128] = "";
        //子进程数据写入
        write(fds_client[1], buffer, sizeof(buffer));

        read(fds_server[0], readBuf, sizeof(readBuf));

        printf("%s(%d):%s child process read ps_pipe by server :%s\r\n", __FILE__, __LINE__, __FUNCTION__, readBuf);

        printf("%s(%d):%s ---pid:%d\r\n", __FILE__, __LINE__, __FUNCTION__, getpid());

    }
    else
    {
    
    
        char buffer[64] = "server send by father process!\n";
        char readBuf[128] = "";
        //父进程读取数据
        read(fds_client[0], readBuf, sizeof(readBuf));

        printf("%s(%d):%s father process read ps_pipe by client :%s\r\n", __FILE__, __LINE__, __FUNCTION__, readBuf);

        write(fds_server[1], buffer, sizeof(buffer));

    }

    printf("%s(%d):%s ---pid:%d\r\n", __FILE__, __LINE__, __FUNCTION__, getpid());


}

2. 名前付きパイプ - FIFO

FIFO: 先入れ先出し、先入れ先出し。( 每个进程都要有个命名文件)

  • Pipe パイプラインと比較すると、2 つのプロセス間の通信というタスクはすでに完了していますが、十分に完了していない、または十分に徹底されていないと言えます。2 つの関連するプロセス間でのみ通信できるため、パイプ パイプラインの適用範囲が大幅に制限されます多くの場合、2 つの独立したプロセス間で通信できるようにしたい場合、パイプは使用できません。そこで、独立したプロセス間の通信を満たすパイプが登場しました。それが fifo パイプです

  • FIFO パイプの本質は、オペレーティング システム内の名前付きファイルです。

  • これは、オペレーティング システム内に名前付きファイルの形式で存在します。オペレーティング システム内で FIFO パイプが確認できます。権限があれば、読み書きすることもできます。

    • コマンドを使用します: mkfifo myfifo
    • 使用関数: int mkfifo(const char *pathname, mode_t mode); 成功: 0; 失敗: -1
  • カーネルは、FIFO ファイルのバッファを開き、FIFO ファイルを操作し、バッファを操作してプロセス通信を実現します。mkfifo を使用して FIFO を作成すると、open を使用して開くことができ、FIFO には一般的なファイル IO 関数が使用できます。例: 閉じる、読み取り、書き込み、リンク解除など。

//进程通信-命名管道(FIFO)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>//创建命名管道头文件
#include <fcntl.h>
#include <string.h>
void ps_fifo_func()
{
    
    
    mkfifo("./test_fifo.fifo", 0666);//创建FIFO命名管道,并设置mode
    pid_t pd = fork();
    if (pd == 0)
    {
    
    
        sleep(1);
        int fd = open("./test_fifo.fifo", O_RDONLY);//打开创建的fifo文件,并申请读权限

        char buffer[64] = "";
        ssize_t len = read(fd, buffer, sizeof(buffer));

        printf("%s(%d):%s read ps_fifo server :%s  len: %d\r\n", __FILE__, __LINE__, __FUNCTION__, buffer, len);

        close(fd);
    }
    else
    {
    
    
        int fd = open("./test_fifo.fifo", O_WRONLY);//打开创建的fifo文件, 并申请读写权限

        char buffer[128] = "hello, I am fifo server!";
        ssize_t len = write(fd, buffer, sizeof(buffer));

        printf("%s(%d):%s ps_fifo server wait success!\r\n", __FILE__, __LINE__, __FUNCTION__);

        close(fd);
    }
}

3. 共有メモリ

( 数据同步时,具有部分时间差,比较耗时)

1. 定義

  • 共有メモリを理解する前に、まず System V IPC 通信メカニズムを理解する必要があります。
    System V IPC メカニズムは、もともと UNIX の AT&T System V.2 バージョンで導入されました。これらのメカニズムは特に IPC (プロセス間通信) に使用され、同じバージョンで使用され、同様のプログラミング インターフェイスを備えているため、System V IPC 通信メカニズムと呼ばれることがよくあります。
    共有メモリは、3 つの System V IPC メカニズムのうちの 2 番目です。共有メモリを使用すると、異なるプロセスが同じ論理メモリを共有できるようになり、すべてのプロセスが制限なくこのメモリにアクセスしたり、変更したりできます。したがって、これはプロセス間で大量のデータを転送する非常に効率的な方法です。「共有メモリにより、さまざまなプロセスが同じ論理メモリを共有できるようになります」、ここで論理メモリです。言い換えれば、メモリを共有するプロセスは、同じ物理メモリにアクセスすることはできません。これに関する明確な規制はありませんが、ほとんどのシステム実装では、プロセス間の共有メモリが同じ物理メモリに配置されます
  • 共有メモリは、実際には IPC メカニズムによって割り当てられる物理メモリの特別な部分であり、プロセスのアドレス空間にマップすることも、アクセス許可を持つ他のプロセスのアドレス空間にマップすることもできます。これは、malloc を使用してメモリを割り当てるのと似ていますが、このメモリは共有できる点が異なります。

2. 共有メモリの作成、マッピング、アクセス、削除

  • IPC は、共有メモリを制御するための一連の API を提供します。共有メモリを使用する手順は通常次のとおりです。
    • 1) 共有メモリを作成または取得します。
    • 2) 前のステップで作成した共有メモリをプロセスのアドレス空間にマップします。
    • 3) 共有メモリにアクセスします。
    • 4) 共有メモリを現在のプロセスのアドレス空間から分離します。
    • 5) この共有メモリを削除します。
  • 詳細は次のとおりです。

1) shmget() 関数を使用して共有メモリを作成します

int shmget( key_t key, size_t size, int shmflg );
key: この共有メモリの名前。システムは共有メモリを区別するためにそれを使用します。同じ共有メモリにアクセスする異なるプロセスは同じ名前を渡す必要があります。
- size: 共有メモリのサイズ
- shmflg: 9ビットからなる共有メモリのフラグであり、内容はファイル作成時のモードと同じです。またはの形式で許可フラグとともに渡すことができる特別なフラグ IPC_CREAT があります。

2) 関数shmat() を使用して共有メモリをマップします

void* shmat( int shm_id, const void* shm_addr, int shmflg );
shm_id: は共有メモリの ID、shmget() 関数の戻り値です。
shm_addr: 共有メモリが現在のプロセスのアドレス空間に接続されている場所を指定します。通常は NULL が渡され、メモリの混乱を防ぐためにシステムが選択を行うことを示します。
shmflg: 共有メモリセグメントが読み取り専用であることを示す一連の制御フラグ (通常は 0 または SHM_RDONLY) を入力できます。
– 関数の戻り値は共有メモリの先頭アドレスポインタです。

3) 関数shmdt() を使用して共有メモリを分離します

int shmdt(void* shm_p);
–shm_p: shmat() の戻り値である共有メモリの先頭アドレスポインタです。
– 成功した場合は 0 を返し、失敗した場合は -1 を返します。

4) shmctl() 関数を使用して共有メモリを制御します

int shmctl( int shm_id, int command, struct shmid_ds* buf );
shm_id: は共有メモリの識別子であり、shmget()の戻り値です。
command: は実行されるアクションであり、次の 3 つの有効な値があります。
ここに画像の説明を挿入します

3. コード例:

#include <sys/ipc.h>
#include <sys/shm.h>//共享内存头文件

//共享的结构体
typedef struct {
    
    
    int id;
    char name[128];
    int age;
    bool sex;
    int signal;
}STUDENT, *P_STUDENT;

void ps_sharememory_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        //shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }
        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
        pStu->id = 666666;
        strcpy(pStu->name, "welcome moon");
        pStu->age = 19;
        pStu->sex = true;
        pStu->signal = 99;
        while (pStu->signal == 99)//同步
        {
    
    
            usleep(100000);
        }
        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);
           
    }
    else {
    
    
        usleep(500000);//休眠500ms,等待父进程写入数据, 1000单位为1ms,100000为100ms
        //shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }
        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
        while (pStu->signal != 99)//同步
        {
    
    
            usleep(100000);
        }
        printf("student msg: %d, %s, %d, %s\n", pStu->id, pStu->name, pStu->age, pStu->sex == true ? "male":"famale");
        pStu->signal = 0;
        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);
    }
}
  • 共有メモリの場合、2 つのプロセスの while ループ処理でデータを同期する必要があります。そうしないと、データにアクセスできません。短所: データ同期中に部分的な時間差が発生し、比較的時間がかかります。
  • 操作結果:
    ここに画像の説明を挿入します

4,信号量

1. 定義

  • 複数のプログラムが共有リソースに同時にアクセスすることによって引き起こされる一連の問題を防ぐには、1 つの実行スレッドのみがリソースの重要な領域にアクセスできるように、トークンを生成して使用して承認できるメソッドが必要です。コードはいつでも。クリティカル セクションは、データ更新を実行するコードを排他的に実行する必要があることを意味します。セマフォはそのようなアクセス メカニズムを提供し、同時に 1 つのスレッドのみがクリティカル セクションにアクセスできるようにします。これは、セマフォが使用されること调协进程对共享资源的访问を意味します。
  • セマフォは特別な変数であり、プログラムによるセマフォへのアクセスはアトミックな只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作操作です最も単純なセマフォは、0 と 1 のみを取る変数です。これは、 と呼ばれるセマフォの最も一般的な形式でもあります二进制信号量複数の正の整数を取ることができるセマフォは と呼ばれます通用信号量ここでは主にバイナリ セマフォについて説明します。

2.動作原理

  • 1. セマフォはシグナルの待機と送信、つまり P(sv) と V(sv) の 2 つの操作のみを実行できるため、それらの動作は次のようになります。 P(sv): sv の値が 0 より大きい場合、デクリメントします。 1; 値が 0 の場合、プロセス
    V(sv) の実行を一時停止します: 他のプロセスが sv を待って一時停止されている場合は、実行を再開します。sv を待って一時停止されているプロセスがない場合は、それに 1 を加えます。
  • 2. たとえば、2 つのプロセスがセマフォ sv を共有します。プロセスの 1 つが P(sv) 操作を実行すると、セマフォを取得し、クリティカル セクションに入り、sv を 1 減らすことができます。また、2 番目のプロセスは、P(sv) を実行しようとすると、クリティカル セクションに入ることがブロックされます。sv は 0 で、最初のプロセスがクリティカル セクションを出て、V(sv) を実行して解放するのを待ってハングアップします。セマフォが完了すると、2 番目のプロセスが実行を再開できます。

3. Linux セマフォの仕組み

  • 現在のセマフォ配列を表示します。
    ipcs -s

1. semget 関数 - 作成

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

2. semop 機能 - PV 操作を実行します。

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

3. semctl関数 - セマフォ情報の制御

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

4. コード例:

  • 共有メモリ モジュールの while ループを置き換えます -> 上記のコード例をセマフォに置き換えます。
//进程通信————————————————————共享内存+信号量

#include <sys/ipc.h>
#include <sys/shm.h>//共享内存头文件
#include <sys/sem.h>//信号量头文件

//共享的结构体
typedef struct {
    
    
    int id;
    char name[128];
    int age;
    bool sex;
    int signal;
}STUDENT, *P_STUDENT;

void ps_sharememory_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        //创建信号量
        key_t key = ftok(".", 2);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int sem_id = semget(key, 2, IPC_CREAT);//信号量id创建,key:代表当前路径,2:创建两个信号量,IPC_CREAT:表示进程通信创建

        //pv 生产者 消费者模式(P通过,V释放)
        //两个信号量
        semctl(sem_id, 0, SETVAL, 0);
        semctl(sem_id, 1, SETVAL, 0);


        //shmget创建共享文件id 
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }
        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
        pStu->id = 666666;
        strcpy(pStu->name, "welcome moon");
        pStu->age = 19;

        pStu->sex = true;

        
        //第一个信号量 v操作
        sembuf sop = {
    
    
            .sem_num = 0,
            .sem_op = 1
        };
        semop(sem_id, &sop, 1);//v操作,

        semctl(sem_id, 0, GETVAL);


        //第二个信号量 P操作
        sop.sem_num = 1;
        sop.sem_op = -1;
        semop(sem_id, &sop, 1);//P操作
        semctl(sem_id, 1, GETVAL);

        //删除共享内存
        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);


        //主进程删除信号量
        semctl(sem_id, 0, IPC_RMID);//释放掉第一个信号量
        semctl(sem_id, 1, IPC_RMID);//释放掉第二个信号量
        sleep(10);//休眠10秒

    }
    else {
    
    
        usleep(500000);//休眠500ms,等待父进程写入数据, 1000单位为1ms,100000为100ms

        //创建信号量
        key_t key = ftok(".", 2);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int sem_id = semget(key, 2, IPC_CREAT);//信号量id创建


        //shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }

        //第一个信号量 P操作
        sembuf sop = {
    
    
           .sem_num = 0,
           .sem_op = -1
        };
        semop(sem_id, &sop, 1);//P操作,
        semctl(sem_id, 0, GETVAL);

        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);

        //第二个信号量 V操作
        sop.sem_num = 1;
        sop.sem_op = 1;
        semop(sem_id, &sop, 1);//V操作,
        semctl(sem_id, 1, GETVAL);

        printf("student msg: %d, %s, %d, %s\n", pStu->id, pStu->name, pStu->age, pStu->sex == true ? "male":"famale");
        pStu->signal = 0;
        

        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);

        sleep(10);//休眠10秒

    }
}

5. メッセージキュー

1. 定義

  • メッセージ キューは、あるプロセスから別のプロセスにデータのブロックを送信する方法を提供します。各データ ブロックにはタイプが含まれていると見なされ、受信プロセスは異なるタイプを含むデータ構造を独立して受信できます。メッセージを送信することで、名前付きパイプの同期とブロックの問題を回避できます。ただし、メッセージ キューには、名前付きパイプ FIFO と同様、データ ブロックごとに最大長制限があります

2. メッセージキュー API

1. msgget 関数 - メッセージキューの作成

この関数は、メッセージ キューの作成とアクセスに使用されます。そのプロトタイプは次のとおりです。
int msgget(key_t key, int msgflg);

  • 他の IPC メカニズムと同様に、プログラムは特定のメッセージ キューに名前を付けるためのキーを提供する必要があります。msgflgは、メッセージキューのアクセス許可を示す許可フラグであり、ファイルのアクセス許可と同じである。msgflg は IPC_CREAT と OR できます。これは、キーで指定されたメッセージ キューが存在しない場合にメッセージ キューを作成することを意味します。キーで指定されたメッセージ キューが存在する場合、IPC_CREAT フラグは無視され、識別子のみが返されます。キーで指定されたメッセージ キューの識別子 (ゼロ以外の整数) を返し、失敗した場合は -1 を返します。

2. msgsnd 関数 - メッセージ キューにデータを追加します。

この関数は、メッセージをメッセージ キューに追加するために使用されます。そのプロトタイプは次のとおりです。
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

  • msgidは、msgget 関数によって返されるメッセージ キュー識別子です。
  • msg_ptr は送信するメッセージへのポインタですが、メッセージのデータ構造には特定の要件があります。ポインタ msg_ptr が指すメッセージ構造は、長整数のメンバ変数で始まる構造である必要があります。受信関数はこのメンバを使用しますメッセージの種類を決定します。したがって、メッセージ構造は次のように定義する必要があります:
    struct my_message{ long int message_type; /* 転送したいデータ*/ };
  • msg_szは、msg_ptrが指すメッセージの長さです。構造全体の長さではなく、メッセージの長さに注意してください。つまり、msg_szは、長整数のメッセージ・タイプのメンバー変数を除いた長さです。
  • msgflg は、現在のメッセージ キューがいっぱいになった場合、またはキュー メッセージがシステム全体の制限に達した場合に何が起こるかを制御するために使用されます。呼び出しが成功すると、メッセージ データのコピーがメッセージ キューに置かれて 0 が返され、失敗した場合は -1 が返されます。

3. msgrcv 関数 - メッセージ キューからデータを取得します。

この関数はメッセージ キューからメッセージを取得するために使用されます。そのプロトタイプは次のとおりです。
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);

  • msgid、msg_ptr、msg_stの関数はmsgsnd関数と同じです。
  • msgtype は単純な受信優先順位を実装できます。msgtype が 0 の場合、キュー内の最初のメッセージを取得します。
    • その値が 0 より大きい場合、同じメッセージ タイプの最初のメッセージが取得されます。
    • ゼロ未満の場合は、タイプが msgtype の絶対値以下である最初のメッセージを取得します。
  • msgflg は、キュー内に受信する対応するタイプのメッセージがない場合に何が起こるかを制御するために使用されます。
    正常に呼び出されると、この関数は受信バッファーに置かれたバイト数を返し、メッセージは msg_ptr が指すユーザー割り当てのバッファーにコピーされてから、メッセージ キュー内の対応するメッセージが削除されます。失敗した場合は -1 を返します。

4. msgctl 関数 - メッセージ キューを制御します。

この関数はメッセージ キューを制御するために使用され、共有メモリの shmctl 関数に似ています。そのプロトタイプは次のとおりです。
int msgctl(int msgid, int command, struct msgid_ds *buf);

  • commandは実行されるアクションであり、3 つの値を取ることができます。
    • IPC_STAT : msgid_ds 構造体のデータをメッセージ キューの現在の関連付け値に設定します。つまり、msgid_ds の値をメッセージ キューの現在の関連付け値で上書きします。
    • IPC_SET : プロセスに十分な権限がある場合、メッセージ キューの現在の関連付け値を msgid_ds 構造体で指定された値に設定します。
    • IPC_RMID : メッセージキューを削除します。
  • buf は、メッセージ キュー モードとアクセス権構造体を指す msgid_ds 構造体へのポインタです。msgid_ds 構造体には少なくとも次のメンバーが含まれます。
//成功时返回0,失败时返回-1.
struct msgid_ds {
    
     
uid_t shm_perm.uid; 
uid_t shm_perm.gid; 
mode_t shm_perm.mode;
 };

3. コード例

//进程通信----------消息队列
#include <sys/msg.h>

typedef struct {
    
    
    int type;
    struct {
    
    
        int id;
        char name[64];
        int age;
        char msg[256];
    }data, *pdata;
}MSG, *PMSG;

void ps_msg_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        int msg_id = msgget(ftok(".", 3), IPC_CREAT | 0666);
        if (msg_id == -1)
        {
    
    
              printf("%s(%d):%s ps_msg server error=%d:%s!\r\n", __FILE__, __LINE__, __FUNCTION__, errno,strerror(errno));

            return;
        }

        MSG msg;
        memset(&msg, 0, sizeof(msg));
        while (true)
        {
    
    
            ssize_t ret = msgrcv(msg_id, &msg, sizeof(msg.data), 1, 0);
            if (ret == -1) {
    
    
                sleep(1);
                printf("sleeping ~");
            }
            else
            {
    
    
                break;
            }
        }
        printf("accept msg: %d, %s, %d, %s\n", msg.data.id, msg.data.name, msg.data.age, msg.data.msg);

        getchar();
        msgctl(msg_id, IPC_RMID, 0);

    }
    else
    {
    
    
        int msg_id = msgget(ftok(".", 3), IPC_CREAT | 0666);
        MSG msg;
        memset(&msg, 0, sizeof(msg));
        msg.data.id = 5555;
        msg.data.age = 19;

        strcpy(msg.data.name, "moon");
        strcpy(msg.data.msg, "hello friend!");

        msgsnd(msg_id, &msg, sizeof(msg), 0);
        printf("send msg: %d, %s, %d, %s\n", msg.data.id, msg.data.name, msg.data.age, msg.data.msg);

        //休眠两秒后,待数据发送出去再做删除
        sleep(2);
        msgctl(msg_id, IPC_RMID, 0);
    }
    
}

  • 実行結果、error->38、38 は、基になる未実装のメソッド ( Function not implemented) を示しています。これは、Ubuntu を使用していて、メッセージ キューが Ubuntu システムで処理できないためです。
    ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/MOON_YZM/article/details/130891757