---
# System prepended metadata

title: Parallel Programming and System LAB 1
tags: [SP]

---

## Env Setting 
1. 安裝 Docker Desktop
2. 進入 Resources > WSL Integration。
3. 確認 "Enable integration with my default WSL distro" 已開啟。
4. reboot 
5. vscode(IDE) install Dev Containers 
6. 在 VS Code 中 F1 (或 Ctrl+Shift+P)。
7. 輸入並選擇：Dev Containers: Add Dev Container Configuration Files... 
8. 右下角 Reopen in Container
9. 選擇環境 "Ubuntu" / "Debian" / "Linux Image (Full System)" 
10. 選擇 Features (功能元件) 也能直接修改自動產生的 `.devcontainer/devcontainer.json`
## Check Docker
```
#正在運行
docker ps
# 已停止的
docker ps -a
# log 
docker logs <container_name 或 ID>
docker logs -f <container>
```
## Eval
```
usr/bin/time -v
# 獲取詳細的 CPU 利用率與記憶體峰值。

perf stat
# 獲取 CPU 週期、指令數、分支預測、快取命中等硬體層級數據。
# sudo apt install linux-tools-virtual linux-cloud-tools-virtual
# 在wsl先安裝

# 還是不行就暴力破解
find /usr/lib/linux-tools -name perf
# 假設你找到的是 /usr/lib/linux-tools/6.5.0-27-generic/perf
sudo ln -sf /usr/lib/linux-tools/ <你找到的版本資料夾> /perf /usr/bin/perf

numactl
# 控制執行緒綁定到特定的 CPU 核心（用於測試 Thread Affinity）。
```
## Default Setting Check
* Env : WSL2/ RTX 3050 / Core : 16
    ```ter
    # Check how many core we have
    lscpu | grep "^CPU(s):"
    
    lscpu -e
    
    htop
    ```
* ENV : MACOS
    ```ter
    # pthread 內建
    
    # openmp 無內建，原gcc 為clang 所以要
    brew install gcc
    
    # check gcc not clang name
    ls /opt/homebrew/bin/gcc*
    
    # Would be like follow, then you can type gcc-(number) -foopenmp 
    /opt/homebrew/bin/gcc-15        /opt/homebrew/bin/gcc-nm-15
    /opt/homebrew/bin/gcc-ar-15     /opt/homebrew/bin/gcc-ranlib-15
    
    # check CPU
    sysctl -n hw.ncpu
    # With logic 
    sysctl hw.physicalcpu hw.logicalcpu
    # instructions, cache
    sysctl -a | grep machdep.cpu
    # full system
    system_profiler SPHardwareDataType | grep "Processors\|Cores\|Threads"
    ```
* Hello World (openmp)
    ```ter
    #include <omp.h>
    #include <stdio.h>
    int main() {
        #pragma omp parallel
        {
            printf("Hello from thread %d\n", omp_get_thread_num());
        }
        return 0;
    }
    ```
    ```ter
    # Execute
    gcc-15 -fopenmp parallel_lab.c -o parallel_lab
    ./parallel_lab
    ```
* pthread testing 
    ```ㄎ
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>

    #define NUM_THREADS 5

    // 執行緒要執行的函數
    void* print_hello(void* thread_id) {
        long tid = (long)thread_id;
        printf("Hello World! 我是執行緒 #%ld\n", tid);
        pthread_exit(NULL);
    }

    int main() {
        pthread_t threads[NUM_THREADS];
        int rc;
        long t;

        for (t = 0; t < NUM_THREADS; t++) {
            printf("在 main 中：正在建立執行緒 %ld\n", t);

            // 建立執行緒
            rc = pthread_create(&threads[t], NULL, print_hello, (void*)t);

            if (rc) {
                printf("錯誤：無法建立執行緒，回傳碼：%d\n", rc);
                exit(-1);
            }
        }

        // 等待所有執行緒執行完畢 (Join)
        for (t = 0; t < NUM_THREADS; t++) {
            pthread_join(threads[t], NULL);
        }

        printf("所有執行緒已完成，主程式結束。\n");
        return 0;
    }
    ```
    ```
    # 編譯指令 (-lpthread 是連結執行緒函式庫)
    gcc test_pthread.c -o test_pthread -lpthread

    # 執行
    ./test_pthread
    ```
