fork 複製程序

fork

現在的程序中,使用fork()複製出子程序,子程序的程式碼執行的位置由fork()開始的程式碼,父程序fork()函式的傳回值是子程序的pid,子程序fork()傳回值為0。

fork_test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
  cout << "父程序開始" << endl;
  // 建立子程序,子程序開始的程式碼
  pid_t pid = fork();
  cout << "fork() return pid = " << pid << endl;
  // 停止30秒,方便以ps -ef | grep 查看父程序與子程序
  sleep(30);
  cout << "結束" << endl;
}
父程序開始
fork() return pid = 145656
fork() return pid = 0
結束
結束

由執行結果可以知道,父程序開始的字只執行一次。

執行上面的程式,打開另一個終端機視窗

ps -ef | grep fork

-e:顯示所有程序,等同於 -A 選項。

-f:顯示完整的格式,包括父程序和其他資訊。

可以發現子程序pid是145773,父程序是145772,有二個程序都跑起來了。

cici      145772  139698  0 14:43 pts/3    00:00:00 ./fork_test
cici      145773  145772  0 14:43 pts/3    00:00:00 ./fork_test

使用pstree查看子父關係

語法

pstree -p 父pid
$ pstree -p 145772
fork_test(145772)───fork_test(145773)

由以上結果可以知,145772下面有一個子程序145773

透過fork傳回值執行不同程式碼

父程序fork()函式的傳回值是子程序的pid,子程序fork()傳回值為0,根據傳回值判斷是父程序或子程序。

父get fork pid = 146095

子get fork pid = 0

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
  pid_t pid = fork();
  if (pid > 0) {
    cout << "父get fork pid = " << pid << endl;
  } else {
    cout << "子get fork pid = " << pid << endl;
  }
  sleep(20);
  cout << "結束" << endl;
}
父get fork pid = 146095
子get fork pid = 0
結束
結束

fork()與變數

fork()函式,會把變數複製一份給子程序,雖然子程序與父程序的變數位址是相同,但實際上是存放不同位址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
  // 學號
  int id = 888;
  // 姓名
  string name = "May";
  // fork傳回值pid
  pid_t pid = fork();
  if (pid > 0) {
    // 父程序
    cout << "父get fork pid = " << pid << endl;
    cout << "id = " << id << ", address = " << &id << endl;
    cout << "name = " << name << ", address = " << &name << endl;
  } else {
    // 子程序
    cout << "子get fork pid = " << pid << endl;
    cout << "id = " << id << ", address = " << &id << endl;
    cout << "name = " << name << ", address = " << &name << endl;
  }
  sleep(20);
  cout << "結束" << endl;
}
父get fork pid = 146276
id = 888, address = 0x7ffef9691170
name = May, address = 0x7ffef9691180
子get fork pid = 0
id = 888, address = 0x7ffef9691170
name = May, address = 0x7ffef9691180
結束
結束

證明不是讀取相同記憶體位址

以下程式碼先讓子程序先執行(子程序有修改變數的值),5秒後,再執行父程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
  // 學號
  int id = 888;
  // 姓名
  string name = "May";
  // fork傳回值pid
  pid_t pid = fork();
  if (pid > 0) {
    // 先讓父程序先睡5秒,讓子程序先執行
    sleep(5);
    // 父程序
    cout << "父get fork pid = " << pid << endl;
    cout << "id = " << id << ", address = " << &id << endl;
    cout << "name = " << name << ", address = " << &name << endl;
  } else {
    // 修改變數值
    id = 777;
    name = "Bill";
    // 子程序
    cout << "子get fork pid = " << pid << endl;
    cout << "id = " << id << ", address = " << &id << endl;
    cout << "name = " << name << ", address = " << &name << endl;
  }
  sleep(20);
  cout << "結束" << endl;
}
子get fork pid = 0
id = 777, address = 0x7ffeb7027c90
name = Bill, address = 0x7ffeb7027ca0
父get fork pid = 146339
id = 888, address = 0x7ffeb7027c90
name = May, address = 0x7ffeb7027ca0
結束
結束

由執行結果可以看出,即便子程序修改了變數值,但父程序仍顯示原來的值(888與Mary)。

子程序在背景執行

子程序在背景執行時,不會被ctrl+c強制終止。

法1

語法

./執行檔名 &

法2

在程式碼第一行加上

if (fork() > 0) return 0;

編譯並執行,執行後面不用加&

./執行檔名 

如果 fork() 的返回值大於 0,表示這是 父程序,執行 return 0 直接結束父程序。

如果 fork() 的返回值等於 0,表示這是 子程序,繼續執行後面的程式碼。

父程序結束後,只有子程序執行程式的剩餘部分。

子程序會進入無限迴圈,並每秒輸出 第X秒。

