Linux シグナルプログラミング シグナル関数例を詳しく解説 (4) -【Linux 通信アーキテクチャシリーズ】

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

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

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

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

1. シグナル機能の紹介

シグナルを受信した後、シグナル関数を使用してそれを無視またはキャプチャできます。次の例を参照してください。

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

void sig_usr(int signo)
{
    
    
	if(signo == SIGUSR1)
	{
    
    
		printf("收到了SIGUSR1信号!\n");
	}else if(signo == SIGUSR2){
    
    
		printf("收到了SIGUSR2信号!\n");
	}else{
    
    
		printf("收到了未捕捉的信号%d!\n", signo);
	}
}
int main(int argc, char *const *argv)
{
    
    
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
	}
	//系统函数。参数1是个信号,参数2是个函数指针,代表一个针对该信号的捕捉处理函数
	if(signal(SIGUSR2, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR2信号!\n");
	}
	for(;;)
	{
    
    
		sleep(1); //休息1s
		printf("休息1s\n");
	}
	return 0;
}

シグナル関数を 2 回呼び出すことで、シグナル SIGUSR1 とシグナル SIGUSR2 に対応する信号処理関数 (sig_usr) がそれぞれ登録され、これら 2 つのシグナルを受信したときに sig_usr が呼び出されます。sig_usr 信号処理関数では、一部の情報出力作業のみが行われます。

コンパイルして実行し、kill コマンドを使用して 2 つのシグナルを送信します。

kill -usr1 4155
kill -usr2 4155

プロセスの表示:
ここに画像の説明を挿入

図 1.1 シグナル USR1 と USR2 がそれぞれ nginx プロセスに送信され、出力ウィンドウにいくつかのプロンプトが表示されます

nginx プロセスが 2 つのシグナルを受信し、影響を受けることなく実行を継続できることがわかります。kill コマンドの別の形式も参照できます。シグナル名を直接使用してプロセスにシグナルを送信します。

この例では、次の 2 つの問題が認識されるはずです。

(1) シグナル関数はシステムの SIGUSR1 および SIGUSR2 シグナルをキャプチャし、独自の関数を使用してそれらを正常に処理します。

如果程序中不捕捉SIGUSR1或者SIGUSR2信号,用kill向改进程发送SIGUSR1或者SIGUSR2信号,进程会有什么表现呢?

回答: もちろん、これら 2 つの信号のシステムのデフォルトのアクションはプロセスを終了することなので、これはプロセスを終了することです。(自分でテストすることもできます)

(2) シグナルは特定のプロセスまたはカーネルによって送信される可能性がありますが、どのように送信されたとしても、ターゲットのプロセス (nginx) はシグナルを受信して​​います。ターゲットプロセスがシグナルを受信したという事実はカーネルによって認識され、この時点でカーネルはアクションを実行します。カーネルアクションとは何ですか?
図に示すように:

ここに画像の説明を挿入

図 1.1 突然の信号によりプロセスがユーザー モードからカーネル モードに切り替わり、処理後にユーザー モードに戻る

2. 拡張された思考の質問 - リエントラント関数の概念

次のコードを見てみましょう。何が問題でしょうか?

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


int g_mysign = 0;
void muNEfunc(int value)
{
    
    
	//...其他处理
	g_mysign = value;//函数muNEfunc能够修改全局变量g_mysign的值
	//...其他处理
}

void sig_usr(int signo)
{
    
    
	muNEfunc(22);
	if(signo == SIGUSR1)
	{
    
    
		printf("收到了SIGUSR1信号!\n");
	}else if(signo == SIGUSR2){
    
    
		printf("收到了SIGUSR2信号!\n");
	}else{
    
    
		printf("收到了未捕捉的信号%d!\n", signo);
	}
}

int main(int argc, char *const *argv)
{
    
    
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
	}
	//系统函数。参数1是个信号,参数2是个函数指针,代表一个针对该信号的捕捉处理函数
	if(signal(SIGUSR2, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR2信号!\n");
	}
	for(;;)
	{
    
    
		sleep(1); //休息1s
		printf("休息1s\n");

		muNEfunc(15);
		printf("g_mysign = %d\n", g_mysign);
	}
	return 0;
}

考えてみてください。このようなコードを書くとどのような問題が発生するでしょうか? (もちろん、この種の問題は極端な場合に発生します。通常は発生しませんし、確認するのは簡単ではありません)