## Phase 1 
> 引言 : 你在程式碼裡每創一個 pthread，Linux 核心就會幫你創一個對應的「排程單位」。
>
> * 平行程式設計視角： 
> 你手上有 4 個任務，你開 4 個執行緒。
> * 作業系統視角： 
> 核心看到 4 個可以分配到不同 CPU 核心去跑的「小單位」
> 
> 所以結合 OS 和 Hardware 的角度來看，這裡有兩種cost
> 1. System Cost
> OS在建立與銷毀Threads，排程等所花費的時間，這包含了Memory Management(Stack、VirtualMem...)、Threads Management
> 2. Executing Cost
> 實際運算所花費的時間
>
>這使得問題變得複雜，工作量與是否需要那麼多人手平行執行之間的權衡變得重要
### User vs. Kernel
在現代 Linux (WSL/Ubuntu) 中，每一個 pthread 實際上對應到核心中的一個 task_struct
* **User-level (pthread 函式庫)**  
pthread_t 標識符、執行緒區域儲存 (TLS)。
* **Kernel(system)-level(Linux 核心管的)**
核心堆疊 (Kernel Stack)、排程實體 (Scheduling Entity)、PID/TGID 管理。

#### Linux Kernel 
* **task_struct**
這是 Linux 核心中最核心的結構。在 Linux 眼中，其實沒有區分「進程」和「執行緒」，它們都叫 task。
* **Kernel Stack**
每個執行緒在執行系統呼叫（System Call）時，需要一個獨立的核心堆疊空間來存放暫存資料。
* **PID/TGID**
在核心裡，每個執行緒都有自己唯一的 PID。但它們共享同一個 TGID (Thread Group ID)，這就是你在 User 空間看到的「Process ID」。

#### TLS (Thread Local Storage)
這是每個執行緒私有的儲存。雖然大家共享大部分記憶體，但 TLS 讓每個執行緒可以擁有自己的全域變數副本（例如 errno）
### **pthread_create 與 clone()** 
> aka 執行緒的誕生

當你呼叫 pthread_create 時，底層發生了什麼事？

1. **clone() 系統呼叫**
這是 Linux 的特點。它不像 Unix 嚴格區分 fork()（生孩子）和 thread_create。

2. **資源共享（CoW）**
clone() 可以透過參數決定要共享什麼。執行緒會共享：
    * 分頁表 (Page Table)
    共享記憶體位址空間。
    * 檔案描述符 (File Descriptors)
    一個執行緒打開檔案，另一個也能讀。
    * 初始化排程器
核心把這個新的 task_struct 放進排程佇列，等待 CPU 執行。

### Parallelism Trade off 
如果你開了 8 個執行緒，在 8 核電腦上，OS能使它們真的在「同時」運作。但這是有trade off的

1. **threads's overhead**
雖然比進程（Process）輕量，但因為要進核心（Kernel Space）跑clone()，頻繁地建立與銷毀執行緒會造成效能瓶頸
2. **Kernel Constraint**
因為 1:1，你的執行緒總數受限於作業系統能開多少 task_struct 

