用C語言學習wait函式|用法、殭屍行程對策與waitpid差異

目次

1. 前言

C 語言在系統程式與嵌入式系統的開發中被廣泛使用,其中程序管理是重要的主題之一。本文將說明 C 語言中的「wait 函式」。wait 函式是一個用於實現進程間同步的系統呼叫,特別有助於等待子進程結束。 透過本文,您可以從 wait 函式的基本用法到應用方法,以及相關主題(例如 waitpid 函式與僵屍進程對策)等,廣泛學習。

2. C 語言的wait函式是什麼?

wait函式概述

wait函式是 UNIX 系統中使用的系統呼叫之一,用於讓父行程等待子行程結束。藉此,父行程可以取得子行程的結束狀態。

基本操作

wait函式的使用方式如下。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid > 0) {
        // 父行程
        wait(NULL); // 等待子行程結束
    } else if (pid == 0) {
        // 子行程
        printf("子行程傳來的訊息!\n");
    }
    return 0;
}
上述程碼中,fork函式產生子行程,父行程使用wait函式等待子行程結束。

返回值與參數

  • 返回值 子行程結束時,回傳其行程 ID。若發生錯誤,回傳-1
  • 參數 透過傳入int* status作為參數,可取得子行程的結束狀態。

3. wait函式的基本用法

簡單的程式碼範例