毎回出力される g_mysign の値は 15 であることが期待されますが、シグナルが受信され、g_mysign の値がシグナル ハンドラー内で変更されるため、printf によって出力される g_mysign は 22 になります。この結果は予想外ではないでしょうか!

そこで、「リエントラント機能」と呼ばれる概念が導入されています

リエントラント関数は、リエントラント関数または非同期シグナル セーフ関数とも呼ばれ、信号処理関数内で安全に呼び出すことができる関数を指します(明らかに muNEfunc は安全ではありません)

⚠️有些周知的函数是不可重入的(在信号处理函数中不要调用的)如malloc分配内存的函数、printf屏幕输出函数等。(实际商业代码中避免在信号处理函数中调用printf函数)。

分析によれば、いくつかの結論と処理方法が得られます。

(1) 信号処理関数では、単純な処理を行うために単純なステートメントを使用するようにし、トラブルを避けるためにシステム関数を呼び出さないようにしてください。
(2) 信号処理関数内でシステム関数を呼び出す必要がある場合は、リエントラント関数のみを呼び出し、非リエントラントシステム関数は呼び出さないでください。
(3) errno の値を変更する可能性のあるリエントラント システム関数を信号処理関数で呼び出す必要がある場合は、事前に errno の値をバックアップし、信号から戻る前に errno の値を復元することを考慮する必要があります。処理機能。( errno的值的系统函数被认为是可重入的系统函数)

#include <errno.h> //用到errno则需要包含此头文件
void sig_usr(int signo)
{
    
    
	int myerrno = errno; //备份errno值
	//......进行一系列处理,如调用可重入函数
	//......
	errno = myerrno; //还原errno值
}

3. シグナルセット(シグナルマスクワード)

思考一个问题:收到一个SIGUSR1信号,开始执行信号处理函数sig_usr,尚未执行完成时,突然又收到一个SIGUSR1信号,系统会不会再次触发sig_usr函数开始执行呢? 一般的にはそうではありません。つまり、信号が受信され、信号処理機能の実行が開始されると、通常、後続の同じ信号は、信号処理機能が実行されるまで (システムによって自動的に処理されます)、「シールド/ブロック」されます。

プロセスは、どのシグナルが現在ブロックされているかを記憶する必要があります。例えば、SIGUSR1 信号を受信すると、処理中の信号のフラグを 1 にセットしてから信号処理関数を実行しますが、信号処理関数が実行されていない状態で再度信号を受信すると、システムは信号処理関数を実行します。シグナルのフラグが完了したことを検出します。これが 1 の場合、後続の SIGUSR1 シグナルはキューに入れられる (処理する信号処理関数の呼び出しを待つ) か、直接無視される (失われる) 必要があります。信号処理関数が実行されると、信号 SIGUSR1 信号に対応するフラグを 0 に戻します。このとき、キューに登録されている SIGUSR 信号または新しく受信した SIGUSR1 信号がある場合は、引き続き信号処理関数を呼び出して処理することができます。 。

このとき、 60 個の信号の状態 (0 または 1) を保存できるシグナル セットと呼ばれるデータ型であるシグナル セット概念が導入されました。特定の信号が受信されていないことを示すには 0 を使用し、特定の信号が受信されて処理中であることを示すには 1 を使用します。

	1、例如如果约定第五个位置表示信号SIGUSR1,程序开始执行后,收到一个SIGUSR1信号,就立即把第5个位置标记1:
		 0000100000,0000000000,0000000000,.....
	2、然后,等待调用信号处理函数处理这个到来的信号;
	3、此时,如果再收到一个SIGUSR1信号,因为第5个位置已经被标记为1,后面的这个SIGUSR1信号就会排队等候或者忽略;
	4、调用完处理函数后,把信号集的第5个位置标记回0:
	     0000000000,0000000000,0000000000,......
	5、此时,如果有排队等候或者新收到的SIGUSR1信号,就会又可以继续调用信号处理函数来处理了。

信号集这种数据类型用 sigset_t 来表示。
sigset_tの構造はおおよそ次のようになります。

typedef struct{
    
    
	unsigned long sig[2]; //long是4字节32位,两个就是64位,代表64个信号
}sigset_t;

4. 信号相関関数

