併發編程(二)

前言

在第三代計算機的發展中由於出現了多道技術使得同一時刻能夠在內存中同時運行多個程序,那麼這樣就產生了進程的概念.linux

在本篇博客中將介紹進程相關的理論知識,對操做系統進一步的認識.算法

進程

什麼是進程

進程( process),是指計算機中已運行的程序.進程曾經是分時系統的基本運行單位.在面向進程設計的系統(如早期的 UNIX,Linux2.4及更早的版本)中,進程是程序的基本執行實體,是操做系統的資源單位;在面向線程設計的系統(如當代多數操做系統, Linux2.6及更新的版本)中,進程自己不是基本運行單位,運行單位變爲線程,進程是線程的容器.程序自己只是指令,數據及其組織形式的描述,進程纔是(指令和數據)的真正運行實例.若干進程有可能與同一個程序相關係,且每一個進程皆能夠同步(循序)或異步(平行)的方式獨立運行.現代計算機系統由於空間共享(空間複用)可在同一段時間內以進程的形式將多個程序加載到存儲器中,並藉由時間共享(時間複用),以在一個處理器上表現出異步(平行)運行的感受.windows

開啓進程須要的資源

用戶下達運行程序的命令後,就會產生進程.同一程序可產生多個進程(一對多的關係),以容許同時又多位用戶運行同一程序,卻不會產生衝突.數組

開啓進程須要一些必要的資源才能完成工做,如 CPU 使用時間,存儲器,文件以及 I/O 設備,且爲依序逐一進行,也就是每一個 CPU 內核任什麼時候間內僅能運行一項進程.安全

進程包含的資源

一個計算機系統集成包括(或者說擁有)下列資源:異步

  • 那個程序的可執行機器代碼的一個在存儲器的映像;
  • 分配到的存儲器(一般是虛擬的一個存儲器區域).存儲器的內容包括可執行代碼,特定於進程的數據(輸入或輸出),調用堆棧(用於保存運行時運輸中途產生的數據);
  • 分配給該進程的資源的操做系統描述符,諸如文件描述符( UNIX 術語)或文件句柄( Windows),數據源和數據終端;
  • 安全特性,諸如進程擁有者和進程的權限集(能夠允許的操做);
  • 處理器狀態,諸如寄存器內容,物理存儲器定址等.當進程正在運行時,狀態一般保存在寄存器,其餘數據保存在存儲器.

進程的狀態

進程在運行中,狀態會隨時發生改變.所謂狀態,就是指進程目前的動做:函數

  • 新生( new):進程新產生重;
  • 運行(running):正在運行;
  • 等待( waiting):等待某事發生,例如等待用戶輸入完成.也稱之爲阻塞;
  • 就緒( ready):等待 CPU;
  • 結束( terminated):完成運行.

各狀態名稱可能雖不一樣的操做可以系統而不一樣,對於單核系統( UP),任什麼時候間可能有多個進程爲等待,就緒,但一定僅有一個進程在運行.ui

進程表

對於一個進程來講,操做系統爲了可以在CPU離開後繼續執行該進程,會把此時進程運行的全部狀態保存下來,爲此,操做系統和會維護一張表格,即進程表( process table),每一個進程佔用一個進程表項(也稱之爲進程控制塊).spa

對於上圖中重要項的解釋以下:操作系統

  • 寄存器:如累加器,變址寄存器,堆棧指針以及通常用途寄存器,情況代碼等,主要用途在於中斷

進程時暫時存儲數據,以便稍後繼續利用;其數量及類別因計算機體系結構有所差別;

  • 程序計數器:接下來要運行的指令地址;
  • 進程狀態:能夠是 new,ready,running,waiting,blocked或 terminated;
  • 優先級( CPU 排班法):優先級,排班隊列等指針以及其餘參數;
  • 存儲管理:如標籤頁表,正文段指針,數據段指針以及堆棧指針等;
  • 會計信息:如 CPU 遇實際時間值使用數量,時限,帳號,工做或進程號;
  • 輸入輸出狀態:配置進程使用 I/O 設備,如磁帶機.

Unix進程

類 Unix 系統進程概念

Unix進程PID

在類 Unix 系統中可使用 ps 命令查詢正在運行的進程,好比 ps -eo pid,comm,cmd,下圖爲執行結果:(-e 表示列出所有進程, -o pis,comm,cmd 表示咱們須要 PID,COMMAND,CMD 信息)

