操作系統: 進程和線程

進程概念

  • 進程是執行的程序。

  • 進程不止是程序代碼,程序代碼有時稱爲文本段(text section)(或代碼段 (code section))。進程還包括當前活動,如程序計數器(Program Counter, PC) 的值和處理器寄存器的內容等。另外進程通常還包括:進程堆棧(stack)(包括臨時數據,如函數參數、返回地址和局部變量)和數據段(data section)(包括全局變量)。進程還可能包括堆(heap),這是進程運行時動態分配的內存。

    內存中的進程

  • 隨着進程的運行,進程的狀態可能會發生改變。進程的狀態可能包括:創建、運行、就緒、等待、終止。

  • 在操作系統內,進程可以用它的**進程控制塊(PCB)**來表示。每個進程控制塊包括:進程狀態、程序計數器(指定進程要執行的下條指令地址)、CPU 寄存器、CPU 調度信息、內存管理信息、統計信息、I/O 狀態信息。

PCB

進程調度

  • 當進程不執行時,進程處於某個等待隊列。操作系統有兩種主要隊列:I/O 請求隊列和就緒隊列。就緒隊列包括所有準備執行並等待執行 CPU 的進程。
  • 長期調度程序或作業調度程序從大容量存儲設備的緩衝池中選擇進程加到內存,以便執行。短期調度程序或 CPU 調度程序從準備執行的進程中選擇進程,並分配CPU。二者的區別主要體現在執行的頻率不同。
  • 有些操作系統如分時系統,可能引入一個額外的中期調度程序,可將進程從內存中換出,從而降低多道程序程度。
  • 將 CPU 切換到另一個進程需要保存當前進程狀態並恢復另一個進程的狀態,此任務稱爲 上下文切換(context switch)

進程運行

進程創建與執行

  • 進程在其執行過程中能通過創建進程系統調用(create-process system call)創建多個新進程。創建進程稱爲 父(parent)進程,被創建的新進程稱爲 子(children)進程 。每個新進程可以再創建其他進程以形成 進程樹(tree)

  • 多數操作系統根據一個唯一的 進程標識符(process identifier,pid) 來識別進程,通常是整數值。

  • 一個進程創建子進程時,子進程可能通過以下方式獲取資源:

    • 從操作系統直接獲取資源
    • 共享父進程資源
  • 父進程與子進程存在兩種執行關係:

    • 父進程和子進程併發執行
    • 父進程等待,直到某個或全部子進程執行完
  • 新進程的地址空間有兩種可能

    • 子進程時父進程的複製品
    • 子進程裝入另一個新程序
  • 在 UNIX 操作系統中,通過 fork() 系統調用來創建新進程,新進程的地址空間複製了原來進程的地址空間。這種機制允許父進程與子進程輕鬆通信。通常,在系統調用 fork() 後,有個進程使用系統調用 exec(),以用新程序來取代進程的內存空間並開始執行。這種方式使得兩個進程能相互通信,且按各自方式運行。如果父進程在子進程運行時沒什麼可做,那麼它可以採用系統調用 wait() 把自己移出就緒隊列,直到子進程終止。

Linux 進程生成

  • Windows 的進程創建採用 CreateProcess(), 類似於 fork(),不過,fork() 讓子進程繼承父進程的地址空間,而 CreateProcess() 需要加載到子進程的地址空間。

進程終止

  • 進程完成執行最後的語句並使用系統調用 exit() 時進程終止,此時進程可以返回狀態值(通常爲整數)給父進程(通過 wait() 獲取),所有進程資源也會被操作系統釋放。
  • 進程可以通過系統調用終止另一個進程,通常只有被終止進程的父進程才能執行該操作。
  • 有的系統中,在父進程退出時,其所有子進程也終止,此現象稱爲級聯終止

進程間通信

  • 操作系統內併發進行的進程可以爲獨立進程或協作進程。如果一個進程不能影響其他進程,也不能被其他進程所影響,則改進程時獨立的,否則改進程是協作的。

  • 進程協作需要進程間通信機制來允許進程相互交換數據,其包括兩種基本模式:

    • 共享內存: 通信進程需要建立內存共享區域。共享內存方法要求,通信進程共享一些變量,進程通過使用這些共享變量來交換信息;共享內存系統,提供通信的責任主要在應用程序員上。
    • 消息傳遞:在分佈式環境中非常有用,通信進程之間必須有通信線路。消息系統方法允許進程交換信息,提供通信的責任主要在應用程序員上。對於直接通信,採用對稱尋址,每個進程必須明確地命名通信的接收者或發送者;間接通信中,通過 郵箱(mailboxes) 或 端口(ports) 來發送和接收消息,每個郵箱都有唯一的標識符,兩個進程僅在其共享至少一個郵箱時可相互通信,一個進程可通過許多不同的郵箱和其他進程通信。

    進程間通信

客戶機/服務器通信

  • 以上兩種進程間通信技術能夠用於客戶機/服務器系統的通信。
  • 客戶機/服務器系統還包括其他三種通信策略:套接字、遠程過程調用和管道。
  • 套接字雖然通用和高效,但是在分佈式進程之間屬於一種低級形式的通信。一個原因是,套接字只允許在通信線程之間交換無結構的字節流。客戶機或服務器程序需要自己加上數據結構。
  • 遠程過程調用 (Remote Procedure Call, RPC) 對於通過網絡連接系統之間的過程調用進行了抽象。遠程過程調用需要解決主機大小端的數據表示問題,且要解決正好調用一次的調用語義。
  • 管道 (pipe) 是一條在進程間以字節流方式傳送數據的通信通道,有 OS 核心的緩衝區(通常幾十KB)來實現,只能單向傳輸。
  • 管道分爲普通管道(又叫匿名管道)和命名管道,匿名管道允許一對進程通信,而且只有當進程相互通信時,普通管道才存在。命名管道通信可以是雙向的(但是是半雙工的),而且通信進程之間的父子關係不是必需的,當建立一個命名管道後,多個進程都可用它通信。

