VinSong's Blog

Back

NTU-SP 系統程式設計 Ch8 Thread modelBlur image

Thread Model#

Thread Concept#

Process Model#

  • Resource Grouping (資源分組):Process 是作業系統分配資源(記憶體、檔案等)的基本單位。
  • Single Thread of Execution:傳統的 Process 中只有一個執行緒(Thread),指令是單一序列執行的。

Thread Model#


Thread Model

  • 概念:在同一個 Process 的生命週期內,可以同時執行多個 Thread。
  • 資源共享 (Resource Sharing): 因為屬於同一個 Process,Thread 之間共享大部分的資源:
    • Global Data / Heap Memory
    • Open Files Descriptor
    • Code (Text Segment)
    • Child Processes
    • Pending Alarms / Signals
  • 獨立擁有 (Per-Thread Resources): 為了能夠獨立執行並進行 Context Switch,每個 Thread 必須擁有自己獨立的:
    • Stack (維護 Function call chain)
    • Registers (包含 Context Switch 時需要保存的狀態)
    • Program Counter (PC) (紀錄目前執行到哪一行指令)
    • Scheduling Policy (排程優先級)
    • Signal Mask (對訊號的遮罩設定)
    • Thread specific data

Process v.s. Thread#

Process#

  • 競爭關係:Process 之間通常處於資源競爭狀態。
  • 資源隔離:預設情況下無法共享資源(基於安全性 Security 考量),若要溝通需透過 IPC (Inter-Process Communication)。
  • 建立成本高:使用 fork() 建立新 Process 是 System Call,需複製記憶體空間與 Page Table,開銷較大。

Thread#

  • 合作關係:通常由單一使用者(Owner)啟動,Thread 之間是為了完成同一任務而合作。
  • 無保護機制 (No Protection):Thread 之間直接共享 Memory,先天沒有隔離保護(假設同一 Process 內的 Thread 彼此信任)。
  • 地位平等:雖然由 Main thread 建立其他 Thread,但在 OS 排程眼中,它們地位通常是平等的(Peer),無階層從屬關係。
  • Context Switch 成本低:切換 Thread 時只需保存暫存器與 Stack 指標,不需要切換 Page Table(Memory Space 不變)。
  • 非同步與併發 (Asynchronous & Concurrency)
    • Thread 可以獨立運作。當 Process 收到 Signal 或需處理 I/O 時,可以只讓負責該工作的 Thread 暫停(Suspend)或處理,其他 Thread 繼續執行,不需整個 Process 停擺。
    • 這種特性增加了程式的 Throughput
  • 回應時間 (Response Time)
    • 透過多執行緒,可以讓負責 UI 或回應的 Thread 保持運作,將耗時的運算或 I/O 交給背景 Thread,大幅提升使用者的操作體驗。
  • 除錯困難 (Debugging)
    • 由於資源共享且執行順序不確定(Race Condition),除錯遠比單一 Process 複雜。
  • 單核處理器 (Single Processor) 的效益
    • 在 Single processor(core) machine 上,Thread model 無法達到真正的「平行運算 (Parallelism)」,只能做到「併發 (Concurrency)」。
    • 但並非沒用:它依然能透過重疊 I/O 等待時間與 CPU 運算時間,有效減少 Response time 並提升系統整體效率。

Thread


Implementing of Thread#

Implementing of Thread (User Space)#


Implementing of Thread 1

  • Runtime System: 一個 Library 去控制所有 thread 出生死亡,用 function call
  • Thread Table: Runtime System 內部 maintain TCB
    • Process 要有 PCB (Process Control Block),同理 Thread 也有 TCB
    • Kernel 並不知道 Process 裡面到底長什麼樣子,只是 schedule process 們
    • Thread 是用 Runtime System 去控制,TCB 也是存在裡面的

Implementing of Thread (Kernel Space)#


Implementing of Thread 2

  • Thread table 會在 kernel 內,可以做不同 process 間的交互處理
  • kernel 知道是 thread 被 block 而不是 process 所以可以 schedule 其他 thread

Hybrid Implementation#


Hybrid Implementation

  • 讓 kernel thread 各自做 user thread 的 context switch

Thread Control#

  • 以前的 modle 都可以當作是 single thread 的 process
  • 我們都講 POSIX 標準

Thread creation#

int pthread_create( pthread_t *tidp, pthread_attr_t *attr, void *(*start_rtn)(void *), void *arg )

  • tidp: thread id
  • attr: attribute
  • void *(*start_rtn)(void *): 指向通用型態的 function
  • arg: 送進 start_rtn

