2024 Fall 台大資工系必修 老師:鄭卜壬
Least-privilege model: 一個 process 只能用最少的 privilege,不要多給。
例子: 期中考 Alice 有個 set-uid 的程式,讓 Bob 執行後,open 一個 Alice 的檔案,再 exec Bob 的程式去做後續操作。因為 Bob 的程式可能沒有 set-uid,那此時就會有問題,Bob 取得多餘的權限。
執行 exec 時,會把 euid 複製到 saved set-uid(saved-suid),這樣在改 euid 後還可以 setuid(...)
把 euid 改回 exec 時借到的 euid。
同時也確保 set-uid program 用 exec 執行 non-set uid program 後,不能夠把 euid 換回 saved set-uid。
因為不能 get saved-suid,所以要在 exec 後趕快把 saved-suid 存起來。(其實 GNU 有 getresuid
)
setuid(uid)
:
seteuid(uid)
,但不常用。setuid
和 seteuid
是一樣的。open source 常用 file lock 來限制一個檔案只能有一個 process 執行,可以在檔案寫 pid。
用同一個數據機 (modem),因此需要限制一次只能有一個 process 用,所以就用一個 file lock,而只有 uucp
可以改 lock,所以會有一個 uucp 的 set-uid 的 program tip
。
而在用 uucp 的 euid 取得 lock 後,還是要 setuid(ruid)
後來存取其他資料 ,因此要先在 exec 後馬上把 euid(=saved-suid) 存起來,在做完事之後設 euid 為 saved-suid,然後 release lock。
討論:
tip
的 owner 不能是 root,否則 setuid(ruid)
就會把所有值都清掉改不回來 root。在有 saved set-uid 之前,BSD 有 setreuid(ruid, euid)
,可以改 ruid 和 euid。
unprivileged user 只能把
基本上是希望透過 swap ruid 和 euid 來達到類似 saved set-uid 的目的。
Linux 其實也有實作。
這是學界跑在業界前面的例子,總是會有許多很創新的想法。
seteuid(euid)
: 一般 user 一樣只能改成 ruid, saved set-uid,跟 setuid
一模一樣。但 root 可以只改 euid,改成任何值。
以上都只會牽扯到 main group,supplementary group 不會被改變。
要記得 set-uid 程式別亂 exec 把權限亂借出去,建議是把 euid 改掉後自己做事。
Interpreter: 直譯器,像是 shell、perl、python 等。
Interpreter files: 開頭 #!
,後面接直譯器的絕對路徑,這樣文字檔案就相當於一個執行檔,exec 會看得懂。
若 interpreter file file
裡面寫 #!/bin/some arg1
,然後執行 ./file arg2
則相當於執行 /bin/some arg1 <pwd>/file arg2
如果有個 interpreter file file.awk
用 awk
寫(#!/bin/awk -f
),把 argv 全部印出來,則執行 ./file.awk xxx
相當於 /bin/awk -f ./file.awk xxx
,在執行時就會只印出 awk xxx
。
如果是寫 awk 'xxx' $*
在檔案裡,並用 execlp
執行,review: p 會去 PATH 找執行檔的位置,允許 filename 是 shell script,所以會把檔案當成 shell script,/bin/sh
再 fork, exec awk
, wait。
用 interpreter file,執行時就會直接執行 awk
,不會多一個 shell。
shm_xxx
buffered I/O。如果兩個人寫,寫超過buffer size可能會interleave: "abcd", "1234", result "a1b2c3d4"
像是開個檔案
回傳是成功/失敗,
不能lseek、讀完不能再讀,跟pipe差不多
Ex
File 在不同系統差異不大, Process 就開始有些差別(ex: wait),而 Signal 差別更大(有些 system call 不reliable),有些實際運行會有 race condition 等。
Ctrl-C 就是一個 signal,系統送給process。
比較:與 Interrupt 的差異
Interrupt 的對象是 CPU,而 signal 是 process
Interrupt 可能是 CPU 的一個接腳或內部產生,讓 CPU 暫停現在的 process 去某個 subroutine(ISR,interrupt subroutine,通常非常短)。
Interrupt先記下發生什麼事,而signal是系統用來通知process。
IRQ:Interrupt quest
Signal 是 kernel 用來通知 process 重要事件的,由 kernel 定義。AKA software interrupt。編號通常是正整數,0 是特別意思(認ID)。
分成同步、非同步,像是 SIGCHILD 你也不知道小孩什麼時候會死,會是非同步。
SIGINT
SIGFPE
, illegal memory access -> SIGSEGV
kill
SIGKILL
SIGPIPE
, alarm clock expires -> SIGALRM
三擇一,沒寫就是 default
SIGKILL
, SIGSTOP
cannot be ignored)SIGFPE
)SIGKILL
, SIGSTOP
cannot be caught)SIGCHILD
再 waitpid
Default 通常是 Terminate、Ignore、Stop
signal: default action
w/core = with core dump
Signal | Default Action | Description |
---|---|---|
SIGABRT |
Terminate w/core | By calling abort() |
SIGALRM |
Terminate | By calling setitimer() , alarm() |
SIGCHLD |
Ignore | Child process state change (e.g., execution, stopping) |
SIGFPE |
Terminate w/core | Division by zero, floating point overflow, etc. |
SIGHUP |
Terminate | Originally used for terminal disconnection; now used to signal background processes (e.g., servers) for rereading config files(avoid stopping) |
SIGINT |
Terminate | DELETE/Ctrl-C |
SIGIO |
Terminate/Ignore | 最厲害非同步,叫系統讀到某個buffer,搞定後傳這個signal。=SIGPOLL |
SIGKILL |
Terminate | Cannot be ignored or caught |
SIGPIPE |
Terminate | reader of pipe/socket terminated |
SIGQUIT |
Terminate w/core | Ctrl-\,terminate foreground process |
SIGSEGV |
Terminate w/core | Segmentation fault |
SIGSTOP |
Stop process | Cannot be ignored or caught |
SIGTSTP |
Stop process | Ctrl-Z,可以catch |
SIGTERM |
Terminate | kill command |
SIGURG |
ignore | 網路課,out-of-band data: 傳資料網路塞車,要維持品質 |
SIGUSR1 |
Terminate | user defined |
SIGUSR2 |
Terminate | user defined |
Note: 鬧鐘、handler 都只有一次有效
Signal | Default Action | Description |
---|---|---|
SIGBUS |
Terminate w/core | Implementation-defined HW fault (e.g., memory alignment error, undefined behavior) |
SIGCONT |
Continue/Ignore | Continue a stopped process |
SIGILL |
Terminate w/core | illegal hardware instruction, rarely seen |
SIGPROF |
terminate | 鬧鐘響 |
SIGPWR |
ignore | |
SIGSYS |
Terminate w/core | Invalid sys call |
SIGVTALRM |
Terminate | 只看user CPU time的alarm |
SIGWINCH |
ignore | terminal size changed |
SIGXCPU |
Terminate w/core | 超過soft CPU time limit |
SIGXFSZ |
Terminate w/core | 超過soft soft file limit |
RLIMIT_CORE
)設sighandler_t
為指向(輸入 int,無輸出的函數)的指標。
signal
會回傳之前的 handler。
都是無輸入、輸出 void*
的函數指標,但數值亂設,讓 signal
去判斷,可當作 handler 使用。出問題時則回傳 SIG_ERR。
想達成:若現在是 Ignore,則設為我的 handler。
由於沒有設計這樣的功能,可以這樣做:
shell 會把 background process 的 interrupt、quit 設為 ignore,反正不會發生(?)
Interrupt 是很重要的機制,因為可以避免 busy waiting(?)
可以這樣寫:
為了簡化,4.2BSD 的 read 如果被中斷會自動 restart,方便但也有很多問題。
因為在 handler 裡可能會再收到一次 signal,動作被中斷,所以 handler 裡要是能確保不會有問題的功能:
Reentrant Functions:可以被 safely recursively called
與 functional programming 的原則很像,但沒那麼極端。原則是不能用:
比如
Reentrant 少到可以正面表列。
Ex: 如果需要回傳一個 array,local variable會消失、static 和 malloc 也不行,解決辦法是一律叫參數直接傳 alloc 好的指標。(有點像 funcional programming 的精神)
printf 雖然不是 reentrant,但為了 debug 還是會用。
Note: 因為原本的程式不知道自己被 signal 中斷,而 errno 在 handler 可能被改,所以 handler 通常在一開始存起來、最後設回去。(但還是有可能其他 signal 在存好前來,導致無法還原,這在 unreliable signal 無解,得要把 signal block 掉)
Signals could get lost!
Ex 1: 註冊完只會有效一次,所以通常會在 handler 再 signal
註冊一次,但如果要處理的 signal 剛好在這之前來就會照 default。
Ex 2: 不想馬上處理 Ctrl-C,且 Ctrl-C 一定會來,來了之後才要繼續做事。
具體故事:
教授上課時知道當天有重要電話,但不能馬上接,可以
pause
直到 flag 被設(待在教室等電話來)。sigsuspend
(上完課再解除飛航模式)。下面是第一種作法:
看起來沒問題,但如果 signal 在5、6行間來就會等到死,所以不能這樣寫。
中間時間為 pending
只能阻止 deliver,不能阻止 generate
A signal is blocked until: 1.) Being unblocked. 2.) Action becomes ignore.
sigpending(*set)
: 檢查 pending 的 signal,blocked signal 也算 pending!sigprocmask(how, *set, *old_set)
: 設定 signal mask (要擋哪些 signal),與 cwd、umask 都是 independently inherited。都是 depend on implementation
int kill(pid, signum)
: send signum
to pid
pid>0
: to processpid==0
: all processes with the same gidpid<0
: to all processes with gid==-pidint raise(signum)
: send signum
to myself.(=getpid
+kill
)Real or effective UID of the sender == that of the receiver (both are processes)
Don't send signal, but still check if a process is alive and we can send signal to it.
success: 0, fail: -1
如果做的事是:如果某 process 是活的,則XXX,這會有TOC-TOU問題(這種問題都是不常發生,但還是要避免)
At least one (pending, unblocked) signal is delivered before kill
or raise
returns(不只這兩個).
應用: abort
,關掉(block)其他 signal,保證結束前會收到自殺 signal。
unsigned int alarm(unsigned int secs)
: set alarm clock, return remaining time. Since default is termination, you have to catch it.int pause(void)
: suspend until signal comes. Return if a signal handler is executed and returns.Powerful but dangerous user-level mechanism for transferring control to an arbitrary location.
When a function is called, a stack frame is created. It contains:
Put in the stack of the virtual memory.
Problems:
setvbuf
不能用 local variable 作為 buffer。f1()
call vfork and return, child exits after calling f2(that may clean pid
in stack). Parent cannot access pid
in the stack.int setjmp(jmp_buf env)
: save the current context in env
, return 0 if directly called, or val
from longjmp()
.void longjmp(jmp_buf env, int val)
: restore the context in env
, return val
.jmp_buf
可能記了不同東西, machine-dependent. 但通常會記:
理論上要用在很深的 stack: A call B, B call C,… When an error occurs in C, then we can directly longjmp to A.
但其實 jmp 其實更 powerful,實際上可以亂跳。
const
: read-onlyvolatile
: may be changed by other means, must stored in memory instead of registerrestrict
: no other pointer can access the object (so no overlap)void *memcpy(void *restrict dest, const void *restrict src, size_t n)
and void *memmove(void *dest, const void *src, size_t n)
雖然 compile 時不一定能檢查會不會重複,但可能會檢查。
Problem: If another signal comes(say SIG_INT
), the SIG_INT
handler saves and tries to restore the errno
. But before the saving is done, the alarm may come and change the unsaved errno
.
雖然不完美但還是有可能這樣寫。
read
). But need to worry about
read
(Linux does)要養成用預設型別的好習慣,像是
sigset_t
,pid_t
,uid_t
等。
int sigemptyset(sigset_t *set)
int sigfillset(sigset_t *set)
int sigaddset(sigset_t *set, int sig_no)
int sigdelset(sigset_t *set, int sig_no)
int sigismember(const sigset_t *set, int sig_no)
sigprocmask(how, *set, *old_set)
old_set
: return the old signal mask.how
can be SIG_BLOCK
, SIG_UNBLOCK
, SIG_SETMASK
kill
and raise
)int sigpending(sigset_t *set)
return the set of signals that are blocked and pending.
特色
signal
可能是 call sigaction
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
act
: new actionoldact
: set to old actionsa_flags
,只寫重要的,並且有些不重要的細節沒寫:
SA_INTERRUPT
: 會中斷 system call,不會自動 restartSA_NODEFER
: 在 call handler 時不會 block signal,注意這是 unreliable。SA_RESETHAND
: 在 call handler 之前把 disposition 設回 default,跟 unreliable 的行為一樣。SA_RESTART
: 會自動 restart system call,但不是所有 system call 都會被 restart。SA_SIGINFO
: 用 sa_sigaction
而不是 sa_handler
,可以得到 siginfo
和 context。sa_mask
: additional signals to be blocked during the handler.siginfo
大致上就是各種錯誤處理會想用的資訊。
ucontext
其實是 ucontext_t
struct,存了更多系統狀態,register、stack pointer 等等,一般不會用到。
int sigsetjmp(sigjmp_buf env, int savemask)
void siglongjmp(sigjmp_buf env, int val)
Saves and restores the current signal mask in env if savemask!=0
. For setjmp
and longjmp
some OS may not do this.
As usual, you should assume local variables are not restored. Again, variables in the register will be restored, but you cannot control what is in the register.
int sigsuspend(const sigset_t *mask)
= sigprocmask
+ pause
相當於若被 on 起來(block)的那些 signal 發生,不會因此結束,會繼續等。
child 和 parent 一起用 unbuffered I/O 每次寫一個字,導致混在一起。
在 Ch15 用 pipe 解決,child 或 parent 要等對方時,用 read 把自己 suspend,等到對方在 pipe 寫東西時才繼續。這也可以用 signal 來做:
TELL_WAIT
: 註冊 signal handler,並用 mask 避免在 sigsuspend
前就收到。TELL
對方: parent 傳 SIGUSR1
,child 傳 SIGUSR2
WAIT
對方: sigsuspend
,並改回原本的 signal mask(假設等對方只有一次)大重點: TELL_WAIT
要在 fork
前執行,不然 fork 完,parent 可能在 child 還沒 TELL_WAIT
時就傳 signal(發生 race condition),導致 sigsuspend
等不到。
一樣是 critical region 的觀念,若是包含 fork 前一小段才能避免剛 fork 完的問題。
Suspend until time expires or signal caught and the handler returns.
Problems: alarm(10)
, 3 secs passed, sleep(5)
Answer: If user set an alarm before, the alarm will be ignored.(Don't wait the remaining 2 secs, replace the SIGALRM) But the sleep may return early if the alarm rings.
Implementation: We can block alarm and use atomic sigsuspend
to avoid SIGALRM
comes between alarm
and pause
in sleep1
.
如同之前寫的
SIGABRT
的 handler 是 SIG_IGN
,設為 SIG_DFL
(讀取、修改都用 sigaction
達成)SIG_DFL
(不是 catch),就先 fflush
SIGABRT
不 block,然後再 kill
自己(=raise
),利用 side effect(有一個不是 ignore 的 signal 才回傳,而只會是 SIGABRT
)。SIGABRT
被 caught,則 fflush
(剛才 catch 的話不會 fflush)、設為 SIG_DFL
並再 kill
一次。要 fflush 因為 abort
不像 exit
會 flush、atexit、釋放記憶體、關閉文件等,而 fflush 算是比較重要的(不然使用者不知道為什麼印不出來)。
避免在這個區塊,一些共享資源被修改造成 race condition(對資源);或是 multi-thread 時,這個區塊被多個 thread 同時執行(對程式)。
這裡就是用 sigprocmask
來避免某些 signal 的 handler 會把某些資源修改,並且 critical region 在 sigsuspend
結束。
要嘛就避免同時 access 資源,要嘛就對資源上鎖(但同時還要避免 deadlock、race condition)。
如果是 32-bit 的架構,那指標就允許 4GB 的 virtual memory,但一般不會用到這麼多,且也會有一些給 kernel space 用。
system call 也許也會 call 其他 function,這不會放在 stack,而是放在 kernel space 裡面的 kernel stack。
a lightweght process, share some resources with other threads in the same process.
A thread has it's own
Process 就像陌生人,thread 就像兄弟姊妹。
Virtual memory
Thread benefits
Thread drawbacks
shared memory 是 IPC 最快的方法,因此 thread 很好用
Kernel 完全不知道有 threads
process 自己模擬 threads,自己做 thread table(tcb),儲存 thread 狀態、switching、scheduling。
好處: system call 比較慢,自己 switch 比較快。
setjmp, longjmp:
- signal handling
- thread switching
- error handling
ex: Old Linux, pthreads(POSIX standard)
preemptive/non-preemptive OS
- preemptive: OS 可以隨時把 CPU 拿走。現在的 OS 大多是 preemptive
- non-preemptive: 只有 thread 自己可以放棄 CPU
Kernel 知道 thread,所以可以用 kernel 的 thread scheduling。
system call 可以 block,比較簡單。
可以在同個 process 或不同 process 跑。
ex: windows 2000/XP
想辦法結合兩者的好處,ex: Solaris
傳統 UNIX 只支援一個 process,所以會用各種 thread library,ex: POSIX pthreads, Win32 threads, SProc on SGI。
這邊只介紹 pthreads。
跟 fork 的概念類似,沒有必要不能 share 那就 share。
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
thread
: New thread's IDattr
: Assign attributes for the new thread. NULL for default.start_routine
: The function that the new thread will run. void* 代表任何型態的 pointer。arg
: A pointer to the argument that will be passed to the start routine.跟 fork 一樣,不一定自己或新 thread 哪個先跑。沒有 parent-child 關係,return code 也不一定是 create 的 thread 接收。
pthread_t pthread_self()
: 得到自己的 thread IDint pthread_equal(pthread_t t1, pthread_t t2)
: 比較兩個 thread ID 是否相同,非零代表相同。pthread_attr_t
是一個 struct,可以設定 thread 的 attributes。
pthreads 中各種東西通常都是 struct,因此需要用 init、destroy 來初始化、銷毀,包含 thread/mutex attributes、mutex 等。
int pthread_attr_init(pthread_attr_t *attr)
: 得到 attributes 的初始值int pthread_attr_destroy(pthread_attr_t *attr)
: 銷毀 attributes,因為 attr 裡面可能還有其他 allocate 的東西,這個會幫忙 free,並把值都設為 invalid 避免誤用。屬性有哪些?
detachstate
: terminate 後是否要讓其他人看 return state 及是否要回收,類似要不要有 zombie processguardsize
: 每個 thread 的 stack 後多一點緩衝,比如 1M 接著 4Kstackaddr
: stack 的起始位置stacksize
: stack 的大小後面三個與 stack 有關,比較不常用。
類似 double fork,讓 thread 死後馬上把資源清乾淨。
pthread_attr_getdetachstate
可以得到目前的 detach state。pthread_attr_setdetachstate
可以設定 detach state。
PTHREAD_CREATE_JOINABLE
: 預設,可以被其他 thread joinPTHREAD_CREATE_DETACHED
: 不能被 join,結束後會自動回收資源正常是要先做好 attr
再 create。但如果已經 create 完也是可以再設為 detach,就像 file 開完後還能用萬能的 fcntl
設定是否 blocking,用到 pthread_detach
,可以 detach 任何一個 thread,注意只是 set attribute,不會真的 detach。
pthread_create
最後一個參數可以是任意型態的 pointer,而不同 threads 間理論上會 share heap,所以可以直接傳 pointer,要確保 routine 能讀到 pointer 內容。傳 array char*
、int*
很正常,不過記得傳 int 要用 &
取址。
因此 start_routine
要 take 一個 pointer 參數。
注意
int
直接轉換成 void*
,但要注意這要求 sizeof(int)<=sizeof(void*)
。pthread_exit
: 比較不正常的正常死亡,會執行 cleanup handler。void pthread_exit(void *retval)
: 結束自己的 thread,並回傳 retval。這邊的「回傳」與 return
的作用是一樣的,都是在 pthread_join
讀取。因此與 return
的差別基本上只有:
如果在 main
裡,return
會結束整個 process,pthread_exit
會先等其他 thread 做完事再結束。
int pthread_join(pthread_t thread, void **retval)
: 等待 undetached thread 結束,回傳值會放在 retval。
cancel
代表異常死亡,所以 retval
會是 PTHREAD_CANCELED
。errno
讀;但 pthread 一律是用 return value 判斷。join
的回傳值會得到 EINVAL
、ESRCH
等錯誤。join
時被 cancel,則其他 thread 還是可以 join
本來要 join 的 thread。wait
或 waitpid(-1)
這樣等任一個 thread 的方法,man page 甚至說如果你覺得需要這樣的功能,你的設計可能有問題。分成 cancelstate
(是否接受別人 cancel) 和 canceltype
(收到 cancel 的話什麼時候要結束)。
判斷流程
cancelstate
是 PTHREAD_CANCEL_DISABLE
cancelstate
是 PTHREAD_CANCEL_ENABLE
(default)
canceltype
是 PTHREAD_CANCEL_ASYNCHRONOUS
canceltype
是 PTHREAD_CANCEL_DEFERRED
(default)
wait
、sigsuspend
,確保不會因為休息而導致資源永遠不釋放,有人真的很需要就可以把人 cancel。pthread_testcancel
,也會結束注意: cancel 完還是得要 join,或是設為 detached,否則還會是 zombie thread。
預設值是 undetached、CANCEL_ENABLE
、CANCEL_DEFERRED
,不像用 signal kill process 會直接死。因為 thread 不像 process 是各自獨立,有許多共用資源,比如 lock 或 heap 內的記憶體,因此要確保 cancel 對象把資源清乾淨才能結束。
cleanup handler: 類似 exit 的 atexit,是 LIFO,應該是用來處理後事(清鎖、刪記憶體之類的)。
void pthread_cleanup_push(void (*routine)(void *), void *arg)
: push 一個 cleanup handler 到 stack。
void pthread_cleanup_pop(int execute)
: pop 一個 cleanup handler,如果 execute 是非零,就執行這個 handler。
clearnup handler 會在 pthread_exit
、pthread_cancel
、pthread_cleanup_pop
時執行。return 因為死亡方式太正常了,所以不會執行。
如果這兩個函數是用 macro 寫的,必須成對出現,不然括號可能出錯。就算沒有也可以寫成對,確保若 return 的話會執行所有 handler。
Process | Thread |
---|---|
fork |
pthread_create |
exit |
pthread_exit |
waitpid |
pthread_join |
atexit |
pthread_cleanup_push |
getpid |
pthread_self |
abort |
pthread_cancel |
就算只是 i++
,也有可能因為具體實作不是 atomic 導致兩個 threads race condition,導致只加到 1。因此需要
相當於 (advisory) lock,希望 lock 住才能讀寫。多個 thread 一起 lock 會是第一個執行的 thread 得到,而通常會是優先度最高的 thread,但也沒有保證。
fork 時會繼承 mutex,所以要小心。review: 不繼承 file lock(因為是在 i-node 裡面存哪個 process 有 lock)。
mutex、mutex_attr 都是 struct,所以要用 init
、destroy
來初始化、銷毀。pthread_mutex_init
、pthread_mutex_destroy
、pthread_mutexattr_init
、pthread_mutexattr_destroy
。
PTHREAD_MUTEX_INITIALIZER
可以靜態初始化 mutex。
sync 三者的 attribute 都有 process-shared
,代表要不要跨 process,除了 mmap 是 Advanced I/O 搬來的,其他 pthreads 系列(mutex, rwlock, condition variable)會是 PTHREAD_PROCESS_SHARED
或 PTHREAD_PROCESS_PRIVATE
,預設是 private。
mutex 獨有,用來處理
deadlock: 如果同一個 thread lock 兩次或兩個 thread 互相 lock 對方持有的 lock 後才要 unlock 自己的,會造成 deadlock。
error checking,其中 error 的可能性有: unlock 但沒有 own lock、或是 unlocked 之後又 unlock。
PTHREAD_MUTEX_<type>
NORMAL: 沒有 error checking、deadlock detection,若發生就放給它爛。
ERRORCHECK: 有 error checking、deadlock detection。若產生會回傳錯誤碼。
RECURSIVE: 有 error checking,lock 會有 counter,變成 0 才相當於 unlock。
DEFAULT: depends on 系統。Linux 是 NORMAL
functions: pthread_mutexattr_gettype
、pthread_mutexattr_settype
int pthread_mutex_[lock|unlock|trylock](*mutex)
因為是 advisory,所以確保正確性仍是 programmer 的責任。在使用資源前都要取得 lock。
不太正統的 shared memory。
把 file map 到 virtual memory,這樣就可以直接讀寫 memory 來達到 File I/O,不用 read/write。
Virtual memory 中 stack 到 heap 中間有很大的空白,所以就是把 file map 到這裡。
copy-on-write 前面寫過,但還是再提一些。好處是 read-only 的部分就不用複製。像是現在大部分電腦都是馮諾伊曼架構,不允許在運行時改程式碼,所以 text 部分就不會更改。
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
addr
: 要 map 到哪裡,通常是 NULL,讓 OS 選擇,就算指定也只是建議。len
: map 多大,可以設為非 page 大小的倍數。prot
: protection,控制rwx,當然不能超過對檔案的權限 PROT_READ
、PROT_WRITE
、PROT_EXEC
、PROT_NONE
flags
:
MAP_FIXED
讓 addr
不是建議而是強制,不常用。MAP_SHARED
、MAP_PRIVATE
,前者是各 process 共享,後者是各 process 獨立 (copy-on-write)。MAP_ANONYMOUS
不是 map file,而是 map anonymous memory,通常用來做 shared memory。fd 會被忽略,所以可以亂寫 -1,要注意有的系統要求寫 -1。回傳的指標就代表 map 到的 vitual memory 的開頭。
之所以說這不太正統,是因為畢竟原本單純是用來 map file to memory,正統的 shared memory 會是 shm 開頭的 system call。
shared memory 是 IPC 最快的方法,比如很多個 ptt server process 因為要可以共用一個 shared memory 來確認密碼。
addr, len, offset 理想上都要是 page size 的倍數才方便,確實系統也會要求 addr len 是倍數。像是這種 system call 確實都比較麻煩,SBRK(malloc 時會呼叫的 system call) 也會要求是 page size 的倍數。
解決辦法: addr 就 NULL,自動選。offset 設 0 讓它從頭開始。系統通常不會要求 len 是倍數,會自動補 0 變成倍數。
開啟兩個檔案 source 和 dest。
把 dest 清空後,lseek
到 source 的大小並寫一個 byte 以確保大小相同。
把兩者都 map 到各自的 memory。
用 memcpy
把 source 的內容複製到 dest。
(理論上要有 unmapping,但跟 close 一樣系統會幫你做)
就像 unbuffered I/O 會有 buffer cache,之後才真正寫到 disk,mmap 後也一樣會先寫到 memory,因此就可以用來做 shared memory。
有一段記憶體 char buf[BUFSIZE]
,要讓兩個 process 共享。
同時舉例 mutex,因為 mutex 也要讓兩個 process 共享,所以就乾脆定義一個 struct 包在一起,但注意還是得要用 attr 把它設為 PTHREAD_PROCESS_SHARED
(雖然我沒實驗過但應該是要這樣寫才對)。
這有兩種實作方法,一個是 fd 用開 /dev/zero
來 map,另一個是用 MAP_ANONYMOUS
。
不過 exec 就沒辦法用了,user space 會被清空。
上面 mutex 有寫到各種 deadlock 的情況,也寫到 trylock 可以避免。這邊再補充一個方法,控制 locking granularity,把多個 lock 整合。
優點:
缺點
概念和 file lock 一樣。
實作上與 mutex 一樣,是一個 pthread_rwlock_t
struct,也有 init
、destroy
、rdlock
、wrlock
、unlock
、tryrdlock
、trywrlock
(自己加 pthread_rwlock_
在前面),以及靜態初始化 PTHREAD_RWLOCK_INITIALIZER
。
(實作上可能會限制一個 rwlock 最多能被 lock 幾次)
mutex 也不是一無是處,因為實作簡單。比如說如果全部都是要 write,那不如直接用 mutex。
有一個 master thread 負責分配工作,另外有一些 worker threads 負責做事,把 jobs 放在 queue 裡。每個 job 可能會指定要用哪個 thread 做,因此可以用 rwlock 實作,append 前取 wrlock,find 前取 rdlock。
這衍生出一個問題,如果 queue 裡沒有工作,那就會造成 busy waiting,因此可以用 condition variable 來解決。
適用於需要等到某個條件成立才繼續的情況,這個條件的檢查、改變是由一個 mutex 保護。
主要操作:wait
、signal
、broadcast
。
pthread_cond_t
一樣要 init
、destroy
,或是靜態初始化 PTHREAD_COND_INITIALIZER
。
signal
會叫醒某個等待該 condition 的 thread,但不知道會是哪個,broadcast
會叫醒所有等待的 thread。
注意: 可以想成 wait 時才會接收 signal。
pthread_cond_wait(cond_t *c, mutex_t *m)
:
signal
or broadcast
在 condition 的觀點,在 lock 到 unlock 間是 critical region,因為 lock 住,其他 thread 無法改變條件,在這裡不會使得條件滿足。
注意: wait return 前必須取得 mutex,所以如果暫時還拿不到的話就會 suspend。
如果在 A 的地方別的 thread 改 x 怎麼辦?反正多通知不虧,沒差。
前面那個 fork 完兩個 process 一起用 unbuffered I/O 一次寫一個字元的例子。之前用過:
這邊用 condition variable 來實作,概念上也很像 signal。想要 spawn 出來的 thread 先跑,則:
其實 unlock
signal
交換也行,都會正常運作,但在 real-time 的應用(比如直播),就很需要讓優先的 thread 先執行,所以先 signal
比較好,因為可以讓先 wait
的人醒來,比較不會等太久。
一行都不能少,否則會有 race condition:
done=1
、while(done==0)
拿掉,可能先 signal 再 wait,就會等到死。done==0
後 修改並 signal,但還沒 wait,一樣會等到死。問題: 如果 fork 一個有很多 thread 的 process,新的 process 會複製所有 threads 嗎?
答案: 不會,只會複製 call fork 的 thread。
因此很有可能其他 thread 的 lock 沒有釋放,那 child 裡那些 lock 就永遠不會釋放。
解決辦法: 最簡單的是用 exec
,直接重跑一個程式。
另外一個方法是 int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void))
,在 fork 前執行 prepare
、return 到 parent 前執行 parent
、return 到 child 前執行 child
,可以用來清理 lock。
通常會把 prepare
設為 lock 所有 lock,parent
設為 unlock 所有 lock,child
也設為 unlock 所有 lock。相當於 prepare 會使得所有其他取得 lock 的 thread 都告一段落並 unlock 後再繼續,很合理。
註冊多次的話,都會被執行,但 prepare 是 FIFO,parent、child 是 LIFO。
引發方式-什麼事情引發
pthread_kill
,給指定的 threadkill
,會給隨便一個沒有 block 這個 signal 的 thread,全 block 就 pendingpthread_sigmask
類似 sigprocmask
,參數一樣int sigwait(sigset_t *set, int *signop)
: 等待 set
裡的 signal,也就是 unblock 它們,與 sigsuspend
相反。signop
會儲存收到的 signal。Process 的 signal 是 asynchronous,因為不知道什麼時候會來。
非常直觀,就對 signal 用專門的 thread 處理(多對一、一對一、多對多都有可能),裡面就是無窮迴圈一直 sigwait
它要等的 signal,其他 threads 把 signal block。
如果還設了 sigaction
,系統決定其中一個,但不會這樣找麻煩。
如果兩個 threads 同時 lseek
並 write
,因為共用 file desc. table,也共用 offset,會導致 race condition。因此需要 atomic operation pread
pwrite
,不會改掉 offset。
可以被多個 threads 同時呼叫的 function,不會用到 global, static variable,或是有好好上 lock。
負面表列某些不 safe 的 functions。像是 rand
有 hidden state,但你可以把它當作參數傳入 rand_r
,這樣就是 thread-safe 了。
Async-Signal Safe 指 signal handler 中可用的 reentrant function。
(絕大多情況) 但 ,因為如果函數有 lock,再 call 一次的話會 deadlock(等上層 unlock)。
因為有 f[lock|unlock|trylock]file(FILE *fp)
,所有 I/O 都會上 recursive lock。
但是因為 lock unlock 會導致變慢,若每次只讀寫一個字元就會超慢(granularity 太細),因此提供後綴 _unlocked
的版本。
每個 process 都以為自己有專用的
把一個 executable(elf) 拆開,會發現剛開始執行不是直接進到 main 而是 _start
。可以用 nm -g
(list symbols of object file) readelf -a
(read elf file) objdump -d
等指令看。
objdump
時會發現有些位置是用相對位置,好處是如果內容不變但相對的點(如_start
)改變,就不用修改。
One unified format for
Better than a.out
format
source code .c .cpp
C Preprocessor gcc -E
: 把 header 檔、macro 展開
preprocessed code .i
C Compiler gcc -S
: 把 C code 編譯成 assembly code
assembly code .s
Assembler gcc -C
: 把 assembly code 編譯成 object code
object code .o
Linker gcc -o
: 把 object code 和 library link 起來
executable file
.text
section: code.data
section: initialized data.bss
section:
.symtab
section: symbol table,寫 compiler 時很重要,會包含 procedure, static variable 在哪的資訊。.rel.text
section: relocation information for .text
section.rel.data
section: relocation information for .data
section.debug
section: debugging information.rel
: 需要在 link 時填入
如果在 a 檔案裡要用 b 檔案的 function 或 variable,在生成 a 的 .o
檔時就會把 rel.text
或 rel.data
的位置設為 0。並在 relocate 時把 b 的位置填進去。
Relocate 時,就是把多個 relocatable object files link 起來,也就是把各自的各 section 合併。
合併時就會發現相對位置真的很重要。
講 ELF 的原因就是因為 text data bss 會對應到 process 的 text data bss segement。
OS 會建一個 page table,把 virtual address 對應到 physical address。但因為 physical memory 有限,所以會有些資料被放在 disk 上(swap)。
VPN: Virtual Page Number,在 virtual memory 中的 page number
PPN: Physical Page Number,在 physical memory 中的 page number
Page table 就是 VPN 到 PPN 的 mapping。而每個 address 會包含 page number(VPN 或 PPN) 和 offset,virtual 轉換成 physical 時 offset 不變。
每個 process 有自己的 page table。page table 會需要是非常高效的,所以會有硬體優化,Memory Management Unit(MMU)。
去年期末考題:setjmp longjmp 時,canjmp 變數要是
static volatile sig_atomic_t
,其實就是不能跨 page,否則很可能需要多個 instruction。(canjump 是確保有 setjmp 過)
stack frame 會有 return address、saved registers、local variables。
可以有
const volatile
,雖然自己改不了,但 somehow 硬體可能可以直接改。
老朋友: malloc
calloc
(nobj, size) realloc
(reuse existing memory or copy to new memory)
sbrk
: system call,很麻煩
alloca
: allocate from stack frame, no need to free! But unsafe(too big=>issue), not standard(so not portable)
系統會 track 哪些地方用多少、沒用過,可能是 Linked list。
Use extern char **environ
instead of char **envp
in arguments! Becuase external variable will change if env is changed.
.a
檔。.so
檔。shared library 就是用 mmap
來做,所以也在 stack、heap 之間,也解釋了 mmap
裡 permission 用 x 的用處。