線程概念

線程是進程內的控制流。線程是 CPU 使用的基本單元,由線程 ID、程序計數器、寄存器集合和堆棧組成。它與同一進程的其他線程共享代碼段、數據段和其他操作系統資源,如打開文件和信號。

線程的概念

多線程的優點包括:用戶響應的改進、進程內資源的共享、經濟和可擴展性的因素(如更有效地使用多個處理核)。

多線程模型

有兩種方法提供線程支持:用戶層的用戶線程(user thread)和內核層的內核線程(kernal thread)。用戶線程位於內核之上,它的管理無需內核支持;而內核線程由操作系統來直接支持與管理。幾乎所有的現代操作系統都支持內核線程。用戶線程對程序員是可見的,而對內核是未知的。通常。用戶線程與內核線程相比,創建和管理要更快,因爲它並不需要內核干預。

三種不同類型模型關聯用戶線程和內核線程。多對一模型將多個用戶線程映射到一個內核線程;一對一模型將每個用戶線程映射到一個對應內核線程;多對多模型將多個用戶線程在同樣(或更少數量)的內核線程之間切換。

線程庫

線程庫爲程序員提供創建和管理線程的 API。實現線程庫的主要方法有兩種:

  • 在用戶空間中提供一個沒有內核支持的庫。此庫的所有代碼和數據結構都位於用戶空間。調用庫中的一個函數只是導致用戶空間中的一個本地函數調用,而非系統調用。
  • 實現由操作系統直接支持的內核級的一個庫,庫內的代碼和數據結構位於內核空間。調用庫中的一個 API 函數通常會導致對內核的系統調用。

常用的主要線程庫有三個: POSIX Pthreads、Windows 線程和 Java 線程。

  • Pthread 創建和執行一個 C 程序樣例:
#include <pthread.h>
#include <stdio.h>
int sum = 0; /* this data is shared by the thread(s) */
void *runner(void *param);  /* thread call this thread */
int main(int argc, char *argv[]){
    pthread_t tid;        /* the thread identifier */
    pthread_attr_t attr;  /* set of thread attributes */
    if (argc != 2){
        return -1;
    }
    if (atoi(argv[1]) < 0){
        return -1;
    }
    /*  get the default attributes */
    pthread_attr_init(&attr);
    // create the thread
    pthread_create(&tid, &attr, runner, argv[1]);
    // wait for the thread to exit
    pthread_join(tid, NULL);
    printf("sum = %d\n", sum);
}

/* The Thread will begin control in this function */
void *runner(void *param){
    int i, upper = atoi(param);
    for (i = 1; i <= upper; i++)
        sum += i;
    pthread_exit(0);
}
  • Windows 線程庫創建線程的例程:
#include <windows.h>
#include <stdio.h>
DWORD sum = 0; /* this data is shared by the thread(s) */

/* The thread runs in this separate function */
DWORD WINAPI Summation(LPVOID Param){
    DWORD Upper = *(DWORD*)Param;
    for (DWORD i = 0; i <= Upper; i++){
        Sum += i;
    }
    return 0;
}

int main(int argc, char *argv[]){
    DWORD ThreadId;
    HANDLE ThreadHandle;
    int Param;
    if (argc != 2){
        return -1;
    }
    if ((Param = atoi(argv[1])) < 0){
        return -1;
    }

    /* Create the thread */
    ThreadHandle = CreateHandle(
        NULL,       // default security attributes
        0,          // default stack size
        Summation,  // thread function
        &Param,     // parameter to thread function
        0,          // default creation flags
        &ThreadId); // returns the thread identifier
    if (ThreadHandle != NULL){
        /* wait for the thread to finish */
        WaitForSingleObject(ThreadHandle, INFINITE);

        /* Close the thread handle */
        CloseHandle(ThreadHandle);
        printf("sum = %d\n", Sum);
    }
}

隱式多線程

除了採用線程庫 API 來顯式創建線程,也可以使用隱式線程,這種線程的創建和管理交由編譯器和運行時庫來完成。隱式線程方法包括:線程池、OpenMP 和 Grand Central Dispath 等。

如果允許所有的併發請求都通過新線程創建來處理,則沒法限制系統內的併發執行線程的數量。無限制的線程可能耗盡系統資源,如 CPU 時間和內存。解決這一問題的一種方法是使用線程池(Thread Pool)。

線程池的主要思想是:在進程開始時創建一定數量的線程,並加到池中以等待工作。當服務器收到請求時,它會喚醒池內的一個線程(如果有可用線程),並將服務的請求傳遞給它。一旦線程完成服務,它會返回到池中再等待工作。如果池內沒有可用線程,那麼服務器會等待,直到有空線程爲止。

線程池具有以下優點:

  • 用現有線程服務請求比等待創建一個線程更快
  • 線程池限制了何人時候可以使用的線程數量
  • 將要執行任務從創建任務的機制中分離出來,允許我們採用不同策略運行任務

多線程問題

多線程程序爲程序員帶來了許多挑戰,包括 fork() 和 exec() 系統調用的語義。其他問題包括信號處理、線程撤消、線程本地存儲(Thread-Local Storage, TLS)和調度激活等。

TLS 與局部變量容易混淆。局部變量只有再單個函數調用時纔可見;而 TLS 在多個函數調用時都可見。在某些方面,TLS 類似靜態 (Static) 數據,不同的是,TLS 數據是每個線程特有的。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章