これらの信号セットタイプを使用すると、sigempty、sigfillset、sigaddset、sigdest、sigprocmask、sigismember次のようないくつかの機能を導入できます。

(1) sigemptyset。信号セット内のすべての信号をクリアします。これは、これらの 60 を超える信号が来ていないことを意味します。

    0000000000,0000000000,000000000,......

(2) シグフィルセット。sigemptyset 関数の逆で、信号セット内のすべての信号を 1 に設定します。受信信号がキューに入れられるか無視されます。

	111111111,1111111111,111111111, ......

(3) 信号セットは 60 を超える信号をサポートしており、特定の信号を信号セットに追加 (信号フラグを 1 に設定) または削除 (信号フラグを 0 に設定) することができます。これは sigaaddset および sigdelset を使用して実行できます。

	sigaddset用于将某个信号设置为1,sigdelset用于将某个信号设置为0。

(4)sigprocmask、sigismember。

	sigpromask函数用于设置进程所对应的信号集(进程有默认的信号集,但可以用sigpromask函数设置其他信号集)。
	sigismember函数用于检测信号集的特定信号是否被置位。

5. sigprocmask などのシグナル関数のデモンストレーション例

例は次のとおりです。

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

//信号处理函数
void sig_quit(int signo)
{
    
    
	printf("收到了SIGQUIT信号!\n");
}
int main(int argc, char *const *argv)
{
    
    
	//定义新的信号集和原有的信号集
	sigset_t newmask, oldmask, pendmask; 
	//注册信号对应的处理函数
	if(signal(SIGQUIT, sig_quit) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
		//退出程序,参数是错误代码,0表示正常退出,非0表示错误,但具体什么错误,没有特别的规定
		exit(1);
	}
	//newmask信号集中所有的信号都清零(表示这些信号都没有来)
	sigemptyset(&newmask);
	//设置newmask信号集中的SIGQUIT信号位为1,再来SIGQUIT信号时进程就收不到
	sigaddset(&newmask, SIGQUIT);
	//设置该进程所对应的信号集
	//第1个参数用了SIG_BLOCK,表明设置进程新的信号屏蔽字为当前信号屏蔽字和第2个参数指向的信号集的并集。
	//一个进程的当前信号屏蔽字,开始全部为0,相当于把当前信号屏蔽字设置成newmask(屏蔽了SIGQUIT)。
	//第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第3个参数里,以备后续恢复用
	if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0){
    
    
		printf("sigprocmask(SIG_BLOCK)失败!\n");
		exit(1);
	}
	printf("我要开始休息10s了-------begin----,此时我无法接受SIGQUIT信号!\n");
	sleep(10);
	printf("我已经休息10s了-------end----!\n");
	//测试一个指定的信号位是否被置位,测试的是newmask
	if(sigismember(&newmask, SIGQUIT))
	{
    
    
		printf("SIGQUIT信号被屏蔽了!\n");
	}else{
    
    
		printf("SIGQUIT信号没有被屏蔽了!\n");
	}
	//测试一个指定的信号位是否被置位,测试的是newmask
	if(sigimember(&newmask,SIGHUP))
	{
    
    
		printf("SIGQUIT信号被屏蔽了!\n");
	}else{
    
    
		printf("SIGQUIT信号没有被屏蔽了!\n");
	}
	//现在取消SIGQUIT信号的屏蔽(阻塞)-- 把信号集还原回去
	//第一个参数用了SIG_SETMASK表明设置进程新的信号屏蔽字为第2个参数指向的信号集,第3个参数没用
	if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
	{
    
    
		printf("sigprocmask(SIG_SETMASK)失败!\n");
	}else{
    
    
		printf("sigprocmask(SIG_SETMASK)成功!\n");
	}
	//测试一个指定的信号位是否被置位,这里测试的是oldmask
	if(sigismember(&oldmask, SIGQUIT))
	{
    
    
		printf("SIGQUIT信号被屏蔽了!\n");
	}else{
    
    
		printf("SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要睡10s!!!!!!!\n");
		int mysl = sleep(10);
		if(mysl > 0)
		{
    
    
			printf("sleep还没睡够,剩余%d\n", mysl);
		}
	}
	printf("再见了!\n");
	return 0;
}

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

図 5.1 ケースの操作結果の分析

6. まとめ

関数をsigaction置き換えるために使用される関数もあります。signal商法でのみ使用されますsigaction理解できます。

おすすめ

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