任何被 create 跟 create 別人的 thread 會根據 priority 來決定跑的順序,跟 fork() 很像

  • 只分成 main thread 跟 spawned thread,而且並非主從關係
  • Normal function call

    Thread creation 1

  • Threaded function call

    Thread creation 2

pthread_t pthread_self(void)

  • thread id 早期是 integer 但有些系統用 pointer,pthread_t 可以 portable
  • 可以獲取自己的 id

int pthread_equal ( pthread_t tid1, pthread_t tid2 )

  • 比較用 function

int pthread_attr_init(pthread_attr_t *attr) int pthread_attr_destroy(pthread_attr_t *attr)

  • 先把 variable 傳入,然後去 initalize 他
    • detachstate: 如果是的話,termenation state 不需要回收,直接清掉,在 TCB 內
    • guardsize: 防止壓到別人,要有多少緩衝區
    • stackaddr
    • stacksize: stack 大小限制
  • 必須要去 destroy 他,因為會佔用 heap
    • 會把 pthread_attr_t 設定為 invalid

int pthread_attr_getdetachstate (pthread_attr_t *attr, int *detachstate) int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate)

  • 可以設定或拿到他的 attr
  • PTHREAD_CREATE_DETACHED, PTHREAD_CREATE_JOINABLE(default),可以設定

int pthread_detach(pthread_t tid)

  • 進行 detach 設定,只要你有 tid 就可以
  • pthread_detach(pthread_self())
  • 並不是殺掉,只是不回收而已
  • 有三種情況會被 detached
    • pthread_detach()
    • PTHREAD_CREATE_DETACHED
    • pthread_join(),被拿走了

Passing Argument#

一定要確定對面的 thread 不可以自己結束,不然會出問題

  • int
    • Pass
      int i = 42;
      pthread_create(..., my_func, (void *)&i);
      c
    • Retrieving
      void *myfunc(void *vptr) {
          int value = *((int *)vptr);
      }
      c
  • Passing string
    • Pass
      char *str = ”NTU”;
      pthread_create(..., my_func, (void *)str);
      c
    • Retrieving
      void *myfunc(void *vptr) {
          char *str = (char *)vptr;
      }
      c
  • Passing array
    • Pass
      int arr[100];
      pthread_create(..., my_func, (void *)arr);
      c
    • Retrieving
      void *myfunc(void *vptr) {
          int *arr = (int *)vptr;
      }
      c

Thread Termination#

  • 有些情況會把所有 thread 一次關掉
    • _exit()
    • signal default 是 terminate
    • returnmain() (main thread)

void pthread_exit (void *rval_ptr)

  • 呼叫後不會有任何的其他動作,只是關掉 thread
  • 其他資源是 process 共享的,所以不會被關掉
  • 在 main thread 裡面呼叫 pthread_exit() 就是要 block 住 main thread 直到所有他 create 出來的 thread return

int pthread_join(pthread_t tid, void **rval_ptr)

  • 也可以呼叫這個 function,意思是等到所有他自己 spawn 出來的 non-detach thread 結束,拿到結束碼
  • thread 異常死亡叫做被他人 cancel,拿結束碼會拿到 PTHREAD_CANCELED
  • thread 被拿走結束碼後就會 detechable
  • 任何出錯都要看 return value 不是一般 fucntion 的 errno
  • non-detechable thread 死掉沒被 join 就是 zombie thread
  • threadID 發完了就不能繼續 create 了
  • 呼叫 join 的 thread 被打斷了就可以用 pthread_timedjoin_np() return ETIMEDOUT

Thread Cancel#

任何 thread 都可以 cancel 其他人

int pthread_cancel(pthread_t tid)

  • 相當於讓要被 cancel 的呼叫 pthread_exit() 然後,然後傳參數給 PTHREAD_CANCELED
  • 被 cancel 的人可以決定自己要不要被 cancel
  • PTHREAD_CANCEL_ENABLE (default)
  • PTHREAD_CANCEL_DISABLE 可以不讓別人 cancel
  • PTHREAD_CANCEL_ENABLE 之下 傳來 cancel 也只是 Only makes the request 而已,會運行直到 cancelation point

int pthread_setcanceltype(int type, int *oldtype)

  • 在可以被 cancel 的情況下
    • PTHREAD_CANCEL_DEFERRED (default) 運行到 cancelation point
    • PTHREAD_CANCEL_ASYNCHRONOUS 直接死