fork_test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
  // 父程序離開,只剩下fork的子程序在執行
  if (fork() > 0) return 0;
  // 無限迴圈
  for (int i = 0; ; i++) {
    cout << "第" << i << "秒" << endl;
    sleep(1);
  }
}

孤兒程序orphan process

父程序結束,子程序沒有父親,子程序變成孤兒,由init系統程序(1號程序)接管

使用ps查看父程序的pid已經變成1號程序

$ ps -ef | grep fork_test
cici      154291       1  0 09:46 pts/3    00:00:00 ./fork_test

刪除背景程序

開另一個終端機,輸入killall -9 程序名kill 程序pid

PID 0 1 2 號

PID 0 號

名稱:Idle 程序(也稱為 swapper 或 scheduler)

特性:

核心中的第一個程序,在系統啟動階段由核心創建。

PID 1 號

名稱:systemd

由 0 號程序創建

作用:

負責系統初始化,如加載其他守護程序、掛載檔案系統、設置網路等。

PID 2 號

名稱:kthreadd(核心多工管理器)

特性:

負責執行例如磁碟 I/O、網絡處理等系統級操作。

PID(程序 ID)為 2,因此被稱為 2 號程序。

程序之間的關係

啟動順序:

系統啟動時,最初由核心創建 0 號程序。

0 號程序創建 1 號程序

0 號程序也會創建 2 號程序

查看0號程序樹

語法

pstree -p 0

zombie process

父程序沒有處理子程序的離開(如:資源釋放),造成僵屍程序,子程序的pid就會一直存在,系統的pid數量是有限的,若存在太多子程序pid,就不會再產生新的程序。

使用以下程式碼會產生僵屍程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main() {
  // 離開子程序
  if (fork() == 0) return 0;
  // 無限迴圈
  for (int i = 0; ; i++) {
    cout << "父程序運行第" << i << "秒" << endl;
    sleep(1);
  }
}

執行時,再用ps查詢程序,可以發現明明子程序154631已經離開,但仍占著pid,後面跟著< defunct >,這就是僵屍程序

$ ps -ef | grep fork_test
cici      154630  139698  0 10:19 pts/3    00:00:00 ./fork_test
cici      154631  154630  0 10:19 pts/3    00:00:00 [fork_test] <defunct>

避免僵屍程序的方法

怱略SIGCHLD

子程序離開前,kernel會向父程序發出SIGCHLD訊號,把它怱略,kernel收到會釋放子程序資源。

語法

signal(SIGCHLD, SIG_IGN);

SIG_IGN代表怱略

程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main() {
  // 呼叫fork()之前,怱略子程序離開的訊號
  signal(SIGCHLD, SIG_IGN);
  // 離開子程序
  if (fork() == 0) return 0;
  // 無限迴圈
  for (int i = 0; ; i++) {
    cout << "父程序運行第" << i << "秒" << endl;
    sleep(1);
  }
}

執行程序時,使用ps查看,發現pid只剩下父程序154674

$ ps -ef | grep fork_test
cici      154674  139698  0 10:22 pts/3    00:00:00 ./fork_test

使用wait

include

#include <sys/wait.h>

語法

pid_t wait(int *stat_loc);
  • pid_t:傳回子程序pid
  • stat_loc指標:用於存放子程序離開的結果

正常離開為以下的狀況,不管x值是什麼,都是正常離開。

  • return x;
  • exit(x);
  • _exit(x)_Exit(x)

異常離開

  • 收到訊號kill 程序pid
  • abort()函式終止

正常離開程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
int main() {
  if (fork() > 0) {
    // 父程序
    int status;  // 用於儲存子程序離開的結果
    pid_t pid = wait(&status);  // 等待子程序離開
    cout << "已終止的子程序pid是:" << pid << endl;
    // 把離開結果傳給WIFEXITED()前置指令,若傳回true,代表正常離開來
    if (WIFEXITED(status)) {
      cout << "正常離開,狀態 : " << WEXITSTATUS(status) << endl;
    } else {
      cout << "異常離開,狀態 : " << WTERMSIG(status) << endl;
    }
  } else {
    // 子程序執行15秒 0 .. 14
    for (int i = 0; i < 15 ; i++) {
      cout << "第" << i << "秒" << endl;
      sleep(1);
    }
    exit(5);
  }
}
第0秒
第1秒
第2秒
第3秒
第4秒
第5秒
第6秒
第7秒
第8秒
第9秒
第10秒
第11秒
第12秒
第13秒
第14秒
已終止的子程序pid是:154866
正常離開,狀態 : 5

使用kill不正常離開1

  • 執行程式
  • 打開另一個終端機視窗
      $ ps -ef | grep fork_test
      cici      154938  139698  0 10:50 pts/3    00:00:00 ./fork_test
      cici      154939  154938  0 10:50 pts/3    00:00:00 ./fork_test
      cici      154942  145543  0 10:50 pts/6    00:00:00 grep --color=auto fork_test
      $ kill 154939
    
  • 回到原本執行程式的終端機視窗
    第0秒
    第1秒
    第2秒
    第3秒
    第4秒
    第5秒
    第6秒
    第7秒
    第8秒
    第9秒
    第10秒
    第11秒
    第12秒
    已終止的子程序pid是:154939
    異常離開,狀態 : 15
    

