Linuxのプロセスプログラミングとfork関数の例題を詳しく解説(5) -【Linux通信アーキテクチャシリーズ】

シリーズ記事ディレクトリ

C++ スキル シリーズ
Linux 通信アーキテクチャ シリーズ
C++ 高性能最適化プログラミング シリーズ
ソフトウェア アーキテクチャ設計の深い理解 シリーズ
高度な C++ 同時スレッド プログラミング

ご注目をお待ちしております!
ここに画像の説明を挿入

现在的一切都是为将来的梦想编织翅膀,让梦想在现实中展翅高飞。
Now everything is for the future of dream weaving wings, let the dream fly in reality.

进程的概念

一个可执行程序执行一次就是一个进程,再执行一次就有是一个进程(多个进程共享同一个可执行文件),换句话说,进程一般定义为程序为程序执行的一个实例。

1. fork 関数と簡単な例を理解する

プロセス内では、fork を使用して子プロセスを作成でき、子プロセスが作成されると、次の fork 関数のステートメント (または fork の戻り値) から親プロセスと同じコードが実行されます。言い換えると、fork函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数调用中返回。

想像してみてください。実行されている親プロセスは 1 つだけで、実行パスは 1 つでしたが、fork を呼び出した後、実行パスは 2 つ (親プロセス用と子プロセス用に 1 つ) になりました。図 1.1 に示すように:
ここに画像の説明を挿入

図 1.1 fork 呼び出し後、プログラムの実行パスが 1 から 2 (親プロセスに 1、子プロセスに 1) に変更されます。

次の例を参照してください。

#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>

//信号处理函数
void sig_usr(int signo)
{
    
    
	printf("收到了SIGUSR1信号,进程ID = %d!\n", getpid());
}

int main(int argc, char *const *argv)
{
    
    
	pid_t pid;
	printf("进程开始执行!\n");
	//先简单处理一个信号
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
		exit(1);
	}
	//创建1个子线程
	pid = fork();
	//要判断子进程是否创建成功
	if(pid < 0)
	{
    
    
		printf("子进程创建失败,很遗憾!\n");
		exit(1);
	}
	//现在,父进程和子进程同时开始运行了
	for(;;)
	{
    
    
		sleep(1);
		printf("休息1s,进程ID = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

コンパイル、リンク、実行すると、結果は次のようになります。

ここに画像の説明を挿入

図 1.2 親プロセスは fork を呼び出して子プロセスを作成し、子プロセスを終了した後、親プロセスは SIGCHLD シグナルを受信します

ここに画像の説明を挿入

図 1.3 子プロセスを強制終了した後も、子プロセス (ゾンビ プロセス) が表示される

(1) プロセス ID 1183 の親プロセス ID は 1182 であることがわかります。これは、プロセス 1182 が fork 関数を呼び出して 1183 プロセスを作成していることを示しています。これら 2 つのプロセスのステータスが S+ であることにも注意してください。S はプロセスがほとんどの時間スリープを実行するため休止状態であり、通常の休止状態です。+ はフォアグラウンド プロセス グループ内にあることを意味します。

(2) ただし、ここで注意が必要なのは、fork関数を呼び出して子プロセスを作成した後、後続のコードが親プロセスで先に実行されるか、子プロセスで先に実行されるかは不確実であるということではありません。プロセス タイム スライスのスケジューリングの問題があるため、親プロセスは高速である必要があります (これはカーネル スケジューリング アルゴリズムに関連しています)。

(3) 印刷結果から、親プロセスと子プロセスの両方がこのシグナルを受信できることがわかります。これは、シグナルが子プロセスと親プロセスの共通コードであるこのコードをキャプチャしていることを示しています (両方に有効)親プロセスと子プロセス、またはこのコード部分は親プロセスと子プロセスの両方にあります。子プロセスは後で fork 関数によって作成されますが、すべてのコードは子プロセスの前に親プロセスによって実行されます。作成されたことは、子プロセスが実行されたことと同じです)。

(4) kill -9 コマンド (-9 は傍受またはキャプチャできない SIGKILL シグナルを表します) によって子プロセスを強制終了し、親プロセスが SIGCHLD シグナルを受信することもわかります。

(5) 図 1.3 から、強制終了された 1183 個のサブプロセスが ps コマンドのリストにまだ存在していることがわかりますが、COMMAND 列には defunct (失敗を意味します) と表示され、STAT 列には Z+ (Z 状態はゾンビ プロセスを意味します) と表示されています。 )。つまり、Z 状態と単語の消滅はどちらもゾンビ プロセスの典型的な兆候です。

