文章目錄
簡 述: 本篇主要講解如下知識點:
- 並行和併發的區別
- 進程控制塊
PCB
和進程的五種狀態 - 使用
fork()
創建子進程,創建多個兄弟子進程(不含孫進程) - 驗證進程之間沒有共享全局變量
exec()
函數族的使用:execl()
和execlp()
編程環境:
💻: uos20
📎 gcc/g++ 8.3
📎 gdb8.0
💻: MacOS 10.14
📎 gcc/g++ 9.2
📎 gdb8.3
並行和併發:
併發:一個時間段,處理請求的個數
eg:在一個時間段內, 處理的請求個數。 只有一個 cpu 對任務進行處理。 (每個人都吃一口飯,但是所有人都沒有吃飽,短時間內讓大量的快速吃,然後這樣輪循環。)
並行:多個進程同時進行任務分配:
可以看做,有多個 cpu 對多個任務進行同時處理。
PCB 和 進程的五種狀態:
進程控制塊 PCB:
此部分了解即可: 每一個進程在內核中都有一個進程控制塊 PCB 來維護進程相關的信息,其是有一個 400 多行的結構體組成,其中主要的需要了解的部分如下:
- 進程 id。系統中每一個進程有唯一的一個 id,在 C 語言中,用 pid_t 類型表示。(凡是 xxx_t 一般都是 #define 重新定一個類型, 這裏實際就上課非負整數)
- 進程的狀態,有就緒,運行,掛起,停止 等狀態。(還有一個初始態 )
- 進程切換時候,需要保存和恢復一些的 CPU 寄存器
- 描述虛擬地址空間的信息(每啓動一個進程,就對應一個虛擬地址空間)
- 描述控制終端的信息
- 當前的工作目錄
- umask 掩碼(執行 umask)
- 文件描述符表,包含很多執行 file 結構體的指針(一個進程最多打開 1024 個)
- 和信號相關的信息
- 用戶 uid 和組 gid (用stat xxx 可以查看)
- 會話(Session)和進程組。(會話就是多個進程組)
- 進程可以使用的資源上限(Resource Limit)。(使用
ulimit -a
可看)
進程的五種狀態:
看着如下的這張圖,想起裏了我之前初看的計算機操作系統的相關知識的時候,感慨頗多。
- 狀態切換的關鍵:
- 獲得 cpu
- cpu 執行
進程控制塊,父進程創建子進程的分析 🎃:
父子進程空間,其虛擬地址空間是完全一樣的(用戶區的所有數據都是一樣的,也是會運行的相同的一段代碼;系統區也是一樣的,唯一不一樣的就是系統區的 id 號)。
創建一個子進程,是使用 int fork(void)
函數來創建,在執行完這個 fork 函數之後,將用戶區的內容全部拷貝(包括接下來要執行的代碼)過去。且該 fork 函數有兩個返回值,執行完 fork() 後,父進程會返回子進程的 id(大於 0),子進程會返回一個 0,後面執行相同的代碼,依靠這個來區分父子進程。 下面寫一個代碼片來表明:
對照上面的兩個圖,可以思考如下問題:
- fork 之後,函數的返回值?
- fork() 返回值 > 0,就是父進程;fork() 返回值 == 0,就是子進程
- 子進程創建成功之後,代碼的執行位置?
- 父進程執行哪裏(才創建子進程),子進程就重哪裏裏開始執行。
- 父子進程的執行順序?
- 不一定,自己去搶佔 cpu 資源。執行順序是程序員無法控制的。(cpu 輪循執行)
- 如何區分父子進程?
- 通過 fork() 返回值
- 如何獲取當前進程和父進程的 id?
- 獲取進程 id:獲取當前進程的 PID:
getpid()
;獲取當前進程的父進程的 PID:getppid()
- 獲取進程 id:獲取當前進程的 PID:
對與上面的問題,寫如下代碼片段進程測試,main.cpp 裏面的代碼是:代碼下載 main.cpp
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
for (int i = 0; i < 4; ++i) {
printf("++++i = %d\n", i);
}
pid_t pid = -1;
pid = fork();
if (pid > 0)
printf("this is a parent process, pid = %d\n", getpid());
else if (pid == 0)
printf("this is a child process, pid = %d ppid = %d\n", getpid(), getppid());
for (int i = 0; i < 4; ++i) {
printf("----i = %d\n", i);
}
return 0;
}
其運行結果如下:
父進程如何創建多個兄弟子進程(不含孫進程)🎃:
看了上面的小的例子之後,發現這樣可以創建子進程;進一步,怎麼創建多個子進程,但是不要孫創建進程?有怎麼判斷子進程的順序編號?怎麼得到最開始父進程?
代碼如下: 代碼下載 childProcess.cpp
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid = -1;
int i = 0;
for (i = 0; i < 3; ++i) {
pid = fork();
if (pid == 0) //如果當前進程是子進程,就跳出此循環,不在進行 fork() 創建新的子進程
break;
}
if (i == 0)
printf("this is a child process (i == 0), pid = %d ppid = %d\n", getpid(), getppid());
if (i == 1)
printf("this is a child process (i == 1), pid = %d ppid = %d\n", getpid(), getppid());
if (i == 2)
printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid());
if (i == 3)
printf("this is a parent process (i == 3), pid = %d ppid = %d\n", getpid(), getppid());
return 0;
}
分析代碼片:
若是沒有上面的 13、14 行代碼,那麼創建的就不止 3 個子進程;就是如下圖片所示的結果; 使用 fork() 創建子進程的時候,是進行虛擬地址控件的拷貝(可在草稿紙上面畫圖理解,是一個地址塊所有的代碼和變量值都複製一份),他們的當時 i 是 1 的時候,創建的新的子進程的 i 也是 1,不會是初值 0;同理,當 i 是 2 時候創建新進程,其新進程的 i 值是 2,而不是 1; 但是禁止子進程創建孫進程,那麼就可以使用 for() 循環生成多個兄弟子進程,且沒有任何的孫進程。
運行結果:
可以自己每運行行代碼,標記一下 i 的值,以及此 i 的值時刻複製當前進程的虛擬地址空間,給新的子進程;所以 可以通過編號 i 的數值,來判斷兄弟子進程的先後順序;最後編號最大的那個,就是最初的父進程。
進程之間是否共享全局變量:🎃
-
讀時共享(同一個物理內存的上面區域,內存地址爲同一個變量)
-
寫的時候複製
-
父子進程之間能否使用全局變量通信?
- 不能。因爲兩個進程之間內存不共享(讀時共享,寫時複製)
代碼驗證,改寫上面的代碼,添加一個全局變量 int g_num = 200,進行驗證:
#include <stdio.h> #include <unistd.h> int g_num = 200; int main(int argc, char *argv[]) { pid_t pid = -1; int i = 0; for (i = 0; i < 3; ++i) { pid = fork(); if (pid == 0) //如果當前進程是子進程,就跳出此循環,不在進行 fork() 創建新的子進程 break; } if (i == 0) { g_num += 5; printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid()); } if (i == 1){ g_num += 5; printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid()); } if (i == 2) { g_num += 5; printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid()); } if (i == 3) { g_num += 100; printf("this is a parent process (i == 3), pid = %d ppid = %d\n", getpid(), getppid()); } printf("g_nmu = %d\n", g_num); return 0; }
運行截圖:
草稿圖分析:
顯示當前進程的狀態 ps:
ps aus
顯示不依賴於終端的進程(終端是用來和用戶進行交互的)ps aux | grep 查找的進程名
使用管道查找指定的進程名稱ps ajx
顯示更多的進程信息; PID、PPID、PGID、SID(進程,父進程,組進程,會話)(會話就是多個組進程)ps ajx | grep 查找的進程名
若是沒有找到,也會有一行顯示信息,顯示的是 grep 進程相關的信息
刪除執行中的程序或工作 kill:
Linux kill命令用於刪除執行中的程序或工作。
kill可將指定的信息送至程序。預設的信息爲SIGTERM(15),可將指定程序終止。若仍無法終止該程序,可使用SIGKILL(9)信息嘗試強制刪除程序。程序或工作的編號可利用ps指令或jobs指令查看。
kill -9 將被殺死的進程
-9 爲 殺死信號kill -l
顯示所有,1-31 通常使用;32 和 33 沒有; 34-51 號通常爲系統預留
使用 kill 命令之後,有一點困惑,使用 kill -l
命令之後;
-
在 Ubuntu 18.4 顯示如下, 和 MacOS 顯示有區別, 有清楚的,可以在下面留言幫我解答一下也可以哦。
-
只有第9種信號(SIGKILL)纔可以無條件終止進程,其他信號進程都有權利忽略。 下面是常用的信號:
HUP 1 終端斷線 INT 2 中斷(同 Ctrl + C) QUIT 3 退出(同 Ctrl + \) KILL 9 強制終止 TERM 15 終止 CONT 18 繼續(與STOP相反, fg/bg命令) STOP 19 暫停(同 Ctrl + Z)
exec 函數族 🎃:
作用: 讓父子進程執行不相干的操作;能夠替換進程空間中源代碼的 .txt
段;當前程序中調用另外一個應用程序 (調用 exec 之前,需要 fork );
exec() 函數族, 也不要判斷返回值:若是函數執行成功,不返回(.text 的代碼立即被全部替換,後面的代碼也不會執行);若是執行失敗,可以調用 perror(“xx 提示:”) 來打印錯誤信息,退出當前進程;
執行指定目錄下的程序 execl():
int execl(const char *path, const char *arg, ... /*, (char *)0 */);
-
path: 需要執行的程序的絕對路徑(推薦絕對路經)
-
變參 arg: 要執行的程序的需要的參數
- 第一個 arg: 佔位(內容隨便寫什麼,但不可爲空)
- 後邊的 arg: 命令的參數
-
參數寫完之後: NULL (作用是哨兵,表示該命令的參數已經輸入完畢)
-
一般是執行自己寫的程序(也可以執行系統自帶的命令)
-
寫一個例子驗證一下:代碼下載
- 代碼如下:
#include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { for (int i = 0; i < 3; i++) printf("-----i = %d\n", i); pid_t pid = fork(); //創建子進程 if (pid == 0) execl("/bin/ls", "佔位參數", "-al", NULL); //ls 程序使用子進程的地址空間 for (int i = 0; i < 3; i++) //這段打印只會(父進程)被執行一遍。 printf("+++++i = %d\n", i); return 0; }
-
過程分析:
第一段打印"-----i =" 內容,是隻會被父進程執行一遍的,子進程不會執行該段代碼(分析見上);結束時候打印 “+++++i =” 內容時候,是隻會執行一遍的,還是被父進程執行的,而子進程不執行 的原因是,當第 12 行的代碼execl(“ls”) 執行結束之後,其子進程的虛擬地址空間的以用戶區域的 .text 代碼段的二進制代碼被替換爲 /bin/ls 這個程序的代碼;其後面面也都是運行 ls 程序的代碼內容。
-
運行結果:
執行 PATH 目錄下的程序 execlp():
int execlp(const char *file, const char *arg, ... /*, (char *)0 */);
- file: 需要執行的程序的名字
- 變參 arg: 要執行的程序的需要的參數
- 第一個 arg: 佔位(內容隨便寫什麼,但不可爲空)
- 後邊的 arg: 命令的參數
- 參數寫完之後: NULL (作用是哨兵,表示該命令的參數已經輸入完畢)
- 一般是執行系統自帶的命令(也就是 /bin 下的程序)
- execlp 執行自定義程序的程序: file 參數絕對路徑
系列地址:
https://github.com/xmuli/linuxExample
歡迎 star 和 fork 這個系列的 linux 學習,附學習進階的路線圖。