一、進程的概念
首先思考一個問題:CPU的核心數是有限的,那麼在運行遠超過CPU核心數數量的程序時,操作系統是如何實現CPU核心數彷彿無限的假象的?
當然是通過虛擬化CPU來實現,也就是讓一個程序只運行一個時間片,然後切換到其他程序,通過高速的上下文切換來僞造一種多個CPU的假象,這也就是時分共享(time sharing)的CPU技術。這種行爲潛在會造成一些性能的損失,也就是不停切換時耗費的COU性能。
CPU的虛擬化想要實現,操作系統需要一些低級機制與高級技能。機制是一些低級方法或協議,用於實現所需的功能,例如上下文切換(context switch),它讓操作系統可以停止運行一個程序而切換到另一個程序繼續運行;目前所有的現代操作系統都有這種分時機制。時分共享相對應的還有空分共享,資源在空間上被劃分給希望使用它的人,例如磁盤空間就是一個空分共享資源,將一塊磁盤分配給文件後,在用戶刪除文件之前不會再將其分配給其他文件。高級智能一般以策略的形式存在,策略就是操作系統做出某種決定時所使用的算法。例如,當前有一組程序在CPU上運行,操作系統該如何決定程序運行的順序?這時就會用到調度策略(scheduling policy),調度策略有多種,可能會根據歷史信息(最近一次被調用的程序是哪個)、工作負載(運行什麼類型的程序)、性能(程序所佔用的系統資源大小)等等來決定最終的結果。在大多數操作系統中,策略與機制是要分開的,這種模塊化的形式也是當今軟件的一種通用原則。例如開篇的問題答案,操作系統的機制就是上下文切換,而對應的策略是切換時選擇哪一個進程被選中;機制與策略共同實現了操作系統想要的結果,然而進程與策略彼此的操作又互不影響。
操作系統對正在運行的程序的抽象,就是進程,也就是說進程就是一個正在運行的程序。要理解進程的構成,就要知道進程的機器狀態(machine state):程序在運行時可以讀取或更新的內容。機器狀態有一個明顯的組成部分,就是內存。指令需要存在內存中,程序讀取和寫入的數據也在內存中,所以進程可以訪問的內存自然就是進程的一部分。機器狀態的另一部分是寄存器,計算機很多指令明確的讀取或更新寄存器,所以寄存器對進程也極爲重要。
二、進程的創建
進程是一個運行的程序,那麼想要得到一個進程就只要運行一個程序就可以了。操作系統要運行一個程序做的第一件事就是將代碼和所有的靜態數據加載到內存中,內存也就成爲了進程的地址空間。當然這個加載過程也是極爲複雜的,但是目前不需要了解,只需要知道在運行程序之前,操作系統會將代碼和數據通過某種方式加載到了內存中。然後就需要爲程序分配運行時棧(run-time stack),在C++中,棧用來存放局部變量、函數參數和返回地址,操作系統將這些內存分配給進程,當然在某些情況下棧是可以由用戶制定分配的,例如VS設置的堆棧空間大小,通過_beginthread創建線程時第二個參數所指定的線程棧空間等等。之後操作系統還會爲程序分配一些堆內存,在C++中,堆內存用於動態申請分配的數據,也就是new所得到的的內存空間。再然後操作系統還會執行一些其他的初始化任務,特別是輸入/輸出(I/O)相關的。例如:UNIX系統中,默認情況下會爲每個進程分配3個打開的文件描述符(file descriptor),用於標準輸入、輸出和錯誤,這些描述符讓程序輕鬆讀取來自中斷的輸入以及打印輸出到屏幕。在這些都結束之後,那麼運行一個程序的準備工作就結束了,這是就可以啓動程序,例如C++/C系列代碼會通過Main()函數進入程序,進入Main()之後,操作系統就將CPU的使用權轉交給了運行起來的程序,程序就可以執行下去了。
三、進程狀態
進程會處於以下三種狀態之一:
● 運行(running) :在運行狀態下,進程正在處理器上運行,也就是正在執行指令。
● 就緒(ready):就緒狀態下,進程已準備好運行,但由於某些原因此時處理器的時間片並沒有分給進程。
● 阻塞(blocked): 阻塞狀態下,一個進程執行了某種操作,直到發生了其他事件纔會準備運行。
狀態映射圖如下:
進程會在如圖的狀態下進行轉換。從就緒到運行意味着該進程已經被調度,從運行轉移到就緒意味着該進程已經取消調度(一般是由於處理器時間片耗光)。一旦進程發起某些操作,例如IO請求,進程就會處於阻塞狀態,知道IO完成,就會纔會結束阻塞。在這些過程中,操作系統起到的是做決策作用。例如當在一個內核中同時存在兩個進程時,假設兩個進程都處於執行狀態,那麼何時由第一個進程切換到下一個進程,就是由操作系統決定,一般是第一個進程將操作系統分配給它的時間片耗光就會切換,這也是很簡單的時間片輪轉策略;如果第一個進程開始進行IO處理,那麼此時即使進程一擁有未用完的時間片,也是無效的,因爲此時接下來的時間片內進程一隻會空佔着處理器,而需要等待IO完成才能繼續執行程序,那麼此時操作系統就可以決定是讓進程一等到時間片耗光還是立刻切換到進程二;在之後,當進程一的IO完成,而此時是由進程二佔用着時間片,那麼是否立即將處理器的控制權交給進程一,這也是由操作系統決定。這些都是操作系統的調度策略。
其實進程還有一些其他的狀態,只是這些狀態在進程的生命週期中佔用的份額極少,所以幾乎可以忽略。例如進程的初始狀態(initial),表示進程在創建過程中處於的狀態。還有一個進程可以處於已退出但是尚未清理的最終狀態(final,在UNIX中這種狀態成爲殭屍狀態),這個最終狀態也是很有用的,可以給其他進程檢查他的返回碼的時間,從而讓其他進程知道其運行結果。
四、進程API
1.windows
windows下可以通過CreateProcess 來創建進程,
通過 WaitForSingleObject 等待進程結束;
#include <iostream>
#include <time.h>
#include <windows.h>
#include <process.h>
#include "assert.h"
using namespace std;
int main()
{
cout << "this is a process ID : %d" << _getpid() << endl;
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
LPTSTR szCmdline = _T("D:\\Setup.exe");
if (!CreateProcess(NULL,
szCmdline,
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&si,
&pi)
)
{
printf("CreateProcess failed (%d).\n", GetLastError());
assert(0);
}
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
2.linux
linux 下可以通過 fork(), vfork() 來創建進程,fork()的返回值 : pid_t fork(void) 創建成功返回0, 失敗返回-1, 父進程返回子進程的pid;vfork()創建成功會返回0。 fork從已經存在的進程中創建一個新的進程;vfork()則創建子進程,子進程和父進程公用同一塊虛擬地址空間,爲了防止調用棧混亂,因此阻塞父進程直到子進程退出調用exit() 退出 或者 進行程序替換;
fork 與 vfork的區別:
1. fork()創建出來的子進程和父進程誰先運行不一定
2. fork() 創建進程是將父進程的所有數據拷貝一份, 包括虛擬地址空間和頁表, 這個時候他們兩個裏面所有的數據的虛擬地址都是一樣的,但是當子進程對一個變量進行修改的時候, 這個時候系統會爲這個 變量重新開闢空間, 也就是我們上篇博客中所說的 寫時拷貝技術, 子進程與父進程代碼共享, 數據獨有
3. vfork()創建出來的子進程與父進程公用同一塊虛擬地址空間, 這個時候我們在子進程中對數據進行拷貝的時候,父進程中會隨着一起改變,有可能會造成函數調用棧混亂, 所以當fork實現了寫時拷貝技術之後vfork基本就被淘汰了。
4. vfork存在的意義是快速創建子進程, 因爲公用一塊虛擬地址空間, 減少了子進程拷貝父進程的消耗, 所以速度快
5. vfork創建出子進程後一定是子進程先運行, 等到子進程exit退出或者exec函數族程序替換之後父進程纔會開始運行。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid< 0)
{
perror("fork error");
return -1;
}
else if(pid == 0)
{
printf("i am child\n");
}
else
{
printf("i am parent\n");
}
return 0;
}
可以通過
exit() exit是庫函數接口, 底層也是調用_exit,但是調用前會刷新緩衝區,做退出前的收尾工作
_exit() _exit是系統調用接口, 直接退出, 釋放資源
來終止進程。
通過 pid_t wait(int *status); 來等待進程
返回值 : 成功返回子進程pid, 失敗返回-1
status : 子進程退出碼, 輸出型參數, 如果不關心子進程返回值可以置爲NULL
注意 wait等待子進程是一個阻塞等待, 死等, 如果子進程沒有退出父進程不會運行.
也可以通過 pid_ t waitpid(pid_t pid, int *status, int options);等待
waitpid返回值 : 正常返回的時候返回子進程的進程ID,如果設置了選項 : WNOHANG, 如果沒有發現已經退出的子進程則返回0,如果調用出錯 : 返回-1, 這時error會被置成異常退出信號值;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <wait.h>
int main()
{
int pid = fork();
if(pid < 0)
{
perror("fork error");
exit(-1);
}
else if(pid == 0)
{
sleep(50);
exit(0);
}
int statu;
int ret;
while((ret = waitpid(pid,&statu,WNOHANG)) == 0)
{
printf("等一下\n");
sleep(1);
}
printf("%d--%d\n",ret,pid);
return 0;
}