void pthread_testcancel(void)

  • 設定成可以 cancel 的 cancel point,在我沒有任何 cancelation point 的情況下

void pthread_cleanup_push(void (*rtn)(void *), void *arg) void pthread_cleanup_pop(int execute)

  • 也是可以 clean up,用 reverse way
  • 可以自行 push, pop
  • rtn 裡面的 function 會被 call 出來的情況
    • pthread_exit()
    • pthread_cleanup_pop()
    • 被 cancel
  • 如果呼叫 return ((void *) 2); 就不會往後 pop 出所有東西
  • 正常異常死亡都會呼叫出 rtn 內的 function 們,用 reverse way

Thread Synchronization#

  • Share memory 太常出現,所以我們必須要產生同步,不然會出現問題,因為一個高階語言指令可能是四五個低階語言指令

Thread Synchronization

  • 兩個 thread 要讀/寫入同一個資料是不同的 code 會競爭到同一個 resource
  • 即便有一個 resource 只有一個 function 可以 access,兩個 thread call 同一個 function 還是會有 competition,還是需要 Synchronization

Mutexes#

  • 全名 Mutual-exclusion interfaces
  • 是一種 binary advisory lock 但是是 for memory resource 而非先前的 file lock
  • A thread 用完 resource unlock 後,BCD 三個 pending lock 就會變成 runable,也就是 unlock 後剩下的人才會被送到 ready queue
    • 再由 system 根據 priority 去決定是誰獲得 mutex lock
    • 其他沒拿到 lock 的 thread 又會被 suspend
    • 只有搶到 lock 才會 return
  • read(), write() 前都需要先進行 mutexes check
    • programmer 要自己去 check,如果跳過這關也不會被阻止,因為 thread 在 OS view 來看是互相 trustable 的

int pthread_mutex_init ( pthread_mutex_t *mutex, pthread_mutexattr_t *attr ) int pthread_mutex_destroy ( pthread_mutex_t *mutex )

  • 宣告一個 pthread_mutex_t 要做 initialize
    • 也可以
      pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
      c
      做靜態宣告
    • 也可以動態宣告,用 pthread_mutex_init() + pthread_mutex_destroy()
  • NULL 就是 default
  • attribute 也要做 initialize
  • Process-share attribute
    • PTHREAD_PROCESS_SHARED 可否接受多個 process share Mutex 的 memory(非 file)
    • compile time 用 _POSIX_THREAD_PROCESS_SHARED 看 system 有沒有這個功能
    • runtime check _SC_THREAD_PROCESS_SHARED 看 system 有沒有這個功能
    • pthread_mutexattr_getpshared(), pthread_mutexattr_setpshared()
  • Type attribute
    • PTHREAD_MUTEX_NORMAL: 不做 error check 也不做 deadlock check
    • PTHREAD_MUTEX_ERRORCHECK
    • PTHREAD_MUTEX_RECURSIVE: relock 幾次都不出事,lockcount 會記錄被鎖幾次,解鎖也要相對次數
    • PTHREAD_MUTEX_DEFAULT: 會是以上三種的其中一種,每個 system 不一樣

int pthread_mutex_lock(pthread_mutex_t *mutex) int pthread_mutex_trylock(pthread_mutex_t *mutex) int pthread_mutex_unlock(pthread_mutex_t *mutex)

  • pthread_mutex_lock() 是 block mode
  • pthread_mutex_trylock() 是 non-block mode 如果出錯會 return EBUSY 為錯誤碼

Share memory#

Memory-mapped I/O#

  • void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

Memory-mapped I/O 1

  • 先把 memory map 到 virtual memory 上,再利用 system call 把它 mapping 回去
  • Virtual 跟 Physical memory 都會切成一個一個 page,兩端 page size 必須相同,必須得 align
  • 對 system call 一個 malloc() 會返回一個可以任意轉換 type 的 address,但對於 align 方法有要求

Memory-mapped I/O 2

  • 不像一般 IO 需要各種搬移
    • Unbuffered I/O: Kernel − standard I/O buffer − our buffer
    • Buffered I/O: Kernel − our buffer
  • 理論上 len 要是 page size 整數倍,但 system 有彈性,會幫你把後面的空(亂)資料也一起 mapping 做 align
  • start_addrNULL 讓系統決定,要知道 heap, stack, page size 才能自己設定
  • addr 對於 system 只是 hint 而已,不是真實值,除非使用 MAP_FIXED flag
  • MAP_SHARED 會寫回 physical memory,而 MAP_PRIVATE 會做 copy on write,延遲讀寫
  • mapping 多大就只能寫多大的檔案,不能動態增生

