## 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