---
# System prepended metadata

title: Process 管理 - 安排
tags: [Linux 系統核心]

---

---
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 系統核心`
