《計算機操做系統》這門課對進程有這樣的描述:進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。linux
進程的概念主要有兩點:程序員
第一,進程是一個實體。每個進程都有它本身的地址空間,通常狀況下,包括文本區域(text region)、數據區域(data region)和堆棧(stack region)。文本區域存儲處理器執行的代碼;數據區域存儲變量和進程執行期間使用的動態分配的內存;堆棧區域存儲着活動過程調用的指令和本地變量。編程
第二,進程是一個「執行中的程序」。程序是一個沒有生命的實體,只有處理器賦予程序生命時,它才能成爲一個活動的實體,咱們稱其爲進程。小程序
進程是操做系統中最基本、重要的概念。是多道程序系統出現後,爲了刻畫系統內部出現的動態狀況,描述系統內部各道程序的活動規律引進的一個概念,全部多道程序設計操做系統都創建在進程的基礎上。數組
操做系統引入進程的概念的緣由:安全
從理論角度看,是對正在運行的程序過程的抽象;服務器
從實現角度看,是一種數據結構,目的在於清晰地刻劃動態系統的內在規律,有效管理和調度進入計算機系統主存儲器運行的程序。數據結構
主要特徵:併發
多個不一樣的進程能夠包含相同的程序:一個程序在不一樣的數據集裏就構成不一樣的進程,能獲得不一樣的結果;可是執行過程當中,程序不能發生改變。異步
進行進程切換就是從正在運行的進程中收回處理器,而後再使待運行進程來佔用處理器。
這裏所說的從某個進程收回處理器,實質上就是把進程存放在處理器的寄存器中的中間數據找個地方存起來,從而把處理器的寄存器騰出來讓其餘進程使用。那麼被停止運行進程的中間數據存在何處好呢?固然這個地方應該是進程的私有堆棧。
讓進程來佔用處理器,實質上是把某個進程存放在私有堆棧中寄存器的數據(前一次本進程被停止時的中間數據)再恢復處處理器的寄存器中去,並把待運行進程的斷點送入處理器的程序指針PC,因而待運行進程就開始被處理器運行了,也就是這個進程已經佔有處理器的使用權了。
在切換時,一個進程存儲在處理器各寄存器中的中間數據叫作進程的上下文,因此進程的切換實質上就是被停止運行進程與待運行進程上下文的切換。在進程未佔用處理器時,進程的上下文是存儲在進程的私有堆棧中的。
在Linux操做系統中,進程在內存裏有三部分的數據,就是「數據段」、「堆棧段」和「代碼段」。簡單的說「代碼段」,顧名思義,就是存放了程序代碼的數據,假如機器中有數個進程運行相同的一個程序,那麼它們就可使用同一個代碼段。
堆棧段存放的就是子程序的返回地址、子程序的參數以及程序的局部變量。而數據段則存放程序的全局變量,常數以及動態數據分配的數據空間(好比用malloc之類的函數取得的空間)。
在Linux下產生新的進程的系統調用就是fork函數。fork是最難理解的概念之一:它執行一次卻返回兩個值。fork函數是Unix系統最傑出的成就之一,它是七十年代UNIX早期的開發者通過長期在理論和實踐上的艱苦探索後取得的成果,一方面,它使操做系統在進程管理上付出了最小的代價,另外一方面,又爲程序員提供了一個簡潔明瞭的多進程方法。與DOS和早期的Windows不一樣,Unix/Linux系統是真正實現多任務操做的系統,能夠說,不使用多進程編程,就不能算是真正的Linux環境下編程。
示例:
void main(){ int i; if (fork() == 0) { /* 子進程程序 */ for (i = 1; i <1000; i++) printf("This is child process\n"); } else { /* 父進程程序*/ for (i = 1; i <1000; i++) printf("This is parent process\n"); } }
程序運行後,就能看到屏幕上交替出現子進程與父進程各打印出的一千條信息了。若是程序還在運行中,你用ps命令就能看到系統中有兩個它在運行了。
那麼調用這個fork函數時發生了什麼呢?一個程序一調用fork函數,系統就爲一個新的進程準備了前述三個段,首先,系統讓新的進程與舊的進程使用同一個代碼段,由於它們的程序仍是相同的,對於數據段和堆棧段,系統則複製一份給新的進程,這樣,父進程的全部數據均可以留給子進程,可是,子進程一旦開始運行,雖然它繼承了父進程的一切數據,但實際上數據卻已經分開,相互之間再也不有影響了,也就是說,它們之間再也不共享任何數據了。而若是兩個進程要共享什麼數據的話,就要使用另外一套函數(shmget,shmat,shmdt等)來操做。如今,已是兩個進程了,對於父進程,fork函數返回了子程序的進程號,而對於子程序,fork函數則返回零,這樣,對於程序,只要判斷fork函數的返回值,就知道本身是處於父進程仍是子進程中。
有個疑問:若是一個大程序在運行中,它的數據段和堆棧都很大,一次fork就要複製一次,那麼fork的系統開銷不是很大嗎?
其實Linux自有其解決的辦法,你們知道,通常CPU都是以「頁」爲單位分配空間的,象INTEL的CPU,其一頁在一般狀況下是4K字節大小,而不管是數據段仍是堆棧段都是由許多「頁」構成的,fork函數複製這兩個段,只是「邏輯」上的,並不是「物理」上的,也就是說,實際執行fork時,物理空間上兩個進程的數據段和堆棧段都仍是共享着的,當有一個進程寫了某個數據時,這時兩個進程之間的數據纔有了區別,系統就將有區別的「頁」從物理上也分開。系統在空間上的開銷就能夠達到最小。
如何搞死Linux系統,看下面的小程序:
void main() { for(;;) fork(); }
這個程序什麼也不作,就是死循環地fork,其結果是程序不斷產生進程,而這些進程又不斷產生新的進程,很快,系統的進程就滿了,系統就被這麼多不斷產生的進程"撐死了"。用不着是root權限,任何人運行上述程序都足以讓系統死掉。哈哈,但這不是Linux不安全的理由,由於只要系統管理員足夠聰明,他(或她)就能夠預先給每一個用戶設置可運行的最大進程數,這樣,只要不是root,任何能運行的進程數也許不足系統總的能運行和進程數的十分之一。這樣,系統管理員就能對付上述惡意的程序了。
下面咱們來看看一個進程如何來啓動另外一個程序的執行。在Linux中要使用exec類的函數,exec類的函數不止一個,但大體相同,在Linux中,它們分別是:execl,execlp,execle,execv,execve和execvp,下面以execlp爲例,其它函數究竟與execlp有何區別,請經過man exec命令來了解它們的具體狀況。 一個進程一旦調用exec類函數,它自己就「死亡」了,系統把代碼段替換成新的程序的代碼,廢棄原有的數據段和堆棧段,併爲新程序分配新的數據段與堆棧段,惟一留下的,就是進程號,也就是說,對系統而言,仍是同一個進程,不過已是另外一個程序了。(不過exec類函數中有的還容許繼承環境變量之類的信息。)
那麼若是當前程序想啓動另外一程序的執行但本身仍想繼續運行的話,怎麼辦呢?那就是結合fork與exec的使用。
下面一段代碼顯示如何啓動運行其它程序:
char command[256]; void main() { int rtn; /*子進程的返回數值*/ while (1) { /* 從終端讀取要執行的命令 */ printf(">"); fgets(command, 256, stdin); command[strlen(command) - 1] = 0; if (fork() == 0) { /* 子進程執行此命令 */ execlp(command, command); /* 若是exec函數返回,代表沒有正常執行命令,打印錯誤信息*/ perror(command); exit(errorno); } else { /* 父進程, 等待子進程結束,並打印子進程的返回值 */ wait(&rtn); printf(" child process return %d\n",rtn); } } }
此程序從終端讀入命令並執行之,執行完成後,父進程繼續等待從終端讀入命令。熟悉DOS和WINDOWS系統調用的朋友必定知道DOS/WINDOWS也有exec類函數,其使用方法是相似的,但DOS/WINDOWS還有spawn類函數,由於DOS是單任務的系統,它只能將「父進程」駐留在機器內再執行「子進程」,這就是spawn類的函數。WIN32已是多任務的系統了,但還保留了spawn類函數,WIN32中實現spawn函數的方法同前述Linux中的方法差很少,開設子進程後父進程等待子進程結束後才繼續運行。Linux在其一開始就是多任務的系統,因此從核心角度上講不須要spawn類函數。
另外,有一個更簡單的執行其它程序的函數system,它是一個較高層的函數,實際上至關於在SHELL環境下執行一條命令,而exec類函數則是低層的系統調用。
首先,進程間通訊至少能夠經過傳送打開文件來實現,不一樣的進程經過一個或多個文件來傳遞信息,事實上,在不少應用系統裏,都使用了這種方法。但通常說來,進程間通訊(IPC:Inter Process Communication)不包括這種彷佛比較低級的通訊方法。Unix系統中實現進程間通訊的方法不少,並且不幸的是,極少方法能在全部的Unix系統中進行移植(惟一一種是半雙工的管道,這也是最原始的一種通訊方式)。而Linux做爲一種新興的操做系統,幾乎支持全部的Unix下經常使用的進程間通訊方法:管道、消息隊列、共享內存、信號量、套接口等等。
管道是進程間通訊中最古老的方式,它包括無名管道和有名管道兩種,前者用於父進程和子進程間的通訊,後者用於運行於同一臺機器上的任意兩個進程間的通訊。
無名管道由pipe()函數建立:#include <unistd.h> int pipe(int filedis[2]);
參數filedis返回兩個文件描述符:filedes[0]爲讀而打開,filedes[1]爲寫而打開。filedes[1]的輸出是filedes[0]的輸入。
下面的例子示範瞭如何在父進程和子進程間實現通訊。
#define INPUT 0 #define OUTPUT 1 void main() { int file_descriptors[2]; /*定義子進程號 */ pid_t pid; char buf[256]; int returned_count; /*建立無名管道*/ pipe(file_descriptors); /*建立子進程*/ if ((pid = fork()) == -1) { printf("Error in fork/n"); exit(1); } /*執行子進程*/ if (pid == 0) { printf("in the spawned (child) process.../n"); /*子進程向父進程寫數據,關閉管道的讀端*/ close(file_descriptors[INPUT]); write(file_descriptors[OUTPUT], "test data", strlen("test data")); exit(0); } else { /*執行父進程*/ printf("in the spawning (parent) process.../n"); /*父進程從管道讀取子進程寫的數據,關閉管道的寫端*/ close(file_descriptors[OUTPUT]); returned_count = read(file_descriptors[INPUT], buf, sizeof(buf)); printf("%d bytes of data received from spawned process: %s/n", returned_count, buf); } }
在Linux系統下,有名管道可由兩種方式建立:命令行方式mknod系統調用和函數mkfifo。下面的兩種途徑都在當前目錄下生成了一個名爲myfifo的有名管道:
方式一:mkfifo("myfifo","rw");
方式二:mknod myfifo p
生成了有名管道後,就可使用通常的文件I/O函數如open、close、read、write等來對它進行操做。
下面便是一個簡單的例子,假設咱們已經建立了一個名爲myfifo的有名管道。
/* 進程一:讀有名管道*/ #include <stdio.h> #include <unistd.h> void main() { FILE * in_file; int count = 1; char buf[80]; in_file = fopen("mypipe", "r"); if (in_file == NULL) { printf("Error in fdopen./n"); exit(1); } while ((count = fread(buf, 1, 80, in_file)) > 0) printf("received from pipe: %s/n", buf); fclose(in_file); } /* 進程二:寫有名管道*/ #include <stdio.h> #include <unistd.h> void main() { FILE * out_file; int count = 1; char buf[80]; out_file = fopen("mypipe", "w"); if (out_file == NULL) { printf("Error opening pipe."); exit(1); } sprintf(buf, "this is test data for the named pipe example/n"); fwrite(buf, 1, 80, out_file); fclose(out_file); }
消息隊列用於運行於同一臺機器上的進程間通訊,它和管道很類似,事實上,它是一種正逐漸被淘汰的通訊方式,咱們能夠用流管道或者套接口的方式來取代它,因此,咱們對此方式也再也不解釋,也建議讀者忽略這種方式。
共享內存是運行在同一臺機器上的進程間通訊最快的方式,由於數據不須要在不一樣的進程間複製。一般由一個進程建立一塊共享內存區,其他進程對這塊內存區進行讀寫。獲得共享內存有兩種方式:映射/dev/mem設備和內存映像文件。前一種方式不給系統帶來額外的開銷,但在現實中並不經常使用,由於它控制存取的將是實際的物理內存,在Linux系統下,這隻有經過限制Linux系統存取的內存才能夠作到,這固然不太實際。經常使用的方式是經過shmXXX函數族來實現利用共享內存進行存儲的。
首先要用的函數是shmget,它得到一個共享存儲標識符。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, int size, int flag);
這個函數有點相似你們熟悉的malloc函數,系統按照請求分配size大小的內存用做共享內存。Linux系統內核中每一個IPC結構都有的一個非負整數的標識符,這樣對一個消息隊列發送消息時只要引用標識符就能夠了。這個標識符是內核由IPC結構的關鍵字獲得的,這個關鍵字,就是上面第一個函數的key。數據類型key_t是在頭文件sys/types.h中定義的,它是一個長整形的數據。
當共享內存建立後,其他進程能夠調用shmat()將其鏈接到自身的地址空間中。
void *shmat(int shmid, void *addr, int flag);
shmid爲shmget函數返回的共享存儲標識符,addr和flag參數決定了以什麼方式來肯定鏈接的地址,函數的返回值便是該進程數據段所鏈接的實際地址,進程能夠對此進程進行讀寫操做。
使用共享存儲來實現進程間通訊的注意點是對數據存取的同步,必須確保當一個進程去讀取數據時,它所想要的數據已經寫好了。一般,信號量被要來實現對共享存儲數據存取的同步,另外,能夠經過使用shmctl函數設置共享存儲內存的某些標誌位如SHM_LOCK、SHM_UNLOCK等來實現。
信號量又稱爲信號燈,它是用來協調不一樣進程間的數據對象的,而最主要的應用是前一節的共享內存方式的進程間通訊。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取情況。通常說來,爲了得到共享資源,進程須要執行下列操做:
維護信號量狀態的是Linux內核操做系統而不是用戶進程。咱們能夠從頭文件/usr/src/linux/include /linux /sem.h中看到內核用來維護信號量狀態的各個結構的定義。信號量是一個數據集合,用戶能夠單獨使用這一集合的每一個元素。要調用的第一個函數是semget,用以得到一個信號量ID。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int flag);
key是前面講過的IPC結構的關鍵字,它未來決定是建立新的信號量集合,仍是引用一個現有的信號量集合。nsems是該集合中的信號量數。若是是建立新集合(通常在服務器中),則必須指定nsems;若是是引用一個現有的信號量集合(通常在客戶機中)則將nsems指定爲0。
semctl函數用來對信號量進行操做。
int semctl(int semid, int semnum, int cmd, union semun arg);
不一樣的操做是經過cmd參數來實現的,在頭文件sem.h中定義了7種不一樣的操做,實際編程時能夠參照使用。
semop函數自動執行信號量集合上的操做數組。
int semop(int semid, struct sembuf semoparray[], size_t nops);
semoparray是一個指針,它指向一個信號量操做數組。nops規定該數組中操做的數量。
下面,咱們看一個具體的例子,它建立一個特定的IPC結構的關鍵字和一個信號量,創建此信號量的索引,修改索引指向的信號量的值,最後咱們清除信號量。在下面的代碼中,函數ftok生成咱們上文所說的惟一的IPC關鍵字。
#include <stdio.h> #include <sys/types.h> #include <sys/sem.h> #include <sys/ipc.h> void main() { key_t unique_key; /* 定義一個IPC關鍵字*/ int id; struct sembuf lock_it; union semun options; int i; unique_key = ftok(".", 'a'); /* 生成關鍵字,字符'a'是一個隨機種子*/ /* 建立一個新的信號量集合*/ id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666); printf("semaphore id=%d/n", id); options.val = 1; /*設置變量值*/ semctl(id, 0, SETVAL, options); /*設置索引0的信號量*/ /*打印出信號量的值*/ i = semctl(id, 0, GETVAL, 0); printf("value of semaphore at index 0 is %d/n", i); /*下面從新設置信號量*/ lock_it.sem_num = 0; /*設置哪一個信號量*/ lock_it.sem_op = -1; /*定義操做*/ lock_it.sem_flg = IPC_NOWAIT; /*操做方式*/ if (semop(id, &lock_it, 1) == -1) { printf("can not lock semaphore./n"); exit(1); } i = semctl(id, 0, GETVAL, 0); printf("value of semaphore at index 0 is %d/n", i); /*清除信號量*/ semctl(id, 0, IPC_RMID, 0); }
套接口(socket)編程是實現Linux系統和其餘大多數操做系統中進程間通訊的主要方式之一。咱們熟知的WWW服務、FTP服務、TELNET服務等都是基於套接口編程來實現的。除了在異地的計算機進程間之外,套接口一樣適用於本地同一臺計算機內部的進程間通訊。