Memory-mapped I/O 3

int
main(int argc, char *argv[])
{
    int         fdin, fdout;
    void        *src, *dst;
    struct stat statbuf;

    if (argc != 3)
        err_quit("usage: %s <fromfile> <tofile>", argv[0]);

    if ((fdin = open(argv[1], O_RDONLY)) < 0)
        err_sys("can't open %s for reading", argv[1]);

    if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
        err_sys("can't creat %s for writing", argv[2]);

    if (fstat(fdin, &statbuf) < 0)   /* need size of input file */
        err_sys("fstat error");

    /* set size of output file */
    if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1)
        err_sys("lseek error");
    if (write(fdout, "", 1) != 1)
        err_sys("write error");

    if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED)
        err_sys("mmap error for input");

    if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, 0)) == MAP_FAILED)
        err_sys("mmap error for output");

    memcpy(dst, src, statbuf.st_size); /* does the file copy */
    exit(0);
}
c
  • 理論上這樣不對,因為做了 mapping 就得做 msycn, unmapping,所以應該要在 exit() 前做這些事情(不寫等於給系統做)
  • 權限會繼承,因此檔案本身要有對的的讀寫權限
typedef struct {
    pthread_mutex_t mlock;
    char buf[ BUFSIZE ];
} buftype;

int main() {
    int fd;
    buftype *bufptr;
    pthread_mutexattr_t mattr;

    fd = open("/dev/zero", O_RDWR);
    bufptr = (buftype *) mmap(NULL, sizeof(buftype), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    // bufptr = (buftype *) mmap(NULL, sizeof(buftype), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&bufptr->mlock, &mattr);

    if (fork() == 0) {
        pthread_mutex_lock(&bufptr->mlock);
        // ...
        pthread_mutex_unlock(&bufptr->mlock);
        // call munmap(), pthread_attr_destroy(), close()
    } else {
        pthread_mutex_lock(&bufptr->mlock);
        // ...
        pthread_mutex_unlock(&bufptr->mlock);
        // call wait(), munmap(), pthread_attr_destroy(), close()
    }
    return 0;
}
c
  • MAP_ANONYMOUS 創造出一個 virtual file 來創造 share memory 的感覺

Memory-mapped I/O 4

  • 兩個 share 到同一份 resource 同時要改,可以上 mutex lock,並且 lock 也要放在 share memory
    • init 要在 fork() 前就上好
    • 在 process 裡面才去爭 lock

Memory-mapped I/O 5

Deadlock#

Resource Order

  • 上各種 lock 都可能會發生 deadlock
  • 解決方法是對 resource 排 order
    ABCD
    T1T_1✔️✔️
    T2T_2✔️✔️
    T3T_3✔️
    • 比如說在這個情況下我可以排 order 是要拿 B 必須先拿 A
    • T2T_2 要拿 B 必須先拿 A lock 這樣 T1T_1,T2T_2 就不會 cycle deadlock
  • 如果上的 lock granular 好管理,但並不平行化

pthread_mutex_trylock()

  • 也可用 pthread_mutex_trylock() non-blocking mode

ReaderWriter Locks#

  • 照理來說 read lock 可以 share 但 mutex 是全部鎖死

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr) int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)

  • 跟 mutex 一樣需要 destroy 因為會動到 heap

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock) int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock) int pthread_rwlock_unlock(pthread_rwlock_t *rwlock) int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock) int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)

Thread Pool#

  • A master thread: 負責排工作,給 ID,把工作分配好後排進 link list
  • A pool of worker threads: 真正做事的 thread,定期去 link list 上看一下有沒有自己的工作,然後改一下 list,自己去做事

Thread Pool

void
job_append(struct queue *qp, struct job *jp)
{
    pthread_rwlock_wrlock(&qp->q_lock);
    jp->j_next = NULL;
    jp->j_prev = qp->q_tail;
    if (qp->q_tail != NULL)
        qp->q_tail->j_next = jp;
    else
        qp->q_head = jp;    /* list was empty */
    qp->q_tail = jp;
    pthread_rwlock_unlock(&qp->q_lock);
}
c
struct job *
job_find(struct queue *qp, pthread_t id)
{
    struct job *jp;

    if (pthread_rwlock_rdlock(&qp->q_lock) != 0)
        return(NULL);

    for (jp = qp->q_head; jp != NULL; jp = jp->j_next)
        if (pthread_equal(jp->j_id, id))
            break;

    pthread_rwlock_unlock(&qp->q_lock);
    return(jp);
}
c
  • 有可能有 busy waiting