2. ゾンビプロセスの生成と解決、SIGCHLD

⚠️(1)僵尸进程是怎么产生的能呢?

Linux オペレーティング システムでは、子プロセスが終了しても親プロセスがまだ生きている場合、親プロセスが () 関数を呼び出して追加の処理 (子プロセスの終了の破棄) を実行しない場合wait / waitpid、子プロセスは1つになります僵尸进程

このゾンビ プロセスは終了され、もう機能しませんが、親プロセスが子プロセスに関する情報をまだ必要としている可能性があるとカーネルが判断しているため、カーネルによって破棄されません。

ゾンビ プロセスは、少なくともプロセス ID (PID) のリソースを占有します。オペレーティング システム全体のプロセス数には制限があるため、作为开发者不应该允许僵尸进程的存在.

⚠️(2)那么怎么能让僵尸进程消失呢?

コンピュータを再起動しますか? このゾンビプロセスの親プロセスを手動で強制終了しますか? どちらも良い考えではありません。コードの観点からは、ゾンビ プロセスの生成を回避する
必要があります

子プロセスが強制終了されると、親プロセスは SIGCHLD シグナルを受け取ります。したがって、ソース コード内のフォーク動作のプロセス (子プロセスを作成する) では、SIGCHLD シグナルをインターセプトして処理する必要があります。

次の例を参照してください。

#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>
#include <sys/wait.h> //waitpid

//信号处理函数
void sig_usr(int signo)
{
    
    
	
	int status;
	switch(signo)
	{
    
    
		case SIGUSR1:
		{
    
    
			printf("收到了SIGUSR1信号,进程ID = %d!\n", getpid());
		}
		break;
		case SIGCHLD:
		{
    
    
			printf("收到了SIGCHLD信号,进程ID = %d!\n", getpid());
			//waitpid获取子进程的终止状态,子进程就不会成为僵尸进程了
			//第一个参数:-1,表示等待任何的子进程
			//第二个参数:保存子进程的状态信息
			//第三个参数:WNOHANG表示不要阻塞,让这个waitpid()立即返回
			pid_t pid = waitpid(-1, &status, WNOHANG);
			
			if(pid == 0)
				return;
			//子进程没结束,会立即返回该数字,但这里应该不是该数字,这里的情况是子进程结束才出发父进程的该信号
			if(pid == -1)
				return;
			//走到这里,表示成功,程序返回
			return;
		}
		break;
	}

}