每一行表明一個進程.每一行分爲三列.第一列爲 PID(Process IDentity)是一個整數,每個進程都有一個惟一的 PID 來表示本身的身份,進程也能夠根據 PID 來識別其餘的進程.第二列 COMMAND 是該進程的簡稱.第三列 CMD 是進程所對應的程序以及運行時所帶的參數.(第三列有一些由[]括起來的,它們是內核的一部分功能)

在第一行的 PID 爲1,名字爲 systemd(18.04,版本爲16.04該名字爲 init).這個進程是執行/sbin/init 這一文件(程序)產生的(不知道個人爲何不是,查看了朋友的是/sbin/init😭).當 Linux 啓動的時候, systemd 是系統建立的第一個進程,這一進程會一直存在,直到關閉計算機.

實際上,當計算機開機時,內核( kernel)只創建了一個systemd 進程. Linux 內核並不提供直接創建新進程的系統調用.剩下的全部進程都是 systemd 進程經過fork 機制創建的.新的進程要經過老的進程複製自身獲得,這就是fork.fork 是一個系統調用.進程存活於內存中.每一個進程都在內存中分配有屬於本身的一片空間(address space).當進程fork 的時候, Linux 在內存中開闢出一片新的內存空間給新的進程,並將老的進程空間中的內容複製到新的空間中,此後兩個進程同時運行.

老進程成爲新進程的父進程(parent process),而相應的,新進程就是老進程的子進程(child process).一個進程除了有一個 PID 以外,還會有一個 PPID(parent PID)來存儲父進程的 PID. 若是咱們循着 PPID 不斷向上追溯的話,總會發現其源頭是 systemd 進程.因此說,全部的進程也構成一個以 systemd 爲根的樹狀結構.

進程樹

使用 pstree命令查看進程樹:

能夠看到 systemd 進程是整個進程樹的根.

fork 一般做爲一個函數調用,這個函數會有兩次返回,將子進程的 PID 返回給父進程,0返回給子進程.實際上,子進程總能夠查詢本身的 PPID 來知道本身的父進程是誰,這樣,一對父子進程就能夠隨時查詢對方.在調用fork 函數後,程序會設計一個 if 選擇結構.當 PID 等於0時,說明該進程爲子進程,那麼讓它執行某些指令;而當 PID 爲一個正整數時,說明爲父進程,則執行另一些指令.由此,就能夠在子進程創建以後,讓它執行與父進程不一樣的功能.

子進程的終結

當子進程終結時,它會通知父進程,清空本身所佔據的內存,並在內核裏留下本身的退出信息( exit code, 若是順利運行,返回0;若是有錯誤或異常情況,爲>0的整數).在這個信息裏,會解釋該進程爲何退出.父進程在得知子進程終結時,有責任對該子進程使用 wait 系統調用.這個 wait 函數能從內核中取出子進程的退出信息,清空該信息在內核中所佔據的空間.可是,若是父進程早於子進程終結,子進程就會成爲一個孤兒(orphand)進程.孤兒進程會過繼給 systemd 進程, systemd 進程也就成了該進程的父進程. systemd 進程負責該子進程終結時調用 wait 函數,

一個糟糕的程序也徹底可能形成子進程的退出信息滯留在內核中的情況(父進程不對子進程調用 wait 函數),這樣的狀況下,子進程成爲殭屍( zombie)進程.當大量殭屍進程積累時,內存空間會被擠佔.

類 UNIX 系統進程和線程的區別

儘管在 UNIX 中,進程與線程是有聯繫但不一樣的兩個東西,但在 Linux 中,線程只是一種特殊的進程.多個線程之間能夠共享內存空間和 IO 接口.因此,進程是 Linux 程序的惟一實現方式.

Linux建立進程

從系統調用fork 中返回時,兩個進程除了返回值 PID 不一樣外,具備徹底同樣的用戶級上下文.在子進程中, PID的值爲0.在系統啓動時有內核建立的進程1是惟一不經過系統調用fork 而建立的進程.也就是上圖的 systemd進程.

內核爲系統調用fork 完成下列操做:

  1. 爲新進程在進程表中分配一個空項;
  2. 爲子進程賦一個唯一的進程標識號 PID;
  3. 作一個父進程上下文的邏輯副本.因爲進程的某些部分,如正文區,可能被幾個進程所共享,因此內核有時只要增長某個區的引用數便可,而不是真的將該區拷貝到一個新的內存物理區;
  4. 增長與該進程相關聯的文件表和索引節點表的引用數;
  5. 對父進程返回子進程的進程號,對子進程返回零.