#### Process
一個執行中的程式實例，擁有獨立的虛擬記憶體空間（包含程式碼、全域變數、Heap 堆積）、打開的文件描述符（File Descriptors）、環境變數，像是獨立的工作室。
### Thread id Code
```c
/* 為什麼要用 void*？ 平行程式設計中，
// pthread_create 規定任務函式必須長這樣。
// 你可以傳入任何資料的位址。

// PID vs TID： 
// getpid() 一樣  # task_struct.tgid (thread group ID) 
// gettid() 不相同 #True process ID
// 它們共用同一個 Process 空間
// 但各自擁有獨立的核心調度實體。
 */
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h> // 為了呼叫 SYS_gettid
#include <sys/types.h>

void* thread_function(void* arg) {
    // 將傳進來的通用指標(void*) 轉回整數指標並取值
    int id = *(int*)arg;
    // getpid(): 獲取進程 ID
    // syscall(SYS_gettid): 獲取 Linux 核心分配給這個執行緒的真正 ID
    printf("執行緒 %d: Process ID (PID) = %d, Thread ID (TID) = %ld\n", 
            id, getpid(), syscall(SYS_gettid));
    
    sleep(10); // 暫停一下，方便我們在終端機觀察
    return NULL;
}

int main() {
    pthread_t t1, t2;
    int id1 = 1, id2 = 2;

    printf("主執行緒: Process ID (PID) = %d, Thread ID (TID) = %ld\n", 
            getpid(), syscall(SYS_gettid));
    
    
    pthread_create(&t1, NULL, thread_function, &id1);
    pthread_create(&t2, NULL, thread_function, &id2);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}

```
### Thread overhead code
```c
#include <pthread.h>
#include <stdio.h>
#include <time.h>

#define ITERATIONS 50000

void* dummy_func(void* arg) { return NULL; }

int main() {
    struct timespec m_start, m_end; // 給 Monotonic 使用
    time_t t_start, t_end;          // 給 time() 使用
    pthread_t thread;

    // 1. 記錄開始
    t_start = time(NULL); 
    clock_gettime(CLOCK_MONOTONIC, &m_start);

    for (int i = 0; i < ITERATIONS; i++) {
        pthread_create(&thread, NULL, dummy_func, NULL);
        pthread_join(thread, NULL);
    }

    // 2. 記錄結束
    clock_gettime(CLOCK_MONOTONIC, &m_end);
    t_end = time(NULL);

    // 3. 計算並顯示
    // .tv_sec 為libc定義的結構成員，表示秒數
    // .tv_nsec 為libc定義的結構成員，表示ns
    double m_diff = (m_end.tv_sec - m_start.tv_sec) + (m_end.tv_nsec - m_start.tv_nsec) / 1e9;
    double t_diff =t_end - t_start;

    printf("精確測量 (clock_gettime): %f 秒\n", m_diff);
    printf("粗略測量 (time): %f 秒\n", t_diff);
    printf("平均每個 thread 建立耗時: %f us\n", (m_diff / ITERATIONS) * 1e6);

    return 0;
}
```

### lifecycle Compare Fuc vs thread
```c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define ITERATIONS 10000

// 最小任務函式
void* minimal_task(void* arg) {
    return NULL;
}

void pure_function_call() {
    minimal_task(NULL);
}

double get_time_ns() {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1e9 + ts.tv_nsec;
}

int main() {
    double start, end;
    pthread_t thread;

    // 1. 基準測試：普通函式呼叫 (User-space only)
    start = get_time_ns();
    for (int i = 0; i < ITERATIONS; i++) {
        pure_function_call();
    }
    end = get_time_ns();
    printf("平均純函式呼叫耗時: %.2f ns\n", (end - start) / ITERATIONS);

    // 2. 執行緒建立與銷毀 (User <-> Kernel switch)
    start = get_time_ns();
    for (int i = 0; i < ITERATIONS; i++) {
        pthread_create(&thread, NULL, minimal_task, NULL);
        pthread_join(thread, NULL);
    }
    end = get_time_ns();
    printf("平均 pthread_create + join 耗時: %.2f ns\n", (end - start) / ITERATIONS);

    return 0;
}
```
#### CMD Command : System Calls Eval
```
strace -c ./lifecycle
```
> 在這裡，clone 與 mmap (分配執行緒堆疊)理論上來說最多，但目前是empty task，可以改用下面用來測/bin/time 的 code ，會比較明顯
##### return
```ter
平均純函式呼叫耗時: 1.07 ns
平均 pthread_create + join 耗時: 323344.39 ns
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 60.96    0.521357          52     10000           clone
 25.32    0.216528          10     20001           rt_sigprocmask
 13.65    0.116749          11     10000     10000 clone3
  0.06    0.000547          11        47        27 futex
  0.00    0.000031          15         2           write
  0.00    0.000000           0         1           read
  0.00    0.000000           0         2           close
  0.00    0.000000           0         9           mmap
  0.00    0.000000           0         5           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         1           rt_sigaction
  0.00    0.000000           0         4           pread64
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         2         1 arch_prctl
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         2           openat
  0.00    0.000000           0         3           newfstatat
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0         1           prlimit64
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         1           rseq
------ ----------- ----------- --------- --------- ----------------
100.00    0.855212          21     40090     10029 total
```
#### CMD Command : System/CPU
```
# Should be run in WSL or other, but no container
/usr/bin/time -v ./lifecycle
```
* Voluntary Context Switches 
Benchmark of **Synchronization** (Waiting)，類似於工作做完，釋放算力，再向OS要task來跑
##### return
```
平均純函式呼叫耗時: 1.03 ns
平均 pthread_create + join 耗時: 66025.08 ns
        Command being timed: "./1"
        User time (seconds): 0.01
        System time (seconds): 0.32
    --> Percent of CPU this job got: 50%
        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.66
        Average shared text size (kbytes): 0
        Average unshared data size (kbytes): 0
        Average stack size (kbytes): 0
        Average total size (kbytes): 0
        Maximum resident set size (kbytes): 1664
        Average resident set size (kbytes): 0
        Major (requiring I/O) page faults: 0
        Minor (reclaiming a frame) page faults: 78
        Voluntary context switches: 9980
        Involuntary context switches: 0
        Swaps: 0
        File system inputs: 0
        File system outputs: 0
        Socket messages sent: 0
        Socket messages received: 0
        Signals delivered: 0
        Page size (bytes): 4096
        Exit status: 0
```
##### Sample Testing Code
```c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define LIMIT 1000000000LL // 十億
#define THREADS 4           // 使用 4 個執行緒

// 傳給執行緒的參數包
typedef struct {
    long long start;
    long long end;
    long long partial_sum;
} ThreadData;

// 每個員工要做的工作：把分配到的範圍加起來
void* sum_range(void* arg) {
    ThreadData* data = (ThreadData*)arg;
    data->partial_sum = 0;
    for (long long i = data->start; i <= data->end; i++) {
        data->partial_sum += i;
    }
    return NULL;
}

int main() {
    pthread_t threads[THREADS];
    ThreadData data[THREADS];
    long long chunk_size = LIMIT / THREADS;

    // 1. 分派任務
    for (int i = 0; i < THREADS; i++) {
        data[i].start = i * chunk_size + 1;
        data[i].end = (i == THREADS - 1) ? LIMIT : (i + 1) * chunk_size;
        pthread_create(&threads[i], NULL, sum_range, &data[i]);
    }

    // 2. 等待所有人收工，並把各組結果加起來
    long long total_sum = 0;
    for (int i = 0; i < THREADS; i++) {
        pthread_join(threads[i], NULL);
        total_sum += data[i].partial_sum;
    }

    printf("計算完成！1 到 %lld 的總和是: %lld\n", LIMIT, total_sum);
    return 0;
}
```
### 更多的 Threads 會更好!?

