- Authors

- Name
- Youngju Kim
- @fjvbn20031
プロセスの概念
プロセス(Process)は実行中のプログラムである。プログラムがディスクに保存された受動的な存在であるのに対し、プロセスはメモリにロードされて実行される能動的な存在である。
プロセスのメモリ構造
[プロセスのメモリレイアウト]
高アドレス
+------------------+
| スタック(Stack) | ローカル変数、関数パラメータ、リターンアドレス
| | | (下方向に成長)
| v |
| |
| ^ |
| | |
| ヒープ(Heap) | 動的割り当てメモリ (malloc, new)
| | (上方向に成長)
+------------------+
| データ(Data) | グローバル変数、静的変数
+------------------+
| テキスト(Text) | プログラムコード(機械語命令)
+------------------+
低アドレス
プロセスの状態
プロセスは実行中に以下の状態を経る。
[プロセス状態遷移図]
ディスパッチ
+--------+-------->+---------+
| 準備 | | 実行 |
| (Ready) |<--------| (Running)|
+--------+ タイマー +---------+
^ ^ 割り込み | |
| | | |
admitted | | I/O完了 | I/O | exit
| | イベント |要求 |
| | | |
+---+ +--------+ | +--------+
|新規| | 待機 |<---+ | 終了 |
|生成| |(Waiting)| |(Terminated)
+---+ +--------+ +--------+
- 新規生成(New): プロセスが生成中
- 準備(Ready): CPU割り当てを待っている状態
- 実行(Running): CPUで命令を実行中
- 待機(Waiting): I/Oまたはイベントの完了を待っている状態
- 終了(Terminated): 実行が完了した状態
プロセス制御ブロック(PCB)
各プロセスはOS内でPCB(Process Control Block)で表現される。
// Linuxカーネルのtask_struct構造体(簡略化)
struct task_struct {
volatile long state; // プロセス状態
pid_t pid; // プロセスID
pid_t tgid; // スレッドグループID
struct mm_struct *mm; // メモリ管理情報
struct files_struct *files; // オープンファイルテーブル
struct thread_struct thread; // CPUレジスタ状態
unsigned int policy; // スケジューリングポリシー
int prio; // 優先度
cpumask_t cpus_allowed; // 実行可能なCPU
struct task_struct *parent; // 親プロセス
struct list_head children; // 子プロセスリスト
// ... 数百の追加フィールド
};
PCBにはプロセス状態、プログラムカウンタ、CPUレジスタ、スケジューリング情報、メモリ管理情報、I/O状態情報などが含まれる。
プロセススケジューリング
スケジューリングキュー
OSは複数のキューを使用してプロセスを管理する。
[プロセススケジューリングキュー]
レディキュー (Ready Queue)
+-----+ +-----+ +-----+ +-----+
| PCB |->| PCB |->| PCB |->| PCB |---> CPU
+-----+ +-----+ +-----+ +-----+
ディスク待ちキュー
+-----+ +-----+
| PCB |->| PCB |---> ディスクコントローラ
+-----+ +-----+
ネットワーク待ちキュー
+-----+
| PCB |---> ネットワークインターフェース
+-----+
コンテキストスイッチ
CPUが別のプロセスに切り替える際、現在のプロセスの状態を保存し、新しいプロセスの状態を復元する必要がある。
[コンテキストスイッチ]
プロセス P0 オペレーティングシステム プロセス P1
実行中 アイドル
| |
|--割り込み/システムコール--> |
| PCB0に状態保存 |
| PCB1から状態復元 |
| ---------->|
アイドル 実行中 |
| |
| <--割り込み/システムコール--|
| PCB1に状態保存 |
| PCB0から状態復元 |
|<--------- |
実行中 アイドル
コンテキストスイッチ時間は純粋なオーバーヘッドである。ハードウェアによって約1〜1000マイクロ秒かかる。
プロセス操作
プロセス生成:fork()
Unix/Linuxではfork()システムコールでプロセスを生成する。fork()は呼び出したプロセスのコピーを作成する。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
printf("fork()呼び出し前 - PID: %d\n", getpid());
pid = fork(); // 子プロセス生成
if (pid < 0) {
// fork失敗
perror("fork失敗");
return 1;
} else if (pid == 0) {
// 子プロセス:fork()が0を返す
printf("子プロセス - PID: %d, 親PID: %d\n",
getpid(), getppid());
} else {
// 親プロセス:fork()が子のPIDを返す
printf("親プロセス - PID: %d, 子PID: %d\n",
getpid(), pid);
wait(NULL); // 子の終了を待つ
printf("子プロセス終了\n");
}
return 0;
}
[fork()の動作]
親 (PID 100) fork()
| |
| |---> 子 (PID 101)
| | 親のアドレス空間をコピー
| | fork()戻り値 = 0
| fork()戻り値 = 101 |
| |
fork()とexec()の組み合わせ
fork()後にexec()を呼び出すと、子プロセスが新しいプログラムを実行する。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子:exec()で新しいプログラムを実行
// exec()は現在のプロセスイメージを新しいプログラムに置き換える
printf("子:lsコマンドを実行\n");
execlp("ls", "ls", "-la", NULL);
// exec()成功時、以下のコードは実行されない
perror("exec失敗");
} else if (pid > 0) {
// 親:子の終了を待つ
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("子の終了コード:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
プロセスの終了
プロセスはexit()システムコールで終了を要求する。親プロセスはwait()で子の終了ステータスを取得する。
- ゾンビプロセス(Zombie): 終了したが親がwait()を呼び出していないプロセス
- 孤児プロセス(Orphan): 親が先に終了したプロセス。init(PID 1)が新しい親になる
プロセス間通信(IPC)
プロセスは独立的(independent)または協調的(cooperating)である。協調的プロセスにはIPCが必要である。
共有メモリ(Shared Memory)
複数のプロセスが同じメモリ領域にアクセスしてデータを交換する。
// POSIX共有メモリ - 生産者
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096
int main() {
// 共有メモリオブジェクトの作成
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, SHM_SIZE);
// メモリマッピング
char *ptr = mmap(NULL, SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
// データの書き込み
const char *message = "Hello from producer!";
memcpy(ptr, message, strlen(message) + 1);
printf("生産者:メッセージ書き込み完了\n");
munmap(ptr, SHM_SIZE);
return 0;
}
// POSIX共有メモリ - 消費者
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096
int main() {
// 既存の共有メモリを開く
int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);
// メモリマッピング
char *ptr = mmap(NULL, SHM_SIZE,
PROT_READ,
MAP_SHARED, shm_fd, 0);
// データの読み取り
printf("消費者:%s\n", ptr);
munmap(ptr, SHM_SIZE);
shm_unlink(SHM_NAME); // 共有メモリの削除
return 0;
}
メッセージパッシング(Message Passing)
OSカーネルを通じてプロセス間でメッセージをやり取りする。共有メモリより遅いが、同期が内蔵されている。
[IPCモデルの比較]
共有メモリモデル: メッセージパッシングモデル:
プロセスA プロセスB プロセスA プロセスB
| \ / | | |
| \ / | | send(M) |
| 共有メモリ | |---> カーネル|
| / \ | | | |
| / \ | | v |
| | | receive(M)|
| ----->|
カーネル介入最小化 カーネルが毎転送ごとに介入
パイプ(Pipes)
パイプは2つのプロセス間の通信チャネルである。
通常パイプ(Ordinary Pipe)
親子関係のプロセス間でのみ使用可能で、単方向である。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd[2]; // fd[0]: 読み取り端、fd[1]: 書き込み端
pid_t pid;
char buffer[256];
// パイプの作成
if (pipe(fd) == -1) {
perror("パイプ作成失敗");
return 1;
}
pid = fork();
if (pid == 0) {
// 子:パイプから読み取り
close(fd[1]); // 書き込み端を閉じる
int bytes = read(fd[0], buffer, sizeof(buffer));
printf("子が受信したメッセージ:%s (%dバイト)\n", buffer, bytes);
close(fd[0]);
} else {
// 親:パイプに書き込み
close(fd[0]); // 読み取り端を閉じる
const char *msg = "こんにちは、子プロセス!";
write(fd[1], msg, strlen(msg) + 1);
printf("親が送信したメッセージ:%s\n", msg);
close(fd[1]);
wait(NULL);
}
return 0;
}
名前付きパイプ(Named Pipe / FIFO)
親子関係でない任意のプロセス間でも使用可能である。
# シェルでの名前付きパイプの使用
mkfifo /tmp/my_pipe
# ターミナル 1: 書き込み
echo "Hello via named pipe" > /tmp/my_pipe
# ターミナル 2: 読み取り
cat /tmp/my_pipe
クライアント・サーバー通信
ソケット(Sockets)
ソケットは通信のエンドポイント(endpoint)である。IPアドレスとポート番号の組み合わせで識別される。
// TCPサーバーの例
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[1024];
// ソケット作成
server_fd = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// バインドとリッスン
bind(server_fd, (struct sockaddr *)&server_addr,
sizeof(server_addr));
listen(server_fd, 5);
printf("サーバー待機中(ポート 8080)...\n");
// クライアント接続の受け入れ
client_fd = accept(server_fd,
(struct sockaddr *)&client_addr,
&addr_len);
// データ受信と応答
int bytes = recv(client_fd, buffer, sizeof(buffer), 0);
buffer[bytes] = '\0';
printf("受信:%s\n", buffer);
const char *response = "サーバー応答:メッセージ受信完了";
send(client_fd, response, strlen(response), 0);
close(client_fd);
close(server_fd);
return 0;
}
リモートプロシージャコール(RPC)
RPCはネットワークを通じてリモートシステムの関数をあたかもローカル関数のように呼び出すメカニズムである。
[RPC動作過程]
クライアント サーバー
| |
|-- 1. ローカル関数呼び出し形式 --- |
| (スタブがパラメータをマーシャリング) |
| |
|-- 2. ネットワーク経由で送信 ------->|
| |-- 3. サーバースタブが
| | アンマーシャリング
| |
| |-- 4. 実際の関数実行
| |
|<-- 5. 結果返却 --------------------|
| (スタブが結果をアンマーシャリング) |
| |
RPCで注意すべき点は以下の通りである。
- データ表現: クライアントとサーバーのデータ形式が異なる場合があるため、XDR(External Data Representation)などの標準形式を使用
- バインディング: クライアントがサーバーを見つける方法。固定ポートまたはランデブーデーモン(ポートマッパー)を使用
- 実行セマンティクス: 「最大1回(at most once)」または「正確に1回(exactly once)」の保証
まとめ
プロセスはOSの核心的な抽象化単位である。fork()とexec()でプロセスを生成し、新しいプログラムを実行する。プロセス間通信には共有メモリ、メッセージパッシング、パイプ、ソケット、RPCなど様々なメカニズムがあり、それぞれ性能と利便性の面でトレードオフがある。