Condition Variable#

int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr) int pthread_cond_destroy(pthread_cond_t *cond)

  • 一定會帶優個 mutex 的 lock
  • 會有兩個 operation
    • wait
    • signal: 發送告訴 thread 你有事了(並非 ch7 的 signal)

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

  • 先拿到 mutex lock
  • Check condition,看看是否為 true
  • 不是的話 Wait until a condition become true.
    • Sleep until signaled
    • Release the mutex just before sleep
    • 直到這裡都會是 atomic operation
    • 醒過來前要拿到 mutex 的 lock 不然要繼續回去睡
  • 因此有兩種情形,有可能 signal 沒來的睡,跟 signal 來了的睡

int pthread_cond_signal(pthread_cond_t *cond)

  • 多個人等的話,讓 system 去選一個人去叫醒

int pthread_cond_broadcast(pthread_cond_t *cond)

  • 直接把所有 wait 的人全都叫醒
int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
void *spawned(void *arg) {
    // spawned thread goes first
    pthread_mutex_lock(&m);
    done = 1;
    pthread_cond_signal(&c);
    pthread_mutex_unlock(&m);
    return NULL;
}
int main(int argc, char *argv[]) {
    pthread_t p;
    pthread_create(&p, NULL, spawned, NULL);
    pthread_mutex_lock(&m);
    while (done == 0)
        pthread_cond_wait(&c, &m);
    pthread_mutex_unlock(&m);
    // main thread continues
}
c
  • 對 main thread:要確認 condition 所以要上 mutex
  • 發現 false 所以要進去等
  • 等到 spawned thread 說 done = 1
  • main thread 回來發現自己還沒拿到 mutex
  • 會去跑 spawned thread unlock mutex
  • 最後 main 才會跑

Thread and fork()#

如果只是一個 process 擁有很多個 thread,fork() 出來的 process 只會有一個 thread,就是呼叫 fork() 的那個

  • Child 會繼承 Parent 的 virtual memory
  • 假設有一個 B thread own 一個 mutex lock,parent 不會出事,B thread 會在結束的時候 unlock
  • 但是若 A thread 在 unlock 前 fork(),那 child 會看到一個被 lock 住的 process,並且沒有任何一個 thread 知道要把 lock 放掉,產生 deadlock

Sloution:

  • call exec() in child(替換掉所有環境變數)
  • 先呼叫一個 fork handler

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void))

  • 只是註冊 function
  • 可以多次註冊,執行順序:
    • prepare: reverse order
    • parent, child: correct order
  • 在呼叫 fork()
    • 先幫你 call prpare function 通常把 parent 的 lock 全部上上去
    • 生 child process
    • parent return 前 call parent function
    • child return 前 call child function,通常把所有 lock 都 unlock

Thread and Signals#

每個 thread 也可以有自己的 signal mask

  • signal disposition 是 process share 的
  • 如果動到了 process level 的 signal 所有 thread 都會受影響
  • 可能可以把一個 thread unblock 其他全都 block,這樣就會在這個 thread deliver

Synchronous & Asynchronous#

How the signal was generatedWhat generated the signalEffective target of the signalHow the signal-processing thread is selected
SynchronouslyThe system, because of an exceptionA specific threadAlways the offended thread
SynchronouslyAn internal thread using pthread_killA specific threadAlways the targeted thread
AsynchronouslyAn external process using killThe process as a wholePer-thread signal masks of all threads in the process
  • 有些 Sychronous 的 signal 擁有非常明顯的 target 可以直接送往
  • 但 Asychronous 的 signal 我們必須檢查 per-thread 的 signal mask 去看誰 unblock

Dedicate signal handling threads#

  • 某些 thread 專門用來執行 signal handler
  • 利用 signal mask 來指定那個 thread 要 handle 哪些 signal
  • 通常會設計成一個 loop,一次執行一個 signal,同一個 handler 可以 handle 多個 signal

Inherent of signal mask#

  • 一個新生的 child process 的第一個 thread 的 signal mask 會繼承 parent process 中 call fork() 的 thread 的 signal mask
  • 若 A thread 要 create B thread,B 會 inherit A create 他的時間點的那個 signal mask

