目次
1. プロセスの概念
教科書の概念: プログラムの実行インスタンス、実行中のプログラムなど
カーネルの観点: システム リソース (CPU 時間、メモリ) を割り当てるエンティティ
コードがコンパイルおよびリンクされると、実行可能プログラムが生成されます. この実行可能プログラムは、本質的にファイルであり、ディスクに保存されます. 実行可能プログラムが実行されると、プログラムは基本的にメモリにロードされます。これは、メモリにロードされた後でのみ、CPU が行ごとにステートメントを実行できるためです。プログラムがメモリにロードされると、このプログラムは実行されません。もはやプログラムと呼べるものではなく、厳密にはプロセスと呼ぶべきものです。
また、プロセスとプログラムは必ずしも対応しているとは限らず、プログラムは同時に複数回実行することができ、複数のプロセスが存在します。
競争力:多くのシステム プロセスがありますが、CPU リソースは少量または 1 つしかないため、プロセスは競争力があります。タスクを効率的に完了し、関連するリソースをより合理的に競うために、優先順位があります。
独立性:マルチプロセス操作では、さまざまなリソースを排他的に使用する必要があり、マルチプロセス操作中に互いに干渉しません。
並列処理:複数のプロセスが複数の CPU で同時に実行されます。これは並列処理と呼ばれます。
並行性:複数のプロセスが 1 つの CPU の下でプロセス切り替えを使用して、複数のプロセスが一定時間内に進行できるようにします。これは並行性と呼ばれます。
2. プロセス PCB の説明
私たちのコンピューターには多数のプロセスがあり、オペレーティングシステムはそれらを管理する必要があります。それを管理する方法は?最初に説明し、後で整理する
オペレーティング システムは、各プロセスを記述し、プロセス制御ブロック (PCB、本質的には構造体) を形成し、これらの PCB を二重にリンクされたリストの形式で編成します。
PCB は実際にはプロセス制御ブロックの総称で、Linux のプロセス制御ブロックは task_struct で、主に次の情報が含まれています。
識別子: このプロセスを説明する一意の識別子は、他のプロセスを区別するために使用されます。
ステータス: タスクのステータス、終了コード、終了シグナルなど。
Priority : 他のプロセスに対する優先度。
Program Counter (pc) : プログラムで実行される次の命令のアドレス。
メモリ ポインタ: プログラム コードおよびプロセス関連データへのポインタ、および他のプロセスと共有されるメモリ ブロックへのポインタを含みます。
コンテキスト データ: プロセスが実行されたときのプロセッサのレジスタ内のデータ。
I/O ステータス情報: 表示された I/O 要求、プロセスに割り当てられた I/O デバイス、およびプロセスによって使用されるファイルのリストを含みます。
請求情報: プロセッサー時間の合計、使用されたクロックの合計、時間制限、請求先アカウント番号などが含まれる場合があります。
追加情報: ...
3. プロセスを確認する
3.1 システム カタログの表示
ルートディレクトリの下に proc というシステムディレクトリがあり、多くのプロセス情報が含まれています。一部のサブディレクトリのディレクトリ名は数字ですが、これらの数字は実際には特定のプロセスの PID であり、対応するプロセスのさまざまな情報が対応するフォルダーに記録されます。PID 1 のプロセスのプロセス情報を表示する場合は、1 という名前のフォルダーを表示できます。
3.2 ps コマンドによる表示
ps コマンドの特定の使用法については、man 1 ps コマンドを使用してドキュメントを表示できます。
Linux オペレーティング システムで ps -l コマンドを使用すると、次の状況が発生します。
- UID: エグゼキュータの ID を表します。
- PID: このプロセスのコード名を表します。
- PPID: このプロセスがどのプロセスから派生したか、つまり親プロセスのコード名を表します。
- PRI:この処理を実行できる優先度を表し、値が小さいほど早く実行されます。
- NI: このプロセスの優れた価値を表します。
4. 処理状況
Linux オペレーティング システムのソース コードには、プロセス状態の次の定義があります。
static const char *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
走行状態 R
実行中のすべてのプロセス (つまり、スケジュール可能なプロセス) は、実行キューに配置されます。オペレーティング システムは、実行するプロセスを切り替える必要がある場合、実行キューで実行するプロセスを直接選択します。プロセスが実行中 (実行中) であるということは、プロセスが実行されている必要があるという意味ではありません。実行中の状態は、プロセスが実行中であるか、実行キューにあることを示します。つまり、R 状態の複数のプロセスが同時に存在できます。
睡眠状態
プロセスがイベントの完了を待っていることを意味します (このスリープ状態は、割り込み可能なスリープとも呼ばれます)。
たとえば、プロセスがループして画面に出力する場合、CPU の処理速度は非常に高速ですが、ディスプレイの速度は比較的遅いため、プロセスはディスプレイのリソースを待機する必要があります (CPU が処理するこの時点で他のプロセス)。このとき、プロセスは常に実行状態とスリープ状態を切り替えますが、CPU の高速性により、観測するとスリープ状態になる可能性が高くなります。
#include <stdio.h>
int main()
{
while(1){
printf("handsome boy!\n");
}
return 0;
}
ステータスを表示するときに + 記号があり、プロセスがフォアグラウンド プロセスであることを示し、そうでない場合はバックグラウンド プロセスであることを示します。
このスリープ状態のプロセスは、 kill コマンドを使用してシグナルを送信するなどして強制終了できます。
ディスク休止状態 D
プロセスはディスク スリープ状態にあります。つまり、プロセスは強制終了されず、オペレーティング システムも強制終了されません。プロセスが自動的に起動したときに強制終了できるのはプロセスだけです。中断できないスリープ状態(uninterruptible sleep)とも呼ばれ、通常、この状態のプロセスは IO の終了を待ちます。
たとえば、プロセスがディスクへの書き込みを必要とする場合、プロセスはディスクへの書き込み中はディープ スリープ状態になり、強制終了できません。プロセスは、対応する応答を行うために、ディスクからの応答 (書き込みが成功したかどうか) を待つ必要があるためです。
dd コマンドを使用してディスクの休止状態をシミュレートする
一時停止状態 T
Linux では、SIGSTOP シグナルを送信することでプロセスを中断状態にすることができ、中断状態のプロセスは SIGCONT シグナルを送信することで実行を継続できます。
ゾンビ状態Z
プロセスが終了しようとしているとき、システム レベルでは、プロセスが適用したリソースはすぐには解放されませんが、オペレーティング システムまたはその親プロセスが読み取れるように、一定期間一時的に保存されます。 , 関連するデータは解放されません. プロセスが終了情報が読み取られるのを待っている場合, プロセスをゾンビ状態で呼び出します. (プロセスの終了情報は、プロセスの task_struct に格納されます)
プロセスの目的は特定のタスクを完了することであるため、ゾンビ状態の存在が必要です。タスクが完了すると、呼び出し元はタスクの完了を知る必要があるため、ゾンビ状態が存在する必要があります。呼び出し元は、対応するフォローアップ操作を実行するために、タスクの完了ステータスを知ることができます。
死の状態X
Death 状態は単なる return 状態であり、プロセスの exit 情報が読み取られると、プロセスが要求したリソースはすぐに解放され、プロセスは存在しなくなります。タスクリストです。
ブロッキング (拡張)
プロセスの実行中は、CPU によってスケジュールされます。つまり、プロセスはスケジューリング時に CPU 資源を使用する必要があり、各 CPU には実行待ちのキュー (runqueue) があり、CPU は実行中にスケジューリングのためにキューからプロセスを取得します。
実行中の待ち行列にあるプロセスは、基本的に CPU リソースを待っています. 実際には、CPU リソースを待っているだけでなく、ロック リソース、ディスク リソース、ネットワーク カード リソースなどの他のリソースも待っています。リソース待機キュー
Linux での状態に対応し、ブロッキングはスリープ状態 S とディスクスリープ状態 D です。
5.ゾンビプロセスとオーファンプロセス
5.1 ゾンビプロセス
5.1.1 ゾンビプロセスの概念
プロセスが終了情報が読み取られるのを待っている場合、そのプロセスはゾンビ状態にあると言います。ゾンビ状態のプロセスはゾンビプロセスです。
次のコードでは、 fork 関数によって作成された子プロセスは、情報を 5 回出力した後に終了しますが、親プロセスは常に情報を出力します。つまり、子プロセスが終了し、親プロセスはまだ実行されていますが、親プロセスは子プロセスの終了情報を読み取らず、子プロセスはゾンビ状態になります。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count){
printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child quit...\n");
exit(1);
}
else if(id > 0)
{
while(1){
printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else{
exit(-1);
}
return 0;
}
5.1.2 ゾンビプロセスの弊害
- 親プロセスがプロセスの終了情報を読み取らない場合、子プロセスは常にゾンビ状態になります。
- ゾンビ プロセスの終了情報は task_struct に格納されます. ゾンビ プロセスが終了しない場合は、PCB を常に維持する必要があります。
- 親プロセスが多くの子プロセスを作成し、それらをリサイクルしない場合、リソースの浪費が発生します。
- ゾンビ プロセスによって要求されたリソースは再利用できないため、ゾンビ プロセスが多いほど、実際に使用できるリソースは少なくなり、ゾンビ プロセスはメモリ リークを引き起こします。
5.2 孤立したプロセス
親プロセスが先に終了すると、将来、子プロセスがゾンビ状態になると、それを処理する親プロセスがなくなります. このとき、子プロセスは孤立プロセスと呼ばれます. 孤立プロセスの終了情報が常に処理されていないと、孤立プロセスが常にリソースを占有し、メモリ リークが発生します。そのため、オーファンプロセスが発生すると、オーファンプロセスは1号のinitプロセスに採用され、オーファンプロセスがゾンビ状態になると、intプロセスによって処理およびリサイクルされます。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(1){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
sleep(1);
}
}
else if(id > 0){ //father
int count = 5;
while(count){
printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("father quit...\n");
exit(0);
}
else{ //fork error
exit(-1);
}
return 0;
}
orphan プロセスは init1 プロセスによって採用されるため、害はありません。
6. プロセスアドレス空間
6.1 プロセスアドレス空間の検証
次のコードは、プロセスのアドレス空間が上の図と一致していることを確認できます。
#include <stdio.h>
#include <stdlib.h>
int un_val;
int init_val = 100;
int main(int argc,char* argv[],char* env[])
{
int i = 0;
int count = 0;
while(env[i] != NULL && count < 5){
printf("环境变量地址: %p\n",env[i]);
++count;
}
for(int i = 0;i < argc; ++i){
printf("命令行参数地址: %p\n",argv[i]);
}
char* p1 = (char*)malloc(10);
char* p2 = (char*)malloc(10);
char* p3 = (char*)malloc(10);
printf("栈区地址: %p\n",&p3);
printf("栈区地址: %p\n",&p2);
printf("栈区地址: %p\n",&p1);
printf("堆区地址: %p\n",p3);
printf("堆区地址: %p\n",p2);
printf("堆区地址: %p\n",p1);
printf("未初始化数据区: %p\n",&un_val);
printf("初始化数据区: %p\n",&init_val);
printf("代码区: %p\n",main);
return 0;
}
スタック領域は下位アドレスに向かって拡張し、ヒープ領域は上位アドレスに向かって拡張します。
6.2 プロセスのアドレス空間を理解する
次のコードを通じて、問題を見つけることができます
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int val = 100;
int main()
{
pid_t id = fork();
if(id < 0)//err
{
exit(-1);
}
else if(id > 0)//father
{
sleep(3);
printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
}
else//id == 0
{
val = 200;
printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
}
return 0;
}
コードでは、fork 関数を使用して子プロセスを作成し、子プロセスにグローバル変数 val を 100 から 200 に変更して出力するように要求します。一方、親プロセスは最初に 3 秒間スリープしてから、値を出力します。グローバル変数。子プロセスによって出力されるグローバル変数の値が 200 であり、子プロセスがグローバル変数を変更した後に親プロセスがグローバル変数を出力するのは当然のことなので、それも 200 になるはずです。しかし、結果はそうではなく、2 つのプロセスの val 変数のアドレスは同じですが、出力結果が一致しないのはなぜですか?
同じ物理アドレスでデータを取得した場合、同じはずですが、同じアドレスで取得された値は異なります。これは、印刷したアドレスが物理アドレスではないことを示しているだけです。
実際、言語レベルで出力する住所は物理的な住所ではなく、仮想の住所です。物理アドレスは、ユーザーからはまったく見えず、オペレーティング システムによって一元的に管理されます。そのため、親プロセスと子プロセスで出力されるグローバル変数のアドレス (仮想アドレス) が同じであっても、2 つのプロセスでのグローバル変数の値は異なります。
6.3 詳細認識
プロセスアドレス空間のアドレスサイズは 0x00000000 から 0xffffffff で、コード領域、ヒープ領域、スタック領域など、さまざまな領域に分かれています。構造体mm_structには、各領域の境界アドレスが記録されます。仮想アドレスは 0x00000000 から 0xffffffff まで直線的に増加するため、仮想アドレスはリニア アドレスとも呼ばれます。
ヒープの上方への成長とスタックの下方への成長は、実際には mm_struct 内のヒープとスタックの境界アドレスを変更しています。
生成した実行プログラムは、実際にはさまざまな領域 (初期化領域、未初期化領域など) に分割されており、Linux カーネルと同じアドレス指定方法を使用しています。実行可能プログラムが実行されているとき、オペレーティングシステムは対応するデータを対応するメモリにロードできます。これにより、オペレーティングシステムの作業効率が大幅に向上します。実行可能プログラムの「分割」操作は実際にはコンパイラーであるため、コードの最適化レベルは実際にはコンパイラーの最終決定権です。
各プロセスが作成されると、対応するプロセス制御ブロック (task_struct) とプロセス アドレス空間 (mm_struct) も作成されます。オペレーティング システムは、mm_struct のアドレスを格納する task_struct に構造体ポインターがあるため、プロセスの task_struct を通じてその mm_struct を見つけることができます。
子プロセスが作成されると、子プロセスと親プロセスのデータとコードが共有されます。つまり、親プロセスと子プロセスのコードとデータは、ページ テーブルを介して物理メモリの同じスペースにマップされます。親プロセスまたは子プロセスがデータを変更する必要がある場合にのみ、親プロセスのデータがメモリにコピーされてから変更されます。プロセス間の独立性を反映して、データの変更が必要な場合にのみコピーするこの技術は、コピー オン ライト技術と呼ばれます。
子プロセスの作成時にデータをコピーしないのはなぜですか?
子プロセスが親プロセスのすべてのデータを使用しない可能性が高く、子プロセスがデータを書き込まない場合は、データをコピーする必要はありません。データを変更する必要があるときに再割り当て (遅延割り当て) を行うことで、メモリ空間を効率的に使用できます。
コードはコピーオンライトになりますか?
ほとんどの場合はそうではありませんが、だからといってコードをオンライトでコピーできないわけではありません.たとえば、プロセス置換を実行する場合、コードのコピーオンライトが必要です.
プロセスのアドレス空間があるのはなぜですか?
- プロセスのアドレス空間とページ テーブルは OS によって作成および管理されます. 不正なアクセスまたはマッピングはオペレーティング システムによって終了されます.システムレベルの範囲外の問題はありません。
- プロセスのアドレス空間では、プロセスのアドレス空間の構成や内部領域の分割順序など、各プロセスは同じ空間範囲を認識しているため、プログラムを書くときは、仮想アドレスに注目するだけで済みます。データが実際に物理メモリに格納されている場所。これにより、プロセスが統一された視点からメモリを確認できるようになり、すべての実行可能プログラムのコンパイルとロードが統一された方法で容易になり、プロセス自体の設計と実装が簡素化されます。
- プロセスのアドレス空間を使用すると、各プロセスはメモリを独占していると考え、プロセスの独立性をより完全にし、メモリ空間を合理的に使用する (実際に使用する必要があるときにメモリ空間を開く) ことができ、プロセスを切り離すことができます。メモリ管理からのスケジューリング。
プロセスはどのように作成されますか?
プロセスの作成には、そのプロセス制御ブロック (task_struct)、プロセス アドレス空間 (mm_struct)、およびページ テーブルの作成が伴います。
7. プロセスの優先度
7.1 優先度の概念
優先度とは、実際には特定のリソースを取得する順序であり、プロセスの優先度とは、実際にはプロセスが CPU リソースの割り当てを取得する順序であり、プロセスの優先度を指します。
7.2 優先権が存在する理由
優先度が存在する主な理由は、リソースが限られていることであり、プロセスの優先度が存在する主な理由は、CPU リソースが限られていることです.CPU は一度に 1 つのプロセスしか実行できず、複数のプロセスが存在する可能性があるため、プロセスの優先度レベルが必要で、プロセスが CPU リソースを取得する順序を決定します。
7.3 PRI与NI
- PRIはプロセスの優先度、つまりCPUが実行するプロセスの順番を表し、値が小さいほどプロセスの優先度が高くなります。
- NI はナイス値の略で、プロセスを実行できる優先度の修正値を表します。
- PRI 値が小さいほど実行速度が速くなり、nice 値を追加すると、PRI は PRI(new) = PRI(old) + NI になります。
- NI 値が負の場合、プロセスの PRI は小さくなります。つまり、その優先度は高くなります。
- Linux では、プロセスの優先順位を調整することは、プロセスの nice 値を調整することです。
- NI の値の範囲は -20 ~ 19 の合計 40 レベルです。
注: Linux オペレーティング システムでは、PRI(old) のデフォルトは 80 です。つまり、PRI = 80 + NI です。
7.4 プロセスのナイス値を変更する
7.4.1 トップコマンド
top コマンドは、Windows オペレーティング システムのタスク マネージャーに相当し、システム内のプロセスのリソース使用状況を動的に監視できます。
top コマンドを使用して "r" キーを押すと、nice 値を調整するプロセスの PID を入力するよう求められます; プロセス PID を入力して Enter キーを押すと、調整された nice 値を入力するよう求められます。価値。終了する場合は、q を入力します。
7.4.2 renice コマンド
renice + 変更されたナイス値 + PID
renice コマンドを使用して NI 値を負の値に調整する場合は、root 権限が必要です
八、環境変数
8.1 コンセプト
環境変数 (環境変数) は、一般に、オペレーティング システムの動作環境を指定するためにオペレーティング システムで使用されるいくつかのパラメーターを指します。たとえば、C/C++ コードを作成する場合、各オブジェクト ファイルがリンクされると、リンクされた動的ライブラリと静的ライブラリがどこにあるかはわかりませんが、正常にリンクして実行可能プログラムを生成することはできます。コンパイラが見つけるのに役立つ変数。
通常、環境変数には何らかの特別な目的があり、通常はシステム内でグローバルです。
8.2 一般的な環境変数
- PATH: コマンドの検索パスを指定します(システムコマンドは基本的に実行プログラムですが、起動時にパスを指定する必要はありません)
- HOME: ユーザーのメインの作業ディレクトリ (つまり、ユーザーが Linux システムにログインするデフォルトのディレクトリ) を指定します。
- SHELL: 現在のシェル、その値は通常 /bin/bash です
8.3 環境変数関連コマンド
echo : 環境変数の値を表示します
export : 新しい環境変数を設定します
env : すべての環境変数を表示します
set : ローカルで定義されたシェル変数と環境変数を表示します
unset : 環境変数をクリアします
8.4 環境変数の編成
Linux システムでは、環境変数は次のように編成されています。
各プログラムは環境変数テーブルを受け取ります。環境テーブルは文字ポインタの配列で、各ポインタは「\0」で終わる環境文字列を指し、最後の文字ポインタは空です。
8.5 環境変数の取得方法
8.5.1 メイン関数のパラメータ
main 関数には実際には 3 つの仮パラメーターがありますが、それらはあまり使用されないため、書き出されていません。
int main(int argc, char* argv[],char* env[])
{ …… }
メイン関数の 2 番目のパラメータは文字ポインタの配列です. 配列の最初の文字ポインタは実行可能プログラムの文字列を格納し, 残りの文字ポインタは指定されたオプションの文字列を格納します. 最後の文字ポインタは空であり、メイン関数の最初のパラメーターは、文字ポインター配列内の有効な要素の数を表します。main 関数の 3 番目のパラメーターは実際に環境変数テーブルを受け取り、main 関数の 3 番目のパラメーターを通じてシステムの環境変数を取得できます。
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; env[i] != NULL; ++i){
printf("%s\n",env[i]);
}
return 0;
}
8.5.2 サードパーティ変数環境
C言語は、環境テーブルにアクセスするために使用できるグローバル変数environを提供します
#include <stdio.h>
int main()
{
extern char** environ;
for(int i = 0;environ[i] != NULL; ++i){
printf("%s\n",environ[i]);
}
return 0;
}
8.5.3 getenv 関数
環境変数は、システムの getenv 関数を呼び出すことで取得できます。getenv 関数は、指定された環境変数名に従って環境変数テーブルを検索し、対応する値を指す文字列ポインターを返すことができます。
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n",getenv("PATH"));
return 0;
}