プロセス信号 1
1. シグナルの使用を開始する
シグナルとは:シグナルは、システム内で特定の種類のイベントが発生したことをプロセスに通知するメッセージです。
シグナルにはさまざまな種類があり、シグナルとイベントが対応しているため、シグナルを受信した後にそれがどのようなイベントであるか、またそのシグナルにどう対処するかを知ることができます。
1. 信号のいくつかの特性
- プロセスはシグナルを受信する前にシグナルをどのように処理すべきかをすでに知っています。つまり、プロセスはシグナルを認識して処理できます。
- シグナルはいつでもプロセスに対して生成される可能性があるため、プロセスとシグナルは非同期です。
- プロセスとシグナルは非同期であるため、シグナル発生時にはプロセスが優先度の高い処理を実行している可能性がありますが、このときプロセスはシグナルをすぐに処理することができず、適切なタイミングで処理する必要があります。この空のウィンドウ内のシグナルを保存できるようにするには、プロセスにシグナルを記録する機能があることを示します。
- プロセスによって記録されるシグナルが多数存在する可能性があるため、プロセスはすべてのシグナルを管理するデータ構造を使用する必要があります。Linux では、シグナル管理はビットマップ構造を採用しており、ビットの位置はシグナルの番号を表します。
- したがって、いわゆる送信信号の本質は、特定のプロセスの信号ビットマップ内の特定のビットを直接変更することです。(
0
->により1
) - プロセス信号のビットマップ構造の本質は
task_struct
依然として内部データに属しているため、プロセス信号のビットマップ構造内のデータの変更はオペレーティング システムによってのみ実行できます。つまり、信号がどのような方法で変更されたとしてもが生成されると、OS は最終的に最終的な送信プロセスを完了する必要があります。
2. 信号処理方式
- デフォルトのアクション (つまり、信号に対してオペレーティング システムによって設定されたデフォルトのアクション) を実行します。
- 信号を無視する
- カスタム アクション (ユーザーがオペレーティング システムによって設定されたデフォルトのアクションを変更し、必要なアクションに変更した) を実行するには、オペレーティング システムが提供する信号処理機能により、カーネルがユーザー モードに切り替える必要があります
signal
。シグナル処理関数の処理時にこのシグナルを実行することを、シグナルをキャッチする(Catch)といいます。
信号キャプチャの概要
信号キャプチャでは主にsignal
関数が使用され、内部でコールバック関数が使用されます。
この関数の機能は、指定された信号のデフォルトの動作を変更して、2 番目のパラメーターに対応する関数を実行することです。この関数は、戻り値がパラメーターである関数でなければなりませvoid
んint
。
- パラメータ:
- 信号の番号。
- コールバック関数への関数ポインタ。
- 戻り値: 前のシグナル ハンドラ関数ポインタを返します。エラーがある場合は戻ります
SIG_ERR(-1)
。
コード例:
キーボードを押しているのはCtrl + C実際には 2 番の信号です 次に、 2
2 番の。
#include <iostream>
#include <signal.h>
#include <unistd.h>
void hander(int sig)
{
std::cout << "get a signal " << sig << std::endl;
}
int main()
{
signal(2, hander);
while (true)
{
std::cout << "我正在运行...,我的PID是: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
操作結果:
Ctrl + C以前はプロセスを終了できないようにしており、それがカスタム アクションになっていることがわかります。
3. Linux でのシグナル
Linuxではkill -l
、コマンドを使用してすべての信号をリストできます。
注意して観察したところ、ここには32 ,33
信号がないことが分かりました。このうち、1~31
スレーブ信号は、通常の信号と34~64
リアルタイム信号である。(ここでは主に通常の信号について説明します)
- 各信号にはマクロ定義の番号と名前があり、次の
/usr/include/bits/signum.h
場所にあります。
- 一般的な信号のデフォルトの処理アクションについては、「 」
man 7 signal
で詳しく説明します。
2. 信号の生成
Linux でプロセス信号を生成する方法はたくさんあります。以下で一緒に見てみましょう。
1. 端子ボタンによる信号の生成
Linux でコマンドを入力すると、Shellの下でフォアグラウンドプロセスを開始できます。フォアグラウンド プロセスを終了したい場合は、 を押し。実際、これもシグナルであり、それに対応するシグナル番号はこれです。デフォルトは次のとおりです。信号に対応する処理アクションは、現在のフォアグラウンド プロセスを終了することです。Ctrl + CCtrl + C2
SIGINT
-
ユーザーがそれを押すとCtrl-C 、キーボード入力によってハードウェア割り込みが生成され、これが OS によって取得され、信号として解釈されて、ターゲットのフォアグラウンド プロセスに送信されます。フォアグラウンド プロセスは信号を受信して、プロセスを終了します。
-
Ctrl-C 生成されたシグナルはフォアグラウンド プロセスにのみ送信できます。コマンドの後にコマンドを追加すると
&
、バックグラウンドで実行できるため、シェルは新しいコマンドを受け入れ、プロセスの終了を待たずに新しいプロセスを開始できます。同様に、そのようなバックグラウンド プロセスを で強制終了することはできませんCtrl-C 。 -
シェルはフォアグラウンド プロセスと任意の数のバックグラウンド プロセスを同時に実行でき、フォアグラウンド プロセスのみがCtrl-C そのような制御キーによって生成された信号を受信できます。
ハードウェア割り込みについて:
- ハードウェア割り込みは、ハードウェア デバイスによってトリガーされる割り込みです。ハードウェア デバイスに処理すべきデータまたはイベントがある場合、CPU に割り込み要求が送信されます。CPU は割り込み要求を受信すると、現在実行中のタスクを直ちに一時停止します。そして、割り込みハンドラに入り、割り込み要求を処理します。
Softirqs について
- Signal はプロセス間のイベントを非同期に通知する方法であり、ソフト割り込みに属します。
2. システム関数を呼び出してプロセスにシグナルを送信します。
a. キル機能
kill
関数はオペレーティング システムによって提供されるシステム コールであり、これを通じて指定されたシグナルを指定されたプロセスに送信できます。
-
パラメータ:
- ターゲットプロセスの
pid
。 - 送信する信号
signal
。
- ターゲットプロセスの
-
戻り値: 呼び出しが成功した場合は戻り
0
、呼び出しが失敗した場合は戻り値-1
。
kill
コマンドは関数を呼び出すことで実装されますが、以下ではコマンドkill
の実装をシミュレーションしてみましょう。kill
コード例:
#include <iostream>
#include <string>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include <signal.h>
#include <sys/types.h>
void Usage(const std::string proc)
{
std::cout << "Usage:" << std::endl;
std::cout << " " << proc << " 信号编号 目标进程" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(-1);
}
pid_t pid = atoi(argv[2]);
int signo = atoi(argv[1]);
int return_val = kill(pid, signo);
if (return_val == -1)
{
std::cout << "错误码:" << errno << " 错误信息:" << strerror(errno) << std::endl;
}
return 0;
}
操作結果:
b. レイズ関数
この関数は、指定されたシグナルを現在のプロセスに送信します。
-
パラメータ: 送信する信号
sig
。 -
戻り値: 呼び出しが成功した場合は を返し
0
、呼び出しが失敗した場合は を返しません0
。
コード例:
関数を使用してraise
現在のプロセスに一時停止信号を送信します。一時停止後、19
SIGSTOP
コマンド ライン18
でプロセスにSIGCONT
実行継続信号を送信できます。
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
sleep(1);
std::cout << "我要被暂停了,我的PID是:" << getpid() << std::endl;
raise(19);
std::cout << "我要继续运行了,我的PID是:" << getpid() << std::endl;
return 0;
}
c. 中止機能
abort
この関数は、現在のプロセスにシグナルを受信させ、異常終了させます。関数abort
は実際にプロセスに6
シグナルを送信します。関数と同様に、関数は常に成功するため、戻り値はありません。シグナルがキャッチされても、呼び出し関数はプロセスを終了します。SIGABRT
exit
abort
6
abort
コード例:
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
int main()
{
std::cout << "begin" << std::endl;
abort();
std::cout << "end" << std::endl;
return 0;
}
これら 3 つの関数はkill
システム コールのみで、他の 2 つは C ライブラリ関数であり、それぞれの機能を比較すると次のようになります。
3. ソフトウェア条件によって生成される信号
SIGPIPE
「パイプライン」で紹介した、ソフトウェアの条件によって生成される信号です。ここでは主にalarm
機能とSIGALRM
信号について紹介します。
この関数を呼び出すと目覚まし時計を設定できます。つまり、数秒alarm
後に現在のプロセスにシグナルを送信するようにカーネルに指示しseconds
、シグナルのデフォルトの処理アクションは現在のプロセスを終了することです。14
SIGALRM
- パラメータ: アラームの秒数。
- 戻り値: この関数の戻り値は少し特殊で、最後に設定された目覚まし時計の時刻からの残り秒数、または 0 (0 は最後の目覚まし時計が妨害されておらず、正しい実行が完了したことを意味します)
コード例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
alarm(1);
int count = 0;
while (true)
{
std::cout << count++ << std::endl;
}
return 0;
}
4. ハードウェア異常による信号の発生
ハードウェア例外信号は、ハードウェアがエラーを生成し、何らかの方法でハードウェアによって検出されてカーネルに通知され、カーネルが現在のプロセスに適切な信号を送信することを意味します。
たとえば、現在のプロセスが0
除算命令を実行すると、CPU の演算器が例外を生成し、カーネルはこの例外を信号として解釈してSIGFPE
プロセスに送信します。別の例としては、現在のプロセスが不正なメモリ アドレスにアクセスし、MMU が例外を生成し、カーネルがこの例外を信号として解釈してSIGSEGV
プロセスに送信する場合があります。
たとえば、次のコードでは除算0
演算を実行します。
#include <iostream>
int main()
{
int a = 10;
a /= 0;
std::cout << a << std::endl;
return 0;
}
コンパイル時に (問題を除いて) 警告が表示され0
、その後コードの実行を続けたところ、プログラムがクラッシュし、システムはこれが浮動小数点例外の問題であることを示すメッセージを表示しました。例外問題は、「ハードウェアが異常です」に対応します。対応する信号は8
番号信号です。SIGFPE
一般原理:コンピュータ内部にはビットマップ構造のステータスレジスタがあり、対応するビットが の場合は、1
今回の計算でデータオーバーフローが発生し、計算結果が正しくなく、CPUの実行が異常であることを意味します。エラーが発生した場合、オペレーティング システムは、プロセスがスケジュールされるたびにステータス レジスタの状態をチェックして、プロセスの実行が正確であることを確認します。
CPU に除算0
演算の実行を許可すると、データ オーバーフローの問題が発生し、ステータス レジスタの対応するビットが 1 に設定されます。オペレーティング1
システムは、ステータス レジスタのビットが 1 に設定されたことを検出し、1
対応するプロセスにSIGFPE
終了するようシグナルを送信します。プロセスを強制終了するため、プロセスを削除すると0
プログラムがクラッシュします。
次に、シグナル キャプチャを使用して、上記の原則と結論を検証します。
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
std::cout << "我是收到 " << sig <<"信号才崩溃了"<< std::endl;
}
int main()
{
signal(SIGFPE, handler);
int a = 10;
a /= 0;
std::cout << a << std::endl;
return 0;
}
操作結果:
私たちのプログラムには印刷の無限ループがあることがわかります。これは、8
数値信号をキャプチャし、プロセスを終了するための元のデフォルトのアクションを印刷アクションに変更したためです。プロセスが信号の処理を完了すると、オペレーティング システムが呼び出します。プロセスが再度実行される この時点では、ステータス レジスタのビットが前回設定されていなかったため0
、オペレーティング システムがプロセスを再度呼び出すと、ステータス レジスタの対応するビットがまだ残っているため、1
プロセスに8
シグナルが送信され、 own定義されたアクションはステータス レジスタを処理したことがないため、無限ループに陥っています。
したがって、通常はシグナルをキャッチした後、プロセスを直接終了させます。
ワイルド ポインターによって引き起こされるハードウェア例外を見てみましょう。
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
int* p = nullptr;
*p = 10;
std::cout << "野指针问题" << std::endl;
return 0;
}
操作結果:
システムは、セグメント障害が発生したことを通知します。ワイルド ポインタ問題の場合、オペレーティング システムからシグナルを受信した後、プロセスが実際にクラッシュしました。このシグナルはシグナルであり、今回のハードウェア異常は MMU ユニット (メモリ管理ユニット)11
ですSIGSEGV
。 )。
一般原則: プロセスで使用されるアドレスはすべて仮想アドレスであるため、プロセスのコードが実際に実行されるときは、仮想アドレスを物理アドレスに変換する必要があり、この変換は MMU の助けを借りて変換する必要があります。 MMU がアドレス変換を実行するとき、MMU ユニットはページ テーブル内のアドレスのマッピング関係を検索し、読み取り許可と書き込み許可が一致しているかどうかを比較します。マッピングがページ テーブルまたはマッピングに見つからない場合は、は見つかりましたが、操作が読み取りおよび書き込み権限と矛盾している場合、変換が失敗し、オペレーティング システムに通知されます。オペレーティング システムはそれを認識した後、対応するプロセスにシグナルを送信し、それによりSIGSEGV
、プロセス。(ここでは、オペレーティング システムがこの変換例外を修復していないことに注意してください。ユーザーがこのシグナルをキャッチしても修復も終了も行わず、オペレーティング システムは常にこのシグナルをプロセスに送信することになります。)
このアドレスについては、オペレーティング システムがアドレスのマッピング関係をまったく確立していないか、マッピング関係は確立されているものの、オペレーティング システムが0
アドレスへの書き込みを許可していない可能性があります。MMUはアドレス変換中に権限が矛盾していることを検出し、例外をトリガーしてオペレーティング システムに報告し、オペレーティング システムがエントリにシグナルを送信します。0
0
*p = 10
SIGSEGV
エピローグ
この章ではプロセス信号の生成について説明しますが、プロセス信号を知るだけでは不十分です。次の章では、プロセス信号の保存について引き続き理解し、信号についての理解を深めていきます。
もちろん、この記事に間違いや不足がある場合は、コメントまたはプライベートメッセージで議論してください。また次回、バイバイ!