The Linux Programming Interface : Chapter 25: 終止行程

本章節介紹如下

  1. 介紹如何使用 exit() 以及 _exit() 結束行程
  2. 探討行程在呼叫 exit() 函式時,會使用結束處理常式 exit handler 進行自動管理
  3. 探討 fork()stdio緩衝區,以及exit()之間的互動

25.1 終止行程: exit()_exit()

_exit():終止行程的 system call

有兩種方式可以終止行程

  • 異常終止(abnormal):如 20.1 節所描述,由訊號 signal 觸發,預設訂做是終止行程,可產生核心傾印檔 (core dump file)
  • 使用 _exit() 系統呼叫將行程正常終止

function declare:

1
2
3
#include <unistd.h>

void _exit(int status);

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
2
3
#include <stdlib.h>

void exit(int status);

或是輸入 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() or setbuf 時使用 main() 的區域變數時 (ch13.2)

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() and mlockall() 所建立的 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
2
3
4
#include <stdlib.h>

/* Returns 0 on success, or nonzero on error */
int atexit(void (*func)(void));

由定義中的 function pointer 我們可以知道

  • 函式不應該傳遞參數
  • 函式不應該有回傳值

    1
    2
    3
    4
    void 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 */
    #include <stdio.h> /* puts */
    #include <stdlib.h> /* atexit */

    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); 則會 return 2,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
2
3
4
#include <stdlib.h>

/* Return 0 on success, or nonzero on error */
int on_exit(void (*func)(int, void*), void *arg);
  • 由以上,我們可以知道 on_exit() 所註冊的函式要依循

    1
    2
    3
    4
    void 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
    12
    int 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
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char *argv[])
{
printf("Hello world\n");
write(STDOUT_FILENO, "Ciao\n", 5);

if (fork() == -1)
errExit("fork");

/* Both child and parent continue execution here */

exit(EXIT_SUCCESS);
}

執行結果

分成兩部分

  • 直接透過 terminal 輸出
  • 導入至文件

分析為何會輸出兩次:

  • printf() 是一個緩衝輸出函式,由 glibc 所提供,為一個標準函式而非系統調用
  • printf() 對於 FILE I/O 的 FILE stream,需要滿足以下才會從緩衝區輸出至文件
    • 緩衝區已滿
    • 寫入的字元含有 ‘\n’ ‘\r’
    • 調用 fflush() 清空緩衝區
    • 調用 scanf() 要從緩衝區讀取內容時
  • 因此 fork() 後就會將緩衝區複製一份
  • 調用 exit() 的時候 parent/child process 就會各自清空自己的緩衝區

因此我們可以使用兩種方式處理

  1. 使用 fflush() 或是 setvbuf() 來清空、設定緩衝區
  2. 子行程調用 _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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

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
2
3
4
5
6
7
8
weintek-timmy@weintek-timmy:~/Timmy/presentation/tlpi_chapter25$ !gcc
gcc -o test test.c
weintek-timmy@weintek-timmy:~/Timmy/presentation/tlpi_chapter25$ ./test
This is parent process, pid = 12279
Child: This is the CHILD ID = 12280
Child is over..
Parent: status = 65280
Parent: WEXITSTATUS(status) = 255

結果分析

我們先來看 wait() 到底做了些什麼?

1
2
3
4
5
6
7
8
WEXITSTATUS(wstatus);
/*
* returns the exit status of the child. This consists of the
* least significant 8 bits of the status argument that the child
* specified in a call to exit(3) or _exit(2) or as the argument
* for a return statement in main(). This macro should be
* employed only if WIFEXITED returned true.
*/

很合理,只看最後 8 bit,也就是 255,所以 WEXITSTATUS(status)

status 的討論

其實討論 status 用此方式,似乎不是很合理,畢竟在子行程調用 exit(-1) 有點奇怪,不過我試了一下,Linux 似乎會將 exit() 的值左移 8 bit,也就是 status * 256

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
int pid = 0;
int status = 0;

pid = fork();

if (pid == 0) {
printf("Child: This is the CHILD ID = %d\n", getpid());
printf("Child is over..\n");
exit(1);
} else if (pid > 0) {
printf("This is parent process, pid = %d\n", getpid());
wait(&status);
printf("Parent: status = %d\n", status);
printf("Parent: WEXITSTATUS(status) = %d\n", WEXITSTATUS(status));
}
return 0;
}

結果如下

1
2
3
4
5
6
weintek-timmy@weintek-timmy:~/Timmy/presentation/tlpi_chapter25$ ./test
This is parent process, pid = 13677
Child: This is the CHILD ID = 13678
Child is over..
Parent: status = 256
Parent: WEXITSTATUS(status) = 1

這部分我還要找些資料才會知道,不過可以確認的是,必須使用 WEXITSTATUS() 才能正確收到 child process 真正 return 的值

Reference