下面是系統調用fork 的算法.內核首先確信有足夠的資源來完成fork. 若是資源不知足要求,則系統調用fork 失敗.若是資源知足要求,內核在進程表中找一個空項,並開始構造子進程的上下文.

輸入:無

輸出:對父進程是子進程的 PID, 對子進程是0

{

    檢查可用的內核資源

    取一個空閒的進程表項和惟一的 PID 號

    檢查用戶沒有過多的運行進程

    將子進程的狀態設置爲'建立'狀態

    將父進程的進程表中的數據拷貝到子進程表中

    當前目錄的索引節點和改變的根目錄(若是能夠)的引用數加1

    文件表中的打開文件的引用數加1

    在內存中作父進程上下文的拷貝

    在子進程的系統級上下文中壓入虛設系統級上下文層

        /* 虛設上下文層中含有使子進程能

        /* 識別本身的數據,使子進程被調度時

        /* 從這裏開始運行
if (正在執行的進程是父進程){

            將子進程的狀態設置爲'就緒'狀態

            return (子進程的 PID)           //從系統到用戶

    }

    else    {

            初始化計時區

            return 0;

    }

}

來看下面的例子.該程序說明的是通過系統調用fork 以後,對文件的共享存取.用戶調用改程序時應有兩個參數,一個是已經有的文件名;另外一個是要建立的新文件名.該進程打開已有的文件,建立一個新文件,而後假定沒有碰見錯誤,它調用fork 來建立一個子進程.子進程能夠經過使用相同的文件描述符來繼承的存取父進程的文件(即父進程已經打開和建立的文件).

固然,父進程和子進程要分別獨立的調用rdwrt 函數並執行一個循環,即從源文件中讀一個字節,而後寫一個字節到目標文件中去.當系統調用 read 碰見文件尾時,函數rdwrt 當即返回.

#include <fcntl.h>

int fdrd, fdwt;
char    c;

main(int argc, char *argv[])
{
    if (argc != 3) {
        exit(1);
    }
    if ((fdrd = open(argv[1], O_RDONLY)) == -1) {
        exit(1);
    }
    if ((fdwt = creat(argv[2], 0666)) == -1) {
        exit(1);
    }

    fork();
    // 兩個進程執行一樣的代碼
    rdwrt();
    exit(0);
}

rdwrt()
{
    for (;;) {
        if (read(fdrd, &c, 1) != 1) {
            return ;
        }
        write(fdwt, &c, 1);
    }
}

在這個例子中,兩個進程的文件描述符都指向相同的文件表項.這兩個進程永遠不會讀或寫到相同的文件偏移量,由於內核在每次 read 和 write 調用後,都要增長文件的偏移量.儘管兩個進程彷佛是將源文件拷貝了兩次,但由於它們分擔了工做任務,所以,目標文件的內容依賴於內核調度兩個進程的次序.若是內核這樣調度兩個進程:是它們交替的執行它們的系統調用,甚至使它們交替的執行每對 read 和 write 調用,則目標文件的內容和源文件的內容徹底一致.但考慮這樣的狀況:兩個進程正要讀源文件中的連續的字符'ab'.假定父進程讀了字符'a',這時,內核在父進程write 以前,作了上下文切換來執行子進程。若是子進程 讀到字符 "b",並在父進程被調度前,將它寫到目標文件,那麼目標文件將再也不含有 字符串 "ab",而是含有 "ba"了。內核並不保證進程執行的相對速率。

另外一個例子:

#include <string.h>

char    string[] = "Hello, world";

main()
{
    int count, i;
    int to_par[2], to_chil[2];      // 到父、子進程的管道
    char    buf[256];

    pipe(to_par);
    pipe(to_chil);

    if (fork() == 0) {
        // 子進程在此執行
        close(0);       // 關閉老的標準輸入
        dup(to_child[0]);   // 將管道的讀複製到標準輸入
        close(1);       // 關閉老的標準輸出
        dup(to_par[1]);     // 將管道的寫複製到標準輸出
        close(to_par[1]);   // 關閉沒必要要的管道描述符
        close(to_chil[0]);
        close(to_par[0]);
        close(to_chil[1]);
        for (;;) {
            if ((count = read(0, buf, sizeof(buf)) == 0)
                exit();
            write(1, buf, count);
        }

    }

    // 父進程在此執行
    close(1);       // 從新設置標準輸入、輸出
    dup(to_chil[1]);
    close(0);
    dup(to_par[0]);
    close(to_chil[1]);
    close(to_par[0]);
    close(to_chil[0]);
    close(to_par[1]);
    for (i = 0; i < 15; i++) {
        write(1, string, strlen(string));
        read(0, buf, sizeof(buf));
    }
}

子進程從父進程繼承了文件描述符0和1(標準輸入和標準輸出)。兩次執行系統調用 pipe 分別在數組 to_par 和 to_chil 中分配了兩個文件描述符。而後該進程 執行系統調用 fork,並複製進程上下文:象前一個例子同樣,每一個進程存取 本身的私有數據。父進程關閉他的標準輸出文件(文件描述符1),並複製(dup)從管道 線 to_chil 返回的寫文件描述符。由於在父進程文件描述符表中的第一個空槽是剛剛 由關閉騰出來的,因此內核將管道線寫文件描述符複製到了文件描述符表中的第一 項中,這樣,標準輸出文件描述符變成了管道線 to_chil 的寫文件描述符。 父進程以相似的操做將標準輸入文件描述符替換爲管道線 to_par 的讀文件 描述符。與此相似,子進程關閉他的標準輸入文件(文件描述符0),而後複製 (dup) 管道 線 to_chil 的讀文件描述符。因爲文件描述符表的第一個空項是原先的標準 輸入項,因此子進程的標準輸入變成了管道線 to_chil 的讀文件描述符。 子進程作一組相似的操做使他的標準輸出變成管道線 to_par 的寫文件描述 符。而後兩個進程關閉從 pipe 返回的文件描述符。上述操做的結果是:當 父進程向標準輸出寫東西的時候,他其實是寫向 to_chil--向子進程發送 數據,而子進程則從他的標準輸入讀管道線。當子進程向他的標準輸出寫的時候, 他其實是寫入 to_par--向父進程發送數據,而父進程則從他的標準輸入 接收來自管道線的數據。兩個進程經過兩條管道線交換消息。

不管兩個進程執行的順序如何,這個程序執行的結果是不變的。他們可能去執行睡眠 和喚醒來等待對方。父進程在15次循環後退出。而後子進程因管道線沒有寫進程而讀 到「文件尾」標誌,並退出。

Windows進程

進程 PID

windows 也是使用 PID 來惟一標識一個進程.

在一個進程內部,使用進程句柄來標識關注的每一個進程.使用 Windows API 從進程 PID 獲取進程句柄:

OpenProcess(PROCESS_ALL_ACCESS, TRUE, procId); //或者PROCESS_QUERY_INFORMATION

使用 API 函數: GETModuleFileNameEx 或 GetProcessImageFileName 或QUeryFullProcessImageName 查詢進程的 exe 文件名,使用 API 函數 GetCurrentProcess能夠獲取本進程的僞句柄(值爲-1),只能用於本進程的 API 函數調用;不能被其餘進程繼承或複製.可用 API 函數 DuplicateHandle 得到進程的真句柄.

Windows建立進程

Windows 系統使用 CreateProcess 建立進程, WaitForSingleObject 可等待子進程的結束.例如:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));
    // Start the child process.
    if (!CreateProcess(NULL,    // No module name (use command line)
                       "demo.exe arg1", // Command line
                       NULL,    // Process handle not inheritable
                       NULL,    // Thread handle not inheritable
                       FALSE,   // Set handle inheritance to FALSE
                       0,       // No creation flags
                       NULL,    // Use parent's environment block
                       NULL,    // Use parent's starting directory
                       &si,     // Pointer to STARTUPINFO structure
                       &pi)     // Pointer to PROCESS_INFORMATION structure,用於給出子進程主窗口的屬性
       ) {
        printf("CreateProcess failed (%d).\n", GetLastError());
        return;
    }
    // Wait until child process exits.
    WaitForSingleObject(pi.hProcess, INFINITE);
    // Close process and thread handles.
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

父子進程關係

建立的子進程能夠繼承父進程的:

  • CreateFile 返回的打開句柄,包括文件,控制檯輸入緩衝區,控制檯屏幕緩衝區,命名管道,串口通訊設備,郵槽;
  • 打開的句柄,包括:進程,線程,互斥鎖,事件對象,信號量,命名管道,匿名管道,文件映射對象;
  • 環境變量;
  • 當前目錄;
  • 控制檯,除非進程脫離( detach)或建立了新的控制檯;
  • 錯誤模式,使用 API 函數 SetErrorMode 設置;
  • 進程親和掩碼( affinity mask),用以指示指望使用 CPU 的哪些核;
  • 在哪一個任務中.

子進程不能繼承:

  • 優先級類別 Priority class;
  • 句柄,有 LocalAlloc,GlobalAlloc,HeapCreate,HeapAlloc 返回;
  • 僞句柄,有 GetCurrentProcess或 GetCurrentThread 返回;
  • DLL 模塊句柄,由 LoadLibrary 返回;
  • GDI 對象句柄或 USER 對象句柄,如 HBITMAP 或 HMENU.

爲繼承句柄,父進程在建立(或者代開,複製)各類可繼承對象句柄時,在 SECURITY_ATTRIBUTES 結構的 blnheritHandle 成員爲 TRUE. 在 CreateProcess 的blnheritHandles 參數爲 TRUE; 若是要繼承標準輸入,標準輸出,標準錯誤的句柄, STARTUPINFO 結構的 dwFlags 成員包含 STARTF_USESTDHANDLES 標誌位.

獲取進程信息的相關函數

  • GetCommandLine:當前進程的命令行字符串
  • GetStartupInfo:當前進程被建立時的STARTUPINFO結構
  • GetProcessVersion:獲取可執行頭的版本信息
  • GetModuleFileName:獲取包含了進程代碼的可執行文件的全路徑與文件名
  • GetGuiResources:獲取使用中的GUI對象的句柄數量
  • IsDebuggerPresent:肯定進程是否被調試
  • GetProcessIoCounters:獲取進程執行的全部I/O操做的薄記信息。
  • GetProcessMemoryInfo:獲取進程的工做集內存的信息
  • GetProcessWorkingSetSize:獲取進程的工做集內存被容許的下限與上限
  • SetProcessWorkingSetSize:設置進程的工做集內存的下限與上限

進程終結

子進程終止時,全部打開的句柄被關閉,進程對象被處罰( signaled).進程的退出碼( exit code)或者 ExitProcess,TerminateProcess 函數中指出,或者是 main,WinMain 函數返回值.若是進程因爲一個致命異常(fatal exception)而終止,退出碼是這個異常值,同時進程的全部執行中的線程的退出碼也是這個異常值.

優雅的關閉其餘進程的方法使用RegisterWindowMessage 登記私有消息,用 BroadcastSystemMessage 播放消息,收到消息的進程用ExitProcess關閉.

獲取特定名字的進程 PID

若是想要獲取特定名字的進程的ID,須要枚舉全部進程。傳統辦法是CreateToolhelp32Snapshot、Process32First、Process32Next函數;也可使用EnumProcesses、EnumProcessModules函數來獲取全部的進程ID,一個進程的全部模塊的句柄。示例以下:

PROCESSENTRY32 pe32;
HANDLE hSnaphot;
HANDLE hApp;
DWORD dProcess;
hSnaphot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // 獲取進程快照
Process32First(hSnaphot, &pe32); // 枚舉第一個進程
do {
    if (lstrcmpi(pe32.szExeFile, _T("NotePad.exe")) == 0) { // 查找進程名稱爲 NotePad.exe
        dProcess = pe32.th32ProcessID;
        break;
    }
} while (Process32Next(hSnaphot, &pe32)); // 不斷循環直到枚舉不到進程
hApp = OpenProcess(PROCESS_VM_OPERATION | SYNCHRONIZE, FALSE, dProcess); // 根據進程 ID 獲取程序的句柄
if (!WaitForSingleObject(hApp, INFINITE)) // 等待進程關閉
    AfxMessageBox(" 記事本已經關閉!");

// 另外一種方法
DWORD aProcId[1024], dwProcCnt, dwModCnt;
HMODULE hMod[1000];
TCHAR szPath[MAX_PATH];

// 枚舉出全部進程ID
if (!EnumProcesses(aProcId, sizeof(aProcId), &dwProcCnt)) {
    //cout << "EnumProcesses error: " << GetLastError() << endl;
    return 0;
}

// 遍例全部進程
for (DWORD i = 0; i < dwProcCnt; ++i) {
    // 打開進程,若是沒有權限打開則跳過
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]);
    if (NULL != hProc) {
        // 打開進程的第1個Module,並檢查其名稱是否與目標相符
        if (EnumProcessModules(hProc, &hMod, 1000, &dwModCnt)) {
            GetModuleBaseName(hProc, hMod, szPath, MAX_PATH);
            if (0 == lstrcmpi(szPath, lpName)) {
                CloseHandle(hProc);
                return aProcId[i];
            }
        }
        CloseHandle(hProc);
    }
}
return 0;
相關文章
相關標籤/搜索