--- title: 'Process 管理 - 安排' disqus: kyleAlien --- Process 管理 === ## OverView of Content 行程管理有關係到 Memory 的規劃,而這有關到 **虛擬記憶體 (Virtual memory)** 的概念,這邊則先忽略 虛擬記憶體的部分 [TOC] ## 行程 - 概念 行程可以理解為一個應用的單位,每個應用都由 1 ~ 多個行程組成,範例 1. Web 伺服器受理多個要求 2. Shell 執行的指令 ### Linux 函數 & System call * Linux 函數對應到的 System call | Linux 函數 | System call | | -------- | -------- | | fork() | clone() | | execve() | execve() | * 主要就是要說明 **^1^ fork 、 ^2^ execve** 函數 ### fork 函數 - 記憶體 * fork 函數: **將 ++相同程式的處理++ 分成複數的行程處理** ! 它會依據一個已經建立的行程 (母行程),在建立一個新的行程 > ![](https://i.imgur.com/ZGLGwaV.png) * 以 Memory 來說,它需要經過幾個階段 (省略虛擬記憶體概念) 1. 母行程發出 fork,建立子行程用的記憶體區塊 2. 藉由核心,複製母行程的記憶體內容,到子記憶體 3. 母行程收到 fork 後的結果 (返回 0 表示成功,-1 則失敗) > ![](https://i.imgur.com/byXxxpw.png) :::warning * Fork 後會子程序會 **自動開始 ++運行++** ::: * 以下是使用 fork 函數的範例 ```c= // fork.c #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <err.h> static void child() { printf("I'm child process! pid is %d.\n", getpid()); exit(EXIT_SUCCESS); } static void parent(pid_t pid) { printf("I'm parent process! pid is %d, and the child pid is %d\n", getpid(), pid); exit(EXIT_SUCCESS); } int main(void) { pid_t ret; // ret is return ret = fork(); if (ret == -1) { err(EXIT_FAILURE, "fork function return faill."); } if (ret == 0) { // 判斷返回值 0 就是成功 child(); } else { // 父行程會得到 child 的 Process ID parent(ret); } err(EXIT_FAILURE, "Fail"); } ``` > ![](https://i.imgur.com/sTD6yy7.png) * 母行程 fork 出來後,兩者就是不同行程,母 & 子行程都不相互等待 (以下使用 sleep 讓母行程休息 3s 觀察 子行程是否等待) ```c= // fork_with_sleep.c #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <err.h> static void child() { printf("I'm child process! pid is %d.\n", getpid()); exit(EXIT_SUCCESS); } static void parent(pid_t pid) { printf("I'm parent process! pid is %d, and the child pid is %d\n", getpid(), pid); exit(EXIT_SUCCESS); } int main(void) { pid_t ret; // ret is return ret = fork(); if (ret == -1) { err(EXIT_FAILURE, "fork function return faill."); } if (ret == 0) { // 判斷返回值 0 就是成功 child(); } else { parent(ret); sleep(3); } err(EXIT_FAILURE, "Fail"); } ``` > ![](https://i.imgur.com/SbJ2Yf7.png) ### execve 函數 - 載入 ELF * execve 函數: 在原行程想 **建立全新一個進程時會使用 execve 函數 (會覆蓋當前的行程資料,但記憶體位置不變)**,並經過以下幾個步驟 1. 原行程讀取 ELF 執行檔案,將必要資訊讀出 (後面會說) > ![](https://i.imgur.com/pfbxWPg.png) 2. 將原行程的記憶體以 新的 (將要建立) 行程 進行複寫 (像是移動記憶體 offset、entry 位置) > ![](https://i.imgur.com/jcMJZXh.png) 3. 從最初的命令開始執行新的 (子) 行程 * execve 整體概念就是 **替換原行程的 ELF 資訊** > ![](https://i.imgur.com/9KEaKIn.png) * ELF 就是透過 cc 指令編譯出來的執行檔案,在需要時會透過記憶體映射將需要資訊 (**輔助資訊**) 讀取出來,這個訊行保存行程運行所需的資料 1. 程式碼所含資料區域的檔案上的 `offset`、`size`、`start address` (虛擬記憶體映射開始位置) 2. 程式碼以外的變數、資料等等訊息 3. 程式執行的進入點 (最初執行命令的記憶體位置) > ![](https://i.imgur.com/3obHVMc.png) :::success * 虛擬記憶體映射開始位置 ? CPU 上所執行的命令是低階語言,必須要依照相同的 CPU 架構撰寫相對的組合語言 ```c= // c 語言內程式 a = b + c; // 組合語言 load m100 r0 // 讀取記憶體 m100 的位置到 r0 暫存器 load m200 r1 // 讀取記憶體 m200 的位置到 r1 暫存器 add r0 r1 r2 // 將 r0 加上 r1 並將結果放到 r2 store r2 m300 // 儲存 r2 暫存器的數值,到記憶體 m100 的位置 ``` ::: * 根據 ELF 執行檔資訊映射到記憶體上 > ![](https://i.imgur.com/Zxqz8OL.png) ### ELF 執行檔 * ELF 全名為 Executable and Linkable Format (可執行可鏈結的格式),並可透過 `readelf` 這個命令來查看相關資訊 1. 使用 `-h` 讀取 file header ```shell= # 找尋當前使用的 sleep 絕對位置 which sleep # 查看 sleep 的 file header readelf -h /usr/bin/sleep ``` > ![](https://i.imgur.com/bv5Madm.png) 2. 使用 `-S` 讀取詳細資訊,Displays the detailed section information. ```shell= # 查看 sleep 的 file header readelf -S /usr/bin/sleep ``` > ![](https://i.imgur.com/P3jjNol.png) :::spoiler 完整 elf 資訊 ```shell= There are 28 section headers, starting at offset 0x8278: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000000238 00000238 000000000000001b 0000000000000000 A 0 0 1 [ 2] .note.gnu.bu[...] NOTE 0000000000000254 00000254 0000000000000024 0000000000000000 A 0 0 4 [ 3] .note.ABI-tag NOTE 0000000000000278 00000278 0000000000000020 0000000000000000 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0000000000000298 00000298 000000000000001c 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 00000000000002b8 000002b8 00000000000005e8 0000000000000018 A 6 3 8 [ 6] .dynstr STRTAB 00000000000008a0 000008a0 00000000000002bb 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 0000000000000b5c 00000b5c 000000000000007e 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 0000000000000be0 00000be0 0000000000000040 0000000000000000 A 6 2 8 [ 9] .rela.dyn RELA 0000000000000c20 00000c20 00000000000004b0 0000000000000018 A 5 0 8 [10] .rela.plt RELA 00000000000010d0 000010d0 0000000000000498 0000000000000018 AI 5 22 8 [11] .init PROGBITS 0000000000001568 00001568 0000000000000018 0000000000000000 AX 0 0 4 [12] .plt PROGBITS 0000000000001580 00001580 0000000000000330 0000000000000000 AX 0 0 16 [13] .text PROGBITS 00000000000018c0 000018c0 00000000000040a0 0000000000000000 AX 0 0 64 [14] .fini PROGBITS 0000000000005960 00005960 0000000000000014 0000000000000000 AX 0 0 4 [15] .rodata PROGBITS 0000000000005978 00005978 0000000000000bae 0000000000000000 A 0 0 8 [16] .eh_frame_hdr PROGBITS 0000000000006528 00006528 000000000000029c 0000000000000000 A 0 0 4 [17] .eh_frame PROGBITS 00000000000067c8 000067c8 0000000000000d70 0000000000000000 A 0 0 8 [18] .init_array INIT_ARRAY 0000000000017af0 00007af0 0000000000000008 0000000000000008 WA 0 0 8 [19] .fini_array FINI_ARRAY 0000000000017af8 00007af8 0000000000000008 0000000000000008 WA 0 0 8 [20] .data.rel.ro PROGBITS 0000000000017b00 00007b00 00000000000000b8 0000000000000000 WA 0 0 8 [21] .dynamic DYNAMIC 0000000000017bb8 00007bb8 0000000000000200 0000000000000010 WA 6 0 8 [22] .got PROGBITS 0000000000017db8 00007db8 0000000000000248 0000000000000008 WA 0 0 8 [23] .data PROGBITS 0000000000018000 00008000 00000000000000e8 0000000000000000 WA 0 0 8 [24] .bss NOBITS 00000000000180e8 000080e8 0000000000000160 0000000000000000 WA 0 0 8 [25] .gnu_debugaltlink PROGBITS 0000000000000000 000080e8 000000000000004a 0000000000000000 0 0 1 [26] .gnu_debuglink PROGBITS 0000000000000000 00008134 0000000000000034 0000000000000000 0 0 4 [27] .shstrtab STRTAB 0000000000000000 00008168 000000000000010f 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), D (mbind), p (processor specific) ``` ::: * 從上面輸出的資訊我們可以看到程式 (.txt) & 資料 (.data) 相關的詳細資訊 | 區域 | Value | 意義 | | -------- | -------- | -------- | | 程式 (.txt) | 18c0 | 程式進入點 | | 資料 (.data) | 18000 | 資料進入點 | * 啟動該程式,並查看對應的虛擬記憶體映射表 (`/proc/<pid>/mmap`),就可以看到記憶體位置在對應的地方 ```shell= ## shell -------------------------------------------- # 執行 sleep /usr/bin/sleep 99999 & # 查看相對訊息 cat /proc/58158/maps ## 輸出結果 -------------------------------------------- # txt 進入點 (18c0 在這個區間內) aaaad0800000-aaaad0808000 r-xp 00000000 b3:02 4007 /usr/bin/sleep ... # data 進入點 (18000 在這個區間內) aaaad0818000-aaaad0819000 rw-p 00008000 b3:02 4007 /usr/bin/sleep ``` > ![](https://i.imgur.com/JJJ3aZs.png) ### fork + execve 範例 * 以下使用 fork 建立一個新行程,並在新行程中使用 execve 來執行 echo 命令,以下說明 execve 的執行步驟 1. 原行程載入 `/bin/echo` ELF 資訊 2. 複寫 原行程的 ELF 資訊 3. 開始執行 `/bin/echo` 的程式 ```c= #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <err.h> static void child() { printf("I'm child process! pid is %d.\n", getpid()); char *args[] = { "/bin/echo", "hello world !", NULL}; fflush(stdout); // 載入 /bin/echo (elf 檔案),並執行 execve("/bin/echo", args, NULL); fflush(stdout); err(EXIT_FAILURE, "execve() fail."); } static void parent(pid_t pid) { printf("I'm parent process! pid is %d, and the child pid is %d\n", getpid(), pid); exit(EXIT_SUCCESS); } int main(void) { pid_t ret; // ret is return ret = fork(); if (ret == -1) { err(EXIT_FAILURE, "fork function return faill."); } if (ret == 0) { child(); } else { parent(ret); } err(EXIT_FAILURE, "Fail"); } ``` > ![](https://i.imgur.com/Yw80oLh.png) ## Process 安排器 Linux 為了要讓多個行程同時運行,會使用 ==**行程安排器**==,而我們現在就要透過一些實驗來研究這個行程安排器 :::info * **CPU 時間切片 Time Slice** 以 **單個 CPU** 來說運行多行程,其實是 ++**看起來**++ 同時運行,並不是真正同時運行,而是透過快速的上下文切換,速度快到人類無法察覺 ::: 一個 CPU 核心同時間內只能處理一件事情 > ![](https://i.imgur.com/ytU8aXI.png) ### Linxu 系統 & CPU 核心 * **多核心 CPU**:Linux 是將 1 個核心看做一個 CPU,市面上就有多核心 CPU > 4 核 CPU,Linux 就會看做 4 個 CPU :::success * **邏輯 CPU** ? 所謂的邏輯 CUP 就是被系統識別為 CPU 者,就稱為 **邏輯 CPU** ::: * **超執行序**:若 CPU 有啟動 **超執行序**,每個超執行續都會被視為一個 **邏輯 CPU**;像是電腦常說的 6 核 12 執行序 (一個核心有 2 個超執行序) > ![](https://i.imgur.com/0GwlwAZ.png) ### Process 安排器 - sched 實驗 * 讓一個 or 多個 CPU 來運行 1 ~ 多個行程,來觀察邏輯 CPU 上哪個行程在運作,各個行程處理的進度又是如何 * 該程式可以輸入 3 個參數,其意義如下表 | 順序 | 參數 | 功能 | | -------- | -------- | -------- | | 1 | nproc | 子行程數量 | | 2 | total | CPU 使用時間 | | 3 | resol | CPU 每次使用時間到 resol 就記錄訊息 | > ![](https://i.imgur.com/kK3xXeW.png) ```c= // sched.c #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <err.h> #define NLOOP_FOR_ESTIMATION 1000000000UL #define NSECS_PER_MSEC 1000000UL // 毫秒 us #define NSECS_PER_SEC 1000000000UL // 10 ns static inline long timespec_utils(struct timespec *time) { return (time->tv_sec * NSECS_PER_SEC + time->tv_nsec); } // 計算 timespec 前後時間差 static inline long diff_nsec(struct timespec before, struct timespec after) { return timespec_utils(&after) - timespec_utils(&before); } // 模擬運行實做 static unsigned long loops_per_msec() { struct timespec before, after; clock_gettime(CLOCK_MONOTONIC, &before); unsigned long i; for(i = 0; i < NLOOP_FOR_ESTIMATION; i++); // 執行規定次數 clock_gettime(CLOCK_MONOTONIC, &after); // 計算 // (規定次數 * us) / (前後運行時間) return NLOOP_FOR_ESTIMATION * NSECS_PER_MSEC / diff_nsec(before, after); } // 運行指定次數的迴圈 static inline void load(unsigned long nloop) { unsigned long i; for(i = 0; i < nloop; i++); } // 子行程 static void child_fn(int id, // 子行程 ID struct timespec *buf, // timespec array (用來記錄子行程完成 loop 的時間) int nrecord, // total / resol unsigned long nloop_per_resol, // 模擬時間 * resol struct timespec start) { // 母行程創建的時間 // 該行程的運行 load 的次數 for (int i = 0; i < nrecord; i++) { struct timespec ts; load(nloop_per_resol); clock_gettime(CLOCK_MONOTONIC, &ts); buf[i] = ts; } for (int i = 0; i < nrecord; i++) { printf("process id: %d\t useTime: %ld\t progress: %d\n", id, // 原行程 ID diff_nsec(start, buf[i]) / NSECS_PER_MSEC, // 從程式開始當下經過的時間 (i + 1) * 100 / nrecord // 運行進度 到了第幾個 resol 區塊 ); } exit(EXIT_SUCCESS); } // 檢查使用者輸入參數 static void check_params(int nproc, int total, int resol) { if(nproc < 1) { fprintf(stderr, "<nproc>(%d) should be >= 1\n", nproc); exit(EXIT_FAILURE); } if(total < 1) { fprintf(stderr, "<total>(%d) should be >= 1\n", total); exit(EXIT_FAILURE); } if(resol < 1) { fprintf(stderr, "<resol>(%d) should be >= 1\n", resol); exit(EXIT_FAILURE); } if(total % resol) { fprintf(stderr, "<total>(%d) should be multiple of <resol>(%d)\n", total, resol); exit(EXIT_FAILURE); } } // 模擬運行 static unsigned long start_estimating(int resol) { puts("estimating workload which takes just one milisecond"); // 單次運行 * resol 次數 unsigned long nloop_per_resol = loops_per_msec() * resol; puts("end estimation"); fflush(stdout); // 返回子行程該運行 for 的總量 return nloop_per_resol; } static pid_t *pids; int main(int argc, char *argv[]) { int ret = EXIT_FAILURE; // first params is command if(argc < 4) { fprintf(stderr, "usage: %s <nproc> <total[ms]> <resolution[ms]>", argv[0]); exit(EXIT_FAILURE); } // atoi 將輸入轉為 int 類型 int nproc = atoi(argv[1]); // 行程數量 int total = atoi(argv[2]); // 總進度 int resol = atoi(argv[3]); // 切分量 check_params(nproc, total, resol); // 計算切分數量 int nrecord = total / resol; // 動態分配 timespec 空間 struct timespec *logbuf = malloc(nrecord * sizeof(struct timespec)); if (!logbuf) err(EXIT_FAILURE, "malloc(logbuf) failed"); // 母行程模擬運行時間 unsigned long nloop_per_resol = start_estimating(resol); // 動態分配子行程數量 pids = malloc(nproc * sizeof(pid_t)); if(pids == NULL) { warn("malloc(pids) failed"); goto free_logbuf; } struct timespec start; clock_gettime(CLOCK_MONOTONIC, &start); printf("nloop_per_resol value: %lu\n", nloop_per_resol); // 創建子行程 int i, ncreated; for(i = 0, ncreated = 0; i < nproc; i++, ncreated++) { pids[i] = fork(); // 判斷子行程是否創建失敗 if(pids[i] < 0) { // 失敗 goto wait_children; } else if (pids[i] == 0) { // 成功 child_fn(i, logbuf, nrecord, nloop_per_resol, start); } } ret = EXIT_SUCCESS; wait_children: if(ret == EXIT_FAILURE) { for(i = 0; i < ncreated; i++) { if(kill(pids[i], SIGINT) < 0) { warn("kill(%d) failed", pids[i]); } } } for(i = 0; i < ncreated; i++) { if(wait(NULL) < 0) { warn("wait() failed."); } } free_pids: free(pids); free_logbuf: free(logbuf); exit(ret); } ``` * 輸出結果也有三個 | 順序 | 簡述 | 說明 | | - | - | - | | 1 | id | 該子行程的 ID | | 2 | diff time | 從母行程開始的時間減去子行程完成 loop 的時間 | | 3 | resol 進度 | 進度區塊到哪 | ```shell= # 編譯執行檔 cc -o sched sched.c ``` ### taskset 指定運行 CPU * 由於目前使用的裝置是多核 cpu (可透過 `proc/cpuinfo` 查看 processor 數量),在運行多行程時會透過 **==負載平衡器== 分配不同邏輯 CPU 執行** > 目前是 4 核心 CPU > > ![](https://i.imgur.com/LWJKMJu.png) * 但我們目前是要測試一個 CPU 是如何切換不同進程,這可以透過 **==taskset -c== 指令指定運行該應用的 CPU** > ![](https://i.imgur.com/MRbVdai.png) ```shell= taskset -c <CPU號> <程式> [參數] ``` * 使用上面 sched 進程測試 1. 1 子行程,運行 30 cpu 時間,1 個切分 ```shell= ## 指定 CPU 0 運行 taskset -c 0 ./sched 1 30 1 ``` > ![](https://i.imgur.com/whscQgn.png) 單純依照比例增加 > ![](https://i.imgur.com/pl2AQfz.png) 2. 2 子行程,運行 30 cpu 時間,1 個切分 ```shell= taskset -c 0 ./sched 2 30 1 ``` > ![](https://i.imgur.com/wnAWuzQ.png) 觀察 **使用時間**,2 個子行程若是按照順序則應該會時間連續,但**由於 CPU 對多行程的分配,所以 2 個行程會透過切換來運行** 並且**運行時間是 1 個行程的 2 倍** (下圖是概念圖) > ![](https://i.imgur.com/UKMyl2k.png) 3. 4 子行程,運行 30 cpu 時間,1 個切分 ```shell= taskset -c 0 ./sched 4 30 1 ``` > ![](https://i.imgur.com/kjyzd7Z.png) 並且**運行時間是 1 個行程的 4 倍** (下圖是概念圖) 不特別畫惹~ 不好畫,知道概念就好 * 從上面兩個例子可以看到 1. **==邏輯 CPU 運行時一次只能運行一個行程==** 2. 多個行程是 **循環運作 (上下文切換)** 3. 美個行程運行時間都差不多,所以越多任務耗費時間越長 ### 改變行程優先序 - nice * 透過 nice() 函數可以調整行程被 CPU 執行的優先度,優先度在 -19(高) ~ 20(低) 之間,越高的優先度越會被 CPU 優先執行 (但不代表會全部執行該行程,仍會有其他行程插入) :::info * 優先度調整 任何人都可以降低行程的優先度,但 **提高行程優先度必須要使用 ==root 權限==** > ![](https://i.imgur.com/QtUgmsd.png) ::: 1. 稍微修改一下 `sched.c`,並在程式中 **使用 `nice` 函數** ```c= // sched_nice.c ...省略部分,不同同上 static void check_params(/*int nproc, */int total, int resol) { /* if(nproc < 1) { fprintf(stderr, "<nproc>(%d) should be >= 1\n", nproc); exit(EXIT_FAILURE); } */ if(total < 1) { fprintf(stderr, "<total>(%d) should be >= 1\n", total); exit(EXIT_FAILURE); } if(resol < 1) { fprintf(stderr, "<resol>(%d) should be >= 1\n", resol); exit(EXIT_FAILURE); } if(total % resol) { fprintf(stderr, "<total>(%d) should be multiple of <resol>(%d)\n", total, resol); exit(EXIT_FAILURE); } } static unsigned long start_estimating(int resol) { puts("estimating workload which takes just one milisecond"); unsigned long nloop_per_resol = loops_per_msec() * resol; puts("end estimation"); fflush(stdout); return nloop_per_resol; } static pid_t *pids; int main(int argc, char *argv[]) { int ret = EXIT_FAILURE; // first params is command if(argc < /*4*/3) { fprintf(stderr, "usage: %s <nproc> <total[ms]> <resolution[ms]>", argv[0]); exit(EXIT_FAILURE); } // 固定兩個行程 int nproc = 2; int total = atoi(argv[1]); int resol = atoi(argv[2]); check_params(/*nproc, */total, resol); int nrecord = total / resol; struct timespec *logbuf = malloc(nrecord * sizeof(struct timespec)); if (!logbuf) err(EXIT_FAILURE, "malloc(logbuf) failed"); unsigned long nloop_per_resol = start_estimating(resol); pids = malloc(nproc * sizeof(pid_t)); if(pids == NULL) { warn("malloc(pids) failed"); goto free_logbuf; } struct timespec start; clock_gettime(CLOCK_MONOTONIC, &start); printf("nloop_per_resol value: %lu\n", nloop_per_resol); int i, ncreated; for(i = 0, ncreated = 0; i < nproc; i++, ncreated++) { pids[i] = fork(); if(pids[i] < 0) { goto wait_children; } else if (pids[i] == 0) { // 當第二個行程時 if(i == 1) { nice(5); // 下修 cpu 優先度 } child_fn(i, logbuf, nrecord, nloop_per_resol, start); } } ret = EXIT_SUCCESS; wait_children: if(ret == EXIT_FAILURE) { for(i = 0; i < ncreated; i++) { if(kill(pids[i], SIGINT) < 0) { warn("kill(%d) failed", pids[i]); } } } for(i = 0; i < ncreated; i++) { if(wait(NULL) < 0) { warn("wait() failed."); } } free_pids: free(pids); free_logbuf: free(logbuf); exit(ret); } ``` 可以看到 pid = 0 的行程優先執行完畢、並且會先使用大多數時間 > ![](https://i.imgur.com/Z5PLMxB.png) CPU 使用時間概念如下,CPU 主力會先處理完 Process 0 行程 > ![](https://i.imgur.com/8Ko6dvR.png) 2. 使用 nice 指令,並觀察 sar 中的 `%nice` 會發現**原本應該執行在 `%user` 的行程會移動到 `%nice`** ```shell= # -n 用來指定優先度 nice -n 10 python3 ./loop.py # 查看收集訊息 sar -P ALL 1 1 # 殺掉測試行程 kill 37010 ``` > ![](https://i.imgur.com/1i9pb2d.png) ### 上下文切換 - Context switch * 在邏輯 CPU 上的行程進行切換時,這個行為就稱為 Context switch (也就上下文切換),以下寫兩個概念程式 Process0、Process1,各自執行不同函數 ```c= // 概念 // Process0.c int main() { a(); b(); } // Process0.c int main() { c(); d(); } ``` 以一個邏輯 CPU 來說,它會分配時段來切換資源,所以 Process0#a 之後並不依定會接著運行 Process0#b 函數 (可能執行另一個行程的 c、d 函數) > 下圖是假設 CPU 的運行切換任務,並非固定 > > ![](https://i.imgur.com/qCdu5RO.png) ## Process 狀態 - PS Linux 系統中運行多少個 Process 可以透過 `ps` 指令查詢 ```shell= # 省略 `-` 號 (BSD-style) # a: 指顯示自身 # u: 包含 user 訊息 # x: 包含 tty 終端 ps aux ``` > * 一般大部分 Process 都處於休眠狀態,以下是 PS 常見狀態,[**參考**](http://puremonkey2010.blogspot.com/2012/08/linux-linuxps-statrsdtzx.html) ```shell= # 詳細可以用 man 來查看 man ps ``` > ![](https://i.imgur.com/jCmbGCl.png) | 符號(STAT) | 狀態 | 說明 | | -------- | -------- | -------- | | R(執行中) | 執行 | 正在使用邏輯 CPU | | R(可執行) | 待命 | 等待 CPU 分配資源 | | T | 暫停 or 跟蹤 | 通常在等待 Socket 通知 | | S(等待訊號) | 可中斷休眠 | 等待事件發生,事件發生前都不會使用到 CPU 時間 | | D(等待儲存裝置存取) | 不可中斷休眠 | 等待事件發生,事件發生前都不會使用到 CPU 時間 | | Z | 殭屍 | 等待母行程結束的子行程 | | X | 退出 | 該進行程將被 kill | * 休眠狀態可能是如下 1. 等待指定時間 sleep 2. 等待鍵盤、滑鼠,使用者輸入 3. 等待讀寫至 HDD、SSD 等儲存裝置讀寫作業的結束 4. 等待網路資料接收 ### CPU & Process 狀態轉換 * 多行程 & CPU 狀態切換如下 (With STAT) > ![](https://i.imgur.com/j6Wawug.png) * 上面測試 sched 的結果,**==一個邏輯 CPU 同時只能運行一個 Process==**,以下假設運行兩個 Process 對照 CPU 狀態 :::warning * Context switch 是否是 System call ? **Context switch 並不會切換到核心模式** ::: > ![](https://i.imgur.com/sJWcEsA.png) * 運行 2 個 loop.py 循環程式,並且該程式不包含 System call ```python= # loop.py while True: pass ``` 使用 taskset 指定運行 CPU、並用 sar 觀察系統狀態 ```shell= # 指定 CPU0 運行 (並背景運行) taskset -c 0 python3 loop.py & taskset -c 0 python3 loop.py & # sar 查看 CPU sar -P ALL 1 1 # kill loop.py kill 4555 4556 ``` 可以看到 CPU0 `%user` 占用 100% 運行,並且 **沒有使用到 `%system`** > ![](https://i.imgur.com/8KVFB0I.png) ## 吞吐量 Throughtput & 延遲 Latency | | 吞吐量 | 延遲 | | -------- | -------- | -------- | | 理解方式 | 固定時間內能處理的程式 | 行程處理時間 | | 說明 | 每個單位時間的總工作量 | 行程從開始到結束所經歷的時間 | | 算法 | Count of done process / FixedTime | EndTime - StartTime | * 吞吐量與閒置狀態是呈現反比,**吞吐量越高越好,而延遲越低越好** 1. 一個行程,並假設每 20ms 就休眠一次 * 吞吐量: 1/120ms = 25 (行程) / 3 (秒) = 8.333 行程/秒 * 延遲: 120ms - 0ms = 120ms * 閒置比例: 3/6 = 0.5 = 50% > ![](https://i.imgur.com/lmX0ltU.png) 2. 兩個行程,並假設每 20ms 就休眠一次 * 吞吐量: 2/140ms = 100 (行程) / 7 (秒) = 14.285 行程/秒 * 延遲: 140ms - 0ms = 140ms * 閒置比例: 6/6 = 0 = 0% > ![](https://i.imgur.com/LoP2p6c.png) * **CPU 使用 ++行程排程器++ 來運行所有行程,這樣才不會導致 Process0、Process1 延遲量不同** * CPU 吞吐量基本上是不會變的 :::warning * 頻繁的上下文切換仍會影響到 CPU 的吞吐量 ::: ### 實際系統 - 吞吐量 & 延遲 1. **延遲**:透過 **sar** `-P` 查看 `%idle` 的比例 ```shell= sar -P ALL 1 ``` > ![](https://i.imgur.com/hO5FAFz.png) 2. **吞吐量**:透過 sar -q 查看 `runq-sz` ```shell= # -q : system load & pressure sar -q 1 ``` > ![](https://i.imgur.com/mBRil4j.png) 個欄位代表意義 > ![](https://i.imgur.com/btc8SV7.png) ## 多邏輯 CPU * 若有多個 Process 又有多個邏輯 CPU 需要處理,那邏輯 CPU 則會透過 **==附載平衡器== (又稱為全域排成器)**,它可以將行程 **公平的** 分配到各個邏輯 CPU (**每個分配時間都相同**) > ![](https://i.imgur.com/zRHsgyi.png) * 查看自身邏輯 CPU * 本身邏輯 CPU 資訊可以在 `/proc/cpuinfo` 找到,查看數量 (processor 數量,從 0 開始數) ```shell= cat /proc/cpuinfo ``` > ![](https://i.imgur.com/OvN9l5i.png) ### 多行程 & 多邏輯 CPU - 實驗 * 使用上面使用到的 `sched.c` 程式來進行實驗 | nproc 子行程數量 | total CPU 使用時間 | resol CPU 每次到 resol 紀錄的時間 | | -------- | -------- | -------- | | 1 | 100 | 1 | | 2 | 100 | 1 | | 4 | 100 | 1 | * taskset `-c` 設定指定 CPU ```shell= taskset -c <指定 CPU 標號> <程式> ``` :::success 如果要較高的獨立性,依照第一個選 CPU0,則應該選用取用總 CPU / 2 者,**這樣這樣兩個 CPU 就沒有 ==共同快取記憶體== 較為獨立** > 如果 CPU 總數是 0 ~ 3,則 0、2 共用、1、3 共用 ```shell= taskset -c 0, 2 <程式> ``` ::: 1. 使用 2 個邏輯 CPU 運行 1 行程:兩個 CPU 處理一個行程,不影響延遲 (時間) 吞吐量 1 process / 96ms 約是 => 10 行程/秒 ```shell= taskset -c 0,2 sched 1 100 1 ``` >![](https://i.imgur.com/avyvx9s.png) 2. 使用 2 個邏輯 CPU 運行 2 行程:兩個 CPU 處理 2 個行程,不影響延遲 (時間) 吞吐量 2 process / 96ms 約是 => 20 行程/秒 ```shell= taskset -c 0,2 sched 2 100 1 ``` >![](https://i.imgur.com/X28gAjQ.png) 3. 使用 2 個邏輯 CPU 運行 4 行程:兩個 CPU 輪流處理 4 個行程,**影響延遲** (時間變兩倍) 吞吐量 4 process / 180ms 約是 => 20 行程/秒 ```shell= taskset -c 0,2 sched 4 100 1 ``` > ![](https://i.imgur.com/9NQHeWz.png) 從上面實驗可以發現如果要處理的行程大於 CPU 總數吞吐量就會固定 (不考慮 CPU 切換,也就是上下文切換),**處理的行程越多越影響到的是時間** ### 經過 & 使用時間 - time 指令 * 使用 time 指令可以監看該程式 **經過時間、使用時間** 1. 經過時間:類似碼表,查看該程序的 endTime - startTime 2. 使用時間:**使用時間是針對 ++CPU 運行時間++進程監測** | time 顯示關鍵字 | 說明 | 其他 | | -------- | -------- | -------- | | real | 經過時間 | | | user | **使用者模式下**,使用 CPU 的時間 | 真正使用時間要加上 sys | | sys | 切換到 **核心模式**的 system call 時間 | 真正使用時間要加上 user | > ![](https://i.imgur.com/mDzYRWq.png) * 同樣做上面的測試 (1 ~ 2 個邏輯 CPU 運行 1、2、4 個行程),以下假設 loops_per_msec 所耗費的時間為 3ms 1. 使用 2 個邏輯 CPU 運行 1 行程:兩個 CPU 處理一個行程,處理時間、CPU 使用時間相同 ```shell= taskset -c 0,2 sched 1 10000 10000 ``` CPU 使用時間 11ms - 3ms = **8ms** >![](https://i.imgur.com/GOJiGmB.png) 2. 使用 1 個邏輯 CPU 運行 2 行程:一個 CPU 處理兩個行程,**處理時間、CPU 使用時間 * 2** (先不在意 loops_per_msec 所耗費的時間) ```shell= taskset -c 0 sched 2 10000 10000 ``` CPU 使用時間 19ms - 3ms = **16ms** > ![](https://i.imgur.com/h2A9IRi.png) 3. 使用 2 個邏輯 CPU 運行 2 行程:兩個 CPU 處理 2 個行程,處理時間、CPU 使用時間相同 ```shell= taskset -c 0,2 sched 2 10000 10000 ``` CPU 使用時間 12ms - 3ms = **9ms** (跟 2 個 CPU 1 個行程差不多) >![](https://i.imgur.com/pQjDHNW.png) 4. 使用 2 個邏輯 CPU 運行 4 行程:兩個 CPU 輪流處理 4 個行程,**處理時間、CPU 使用時間 * 2** (先不在意 loops_per_msec 所耗費的時間) ```shell= taskset -c 0,2 sched 4 10000 10000 ``` 經過時間同 1 個 CPU 處理 2 個行程 CPU 使用時間 34ms - 3ms = **32ms** >![](https://i.imgur.com/41UGYvQ.png) * 從上面實驗可以看 CPU 使用時間大部分都會比經過時間還要長 (若行程超出 CPU 個數 & 有在使用 CPU,有幾個 CPU 就有幾倍),但注意下一個小節 > ![](https://i.imgur.com/8TwffDz.png) ### 休眠 sleep - time * 使用 sleep 指令行程休眠,來觀察 CPU 使用時間、經過時間 ```bash= time sleep 10 ``` 可以看到經過時間 10ms,但都沒有使用到邏輯 CPU 所以 CPU 使用時間幾乎為 0ms > ![](https://i.imgur.com/NFUtY9w.png) ## 觀察實際 Linux 系統 使用 ps 可以查看當前系統的所有進程 (如果沒有下達其他命令,則會只顯示當前進城的訊息) ```shell= ps ``` ![](https://i.imgur.com/JavOz28.png) * ps 常用參數,詳細請用 man ps 查詢 | 參數 | 說明 | 其他 | | -------- | -------- | -------- | | a | 所有行程 | 等同於 `-e`、`-A` | | -u | 顯示該行程 user | | | x | 必須包含 tty | | | -o | 指定輸出格式 | 有 pid、comm、tname、etime、time、comm、command... 等等可以使用 | ### 觀察 - 系統行程運行 * 使用 `-o` 指定格式可以查詢到 CPU 使用時間 & 經過時間 ```shell= ps -eo pid,comm,etime,time ``` > ![](https://i.imgur.com/s9uVuJP.png) * 查看遊覽器 Fox 可以看到運行時間 (etime) 已經 21 小時,而 cpu 使用時間 (time) 只經過了 22 分鐘,代表瀏覽器 (Fox) 行程大部分時間都在休眠 ```shell= ps -eo pid,comm,etime,time | grep fox ``` > ![](https://i.imgur.com/v2A0ilr.png) ### 觀察 - 無限循環 * 撰寫一個簡單的 loop.py 無限循環程式 ```python= while True: pass ``` * 指定 cpu & 運行多個 loop.py 行程,就可以觀察到 cpu 使用時間、運行時間的數值 1. 指定 2 個邏輯 CPU & 運行一個行程 ```shell= taskset -c 0,2 python3 ./loop.py & ps -eo pid,comm,etime,time | grep python kill 36363 ``` 運行時間 & 啟動時間相同 > ![](https://i.imgur.com/KdMhUtj.png) 2. 指定 2 個邏輯 CPU & 運行 2 個行程 ```shell= taskset -c 0,2 python3 ./loop.py & taskset -c 0,2 python3 ./loop.py & ps -eo pid,comm,etime,time | grep python kill 36399 36400 ``` 運行時間 & 啟動時間相同 > ![](https://i.imgur.com/XyaQA0Y.png) 3. 指定 2 個邏輯 CPU & 運行 4 個行程 ```shell= taskset -c 0,2 python3 ./loop.py & taskset -c 0,2 python3 ./loop.py & taskset -c 0,2 python3 ./loop.py & taskset -c 0,2 python3 ./loop.py & ps -eo pid,comm,etime,time | grep python kill 36399 36400 ``` 運行時間 & 啟動時間不相同,行程不須分配 CPU 使用,不能一直占用 > ![](https://i.imgur.com/Ev61UN4.png) ## Appendix & FAQ :::info ::: ###### tags: `Linux 系統核心`