int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset)

  • 用這個 system call 去做 sigpromask() 的動作

int sigwait(const sigset_t *restrict set, int *restrict signop)

  • sigsuspend() 一樣,製作出 critical region
  • 雖然一樣,但是裡面的參數,你要等誰你就要在 set 裡放誰,signal 來的時候我們就把它 unblock
  • 這個 function 被 return 就代表 signal 被 deliver 了

Sychronous handling signal#

  • Signal 在 process 處理的時候是 asychronous,因為你不知道他何時來,有可能產生 re-entrance function 等一系列問題
  • 在 thread 的情況,有很多 dedicate signal handling,先天就是平行的 model,本來就會保護,沒有在任意地方被中斷的問題,所以可以 sychronous 的處理,自己會獨立出去執行一個 thread
  • 如果同時註冊了 process handler 跟 thread handler,system 會根據自己的 implementation 去做選擇(不建議這樣寫)
pthread_t stats_thread;
pthread_mutex_t stats_lock = PTHREAD_MUTEX_INITIALIZER;
extern int
main(void)
{
    ...
    sigset_t sigs_to_block;
    ...
    /* Set main thread's signal mask to block SIGUSR1.
       All other threads will inherit mask and have it blocked too
    */
    sigemptyset(&sigs_to_block);
    sigaddset(&sigs_to_block, SIGUSR1);
    // block first in here
    pthread_sigmask(SIG_BLOCK, &sigs_to_block, NULL);
    ...
    pthread_create(&stats_thread, NULL, report_stats, NULL);
    ...
}
c
void * report_stats(void *p)
{
    sigset_t sigs_to_catch;
    int caught;
    sigemptyset(&sigs_to_catch);
    sigaddset(&sigs_to_catch, SIGUSR1);
    for (;;) {
        sigwait(&sigs_to_catch, &caught);
        /* Proceed to lock mutex and display statistics */
        pthread_mutex_lock(&stats_lock);
        display_stats();
        pthread_mutex_unlock(&stats_lock);
    }
    return NULL;
}
c
  • 上面那個區間,也就是 thread_create()sigwait() 之間要做成 critical region,不然 signal 來的話會 get lost,並且 block 的動作要在 create 以前就先做了,不然會有 race condition
  • sigwait() unblock 完畢又 block 住後,會把直到下一次 sigwait() unblock 前,又會形成下一次的 critical region

Thread-safe / Atomic IO#

  • 在 thread 裡面,所有 file descriptor 跟 file stream 都是 share entry 的,所以必須要是 atomic operation 不然 offset 會跑掉
  • pread()/pwrite() 用這兩個 system call 來做改動

ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset); ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);

  • 讀寫完畢並不會改變 open fd table 裡面的 current file offset (lseek() 的值)

Thread safe#

有些 function 在 thread 裡面呼叫是被禁止的

  • Async-signal safe: 在傳統的 signal handler 裡面做 reentrant 是沒問題的 system call 或是 function call
  • Thread-safe: 可以同時被很多 thread 呼叫,multiple threads reentrant
  • _POSIX_THREAD_SAFE_FUNCTIONS, _SC_THREAD_SAFE_FUNCTIONS 去 check
  • 一個 function thread safe 不一定 Async-signal safe,但在大部分情況下(不惡搞)Async-signal safe implies
  • 以下這些 function 並不保證是 thread-safe(但其實也可以找到 thread safe 的版本)

Thread safe 1

Thread-Safe FILE Objects#

void flockfile(FILE *fp) int ftrylockfile(FILE *fp) void funlockfile(FILE *fp)

  • call flockfile() 就會用 block mode 做 lock
  • ftrylockfile() 是 non-block mode
  • 最後要funlockfile() 做 unlock
  • 是一個 recussive lock(lock again without deadlocking)
  • 所有 Standard I/O 都會隱性幫你做 lock,雖然安全,但有時候做一些小的 I/O 會有效率問題(lock 上的太頻繁 granularity)
    • 可以用 Unlocked version 的 character-based buffer I/O 做一次的 lock,兼具效率跟安全
    • getchar_unlocked(), getc_unlocked() 做最後的 unlock

Back to the content

NTU PJ System Programming

2025 Fall

← Back to the content


NTU-SP 系統程式設計 Ch8 Thread model
https://vinsong.csie.org/notes/sp/ch08-thread.html
Author VinSong
Published at 2025年11月30日