本章節介紹如下
- 介紹如何使用
exit()
以及_exit()
結束行程 - 探討行程在呼叫
exit()
函式時,會使用結束處理常式 exit handler 進行自動管理 - 探討
fork()
、stdio
緩衝區,以及exit()
之間的互動
25.1 終止行程: exit()
與 _exit()
_exit()
:終止行程的 system call
有兩種方式可以終止行程
- 異常終止(abnormal):如 20.1 節所描述,由訊號 signal 觸發,預設訂做是終止行程,可產生核心傾印檔 (core dump file)
- 使用
_exit()
系統呼叫將行程正常終止
function declare:
1 |
|
status 的討論
- status 參數定義形成的終止狀態(termination status),父行程可以呼叫 wait() 取得該狀態,雖然型別定義為
int
,但實際上服行程只能取得 status 參數的 後 8 個位元。 - 依據慣例:
- 結束狀態為 0 表示行程順利結束
- 非 0 的狀態則表示行程異常終止
- 目前對於如何解釋非零的狀態並無固定規則,不同的應用程式有自家慣例可以遵循
- 書中大多數的程式都是遵循
SUSv3
制訂的兩個常數EXIT_SUCCESS(0)
EXIT_FAILURE(1)
行程一定可以藉由
_exit()
順利終止 (即呼叫_exit()
之後不會繼續執行)雖然 status 可以透過 0-255 之間的任意值傳遞給父行程,但若將 status 的數值設定大於 128 ,則會在 shell 腳本產生混淆,原因在於若指令是經由訊號終止的,則 shell 會將
$?
變數值設定為 128 加上訊號編號來表示現實情況,當行程用一樣的 status 的數值呼叫_exit()
時會無法分辨。
exit()
:較常使用的函式庫函式 (並非 system call)
通常不會直接呼叫 _exit()
,而是呼叫 exit()
函式庫函式,此函式可在呼叫 _exit()
之前執行一些動作
function declare
1 |
|
或是輸入 man exit
取得更詳細的說明
exit()
的執行動作
會進行下列動作
- 以反向註冊程序 (ch25.3),呼叫 Exit 處理常式 (
atexit()
與on_exit()
註冊的函式) - flush stdio 串流緩衝區
- 使用 status 提供的值執行
_exit()
系統呼叫
exit()
的討論
exit()
與_exit()
不同點:_exit
是 UNIX 特有的,而exit()
是 libc 的 funciton,簡單說任何 C 語言實作都可以使用 exit()- 在
main()
中執行return
是對等的,return n
等同於執行exit(n)
- 有一種情況,呼叫
exit()
會從main return
會不等價- using
setvbuf()
orsetbuf
時使用main()
的區域變數時 (ch13.2)
- using
25.2 細說行程的終止
行程終止的過程
終止過程中,會有下列動作:
- close descriptors (ch18.8)
- open file descriptors
- directory stream
- message catalog descriptor
- conversion descriptor
- close file lock of the process (ch55)
- unattached System V share memory block (ch48.8)
- 對於行程已設定 semadj 值的每個 semaphore,會將 semadj 值加到 semaphore 的值 (ch 47.8)
- 若為 controlling terminal 的 controlling process,則會將
SIGHUP
訊號傳給每個行程的控制終端機的 foreground process gruop,並移除 termnal 與 session 的關聯 (ch34.6) - 呼叫類似
sem_close()
的方式關閉 semaphore - 呼叫類似
mq_close()
的方式關閉 message queue - 此行程結束會使得此行程孤立狀態,群組中每個行程都會收到
SIGHUP
訊號,並接著收到SIGCONT
訊號 (ch34.7.4) - 移除使用
mlock()
andmlockall()
所建立的 memory lock (ch50.2) - 移除此行程使用
mmap()
建立的記憶體映射
25.3 結束處理常式 (Exit Handler)
應用程式有時要在行程結束時自動執行一些操作,以應用程式的函式庫為例,若在行程的生命週期期間有用到函式庫,則當該行程離開時,需要進行一些自動清理動作。因為函式庫無法掌控行程何時結束,也不能授權主程式在結束之前一定要呼叫一個函式庫指定的清理函式,這樣就不能保證行程結束時會做清理動作。
因此使用結束處理常式,在 System V 所使用的術語是程式終止常式 (program termination routine)
結束處理常式
- 通常是由程式設計是提供的函式
- 在行程的生命週期內在一些位址上註冊
- 在行程進行正常終止期間透過
exit()
自動呼叫 - 若程式直接呼叫
_exit()
則不會呼叫 Exit Handler - 以信號 (signal) 終止行程並不會呼叫 Exit Handler
- 所以最佳的作法就是盡可能幫行程收到的訊號建立 Exit Handler
- 但是依舊無法處理
SIGKILL
- 因此使用者要避免使用
SIGKILL
並改用SIGTERM
註冊 Exit Handler
function declare
1 |
|
由定義中的 function pointer 我們可以知道
- 函式不應該傳遞參數
函式不應該有回傳值
1
2
3
4void func(void)
{
/* progress */
}atexit()
在出錯時會傳回「非零值」(不一定是 -1)- 一個行程可以註冊多個 Exit Handler,甚至可以多次註冊相同的 Exit Handler
- 在程式呼叫
exit()
時,呼叫這些函式的順序會與註冊時相反 簡單的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/* atexit example */
void fnExit1 (void)
{
puts ("Exit function 1.");
}
void fnExit2 (void)
{
puts ("Exit function 2.");
}
int main ()
{
atexit (fnExit1);
atexit (fnExit2);
puts ("Main function.");
return 0;
}最後的結果為
1
2
3
4$ /tmp/test
Main function.
Exit function 2.
Exit function 1.若某個 Exit Handler 呼叫了
_exit()
或是收到了訊號終止,則不會繼續呼叫剩餘的 Exit Handler- 若 Exit Handler 自己呼叫
exit()
,則會有不可預期的後果,應避免在 Exit Handler 中使用exit()
- Linux 會繼續呼叫剩餘的 Exit Handler
- 有些系統會進入遞迴,直到 stack overflow 後才終止
- SUSv3 要求系統要能讓一個行程至少可註冊 32 個 Exit Handler,在 Linux 若使用
sysconf(_SC_ATEXIT_MAX);
則會 return2,147,483,647
,換句話說,可能未到達上限之前,記憶體就不足導致問題發生了 - 透過
fork()
建立的 child process 會繼承 parent process 的註冊 Exit Handler 複本 atexit()
的兩個限制atexit()
無法傳遞狀態給exit()
。有時候狀態是必要的- 無法給 Exit Handler 傳遞參數,這樣表示只能透過全域變數旗標來區分 Exit Handler 較細部的動作
為了擺脫上述的限制,glibc 提供了一個非標準替代方法:on_exit()
on_exit()
的討論
on_exit()
function declare
1 |
|
由以上,我們可以知道
on_exit()
所註冊的函式要依循1
2
3
4void func(int status, void *arg)
{
/* Perform cleanup actions */
}void *arg
這個參數可以帶入各種型態,或是使用轉型(cast),傳入純量on_exit(onExitFunc, (void *)10);
- Example
1
2
3
4
5
6
7
8
9
10
11
12int main(int argc, char *argv[])
{
if (on_exit(onexitFunc, (void *) 10) != 0)
fatal("on_exit 1");
if (atexit(atexitFunc1) != 0)
fatal("atexit 1");
if (atexit(atexitFunc2) != 0)
fatal("atexit 2");
if (on_exit(onexitFunc, (void *) 20) != 0)
fatal("on_exit 2");
exit(2);
}
25.4 fork(), stdio buffer 與 _exit()
之間的關係
程式碼
1 | int main(int argc, char *argv[]) |
執行結果
分成兩部分
- 直接透過 terminal 輸出
- 導入至文件
分析為何會輸出兩次:
printf()
是一個緩衝輸出函式,由 glibc 所提供,為一個標準函式而非系統調用printf()
對於 FILE I/O 的 FILE stream,需要滿足以下才會從緩衝區輸出至文件- 緩衝區已滿
- 寫入的字元含有 ‘\n’ ‘\r’
- 調用
fflush()
清空緩衝區 - 調用
scanf()
要從緩衝區讀取內容時
- 因此
fork()
後就會將緩衝區複製一份 - 調用
exit()
的時候 parent/child process 就會各自清空自己的緩衝區
因此我們可以使用兩種方式處理
- 使用
fflush()
或是setvbuf()
來清空、設定緩衝區 - 子行程調用
_exit()
而非exit()
- 設計通用原則:父行程調用
exit()
,由父行程產生的子行程則是調用_exit()
從而確保只有一個進程調用 Exit Handler 並刷新 stdio buffer
- 設計通用原則:父行程調用
分析為何 write()
會優先執行
因為 write()
是系統調用,所以會直接輸出到 kerenl space 的緩衝區,而 printf() 是 user space 的緩衝區
Exercise
Question
如果 fork()
出的子行程執行 exit(-1),父程序會收到哪個數值 (使用 WEXITSTATUS
回傳)
Answer
Practice code
我最後練習的程式碼如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int main()
{
int pid = 0;
int status = 0;
int c = 0;
//atexit(end);
pid = fork();
if (pid == 0) {
printf("Child: This is the CHILD ID = %d\n", getpid());
while (c < 5) {
sleep(1);
printf("CHLID TIMER: %d\n",c);
c++;
}
printf("Child is over..\n");
exit(-1);
} else if (pid > 0) {
printf("This is parent process, pid = %d\n", getpid());
sleep(3);
wait(&status);
printf("Parent: status = %d\n", status);
printf("Parent: WEXITSTATUS(status) = %d\n", WEXITSTATUS(status));
}
return 0;
}
執行結果
1 | weintek-timmy@weintek-timmy:~/Timmy/presentation/tlpi_chapter25$ !gcc |
結果分析
我們先來看 wait()
到底做了些什麼?
1 | WEXITSTATUS(wstatus); |
很合理,只看最後 8 bit,也就是 255,所以 WEXITSTATUS(status)
status 的討論
其實討論 status 用此方式,似乎不是很合理,畢竟在子行程調用 exit(-1)
有點奇怪,不過我試了一下,Linux 似乎會將 exit()
的值左移 8 bit,也就是 status * 256
1 | int main() |
結果如下
1 | weintek-timmy@weintek-timmy:~/Timmy/presentation/tlpi_chapter25$ ./test |
這部分我還要找些資料才會知道,不過可以確認的是,必須使用 WEXITSTATUS()
才能正確收到 child process 真正 return 的值
Reference
- https://stackoverflow.com/questions/13328332/waitpid-wrong-usage
- https://stackoverflow.com/questions/26435730/forked-child-exits-with-1-but-wexitstatus-gets-255
- https://stackoverflow.com/questions/52731842/how-to-get-the-return-value-of-child-process-to-its-parent-which-was-created-usi
- https://en.cppreference.com/w/c/io/setvbuf
- http://www.cplusplus.com/reference/cstdlib/atexit/
- http://man7.org/linux/man-pages/man2/waitpid.2.html