使用kill不正常離開2

  • 執行程式
  • 打開另一個終端機視窗
      $ ps -ef | grep fork_test
      cici      155000  139698  0 10:55 pts/3    00:00:00 ./fork_test
      cici      155001  155000  0 10:55 pts/3    00:00:00 ./fork_test
      cici      155003  145543  0 10:55 pts/6    00:00:00 grep --color=auto fork_test
      $ kill -9 155001
    
  • 回到原本執行程式的終端機視窗
    第0秒
    第1秒
    第2秒
    第3秒
    第4秒
    第5秒
    第6秒
    第7秒
    第8秒
    第9秒
    第10秒
    第11秒
    第12秒
    已終止的子程序pid是:155001
    異常離開,狀態 : 9
    

操作nullptr不正常離開

在子程序的部分,增加操作nullptr的程式碼

    int* ptr = nullptr;
    *ptr = 10;
訊號名 訊號值 發出訊號原因
SIGSEGV 11 操作nullptr指標或超出陣列索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
int main() {
  if (fork() > 0) {
    // 父程序
    int status;  // 用於儲存子程序離開的結果
    pid_t pid = wait(&status);  // 等待子程序離開
    cout << "已終止的子程序pid是:" << pid << endl;
    // 把離開結果傳給WIFEXITED()前置指令,若傳回true,代表正常離開來
    if (WIFEXITED(status)) {
      cout << "正常離開,狀態 : " << WEXITSTATUS(status) << endl;
    } else {
      cout << "異常離開,狀態 : " << WTERMSIG(status) << endl;
    }
  } else {
    // 子程序執行15秒 0 .. 14
    for (int i = 0; i < 15 ; i++) {
      cout << "第" << i << "秒" << endl;
      sleep(1);
    }
    int* ptr = nullptr;
    *ptr = 10;
    exit(5);
  }
}
第0秒
第1秒
第2秒
第3秒
第4秒
第5秒
第6秒
第7秒
第8秒
第9秒
第10秒
第11秒
第12秒
第13秒
第14秒
已終止的子程序pid是:155059
異常離開,狀態 : 11

補捉SIGCHLD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
/**
 補捉SIGCHLD子程序離開訊號
 */
void func(int signal) {
  int status;  // 用於儲存子程序離開的結果
  pid_t pid = wait(&status);  // 等待子程序離開
  cout << "已終止的子程序pid是:" << pid << endl;
  // 把離開結果傳給WIFEXITED()前置指令,若傳回true,代表正常離開來
  if (WIFEXITED(status)) {
    cout << "正常離開,狀態 : " << WEXITSTATUS(status) << endl;
  } else {
    cout << "異常離開,狀態 : " << WTERMSIG(status) << endl;
  }
}
int main() {
  // 補捉子程序離開的訊號
  signal(SIGCHLD, func);
  if (fork() > 0) {
    // 父程序執行30秒 0 .. 29
    for (int i = 0; i < 30 ; i++) {
      cout << "父 : 第" << i << "秒" << endl;
      sleep(1);
    }
  } else {
    // 子程序執行15秒 0 .. 14
    for (int i = 0; i < 15 ; i++) {
      cout << "子 : 第" << i << "秒" << endl;
      sleep(1);
    }
    // 子程序執行15秒結束
    exit(5);
  }
}
父 : 第0秒
子 : 第0秒
父 : 第1秒
子 : 第1秒
父 : 第2秒
子 : 第2秒
父 : 第3秒
子 : 第3秒
父 : 第4秒
子 : 第4秒
父 : 第5秒
子 : 第5秒
父 : 第6秒
子 : 第6秒
父 : 第7秒
子 : 第7秒
父 : 第8秒
子 : 第8秒
父 : 第9秒
子 : 第9秒
父 : 第10秒
子 : 第10秒
父 : 第11秒
子 : 第11秒
父 : 第12秒
子 : 第12秒
父 : 第13秒
子 : 第13秒
父 : 第14秒
子 : 第14秒
父 : 第15秒
已終止的子程序pid是:155462
正常離開,狀態 : 5
父 : 第16秒
父 : 第17秒
父 : 第18秒
父 : 第19秒
父 : 第20秒
父 : 第21秒
父 : 第22秒
父 : 第23秒
父 : 第24秒
父 : 第25秒
父 : 第26秒
父 : 第27秒
父 : 第28秒
父 : 第29秒

取得父程序的id

pid_t getpid(void);  // 取得目前程序pid
pid_t getppid(void);  //取得父程序pid

results matching ""

    No results matching ""