wait函式的基本用法如下所示。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();
    if (pid > 0) {
        int status;
        wait(&status); // 取得子行程的結束狀態
        if (WIFEXITED(status)) {
            printf("Child exited with status: %d
", WEXITSTATUS(status));
        }
    } else if (pid == 0) {
        printf("Child process running...
");
        _exit(0); // 子行程的結束
    }
    return 0;
}

範例程式碼說明

  1. 使用 wait(&status) 等待子行程的結束。
  2. 使用 WIFEXITED(status) 來確認子行程是否正常結束。
  3. 使用 WEXITSTATUS(status) 取得子行程的結束代碼。
透過使用 wait 函式,父行程可以精確掌握子行程的結束狀態。

4. 殭屍行程與 wait 函式的關係

什麼是殭屍行程

殭屍行程是指子行程結束後,父行程未適當回收其結束狀態時產生的行程。在此狀態下,子行程本身已結束,但行程資訊仍持續留在系統中。 如果系統內存在大量殭屍行程,會壓迫行程表,可能導致其他行程無法正常運作。

殭屍行程的發生範例

以下是一個會產生殭屍行程的簡單範例。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();
    if (pid > 0) {
        // 父行程什麼也不做,等待
        sleep(10);
    } else if (pid == 0) {
        // 子行程
        printf("子行程即將結束...
");
        _exit(0);
    }
    return 0;
}
在此範例中,父行程未回收子行程的結束狀態,導致子行程在結束後變成殭屍狀態。

wait 函式防止殭屍行程

使用 wait 函式,父行程可以適當回收子行程的結束狀態,從而防止殭屍行程。 以下是修正版的範例。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();
    if (pid > 0) {
        // 父行程
        wait(NULL); // 等待子行程結束以防止殭屍行程
        printf("父行程:子行程已被回收.
");
    } else if (pid == 0) {
        // 子行程
        printf("子行程即將結束...
");
        _exit(0);
    }
    return 0;
}
在此程式中,透過使用 wait(NULL),父行程會等待子行程結束,從而防止殭屍行程的產生。

殭屍行程對策要點

  1. 務必使用 waitwaitpid 透過適當回收子行程的結束狀態,可防止殭屍行程。
  2. 利用訊號處理程序的方法 可捕捉 SIGCHLD 訊號,於子行程結束時自動回收。
以下是範例。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void sigchld_handler(int signo) {
    while (waitpid(-1, NULL, WNOHANG) > 0); // 回收所有已結束的子行程
}

int main() {
    signal(SIGCHLD, sigchld_handler); // 設定 SIGCHLD 的處理程序

    pid_t pid = fork();
    if (pid == 0) {
        // 子行程
        printf("子行程即將結束...
");
        _exit(0);
    } else if (pid > 0) {
        // 父行程
        printf("父行程執行其他工作...
");
        sleep(10); // 在其他工作期間觸發 SIGCHLD 處理程序
    }
    return 0;
}
此方法中,父行程即使未明確呼叫 wait,結束的子行程也會自動被回收。

5. waitpid函式的差異

waitpid函式是什麼?

waitpid函式與wait函式相同,都是用於等待子行程結束的系統呼叫,但提供更彈性的行程管理。使用waitpid可以指定特定的子行程,或以非阻塞模式等待。

基本用法與語法

waitpid函式的語法如下。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

pid_t waitpid(pid_t pid, int *status, int options);
  • pid 指定要等待的子行程 ID。以下的特殊值可用來控制行為。
  • pid > 0:指定特定的行程 ID 以等待。
  • pid == 0:等待與父行程同一行程群組內的任一子行程。
  • pid < -1:等待指定行程群組內的所有行程。
  • pid == -1:等待任意子行程(wait函式相同)。
  • status 用於存放子行程結束狀態的指標。
  • options 指定改變行為的選項。主要值如下。
  • WNOHANG:非阻塞模式。如果沒有已結束的子行程,會立即返回。
  • WUNTRACED:也將處於停止狀態的子行程納入目標。
  • 返回值
  • 正常結束時:已結束子行程的 PID。
  • 沒有子行程時:0(若指定 WNOHANG)。
  • 發生錯誤時:-1

waitpid函式範例

以下是使用waitpid函式等待特定子行程的範例。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid1 = fork();
    if (pid1 == 0) {
        // 子行程 1
        printf("子行程 1 正在執行...
");
        sleep(2);
        _exit(1);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // 子行程 2
        printf("子行程 2 正在執行...
");
        sleep(4);
        _exit(2);
    }

    int status;
    // 等待特定的子行程 pid1
    pid_t ret = waitpid(pid1, &status, 0);
    if (ret > 0 && WIFEXITED(status)) {
        printf("子行程 1 結束,狀態碼: %d
", WEXITSTATUS(status));
    }

    // 等待剩餘的子行程
    waitpid(pid2, &status, 0);
    printf("子行程 2 結束,狀態碼: %d
", WEXITSTATUS(status));

    return 0;
}

waitwaitpid的主要差異

項目wait函式waitpid函式
等待目標任意的子行程可指定特定子行程
阻塞始終阻塞可使用非阻塞
選項指定不可WNOHANGWUNTRACED
彈性有限

waitpid適用情況

  • 想要管理特定子行程的情況 程式產生多個子行程,且需要個別控制時適用。
  • 需要非同步處理的情況 想在不阻塞其他處理的情況下確認行程結束狀態時,WNOHANG選項很方便。

waitwaitpid的選擇指南

  1. 簡單的程式 使用wait函式已足夠。只要等待任意子行程,則不需要彈性。
  2. 複雜的行程管理 時,建議使用waitpid函式。特別是需要非同步處理或控制特定行程時,使用waitpid可更有效率地管理。

6. 多執行緒環境中的同步處理

程序同步與執行緒同步的差異

在 C 語言中,程序與執行緒是不同的管理單位。程序同步(例如 waitwaitpid 函式)用於控制多個程序之間的結束狀態與資源共享。另一方面,執行緒同步則在同一程序內的執行緒之間進行資源管理與順序控制。

多執行緒環境中的同步處理

在執行緒之間進行同步時,通常使用條件變數或互斥鎖。此處說明利用 pthread 函式庫的條件變數進行同步的方法。

使用條件變數的同步基礎

使用條件變數(pthread_cond_t)即可有效地在執行緒之間進行等待與通知。

條件變數的基本函式

  • pthread_cond_wait 等待條件滿足為止。等待期間會釋放互斥鎖。
  • pthread_cond_signal 喚醒一個正在等待的執行緒。
  • pthread_cond_broadcast 喚醒所有正在等待的執行緒。

使用條件變數的同步範例

以下是一個使用條件變數在多個執行緒之間同步的簡單範例。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 互斥鎖與條件變數的初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int shared_data = 0;

void* producer(void* arg) {
    pthread_mutex_lock(&mutex);

    printf("Producer: producing data...
");
    shared_data = 1;

    // 使用條件變數通知
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);

    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);

    // 等待條件滿足為止
    while (shared_data == 0) {
        printf("Consumer: waiting for data...
");
        pthread_cond_wait(&cond, &mutex);
    }

    printf("Consumer: consumed data!
");
    pthread_mutex_unlock(&mutex);

    return NULL;
}

int main() {
    pthread_t producer_thread, consumer_thread;

    // 建立執行緒
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    sleep(1); // 讓消費者先等待的睡眠
    pthread_create(&producer_thread, NULL, producer, NULL);

    // 等待執行緒結束
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    return 0;
}

範例程式碼說明

  1. 互斥鎖的利用 使用 pthread_mutex_lockpthread_mutex_unlock 來控制對共享資源的排他存取。
  2. 條件變數的等待與通知
  • 消費者執行緒會使用 pthread_cond_wait 等待 shared_data 更新。
  • 生產者執行緒會使用 pthread_cond_signal 通知消費者執行緒。
  1. 執行緒間的協調動作 實現了生產者產生資料,消費者接收資料的簡單流程。

執行緒同步的注意事項

  • 防止死結 需要注意互斥鎖的鎖定與解鎖順序。
  • 避免競爭條件 當執行緒同時嘗試修改條件時,需要正確結合條件變數與互斥鎖。
  • 考慮可擴展性 為了在多執行緒間有效同步,需盡量減少不必要的等待與鎖定。

在多執行緒環境中使用 wait 函式的注意事項

wait 函式是用於程序間同步的,並不適用於執行緒層級的同步。在執行緒間同步時,使用條件變數或互斥鎖較為安全且有效率。

7. 疑難排解與最佳實踐

常見錯誤與對策

C 語言中使用 waitwaitpid 時,可能會發生一些典型的錯誤。以下說明其原因與解決方案。

1. wait函式回傳錯誤

原因
  • 子行程不存在的情況。
  • 系統呼叫被中斷的情況(EINTR 錯誤)。
解決方案
  • 確認子行程是否存在。
  • 若系統呼叫被中斷,則在迴圈中重新嘗試。
int status;
while (wait(&status) == -1) {
    if (errno != EINTR) {
        perror("等待失敗");
        break;
    }
}

2. 僵屍行程產生

原因
  • 父行程未回收子行程的結束。
解決方案
  • 在父行程中適當使用 waitwaitpid
  • 設定訊號處理程序,讓結束自動被回收。

3. 競爭條件導致的不穩定行為

原因
  • 多個父行程嘗試等待同一子行程的情況。
  • 子行程的結束狀態未被正確回收。
解決方案
  • 若需明確指定行程 ID,請使用 waitpid
  • 設計時避免多個父行程同時管理同一子行程。

最佳實踐

1. waitwaitpid 的適當選擇

  • 在簡單程式中 wait 已足夠。
  • 在需要複雜行程管理(特定子行程的控制或非同步處理)時,使用 waitpid

2. 訊號處理程序的活用

使用訊號處理程序,即使父行程未明確呼叫 wait,也能自動回收子行程的結束狀態。這樣既可防止僵屍行程,又能保持父行程程式碼簡潔。

3. 錯誤處理的徹底

waitwaitpid 為系統呼叫,可能會產生錯誤。請對所有呼叫檢查返回值,並適當處理。
pid_t pid = wait(NULL);
if (pid == -1) {
    perror("等待錯誤");
}

4. 非同步處理的實作

在執行非同步處理時,建議採用以下設計。
  • 不阻塞主流程,定期以 WNOHANG 方式呼叫 waitpid
  • 輪詢子行程的結束狀態,並在需要時回收。
例:
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
    printf("子行程 %d 已終止。\n", pid);
}

5. 行程數量的管理

在產生大量子行程的程式中,需要控制行程數量。
  • 限制同時產生的子行程數量。
  • 採用在子行程結束前不產生新行程的設計。

8. 常見問題(FAQ)

Q1: wait 函式如何在不使用的情況下防止僵屍行程?

A: wait 函式即使不使用,也可以透過使用訊號處理程序來防止僵屍行程。父行程捕捉 SIGCHLD 訊號,即可自動回收子行程的結束狀態。 以下是使用訊號處理程序的範例。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void handle_sigchld(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

int main() {
    signal(SIGCHLD, handle_sigchld);

    if (fork() == 0) {
        // 子行程
        _exit(0);
    }

    // 父行程的處理
    sleep(5);
    printf("Parent process completed.
");
    return 0;
}

Q2: 什麼情況下應該使用 waitpid

A: waitpid 適用於以下情況。
  • 只想等待特定的子行程時(指定 pid)。
  • 想以非同步方式管理行程時(使用 WNOHANG 選項)。
  • 想確認處於停止狀態的子行程時(使用 WUNTRACED 選項)。

Q3: 為何 wait 函式不返回值?

A: 導致 wait 函式不返回值(返回 -1)的原因可能如下。
  1. 子行程不存在。
  • 行程可能已經結束,或 fork 失敗。
  1. 系統呼叫被中斷(EINTR 錯誤)。
  • 若因訊號中斷,請在迴圈中重新嘗試。
int status;
pid_t pid;
while ((pid = wait(&status)) == -1) {
    if (errno != EINTR) {
        perror("wait error");
        break;
    }
}

Q4: 在多執行緒程式中安全使用 wait 函式的方法?

A: 在多執行緒程式中使用 wait 時,請注意以下要點。
  • wait 在行程層級運作,若要在執行緒層級同步,請使用條件變數或互斥鎖。
  • 子行程的管理通常限制在主執行緒較為安全。其他執行緒呼叫 wait 可能會導致未預期的行為。

Q5: 有 wait 函式的替代方案嗎?

A: 以下提供替代方案。
  • waitpid 函式 具備高度彈性,可控制特定行程。
  • 訊號處理程序 以非同步方式處理子行程的結束。
  • 事件驅動程式設計 也可以使用事件迴圈(例如 selectpoll)來管理行程結束。

Q6: 若子行程無法結束,該如何處理?

A: 子行程可能處於停止狀態。此時請依以下步驟處理。
  1. 使用 WUNTRACED 選項確認處於停止中的子行程。
  2. 視需要使用 kill 系統呼叫終止子行程。

Q7: wait 函式取得的結束狀態碼代表什麼?

A: wait 函式取得的 status 包含以下資訊。
  • 正常結束WIFEXITED(status) 為真時,可透過 WEXITSTATUS(status) 取得結束代碼。
  • 異常結束:若因訊號結束,WIFSIGNALED(status) 為真,結束訊號可由 WTERMSIG(status) 取得。
int status;
wait(&status);
if (WIFEXITED(status)) {
    printf("Exited with code: %d
", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
    printf("Killed by signal: %d
", WTERMSIG(status));
}

Q8: 如何同時管理多個子行程?

A: 若要管理多個子行程,通常會在迴圈中使用 waitpid 來回收所有行程。
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, 0)) > 0) {
    printf("Child process %d exited.
", pid);
}

9. 總結

在本文中,我們以 C 語言的 wait 函式為中心,從基本用法到應用範例、相關主題(waitpid 函式與僵屍行程對策)進行了全面說明。以下整理出重要的要點。

wait函式的基本

  • wait函式是讓父行程等待子行程結束,並回收其結束狀態的系統呼叫。
  • 使用 wait(NULL),即可在不檢查結束狀態的情況下簡單地等待子行程。

應用與相關主題

  • 僵屍行程對策 如果不回收子行程的結束狀態,會產生僵屍行程。適當使用 waitwaitpid 可防止此情況。
  • waitpid函式 在需要指定特定子行程等待,或想執行非同步處理時,非常有用。
  • 訊號處理程式 利用 SIGCHLD 訊號,即使父行程未明確呼叫 wait,也能自動回收結束狀態。

多執行緒環境的注意事項

  • wait函式用於行程間的同步,但在執行緒層級的同步應使用條件變數或互斥鎖。
  • 重要的是設計時避免多個執行緒同時呼叫 wait

故障排除與最佳實踐

  • 務必檢查 waitwaitpid 的返回值,並適當處理錯誤。
  • 若需要非同步處理,可利用 WNOHANG 選項實現更有效的行程管理。
  • 保持設計簡潔,避免不必要的行程產生與複雜的依賴關係。

接下來應學習的內容

在了解 wait 函式與行程同步的基礎之後,建議學習以下主題:
  • 行程間通訊(IPC) 使用管道、訊息佇列、共享記憶體進行資料交換的方法。
  • 非同步程式設計 事件驅動程式設計與非同步 I/O 的活用方式。
  • fork函式與行程產生的細節 子行程產生時的記憶體管理與行程分支行為。
透過本文,希望您能深入了解 C 語言的 wait 函式及其相關知識。若能實現適當的行程管理,將有助於開發高效且穩定的程式。
侍エンジニア塾