int main(int argc, char *const *argv)
{
    
    
	pid_t pid;
	printf("进程开始执行!\n");
	//先简单处理一个信号
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
		exit(1);
	}
	//增加SIGCHLD信号的捕捉
	if(signal(SIGCHLD, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGCHLD信号!\n");
		exit(1);
	}
	//创建1个子线程
	pid = fork();
	//要判断子进程是否创建成功
	if(pid < 0)
	{
    
    
		printf("子进程创建失败,很遗憾!\n");
		exit(1);
	}
	//现在,父进程和子进程同时开始运行了
	for(;;)
	{
    
    
		sleep(1);
		printf("休息1s,进程ID = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

コンパイル、リンク、実行すると、結果は次のようになります。
ここに画像の説明を挿入

図 2.1 子プロセスを強制終了した後、ゾンビ プロセスが存在しない

3. プロセスのメモリ空間とプロセスの世代

fork新しいプロセスの生成速度は非常に速く、生成された新しいプロセスは元のプロセスのメモリ空間をコピーするのではなく、元のプロセス(親プロセス)とメモリ空間を共有します。このメモリ空間の特徴は「コピーオンライト」です。つまり、元のプロセスとフォークされた子プロセスは同時にメモリを自由に読み取ることができますが、子プロセス (または親プロセス) がメモリを変更すると、メモリ空間内の他のプロセスの使用に影響を与えないように、プロセスのみが使用するコピーを作成します。

次の例を参照してください。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char * const * argv)
{
    
    
	fork(); //一般fork都会成功,所以不判断返回值了
	fork();
	
	for(;;)
	{
    
    
		sleep(1); //休息1s
		printf("休息1s, 进程ID = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

⚠️上面的代码执行后会产生几个进程?

forkの能力は単純に2つに分かれます(1つのルートが2つのルートに分割される/1つのプロセスが2つのプロセスになります)。

コードでは、最初のフォークが2つに分かれ、2行が同時に下りていますが、どちらの行も2回目のフォークを経験しており、それぞれのフォークが2つに分かれているので、4つに分割されています(最終的に4つのプロセスが終了します)生成されます)

次に、実行結果を見てみましょう。
ここに画像の説明を挿入

図 3.1 フォークを 2 回呼び出し、結果として 4 つの nginx プロセスが生成される

4番目に、親プロセスと子プロセスの実行ブランチを決定します。

fork が実行されると 1 つのルートが 2 つのルートになるため、fork 関数は実際には 2 回戻ります (親プロセスが 1 回返し、子プロセスも 1 回戻ります)。

親プロセスの fork 関数が返す値と子プロセスが返す値が異なることを基に、親プロセスか子プロセスかを識別するコードを記述して、親プロセスと子プロセスが一致するようにすることができます。さまざまなコード分岐を実行します。

次のコード例を観察すると、プログラムが正確に次のとおりであることがわかります。通过判断fork的返回值来决定父进程执行哪些代码、子进程执行哪些代码。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int g_mygbltest = 0;
int main(int argc, char * const * argv)
{
    
    
	pid_t pid;
	printf("进程开始执行!\n");
	//创建一个子进程
	pid = fork();
	//要判断子进程是否成功
	if(pid < 0)
	{
    
    
		printf("子进程创建失败,很遗憾!\n");
		exit(1);
	}
	//走到这里,fork()成功,执行后续代码的可能是父进程,也可能是子进程
	if(pid == 0)
	{
    
    
		//子进程,因为子进程的fork()返回值会是0
		//这是专门针对子进程的处理代码
		while(1)
		{
    
    
			g_mygbltest++;
			sleep(1); //休息1s
			printf("我是子进程,我的进程ID = %d, g_mygbltest = %d\n", getpid(), g_mygbltest);
		}
	}else{
    
    
		//这里就是父进程,因为父进程的fork()返回值会 >0
		//这是专门针对父进程的处理代码
		while(1)
		{
    
    
			g_mygbltest++;
			sleep(5);
			printf("我是父进程,我的进程ID = %d, g_mygbltest = %d\n", getpid(), g_mygbltest);
		}
	}
	return 0;
}

操作の結果は次のようになります。
ここに画像の説明を挿入

図4.1 親プロセスと子プロセスの実行分岐実行結果

上記の結果を観察し、g_mygbltest グローバル変数の値に注目すると、親プロセスと子プロセスのグローバル変数の値が異なり、各プロセスが個別にカウントされていることがわかります。

上記の例から、次の結論が得られます。

fork は子プロセスの場合は 0 を返しますが、親プロセスの場合、戻り値は新しく作成された子プロセスの ID です。

親プロセスと子プロセスのグローバル変数 g_mygbltest の値も異なり、これら 2 つのプロセスには書き込みアクション (グローバル変数 g_mygbltest の値の書き換え、つまりメモリの書き換え) があるため、各プロセスは異なる値を持ちます。 )、カーネルは各プロセスに独自の使用のためだけにメモリを割り当てるため、各プロセスの g_mygbltest 値は相互に干渉しません。

5. フォーク実行に関する論理的判断

#include<stdio.h>
#include<stdlib.h> //malloc,exit
#include<unistd.h> //fork
#include<signal.h>

int main(int argc, char * const * argv)
{
    
    
	((fork() && fork()) || (fork() && fork()));
	for(;;)
	{
    
    
		sleep(1);
		printf("休息1s, 进程id = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

操作の結果は次のようになります。
ここに画像の説明を挿入

図 5.1 ps コマンドを使用して、(fork() && fork()) || (fork() && fork()); によって生成された 7 つのプロセスを表示します。

6. フォーク失敗の考えられる理由の概要

(1) システム内のプロセスが多すぎます。

  • ゾンビプロセスが多すぎるなど、何か問題があるはずです。システム全体では、ps コマンドでプロセスを一覧表示するときに表示されるプロセス ID (PID) が制限されており、作成される子プロセスの ID 値は親プロセスの ID 値より 1 大きくなり、プロセス ID は次のようになります。たとえば、プロセスが終了すると、一定期間が経過すると、オペレーティング システムはこのプロセスの ID を、使用 (リサイクル) するために新しく作成された他のプロセスに割り当てます。

  • デフォルトでは、プロセス ID の最大値は通常 32767 です。0 から 32767 までのすべての数値が占有されている場合、フォークは失敗します。もちろん、これは極端な状況です。

(2) 作成したプロセス数が、現在のユーザーが許可する最大プロセス数を超えています。

  • 各ユーザーには、開くことを許可されるプロセスの合計数があります。
printf("每个用户允许创建的最大进程数 = %ld\n", sysconf(_SC_CHILD_MAX));

おすすめ

転載: blog.csdn.net/weixin_30197685/article/details/131343093