從最開始的測量Threads overhead到最後的System/CPU Eval ，看得出純僱傭一個人手的開銷與中間的溝通成本，在最後的System/CPU中，可以看到core的利用率和user/system time，即便利用率高，但若工作量本身低，則多工的溝通與僱傭成本將拖垮效能。

而記憶體層面則根據`ulimit -s`確認stack的預設值，進而簡單推導所被預留的空間佔用。

---
`ulimit -s ` 是 User Limit 的縮寫，而 -s 代表 Stack（堆疊）。

* 定義
它規定了作業系統分配給每一個執行緒（或進程）的最大堆疊空間。
* 預設值
在大多數 Linux 系統中，這個值通常是 8192 KB (8MB)。
* 平行程式
在 1:1 下，每創一個pthread，核心就會在虛擬記憶體中「預留」8MB 的空間給該執行緒存放區域變數與函數呼叫記錄。 
> 這很可怕，假設創了1000個threads，他就預留了8MB x 1000 = 8GB，的VM 

可以透過CMD指令來知道thread的預設stack 單位空間
```
ulimit -s
```
## Question
#### 但為什麼分配的是「虛擬記憶體」而不是「實體記憶體」?
* 虛擬記憶體（Virtual Memory）
當你建立一個執行緒，OS 會給它一個 8MB 的 Stack 空間。這是紙面上預留的，並非真實占用，所以當下Memory是空的，只是被劃下來，直到thread 執行，才分配實體stack

> 但如果被劃分到滿了呢?

即便你只有 16GB 的實體 RAM，你依然可以讓程式預留遠超過16GB的VM，如果真的溢滿，則啟動這三種方式
1. **Denied**直接拒絕
2. **Swap**根據記憶體管理策略進行DRAM與RAM間的data swap 通常是LRU
3. **OOM Killer** 直接砍掉占比最大或消耗資源大的程式，解放空間
## Analysis Code 
### Time 
```c
#include <time.h>
 struct timespec m_start, m_end;
// Begin Eval
clock_gettime(CLOCK_MONOTONIC, &start)

// your code 

clock_gettime(CLOCK_MONOTONIC, &start)
// end
 double thread_time = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;

// Current
time_t start = time()
// code here
time_t end = time()
int cost = end - start;
```

LAB 2
https://hackmd.io/@1p0_VOUHTXGcHlBU9A0OEA/HkyDmOzsWx