搭配服用 http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html
繁體中文 | english | |
---|---|---|
作業系統 | 操作系统 | operating system |
匯流排 | 总线 | bus |
記憶體 | 主存, 內存 | main memory |
快取 | 高速缓存 | cache |
位元組 | 字节 | byte |
區段 | 节 | section |
載入 | 加载 | load |
組譯 | 汇编 | assemble |
堆疊 | 棧 | stack |
並行 | 并发 | concurrent |
平行 | 并行 | parallel |
程式 | 程序 | program |
过程 | procedure | |
行程 or 程序 | 进程 | process |
執行緒 | 线程 | thread |
立即数 | immediate | |
核心 | 内核 | kernel |
網際網路 | 万维网 | WWW |
1 補數 | 反码 | ones' complement |
2 補數 | 补码 | two's complement |
端法 | endian | |
信号量 | semaphore | |
例外 | 异常 | exception |
排程 | 调度 | scheduling |
暫停 | 挂起 | suspend |
資料結構 | 数据结构 | data structure |
韌體 | 固件 | firmware |
套接字 | socket | |
数据报 | datagram | |
埠 | 端口 | port |
条件跳转 | conditional jump | |
条件传送 | conditional move | |
内联替换 | inline substitution | |
最小平方法 | 最小乘积拟合 | least squares fit |
內存別名 | memory aliasing | |
发射时间 | issue time |
jserv :第 2 章的習題每一題都要做
編譯參數
-m32
:為 32 位元電腦編譯-m64
:為 64 位元電腦編譯告訴大家一個神器: man ascii
C 語言標準沒有明確定義 bitwise 右移,也沒有規定有號數該如何表示
symbol | 32 位元 | |
---|---|---|
UINT_MAX |
0xFFFFFFFF | |
INT_MIN |
0x80000000 | |
INT_MAX |
0x7FFFFFFF |
A portable printf()
may be implemented by macro, such as
C 標準沒有定義,但大多數的系統依循不改變位元更改詮釋方式
For an signed number , the conversion to unsigned can be described by
For an unsigned number ,
unsigned 和 signed 運算的結果會被 implicitly 轉換成 unsigned
在定義 INT_MIN 時最好寫成 -INTMAX - 1
一個數字被擴展時會使用 sign extension ,例如:
在我的電腦 (little endian) 上的執行結果是:
這是我所用的 show_bytes()
函式
我發現如果把 unsigned char
改成 char
就會錯,但是我不知道為什麼
一個數字被截斷時會無視 sign 逕行截斷,例如:
會得到 -2 而不是 32766 。
size_t 是無號的, ssize_t 是有號的
無號數加法類型為模加法,無號數發生 overflow 不會終止程式 (不會發 signal) 。
在 unsigned 情境下,令 :
而因為模加法的特性,使無號數形成 Abelian group 。
在 signed 的情況下,令 :
對某數 x
取 2 補數的方式,除了 ~x + 1
之外,可以等價地使用:
x
的 least significant set bit~
flip對無號數一樣是模乘,因為一樣會 truncate 到 內。
有號數一般需要將負值皆轉正,處理完 magnitude 後再處理 sign ,有些算法可以加速,例如 Booth’s Algorithm 。
使用 malloc(sizeof(x) * n)
時須小心 argument 的 overflow 問題,雖然一般使用上不會溢出,但是得要防範惡意輸入。若傳入值 n
與 input 有關,則呼叫 malloc()
前應先行檢查以避免安全漏洞。
速度甚至比乘法要慢得多,若有效能考量應避免使用。
page pointer : 72
x86 assembly
b
l
q
immediate 在 $
後面接 C 語言數字表示法
1, 2 byte 指令會保持 8 byte 內的其它 byte 不變
4 byte 指令會把高位的 4 個 byte 清零
x86 的 store 和 load 都是用 mov
來實現
cltq
為用於 %eax
到 %rax
的 sign extension
%rsp
會跟著更新
The shift operations 的 k
需要是 immediate 或是放在 %cl
中的值, %cl
是一個 8 bit 暫存器。此處所有指令都會去設置 conditional code 。
load effective address ,也就是讀地址,沒有其它 size 的變種。
不會改變 conditional code ,也因此常常被借用去進行跟取地址無關的簡單算術運算,有各種神奇用法,僅要求 D
需為 register 。
乘法將 S
%rax
之後放到 %rdx
:%rax
( 高位放在 %rdx
) 。
除法將商放在 %rax
,將餘數放在 %rdx
。
cqto
指令將 %rax
sign extend 到 %rdx
:%rax
。
conditional code 欄位
此二指令一樣可以有 b, w, l, q 尾綴。
若 S1
, S2
相等則 cmp
後 ZF 會被設為 1 。
可以藉由 testq %rax, %rax
來檢查 %rax
為 (大於)、(小於)或(等於) 0 。
通常不會直接讀 conditional code 的值,而會:
指令 | 同義 | 條件 | |
---|---|---|---|
sete D |
setz |
D <- ZF |
|
setne D |
setnz |
D <- ~ZF |
|
sets D |
D <- SF |
||
setns D |
D <- ~SF |
||
setg D |
setnle |
D <- ~(SF^OF)&~ZF |
大於 |
setge D |
setnl |
D <- ~(SF^OF) |
大於等於 |
setl D |
setnge |
D <- SF^OF |
小於 |
setle D |
setng |
D <- (SF^OF)|ZF |
小於等於 |
seta D |
setnbe |
D <- ~CF&~ZF |
大於 |
setae D |
setnb |
D <- ~CF |
大於等於 |
setb D |
setnae |
D <- CF |
小於 |
setbe D |
setna |
D <- CF|ZF |
小於等於 |
D
的 1 個 byte 會被設為 1 或 0 。
中間那組為有號數使用,最後那組為無號數使用, a, b 代表 above 和 below 。
sete
是在查看 zero flag 的,被用來當作 set when equal
跳到某個 label
指令 | 同義 | 條件 | |
---|---|---|---|
jmp Label |
|||
jmp *Operand |
with addressing mode | ||
je Label |
jz |
ZF |
|
jne Label |
jnz |
~ZF |
|
js Label |
SF |
||
jns Label |
~SF |
||
jg Label |
jnle |
||
jge Label |
jnl |
||
jl Label |
jnge |
||
jle Label |
jng |
||
ja Label |
jnbe |
||
jae Label |
jnb |
||
jb Label |
jnae |
||
jbe Label |
jna |
後面幾個 postfix 的語意跟 conditional 的表相同。
assembler 和 linker 較常用的跳轉地址編碼方式是 PC-relative ,少數時候才使用 absolute address 。
類似 MUX 的概念,直接計算出每個 branch 的結果,再依 condition 選擇其中一個結果。
邏輯與前述相似。
在 conditional move 可以取代 conditional jump 的情況下,可以使用 cmov 來避免 instruction miss 的 penalty ,但這麼做也不一定會提高效能,甚至 cmov 會導致非法取值。
例如:
此情境無法使用 conditional move,因為在 p
為 NULL 的情況下,*p
會 dereference a NULL pointer。
此外,假設有 C 程式碼:
那麼在 then
或 else
很複雜的情況下,conditional jump 可能會更有效率,因為只需要做一邊的運算。
GCC 大多時候還是會選擇 conditional jump,只有在兩個分支都非常簡單時才會選用 conditional move。
assembly 基本上都是用 jump 和 conditional jump 來實現各種 loop。
do…while loop 會被等價翻譯成:
gcc 翻譯 while loop 有兩種可能的做法:
空間效率較好:
時間效率較好:
如果 compiler 認為初始的 test expression 一定成立,它可以砍掉前面的 if statement
在 body 內沒有 continue
的情況下,其等價於
當 case 很多時 (say >= 4),switch…case 會用 jump table 來實現以提高效率,使得跳轉的速度與 case 的數量無關,即使有上百個 case 也只要訪問一次 jump table。
gcc 引入了一些 C extension 來實現 jump table,使用 &&
prefix 來表示 instruction 的地址,並支持 computed goto
。
抽象化的 function 稱為 procedure
%rbp
指向 stack 底部 (高位址)%rsp
指向 stack 的頂端 (低位址)底部是 stack 開始的地方,頂端是最外面,stack 的量為 %rbp - %rsp
。
x86-64 通常是用 pushq
和 popq
p.166 圖 3-27
在 x86 使用 pass by registers 最多可以傳 6 個參數,registers 的順序有規定
傳遞時需要使用 stack 的情況:
&x
),需要把 local variable 放到 memory 才有地址。原先 local variable 可能完全使用 register 而根本沒放在 memory使用 pass by stack 時每個參數都要 align 到 8 (if in x86-64);注意到在第二種情況只是要把變數存到 stack,並非 pass by stack,不需要每個都 align 到 8,詳細可參考 p.172 圖 3-33。
procedure 的轉換之間 register contents 不會被覆蓋,也就是說 registers 在 procedure 之間是共享的。x86-64 採用了一組統一的 conventions:
%rbx
, %rbp
, $r12
~%r15
列為 callee-saved,callee 必須在 return 時將它們回復原狀%rsp
和 callee-saved registers 以外的所有 registers 都為 caller-saved,也就是 caller 要自己負責保管好 (負責 save) 它們,途中任何 procedure 都能隨意修改這些數值而不恢復使用 gcc -O1 編譯:
注意到第 4, 9 行即在實現 callee-saved convention for %rbx
。
若使用到 -O2 則會基於 tail recursion 直接砍掉 function call:
x86 的 addressing mode 設計是對 pointer arithmetic 很友善的
p.194
page pointer : 201
在一般的走訪字串的操作中,常見如下迴圈:
strlen(s)
是個 運算,編譯器會嘗試將其改成:
以避免每次 loop 都呼叫一次 ,最終變成了 複雜度。
然而,若是迴圈裡有一些對於 s
的修改,編譯器可能就不知道 strlen(s)
的值是否改變,使得它放棄進行上述的優化,最終產生效能很差的 object code。
這段程式碼反覆去讀寫 dest
所指到的位址,大量 memory access 導致了很差的效能。一個常見的優化方式為:
引入一個暫時的變數來讓 access dest
位址的次數降低到讀、寫各一次。
然而,因為編譯器不知道 dest
和 data
是否有 memory aliasing 的情況,所以也可能會放棄這樣的優化。
CPU 的性能參數:
在一般的 CPU 上,例如普通的家用 Intel CPU,加法、整數乘法、浮點乘法通常被認為是重要功能,所以 CPU 設計者會為這些運算在 low latency 和 high throughtput 方面做較多的優化。
throught put 大致上是 , ,簡單運算例如加法會藉由增加 capacity,而複雜運算例如乘法會藉由降低 issue time 方式。
i++
使用 CPU 中的加法器,而 acc
運算使用乘法器,即使在同一個 CPU 中也是可以平行執行的。
loop unrolling 可以減少運算 i
的次數,但是因為這裡的效能瓶頸是在乘法上,因此做 unrolling 優化雖有改進但效果有限。注意到因為每個 acc
都在等上一個 acc
算完,所以這個乘法運算無法 pipeline。
編譯器會嘗試對 operands 做一些交換和 divide-and-conquer 來運用 CPU 性能,但是因為浮點數沒有交換律,所以大多數 compiler 不會亂換浮點數運算的順序。
例如將 改成
下例說明改變順序的強效,將 unrolling 提到的例子改變順序:
在這個情況下,因為 data
相乘不 depend on acc
所以在 update acc
時可以讓下一輪的 data
進乘法 pipeline。這顯然相較原先加速許多,overall 的 critical path 可以變成一半。
p.388
首先編譯程式
-pg
命令防止函式被 inline
page pointer :
DRAM 以電容儲存,因此容易被干擾,通常幾個 ms 就需要更新一次。
DRAM 有 個 DRAM 單元,每個單元有 個 supercell ,每個 supercell 存 1 bit 。
DRAM 單元是二維的, (row, column)
加強版:
Nonvolatile Memory:
跳過 disk 的部份 p.407
I/O bus 連接各種 I/O ,雖然比 system bus 和 memory bus 慢,但是相容性較佳。
使用 PCI bus 可以達成 CPU architecture independence ,目前被 PCI express 取代
Universal Serial Bus (USB)
p.413 skip
SSD 中有 translation layer 和 flash memory,通常 SSD 的讀比寫更快。
假定 flash memory 有 個區塊,每個區塊有 個 page。
廠商通常會在 translation layer 的邏輯中,盡量讓每個區塊的使用次數一樣多,以平均磨損情況,延長 SSD 壽命。
對於 memory space 範圍到 的記憶體
每個 block 包含:
page pointer : 425
Linking 的時機可以在:
本章討論 Linux on x86-64,並以 ELF-64 目標格式為例。
pre processor compiler assembler linker loader
主要做兩件事:
基本上 linker 不太知道 computer architecture,相關的事務已由 compiler 和 assembler 完成。
可歸類為三種:
系統選擇:
在 object file 中不為 .bss section 分配位置以節省空間,執行時才在 memory 分配
Symbol 的結構:
section
紀錄該 symbol 在 ELF 中的哪個 section,另外有三個特殊的 pseudosection for relocatable object file
可以藉由 readelf
工具去看 ELF,例如:
Linux linker 按照下列規則來處理 duplicate symbol:
int x = 3
、progb.c 裡面有 int x
,那麼在 progb.c 裡面指派 x = 2
會導致 proga.c 裡的 x
也被改成 2。可以在 gcc 編譯時使用 -fno-common
來回報此錯誤,或是藉由初始化所有 global variable 來避免此問題
standard functions 會被分成一些集合,例如 ISO C99 有 libc 和 libm 等等
交給 compiler 去辨識程式中的 standard functions
pros
cons
把 standard functions 的集合做成一個 relocatable .o 檔
pros
cons
引入 static library,把每個集合編譯成獨立的 module,並且當程式引用 standard function 時只會複製該 function,不會複製整個 module
pros
cons
在 Linux 中,static library 以 archive 格式儲存,副檔名為 .a。可以藉由 ar
command 來建立 static library。
如果 libraries 的相依關係為:
使用 gcc 編譯時必須將被依賴的 library 放在右邊
如果在上述的例子中,libz.a 的某些 function 也依賴於 libx.a,那便要如此編譯:
assembler 會生成 relocation entry,讓之後的 linker 知道該如何對 symbol 做 relocate
ELF 定義了 32 種 relocation type
page pointer : 485
檢測到事件發生時,會經由 exception table 跳到特定的 handler 。 handler 處理完之後有三種可能:
每個 exception 都有 unique and nonegative 的 exception number ,此 number 可能由 CPU architechture 提供或 OS 提供。
通常由 CPU 分配的:
通常由 OS 分配的:
類別 | 原因 | ||
---|---|---|---|
interrupt | from I/O signal | asynchronous | 返回下一條指令 |
trap | 故意生成的 | synchronous | 返回下一條指令 |
fault | 可能可以恢復 | synchronous | 可能會返回當前指令 |
abort | 不可恢復的錯誤 | synchronous | 不返回 |
ctrl + c
產生的 interrupt 和 timer interrupt 是一樣的嗎?
使用 system call 之前透過 trap 來產生 user process 和 kernel 之間的接口
例如 page fault ,將 page 從 disk 讀到 memory 之後就解決問題、可返回繼續執行了。
通常是硬體損壞、故障造成的
C 語言可以使用 syscall
(trap instruction) 進行系統呼叫,但通常可以用 C standard library 包裝後的函數,例如 write()
之於 printf()
。
Arguments of system call is passed by registers rather than stack.
抽象化
user mode 不可執行 privileged instruction
Linux 藉由 /proc
提供 user mode 取得 kernel 資訊、資料結構
context
Unix system function 遇到錯誤時通常會返回 -1 並設置 global variable errno
。做成例外處理包裝函式可以簡化程式碼。
除了 PID 以及 physical address 之外幾乎完全相同,包括打開的 file descriptor 。
return 兩次:
fork()
之後父子的執行順序不被保證
man waitpid
waitpid()
來回收 (reap) 已終止的 child processesinit 為 PID = 1 的 process ,它不會被終止。
waitpid()
:等待自己的 child 終止,用法複雜
The value of pid can be:
< -1 meaning wait for any child process whose process group ID is equal to the absolute value of pid.-1 meaning wait for any child process.
0 meaning wait for any child process whose process group ID is equal to that of the calling process at the time of the call to waitpid().
> 0 meaning wait for the child whose process ID is equal to the value of pid.
wait()
:簡化版
children 回收的順序不固定,除非指定 pid 依序回收。
執行 pathname
對應的 executable file
stack 圖見 p 522.
man 7 signal
A signal is a small message that notifies a process that an event of some type has occurred in the system. [source]
Defined in <signal.h> in C standard [C99 7.14]:
SIGABRT
SIGFPE
SIGILL
SIGINT
SIGSEGV
SIGTERM
其它定義在 POSIX 標準並由 Linux 所支援的:
SIGTSTP
SIGALRM
SIGKILL
SIGCHLD
待處理信號 pending signal
同類型的重複 pending signal 會被丟棄不會排隊
process group 可以用 getpgrp()
取得 PGID
pgid 預設為 parent 的 pid
發 signal :
在 shell 使用 ctrl + c
會送 SIGINT 給前景 process group 中的所有 processes
接收到 signal 後的行為:
當 process 從 kernel mode 回到 user mode 時 (例如 system call 結束或 context switch) ,kernel 會檢查是否有 nonblocked 的 pending signal ,若有則選一個 signal 接收,若無則繼續下一道指令。
每種 signal 會有對應的 default action 。
If the signal signum is delivered to the process, then one of the following happens:
- If the disposition is set to SIG_IGN, then the signal is ignored.
- If the disposition is set to SIG_DFL, then the default action associated with the signal (see signal(7)) occurs.
- If the disposition is set to a function, then first either the disposition is reset to SIG_DFL, or the signal is blocked (see Portability below), and then handler is called with argument signum. If invocation of the handler caused the signal to be blocked, then the signal is unblocked upon return from the handler.
The signals SIGKILL and SIGSTOP cannot be caught or ignored.
applications 可以用 sigprocmask()
來 block 或 unblock 特定信號,等 unblock 之後再處理的意思
how
值:
signal handler 應
write()
errno
,因此需要先存下原本的 errno
並在 handler 要返回時恢復原值volatile
,因其可能被外部更改sig_atomic_t
關於「安全的函式」可見: signal-safety(7) — Linux manual page
因為重複的 pending signal 會被丟棄不會排隊,所以不應該使用 signal 來 count 。
為了 portability ,建議使用 sigaction()
來取代 signal()
,其有包裝版本 Signal()
,用法與 signal()
相同。
在 fork()
之前先 block SIGCHLD
信號可以避免一些 race condition (可能在 parent 要對 child 操作之前, child 就已經結束了) 。
p.542
使用
會造成 busy waiting 的浪費,取而代之或許可使用
然而若在這兩行之間收到 interrupt ,那麼就再也無法從 pause()
醒來。一個好的解法是使用 sigsuspend()
setjmp() and sigsetjmp() return 0 when called directly; on the "fake" return that occurs after longjmp() or siglongjmp(), the nonzero value specified in val is returned.
使用 jmp_buf
型態的 env
保存環境狀態,包括:
setjmp()
的回傳值不能做 assignment
longjmp()
跳到最近初始化 env
的那個 setjmp()
setjmp()
return 很多次, longjmp()
不 return
sig__ 版本是用來做 signal handling 的
strace
:印出使用的 system call ,若程式碼經過 static 編譯則可以看到更純淨的結果ps
: report a snapshot of the current processestop
: (我覺得 htop
更好用在有 virtal addressing 的系統上,CPU 存取 virtual address,但此地址在真正到達 main memory 之前會被 MMU 翻譯成 physical address。
address space 是地址的集合,如果 address space 中的整數是連續的,則稱為 linear address space。
為方便稱呼,將 L1, L2, L3 cache 稱為 SRAM cache,將 disk 在 main memory 的 page cache 稱為 DRAM cache。
由於 DRAM 慢 SRAM 十倍而 disk 慢 DRAM 數萬倍,因此 DRAM cache miss 的懲罰是非常昂貴的。因為此懲罰非常昂貴,因此作業系統在此通常用上非常精密複雜的演算法來做 page 替換。
main memory 中會 maintain 一份 page table 來紀錄 page 映射,page table entry 包括 valid bit 及 physical address,而 table entry 的 index 就是 page 在 disk 上的位置。
p.564 圖
發生 page fault 會發 signal,如第 8 章所述。
上面舉的都是使用 main memory cache disk 的例子,在這種情況下 virtual memory space 會比 physical memory space 大,但 virtual memory space 在其它情形也可能大於 physical memory space。
virtual memory 可以用於記憶體管理,通常帶來幾個好處:
.text
區段總是從 0x400000
開始。mmap()
system call 允許 application 自己做 memory map。提供 virtual address,使得存取位址是否合法變得非常容易辨認。page table entry 上可以增加一些額外的 bit 來紀錄該位址的存取/寫入是否允許。
在第 2 步驟之後若 page table 中該 entry 的 valid bit 是 0,那 MMU 就觸發了 exception,交由 kernel 的 handler 處理。
通常 SRAM cache 當中是紀錄 physical address,因為地址翻譯發生在它之前。
translation lookaside buffer
由於 MMU 每次都要去查 page table entry,因此在 MMU 端加入一個比 L1 cache 更近的 TLB 以快取 page table entry。
TLB 紀錄了 TLB index 及 TLB tag,兩者合起來的長度為 page number 的 bit 數。其邏輯大致與 cache 相同,都是使用 modular 的方式 hash。
context switch 時 TLB 會置換嗎?我推測是需要
在 virtual space 很大的情況下,page table 也會佔據非常大的空間。可能的解決方法是保存更小的 level 1 page table 於 main memory,並將完整的 level 2 page table 放在 disk 中。
p.572 圖
藉由 TLB 的幫助,多級 page table 並不會比單級 page table 慢很多。
CR3
register 紀錄 level 1 page table 的起始位置
實際上的 page table entry 還會紀錄一些包括 access permission 的其它資訊:
…等等。
通常會精巧設計 bit 數,使得可以同時發 virtual page number 給 TLB 和發 virtual page offset 給 L1 cache,那麼在 TLB hit 之後就可以馬上回傳地址了。
Linux 為每個 process 維護一個 virtual address space
p.580 圖 9-26
kernel 為每個 process 維護一個 task_struct,每個 task_struct 中有一個 entry 指向 mm_struct,這個 struct 描述了 virtual memory 目前的狀態。而 mm_struct 又指向一條 lists of vm_area_structs。
p.580 圖 9-27
mm_struct contains:
pgd
指向 level 1 page tablemmap
指向 vm_area_structvm_area_struct contains:
vm_start
area 在 virtual memory 的起始地址vm_end
The first byte after our end address within vm_mmvm_prot
權限紀錄vm_flags
vm_next
linked-list 用task_struct 在
<linux/sched.h>
中
mm_struct 及 vm_area_struct 都在
<linux/mm_types.h>
中
觸發 page fault 之後會依序進行:
一個 object 可以被 map 到 virtual memory 的一個 area,作為 shared object 或 private object。
private object 使用 copy-on-write 技術
copy-on-write 技術避免了不必要時的副本,節省了 memory 的用量。
當某 process 呼叫 fork()
時,kernel 會複製原有的 mm_struct、vm_area_struct、page table 給新 process。並把兩個 process 中的每個 page 都標記為 read-only,當之後有 process 想寫時用 copy-on-write。
execve()
要做的事:
p.586 圖 9-32
man mmap
嘗試撰寫練習題 9.5:
略過很多 return value 的檢查
http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/lectures/19-malloc-basic.pdf
藉由 brk()
和 sbrk()
來控制 heap 大小。
allocator 的設計圍繞在幾個問題上:
需要配置的空間稱為 payload
分為:
malloc()
and free()
in CPlacement policy:
幾種常見的方式:
一個 block 前面通常會有一段 header 來描述 block size
兩個被釋放的連續 blocks 可以合併 (coalescing),假設 A, B 兩塊相連
通常釋放之後不會馬上合併,以避免合併之後又馬上分割的情況
使用 doubly-linked list 串起每個空的 block,走訪的成本從走過所有 blocks 降到走過所有空的 blocks。此外 list 的順序也更加自由,假設數字代表位址,在 list 上可以是: 這樣的順序,而不一定要像 這樣遵從 address order。
一開始就將空間切成一些 size class 等價類,可能是每個 class 各一條 list,例如 , , , 。那麼當需要配置 size 6 時就去 class 找,找不到再往上找。
假設 size 6, 7 的 blocks 已經用完了,那麼在被請求 size 6 時有兩種 policy:
關於 glibc 可以參考:glibc 的 malloc/free 實作
buddy system 是一種特例,每種 size 都固定為 power of 2,其缺點為若需求都是 5, 9, 17 這種 的數,buddy system 就會面臨嚴重的 internal fragmentation 問題。
Garbage collector 將 memory 視為一張 directed graph,節點被分為 root node 和 heap node。如果有 heap node 無法從某個 root node 到達,那麼它就是 unreachable,即為 garbage。
C 語言很難做 garbage collection,因為有時候 pointer 會用 integer 的形式存放,這會使 collector 認為該空間已經 unreachable。
搭配 http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/lectures/16-io.pdf
一個 Linux file 就是 個 byte 的 sequence
所有 I/O 包括網路和 disk 都被抽象成 file ,可以進行:
每個 process 都 maintain 一份 descriptor table ,建立時都已預設打開 0, 1, 2
file type :
.
和 ..
linkman open
其功能為將 file 轉換為對應的 file descriptor 數字。
flags
:
mode
為文件的權限設定
使用 close()
關閉。
read()
和 write()
有 short count 的問題
lseek()
用於調整 current file position
CSAPP 提供
rio_readinitb()
用於初始化,將 fd
和 buffer rp
連接起來。
rio_readnb()
讀取最多 n
個 byte 。
rio_readlinb()
從一行讀取最多 maxlen
- 1 個 byte ,最後一個位置放 NULL。 (?
rio_read()
函式
對於 application 來說 rio_read()
和 Linux read()
有相同語意。
stat()
和 fstat()
分別用 path 和 fd 來查詢一個 file 的資訊。 lstat()
則主要用於 symbolic link 。
struct stat 定義如下:
https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/bits/stat.h
試用
返回值為指向 directory stream 的指標, stream 是對 ordered list 的抽象。
kernel 使用三個 data structure 來表示被打開的 file :
當 refcnt
為 0 時該 file table 會被刪除
file pos 是指 offset 還是 path ?如果是 offset ,那麼所謂 processes 共享是何意?
可能有不同 fd 對應到同一個 file 的情形,例如使用 open()
打開兩次
I/O redirection 可以透過 dup2()
實現
standard I/O streams :
型別為 FILE
的 stream 是一種對 fd 和 stream buffer 抽象
fflush()
用於清除緩衝區
scanf()
) 來讀 binary file ,比如說裡面可能有很多 0xa
跟 network 有關的輸入輸出會在第 11 章探討
client-server transaction
TCP/IP 訂定的 network byte order 是 big endian ,轉換 byte order 的函式:
n 代表 net , h 代表 host
IP address 的整數表示和 dotted-decimal format 轉換:
af
用於選擇 network address structure,必須是 AF_INET
或 AF_INET6
;這些函式的 IP address 整數表示總是以 network byte order
n 同樣代表 net;p 代表 representation,也就是指 ddd.ddd.ddd.ddd 字串
inet_pton
with AF_INET
的情境下, dst
為 pointer to struct in_addr
<netinet/in.h>
inet_pton
with AF_INET6
的情境較為複雜,dst
為 pointer to struct in6_addr
,詳細可參考 man inet_pton
nslookup 工具程式可由網址查詢 IP address。
port 有 16 bit,可表示範圍為 0 ~ 65535
每一個 connection 都唯一映射到一組 socket pair (cliaddr:cliport, servaddr:servport)
cat /etc/services 可以看所有 services
若命名前後綴有 in
都是表示 internet
socket 對 Linux 來說就是有對應 descriptor 的 opened file。
於 <sys/socket.h> 中之結構體如下:
展開 macro __SOCKADDR_COMMON
後為:
client 和 server 透過 socket()
來創造 socket descriptor
man socket
domain
選擇一種 protocol family,最常見的就是填入 AF_INET
和 AF_INET6
type
指定 communication semanticsprotocal
在選定的 domain
下選一種特定的 protocol,一般來說每個 protocol family 中都只有一個 protocal,此時可以填入 0,不過也有一些例外,詳見 man 5 protocols
最終 socket()
(如果沒有錯誤) 會返回一個打開的 socket fd,不過 read/write 權限還不會開啟。
client 使用 connect()
來連 server
connect()
嘗試與 socket address 為 addr
的 server 建立 Internet connectionconnect()
會 blocking 直到連接成功或發生錯誤sockfd
就可以讀寫了bind()
, listen()
, accept()
都是由 server 所使用。
bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd.
listen()
將 sockfd
轉化成 listening socket
The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow.
server 透過 accept()
來等待來自 client 的 connect()
,並且會 blocking 在 accept()
,直到成功時返回一個已連線的 descriptor。client 的 socket address 會被填在 addr
。
listenfd 通常只會被建一次,例如 3 是 listenfd,如果有人來 connect,那 accept()
可能會回傳 connfd 4 專門服務該 clientfd,而 3 還是那個 listenfd。
listen()
有點像是建立一個 listenfd 但不會開始聽,要用 accept()
才會開始聽
man getaddrinfo
node
可以是 domain name 或 IP address with dotted-decimal format;service
可以是 service name 或 port number,如果填入 service name,例如 http,它會依此產生對應的 port number。
Either node or service, but not both, may be NULL.
回傳一個或多個 struct addrinfo
在 res
,*res
指向一個 linked list 資料結構,其中每個 addrinfo
都可以用於 call bind()
或 call connect()
。
There are several reasons why the linked list may have more than one addrinfo structure, including: the network host is multihomed, accessible over multiple protocols (e.g., both AF_INET and AF_INET6); or the same service is available from multiple socket types (one SOCK_STREAM address and another SOCK_DGRAM address, for example). Normally, the application should try using the addresses in the order in which they are returned. The sorting function used within getaddrinfo() is defined in RFC 3484; the order can be tweaked for a particular system by editing /etc/gai.conf (available since glibc 2.5).
p.657 圖 11-15
getaddrinfo()
中的 hints
只有 flags
, family
, socktype
和 protocol
能設置,其餘都要設為 0 或 NULL,它會自動完成。
使用這個函式的優點是讓我們在寫 client 和 server 程式碼的時候可以不用去管 protocol 是什麼,直接把 ai_family
, ai_addrlen
傳給 socket()
、把 ai_addr
和 ai_protocol
傳給 connect()
和 bind()
就好。
與前一個函式相反,將 struct sockaddr 轉成 host 和 service 字串
page pointer : 658
搭配 http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/lectures/23-concprog.pdf
並行的實現方式:
獨立定址,缺點是 interprocess communication 的高成本
select()
函式處理 fd_set
的集合
FD_ZERO
, FD_SET
, FD_CLR
, FD_ISSET
來設定和檢查
FD_CLR
設定指定的 bit fd 為 0FD_ISSET
check if setFD_SET
設定指定的 bit fd 為 1FD_ZERO
全設為 0select()
會一直 block 直到有 listenfd 或 stdin 準備好可讀
I/O multiplexing 可以做成 event-driven , event 會使 flow 往前走
I/O multiplexing 版本使用單一 process ,易於使用 gdb 等工具,另外不需要 IPC 所以效率更佳;缺點是程式碼較長且無法利用多核處理器。
thread context
有獨立的 stack 但不會阻止其它 thread 存取 (透過指標等方式)
threads 共享的部份:
opened file or opened file descriptor?
在本機執行
每次結果可能有所不同
注意 cnt
受到 static
的影響!
threads 不像 processes 那樣有父子關係,大家都是對等的,因此每個人都可以殺死每個人
pthread_create()
pthread_self()
取得 TIDpthread_join()
main thread 等待 peer thread 終止,但它的邏輯不像是 wait()
,它只能等待一個 threadpthread_detach()
分離的 thread 不會被別的 thread 殺死A thread is either joinable or detached.
終止方法:
pthread_exit()
terminate calling thread ,不會 return
exit()
時終止該 processpthread_cancel()
終止其它 threadsem_wait() decrements (locks) the semaphore pointed to by sem. If the semaphore's value is greater than zero, then the decrement proceeds, and the function returns, immediately. If the semaphore currently has the value zero, then the call blocks until either it becomes possible to perform the decrement (i.e., the semaphore value rises above zero), or a signal handler interrupts the call.
sem_post() increments (unlocks) the semaphore pointed to by sem. If the semaphore's value consequently becomes greater than zero, then another process or thread blocked in a sem_wait(3) call will be woken up and proceed to lock the semaphore.
一個函式是 thread-safe iff 在 multi-thread 情境總是能產生正確結果。
thread-unsafe 的例子:
rand()
解決方法:
書中表示 rand()
是 thread-unsafe,但是 man 寫它是 thread-safe 和 MT-safe
如果函式執行到一半被 context switch,此時重新呼叫一次仍然安全,該函式即為 reentrance。
reentrant 和 thread-safe 的關聯?
不可使用 shared variable,如果函式的 parameters 沒有指標,並且沒有使用靜態變數和全域變數,那該函式就是 reentrant,但若包含指標就不容易判定有沒有觸及 shared variable。
standard library 中 _r
postfix 的函式就是 reentrant 版本。
lock 順序需要跟 unlock 順序相反以避免 deadlock