---
# System prepended metadata

title: 'Linux 核心設計: Process'
tags: [NCKU Linux Kernel Internals, ' 作業系統']

---

---
tags: NCKU Linux Kernel Internals, 作業系統
---
# Linux 核心設計: Process

[Linux 核心設計: 不僅是個執行單元的 Process](https://hackmd.io/@sysprog/linux-process?type=view)


:::success
:video_camera: 此筆記僅補充一些有興趣的議題，詳細請參考課程錄影 
:::

## Overview 

### Process
Reference to [CSE 506, Spring 2016: Operating Systems](http://www.cs.unc.edu/~porter/courses/cse506/s16/): [Process Address Spaces and Binary  Formats](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/address-spaces.pdf)

首先我們要問，process 是甚麼呢？
* process 是一段虛擬空間，映射到 kernel 中的實體記憶體
* process 中包含
    * Memory-mapped	files(包含 program binary 本身)
    * Anonymous	pages(heap, stack 等非對應檔案的記憶體)

既然 process 是一段對 kernel 的映射，那麼勢必需要資料結構表示此映射關係，這就是 **vm_area_struct**

### vm_area_struct in Linux

Linux 透過紀錄在 `struct task_struct` -> `struct mm_struct` 中的 [struct vm_area_struct](https://elixir.bootlin.com/linux/latest/source/include/linux/mm_types.h#L301) 這個資料結構，管理 userspace 的 address space，結構中包含的成員有：

* vm_start: 映射 virtual address 的起始位置
* vm_end: 映射 virtual address 的結束位置
    * 考慮到 page aligned，`vm_start - vm_end` 可能會略大於要求配置的範圍
* vm_next, vm_prev: 串接 VMA 單元的 linked list (sorted by address)
* vm_flags: 可讀？可寫？可執行？
* vm_file: 映射到的 file(可為 NULL)

等等...

如講義中圖示，簡易的表達 VMA 與定址空間的關係類似：
![](https://i.imgur.com/9ilLsDO.png)

不過， VMA 資料結構在 kernel 中的操作十分頻繁，如講義中說：

> Source code (mm/mmap.c): claims 35% hit rate	

簡單的 linked list 勢必在 search 上的 O(n) 複雜度會導致效能的低下，因此 linux 會透過紅黑樹來優化搜尋速度。此外，前段時間被 access 的 VMA 單元很可能會再度被使用，因此使用 pointer 去 cache 前一次使用的 VMA 單元，增進 locality。

#### Demand paging
當使用者透過 mmap 等 syscall 要求建立一個 VMA 時，kernel 可以不必直接建立真正對於實體記憶體的映射。直到該 VMA 真正被存取時，會觸發 page fault，此時再真正去映射實體記憶體，這就是 [demand paging](https://en.wikipedia.org/wiki/Demand_paging)。

這種設計可以取得以下優點:
* 僅載入執行時真正需要的 page，減少了記憶體的浪費
* 越多可用的 memory，表示有越多的 process 可以被 cache 在 memory 中，減少 context switch 需要的時間

當然也伴隨著一些 trade-off:
* page fault 的處理是相對的成本，如果 page fault 發生的頻率太過頻繁(試想多個 process 都需要 memory 而彼此搶奪資源，造成 [thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science)))，反而可能造成效能更差
* page replacement / swapping 是 demand paging 強大的關鍵，但並不是每個硬體架構都有 [MMU](https://en.wikipedia.org/wiki/Memory_management_unit) 可以支援此技術。(如果沒有 MMU 的話，由於無法保證置換回 secondary storage 的 page 再被載回時是同 address，而會發生問題。然而在有 MMU 的狀況下，雖然 physical memory 不同，但可以映射到相同的 virtual memory 上)

### Signal
Reference to [Signals and Inter-Process Communication](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/ipc.pdf)

#### What is signal?
根據維基百科 [Signal (IPC)](https://en.wikipedia.org/wiki/Signal_(IPC)) 的說法:

> Signals are a limited form of inter-process communication (IPC), typically used in Unix, Unix-like, and other POSIX-compliant operating systems. A signal is an asynchronous notification sent to a process or to a specific thread within the same process in order to notify it of an event that occurred.

Signal 類似於 interrupt，只是目標對象不是 kernel，而是某個 application(process)，是 process 間交換訊息的一種有限的形式。process 可以 signal 另一個 process 或 thread，並且，每個 process 或 thread 也可以自定義收到每個 signal 的對應行動。

Signal 與 hardware exception 也有密切的關聯: 當程式運行中發生 exception，OS 會轉至 kernel exception handler 來解決問題，然後恢復原本程式的運行。然而，有些 exception 可能是 kernel 無法解決的，因此 kernel 會通過 signal 將 exception 轉回 process，告訴 process 需自行解決問題。舉例來說，當一個 x86 CPU 上的 process 嘗試除零時，會產生 divide error exception，並使得 kernel 將 SIGFPE signal 轉至 process。

signal 的類型可以閱讀 [signal(7) — Linux manual page](https://man7.org/linux/man-pages/man7/signal.7.html)

![](https://i.imgur.com/8yB45Qw.png)

如圖，例如在 linux 中，當 process 收到 signal，會透過 `do_signal` 檢查 signal 的類型進行對應的處理。如果是 kernel 無法處理的 signal 的話，則先把原本切換回 user-mode 要執行的 context 儲存起來，並且建立對應的 signal handler stack frame，把 program counter 指過去執行。當 signal handler 處理完 signal 會透過 system call 回到 kernel，再回復原本儲存起來的 context，恢復原本程式的運行流程。

:::warning
:warning: 上面這一段是憑著一些二手的資料跟我之前修課的記憶寫的，描述得相當籠統，而且也不保證沒有錯誤的地方，好奇的人建議還是實際 trace code 或者透過實驗去測試吧~~

未來也許會再回頭補充清楚?
:::

#### Nested	Signals
如果在 signal handler 階段又被 signal 會如何呢?
```c=
int a = 0;

int main() {
/* ... */
signal(SIGINT, &handler);
signal(SIGTERM, &handler);
/* ... */
} 

int handler() {
a++;
/* do something */
printf("this is the %d time of handler\n", a);
} 
```

以一個簡單的例子來說，也許你預期如果 handler 每次被運行就印出自己是第幾次被執行，也就是印出的 pattern 是:
```
this is the 1 time of handler
this is the 2 time of handler
```
但是設想一下，如果第一個 handler 在 `/* do something */` 的途中又被 signal，於是在第一個 handler `printf` 先執行了 `a++`，結果也許就變成:
```
this is the 2 time of handler
this is the 2 time of handler
```

為了避免非預期的結果，建議將 `signal()` 換成 `sigaction()`，後者就類似在 interrupt handler 執行時會關掉 interrupt 一樣，避免前述的問題發生。

順帶一提，application 和 kernel 的 signal 不大相同:

* 對於 application 來說，signal 會被立即傳遞出去。 
* kernel 會先 pending signal ，再從 interrupt 或者 system call 返回 userspace 時檢查是否有 pending signal 並傳遞出去。




### Native POSIX Threading Library (NPTL)

甚麼是 thread?
* thread 是 OS 進行排程與執行的單位
* 在一個 address space(process) 中，可以存在一至多個 thread 
* 以 linux 而言，也就是多個 `task_struct` 共享同一個 `mm_struct`

早期的 linux 中(2.6 版以前)，process 與 thread 的關係是充滿曖昧的。由於 linux 初期是以 single thread 為導向的作業系統，因此 process 是最小的排程單元。但提供了 system call [clone](https://man7.org/linux/man-pages/man2/clone.2.html)，可以用來提供共用 address space 的 **process**，你可以在 man page 中讀到：

> For example, using these system calls, the caller can control whether or not the two processes share the virtual address space

意即: 用 clone() 產生的 thread，本質上仍是 process，可以說 Linux 核心中定義的 “thread” 只做到了資源共享，而不構成執行工作的最基本單位。這也導致了 linux 中的 thread 與 POSIX 的標準不兼容。舉例來說，因為不是真正的 thread，如果某個 thread 的 signal 被 block 住，其他同一個 address space 下的 signal 也會連帶被 block 掉。

延伸閱讀： [Linux threading models compared: LinuxThreads and NPTL](https://pdfs.semanticscholar.org/a1a7/451a1332d62ea6a6414f3e474e5415e523eb.pdf?_ga=2.105282755.1976655423.1597985306-1100633812.1597985306)

## TODO
- [ ] 研究 linux source code 如何透過紅黑樹加速 VMA 的 access 時間
- [ ] 研究 linux source code 的 signal 機制，包含 system call 的流程與 kernel 如何處理
- [ ] 研究 task_struct 的設計，包含 fork 、 clone 等相關的 system call，以及 thread、process 與之的關係。