---
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 函數: **將 ++相同程式的處理++ 分成複數的行程處理** ! 它會依據一個已經建立的行程 (母行程),在建立一個新的行程
> 
* 以 Memory 來說,它需要經過幾個階段 (省略虛擬記憶體概念)
1. 母行程發出 fork,建立子行程用的記憶體區塊
2. 藉由核心,複製母行程的記憶體內容,到子記憶體
3. 母行程收到 fork 後的結果 (返回 0 表示成功,-1 則失敗)
> 
:::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");
}
```
> 
* 母行程 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");
}
```
> 
### execve 函數 - 載入 ELF
* execve 函數: 在原行程想 **建立全新一個進程時會使用 execve 函數 (會覆蓋當前的行程資料,但記憶體位置不變)**,並經過以下幾個步驟
1. 原行程讀取 ELF 執行檔案,將必要資訊讀出 (後面會說)
> 
2. 將原行程的記憶體以 新的 (將要建立) 行程 進行複寫 (像是移動記憶體 offset、entry 位置)
> 
3. 從最初的命令開始執行新的 (子) 行程
* execve 整體概念就是 **替換原行程的 ELF 資訊**
> 
* ELF 就是透過 cc 指令編譯出來的執行檔案,在需要時會透過記憶體映射將需要資訊 (**輔助資訊**) 讀取出來,這個訊行保存行程運行所需的資料
1. 程式碼所含資料區域的檔案上的 `offset`、`size`、`start address` (虛擬記憶體映射開始位置)
2. 程式碼以外的變數、資料等等訊息
3. 程式執行的進入點 (最初執行命令的記憶體位置)
> 
:::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 執行檔資訊映射到記憶體上
> 
### ELF 執行檔
* ELF 全名為 Executable and Linkable Format (可執行可鏈結的格式),並可透過 `readelf` 這個命令來查看相關資訊
1. 使用 `-h` 讀取 file header
```shell=
# 找尋當前使用的 sleep 絕對位置
which sleep
# 查看 sleep 的 file header
readelf -h /usr/bin/sleep
```
> 
2. 使用 `-S` 讀取詳細資訊,Displays the detailed section information.
```shell=
# 查看 sleep 的 file header
readelf -S /usr/bin/sleep
```
> 
:::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
```
> 
### 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");
}
```
> 
## Process 安排器
Linux 為了要讓多個行程同時運行,會使用 ==**行程安排器**==,而我們現在就要透過一些實驗來研究這個行程安排器
:::info
* **CPU 時間切片 Time Slice**
以 **單個 CPU** 來說運行多行程,其實是 ++**看起來**++ 同時運行,並不是真正同時運行,而是透過快速的上下文切換,速度快到人類無法察覺
:::
一個 CPU 核心同時間內只能處理一件事情
> 
### Linxu 系統 & CPU 核心
* **多核心 CPU**:Linux 是將 1 個核心看做一個 CPU,市面上就有多核心 CPU
> 4 核 CPU,Linux 就會看做 4 個 CPU
:::success
* **邏輯 CPU** ?
所謂的邏輯 CUP 就是被系統識別為 CPU 者,就稱為 **邏輯 CPU**
:::
* **超執行序**:若 CPU 有啟動 **超執行序**,每個超執行續都會被視為一個 **邏輯 CPU**;像是電腦常說的 6 核 12 執行序 (一個核心有 2 個超執行序)
> 
### Process 安排器 - sched 實驗
* 讓一個 or 多個 CPU 來運行 1 ~ 多個行程,來觀察邏輯 CPU 上哪個行程在運作,各個行程處理的進度又是如何
* 該程式可以輸入 3 個參數,其意義如下表
| 順序 | 參數 | 功能 |
| -------- | -------- | -------- |
| 1 | nproc | 子行程數量 |
| 2 | total | CPU 使用時間 |
| 3 | resol | CPU 每次使用時間到 resol 就記錄訊息 |
> 
```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
>
> 
* 但我們目前是要測試一個 CPU 是如何切換不同進程,這可以透過 **==taskset -c== 指令指定運行該應用的 CPU**
> 
```shell=
taskset -c <CPU號> <程式> [參數]
```
* 使用上面 sched 進程測試
1. 1 子行程,運行 30 cpu 時間,1 個切分
```shell=
## 指定 CPU 0 運行
taskset -c 0 ./sched 1 30 1
```
> 
單純依照比例增加
> 
2. 2 子行程,運行 30 cpu 時間,1 個切分
```shell=
taskset -c 0 ./sched 2 30 1
```
> 
觀察 **使用時間**,2 個子行程若是按照順序則應該會時間連續,但**由於 CPU 對多行程的分配,所以 2 個行程會透過切換來運行**
並且**運行時間是 1 個行程的 2 倍** (下圖是概念圖)
> 
3. 4 子行程,運行 30 cpu 時間,1 個切分
```shell=
taskset -c 0 ./sched 4 30 1
```
> 
並且**運行時間是 1 個行程的 4 倍** (下圖是概念圖)
不特別畫惹~ 不好畫,知道概念就好
* 從上面兩個例子可以看到
1. **==邏輯 CPU 運行時一次只能運行一個行程==**
2. 多個行程是 **循環運作 (上下文切換)**
3. 美個行程運行時間都差不多,所以越多任務耗費時間越長
### 改變行程優先序 - nice
* 透過 nice() 函數可以調整行程被 CPU 執行的優先度,優先度在 -19(高) ~ 20(低) 之間,越高的優先度越會被 CPU 優先執行 (但不代表會全部執行該行程,仍會有其他行程插入)
:::info
* 優先度調整
任何人都可以降低行程的優先度,但 **提高行程優先度必須要使用 ==root 權限==**
> 
:::
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 的行程優先執行完畢、並且會先使用大多數時間
> 
CPU 使用時間概念如下,CPU 主力會先處理完 Process 0 行程
> 
2. 使用 nice 指令,並觀察 sar 中的 `%nice` 會發現**原本應該執行在 `%user` 的行程會移動到 `%nice`**
```shell=
# -n 用來指定優先度
nice -n 10 python3 ./loop.py
# 查看收集訊息
sar -P ALL 1 1
# 殺掉測試行程
kill 37010
```
> 
### 上下文切換 - 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 的運行切換任務,並非固定
>
> 
## 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
```
> 
| 符號(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)
> 
* 上面測試 sched 的結果,**==一個邏輯 CPU 同時只能運行一個 Process==**,以下假設運行兩個 Process 對照 CPU 狀態
:::warning
* Context switch 是否是 System call ?
**Context switch 並不會切換到核心模式**
:::
> 
* 運行 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`**
> 
## 吞吐量 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%
> 
2. 兩個行程,並假設每 20ms 就休眠一次
* 吞吐量: 2/140ms = 100 (行程) / 7 (秒) = 14.285 行程/秒
* 延遲: 140ms - 0ms = 140ms
* 閒置比例: 6/6 = 0 = 0%
> 
* **CPU 使用 ++行程排程器++ 來運行所有行程,這樣才不會導致 Process0、Process1 延遲量不同**
* CPU 吞吐量基本上是不會變的
:::warning
* 頻繁的上下文切換仍會影響到 CPU 的吞吐量
:::
### 實際系統 - 吞吐量 & 延遲
1. **延遲**:透過 **sar** `-P` 查看 `%idle` 的比例
```shell=
sar -P ALL 1
```
> 
2. **吞吐量**:透過 sar -q 查看 `runq-sz`
```shell=
# -q : system load & pressure
sar -q 1
```
> 
個欄位代表意義
> 
## 多邏輯 CPU
* 若有多個 Process 又有多個邏輯 CPU 需要處理,那邏輯 CPU 則會透過 **==附載平衡器== (又稱為全域排成器)**,它可以將行程 **公平的** 分配到各個邏輯 CPU (**每個分配時間都相同**)
> 
* 查看自身邏輯 CPU
* 本身邏輯 CPU 資訊可以在 `/proc/cpuinfo` 找到,查看數量 (processor 數量,從 0 開始數)
```shell=
cat /proc/cpuinfo
```
> 
### 多行程 & 多邏輯 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
```
>
2. 使用 2 個邏輯 CPU 運行 2 行程:兩個 CPU 處理 2 個行程,不影響延遲 (時間)
吞吐量 2 process / 96ms 約是 => 20 行程/秒
```shell=
taskset -c 0,2 sched 2 100 1
```
>
3. 使用 2 個邏輯 CPU 運行 4 行程:兩個 CPU 輪流處理 4 個行程,**影響延遲** (時間變兩倍)
吞吐量 4 process / 180ms 約是 => 20 行程/秒
```shell=
taskset -c 0,2 sched 4 100 1
```
> 
從上面實驗可以發現如果要處理的行程大於 CPU 總數吞吐量就會固定 (不考慮 CPU 切換,也就是上下文切換),**處理的行程越多越影響到的是時間**
### 經過 & 使用時間 - time 指令
* 使用 time 指令可以監看該程式 **經過時間、使用時間**
1. 經過時間:類似碼表,查看該程序的 endTime - startTime
2. 使用時間:**使用時間是針對 ++CPU 運行時間++進程監測**
| time 顯示關鍵字 | 說明 | 其他 |
| -------- | -------- | -------- |
| real | 經過時間 | |
| user | **使用者模式下**,使用 CPU 的時間 | 真正使用時間要加上 sys |
| sys | 切換到 **核心模式**的 system call 時間 | 真正使用時間要加上 user |
> 
* 同樣做上面的測試 (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**
>
2. 使用 1 個邏輯 CPU 運行 2 行程:一個 CPU 處理兩個行程,**處理時間、CPU 使用時間 * 2** (先不在意 loops_per_msec 所耗費的時間)
```shell=
taskset -c 0 sched 2 10000 10000
```
CPU 使用時間 19ms - 3ms = **16ms**
> 
3. 使用 2 個邏輯 CPU 運行 2 行程:兩個 CPU 處理 2 個行程,處理時間、CPU 使用時間相同
```shell=
taskset -c 0,2 sched 2 10000 10000
```
CPU 使用時間 12ms - 3ms = **9ms** (跟 2 個 CPU 1 個行程差不多)
>
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**
>
* 從上面實驗可以看 CPU 使用時間大部分都會比經過時間還要長 (若行程超出 CPU 個數 & 有在使用 CPU,有幾個 CPU 就有幾倍),但注意下一個小節
> 
### 休眠 sleep - time
* 使用 sleep 指令行程休眠,來觀察 CPU 使用時間、經過時間
```bash=
time sleep 10
```
可以看到經過時間 10ms,但都沒有使用到邏輯 CPU 所以 CPU 使用時間幾乎為 0ms
> 
## 觀察實際 Linux 系統
使用 ps 可以查看當前系統的所有進程 (如果沒有下達其他命令,則會只顯示當前進城的訊息)
```shell=
ps
```

* 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
```
> 
* 查看遊覽器 Fox 可以看到運行時間 (etime) 已經 21 小時,而 cpu 使用時間 (time) 只經過了 22 分鐘,代表瀏覽器 (Fox) 行程大部分時間都在休眠
```shell=
ps -eo pid,comm,etime,time | grep fox
```
> 
### 觀察 - 無限循環
* 撰寫一個簡單的 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
```
運行時間 & 啟動時間相同
> 
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
```
運行時間 & 啟動時間相同
> 
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 使用,不能一直占用
> 
## Appendix & FAQ
:::info
:::
###### tags: `Linux 系統核心`