复习(三)—— 进程管理详解
文章目錄
- 一、進程概念
- 1.1、進程與程序
- 1.2、進程結構
- 1.3、進程三種基本狀態
- 1.4、進程三種狀態間的轉換
- 1.5、進程標識
- 二、進程創建
- 2.1、fork系統調用
- 2.2、exec族
- 三、exit和_exit
- 3.1、exit和_exit函數說明
- 3.2、exit和_exit函數語法
- 四、wait和waitpid
- 4.1、wait和waitpid函數說明
- 4.2、wait和waitpid函數說明
- 4.3、使用實例
- 五、進程間通信
- 5.1、pipe管道和FIFO有名管道
- 5.2、信號
- 5.3、消息隊列
- 5.4、共享內存映射
- 六、守護進程
- 6.1、概念
- 6.2、 模型(守護進程編程步驟)
一、進程概念
1.1、進程與程序
程序只是一個普通文件,是一個機器代碼指令和數據的集合,這些指令和數據存儲在磁盤上的一個可執行映象中,所以,程序是一個靜態的實體。簡單的來說,程序是存放在磁盤文件中的可執行文件。
程序代表你期望完成某工作的計劃和步驟,它還浮在紙面上,等待具體實現。而具體的實現過程就是有進程來完成的,進程可以理解人為是執行中的程序,它除了包含程序中的所有內容外,還包含一些額外的數據。簡單的理解就是:進程是程序的執行實例。
1.2、進程結構
Linux系統是一個多進程的系統,進程之間具有并行性、互不干擾的特點。
Linux中進程包含PCB(進程控制塊)、程序以及程序所操縱的數據結構集,可分為“代碼段”、“數據段”和“堆棧段”。
進程是程序的一次執行,是運行在自己的虛擬地址空間的一個具有獨立功能的程序。進程是分配和釋放資源的基本單位當程序執行時,系統創建進程,分配內存和CPU等資源;進程結束時,系統回收這些資源。進程由PCB(進程控制塊)來描述。
1.3、進程三種基本狀態
進程在運行中不斷地改變其運行狀態。通常,一個運行進程必須具有以下三種基本狀態。
就緒(Ready)狀態
當進程已分配到除CPU以外的所有必要的資源,只要獲得處理機便可立即執行,這時的進程狀態稱為就緒狀態。
執行(Running)狀態
當進程已獲得處理機,其程序正在處理機上執行,此時的進程狀態稱為執行狀態。
阻塞(Blocked)狀態
正在執行的進程,由于等待某個事件發生而無法執行時,便放棄處理機而處于阻塞狀態。引起進程阻塞的事件可有多種,例如,等待I/O完成、申請緩沖區不能滿足、等待信件(信號)等。
1.4、進程三種狀態間的轉換
一個進程在運行期間,不斷地從一種狀態轉換到另一種狀態,它可以多次處于就緒狀態和執行狀態,也可以多次處于阻塞狀態。下圖描述了進程的三種基本狀態及其轉換。
(1) 就緒→執行
處于就緒狀態的進程,當進程調度程序為之分配了處理機后,該進程便由就緒狀態轉變成執行狀態。
(2) 執行→就緒
處于執行狀態的進程在其執行過程中,因分配給它的一個時間片已用完而不得不讓出處理機,于是進程從執行狀態轉變成就緒狀態。
(3) 執行→阻塞
正在執行的進程因等待某種事件發生而無法繼續執行時,便從執行狀態變成阻塞狀態。
(4) 阻塞→就緒
處于阻塞狀態的進程,若其等待的事件已經發生,于是進程由阻塞狀態轉變為就緒狀態。
1.5、進程標識
操作系統會為每一個進程分配一個唯一的整型ID,作為進程的標識(pid)。進程除了自身的ID外,還有父進程ID,所有進程的祖先進程是同一個進程,它叫做init進程,ID為 1,init 進程是內核自舉后的一個啟動進程。
獲取進程標識相關函數
#include <sys/types.h> #include <unistd.h>pid_t getpid(void); 返回:調用進程的進程ID pid_t getppid(void); 返回:調用進程的父進程ID uid_t getuid(void); 返回:調用進程的實際用戶ID uid_t geteuid(void); 返回:調用進程的有效用戶ID gid_t getgid(void); 返回:調用進程的實際組ID gid_t getegid(void); 返回:調用進程的有效組ID二、進程創建
2.1、fork系統調用
#include <unistd.h>pid_t fork(void); 返回值:子進程中為0,父進程中為子進程I D,出錯為-1說明:
fork函數用于從已存在進程中創建一個新進程。新進程稱為子進程,而原進程稱為父進程。
注意:
1、子進程復制父進程的0~3g空間和父進程內核中的PCB,但id號不同。
2、fork調用一次返回兩次:
① 父進程中返回子進程ID
② 子進程中返回0
3、讀時共享,寫時復制
4、使用fork函數得到的子進程從父進程的繼承了整個進程的地址空間,包括:進程上下文、進程堆棧、內存信息、打開的文件描述符、信號控制設置、進程優先級、進程組號、當前工作目錄、根目錄、資源限制、控制終端等。
5、fork系統調用之后,父子進程將交替執行。如果父進程先退出,子進程還沒退出那么子進程的父進程將變為init進程。(注:任何一個進程都必須有父進程)如果子進程先退出,父進程還沒退出,那么子進程必須等到父進程捕獲到了子進程的退出狀態才真正結束,否則這個時候子進程就成為僵進程。
子進程與父進程的區別:
1、父進程設置的鎖,子進程不繼承
2、各自的進程ID和父進程ID不同
3、子進程的未決告警被清除;
4、子進程的未決信號集設置為空集
示例:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h>int main(void) {pid_t result;/*調用fork函數,其返回值為result*/result = fork();/*通過result的值來判斷fork函數的返回情況,首先進行出錯處理*/if(result == -1){perror("fork");exit;}/*返回值為0代表子進程*/else if(result == 0){printf("The return value is %d\nIn child process!\nMy PID is %d\n",result,getpid());}else{printf("The return value is %d\nIn father process!\nMy PID is %d\n",result,getpid());}return 0; }運行結果:
The return value is 0 In child process! My PID is 27424 The return value is 27424 In father process! My PID is 274232.2、exec族
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。
exec 族語法
實際上,在Linux中并沒有exec函數,而是有6個以exec開頭的函數族,它們之間語法有細微差別,以下列舉了exec函數的6個成員函數語法:
這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回,如果調用出錯則返回-1,所以exec函數只有出錯的返回值而沒有成功的返回值。
記憶這6個函數
這六個exec函數的參數很難記憶。函數名中的字符會給我們一些幫助:
①字母 p表示可以只給出可執行文件名,系統會自動按照PATH環境變量所指定的路徑尋找可執行文件,它與字母e互斥。
②字母l表示該函數使用一個參數列表傳遞參數,它與字母v互斥。
③字母v表示該函數使用一個char * argv[ ]傳遞參數。
④字母e表示該函數使用char * envp[ ] 傳遞環境量變,而不使用當前環境。
注意事項:
由exec啟動的新進程繼承了原進程的許多東西,已經打開了的文件描述符在新進程里仍將是打開的,除非它們的“exec 調用時關閉此文件”標志被置了位
exec使用實例:
//使用文件名的方式來查找可執行文件,同時使用參數列表的方式 if(fork()==0){ /*調用execlp 函數,這里相當于調用了“ps-f”命令*/if (execlp("ps","ps","-ef",NULL)<0){perror("execlp error!");exit(1);} }--------------------------------------- //使用完整的文件目錄來查找對應的可執行文件 if(fork()==0){/*調用execl 函數,注意這里給出ps程序的完整路徑*/if (execl("/bin/ps","ps","-ef",NULL)<0){perror("execl error!");exit(1);} }--------------------------------------- //將環境變量添加到新建的子進程中去 env:查看當前進程環境變量 /*命令參數列表,必須以NULL結尾*/ char *envp[]={"PATH=/tmp","USER=sunnq",NULL}; if(fork()==0){/*調用execle 函數,注意這里也要指出env的完整路徑*/if (execle("/bin/env","env",NULL,envp)<0){perror("execle error!");exit(1);} }--------------------------------------- //通過構造指針數組的方式來傳遞參數,注意參數列表一定要以NULL作為結尾標識符 char*arg[]={"ls", "-a", NULL}; if(fork()==0){if (execve("/bin/ls",arg,NULL)<0){perror("execve error!");exit(1);} }exec族使用注意點:
在使用exec函數時,最好加上錯誤判斷語句。因為exec很容易執行失敗,其中最常見的原因有:
①找不到文件路徑,此時error被設置為ENOENT;
②數組argv和envp忘記用NULL結束,此時error被設置為EFALUT;
③沒有對應可執行文件的運行權限,此時error被設置為EACCESS;
事實上,這6個函數中真正的系統調用只有execve,其他5個都是庫函數,它們最終都會調用execve這個系統調用。
三、exit和_exit
3.1、exit和_exit函數說明
exit和_exit用于中止進程;當程序執行到exit或_exit時,進程會無條件地停止剩下的所有操作,清楚包括PCB 在內的各種數據結構,并終止本進程的運行。但是,這兩個函數還是有區別的,這連個函數的調用過程如下圖所示:
由圖可以看出,_exit的作用:直接使進程停止運行,清除其使用的內存空間,并清除其在內核中的數據結構;
exit與_exit函數不同,exit函數在調用exit系統之前要檢查文件打開情況,把文件緩沖區的內容寫回文件中去,就是圖中的“清理I/O緩沖”一項
3.2、exit和_exit函數語法
#include <stdlib.h> //exit #include <unistd.h> //_exitvoid exit(int status) void _exit(int status)參數: status: 0 代表正常結束;其他數值表示出現了錯誤,進程非正常結束四、wait和waitpid
僵尸進程: 子進程退出,父進程沒有回收子進程資源(PCB),則子進程變成僵尸進程。
孤兒進程: 父進程先于子進程結束,則子進程成為孤兒進程,子進程的父進程成為1號。
init進程:1號進程,負責收留孤兒進程,成為他們的父進程。
4.1、wait和waitpid函數說明
wait函數用于使父進程(也就是調用wait的進程)阻塞,直到一個子進程結束或者該進程接收到一個指定信號為止。如果該父進程沒有子進程或者他的子進程已經結束,則wait就會立即返回。
waitpid的作用和wait一樣,但它并不一定要等待第一個終止的子進程,它還有若干選項,如可提供一個非阻塞版本的wait功能,也能支持作業控制。實際上wait 函數只是waitpid 函數的一個特例,在Linux內部實現wait函數時直接調用的就是wait函數。
4.2、wait和waitpid函數說明
#include <sys/types.h> #include <sys/wait.h>pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);pid是進程號 status:<-1 回收指定進程組內的任意子進程-1 回收任意子進程0 回收和當前waitpid調用一個組的所有子進程>0 回收指定ID的子進程 options:WNOHANG:若由pid指定的子進程不立即可用,則waitpid不阻塞,此時返回值為0WUNTRANCED:若實現某支持作業控制,則由pid指定的任一子進程狀態已暫停,且其狀態自暫停以來還未報告過,則返回其狀態0:同wait,阻塞父進程,等待子進程退出4.3、使用實例
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h>int main(void) {pid_t pc,pr;pc = fork();if (pc <0){printf("Error fork!\n");}/*子進程*/else if(pc==0){/*子進程暫停3秒*/Sleep(3);/*子進程正常退出*/exit(0);}/*父進程*/else{/*循環測試子進程是否退出*/do {/*調用waitpid,且父進程不阻塞*/pr=waitpid(pc,NULL,WNOHANG);/*若子進程還未退出,則父進程暫停1s*/if(pr==0){printf("The child process has not exited\n");Sleep(1);}} while (pr==0);/*若發現子進程退出,打印出相應情況*/if (pr==pc){printf("Get child%d\n",pr);}else{printf("some error occured\n");}}return 0; }執行結果:
The child process has not exited The child process has not exited The child process has not exited Get child32225五、進程間通信
5.1、pipe管道和FIFO有名管道
具體內容看我博客:管道
5.2、信號
看我博客:信號
5.3、消息隊列
消息隊列(Message Queue),是分布式系統中重要的組件,其通用的使用場景可以簡單地描述為:當不需要立即獲得結果,但是并發量又需要進行控制的時候,差不多就是需要使用消息隊列的時候。消息隊列主要解決了應用耦合、異步處理、流量削鋒等問題。當前使用較多的消息隊列有RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,而部分數據庫如Redis、Mysql以及phxsql也可實現消息隊列的功能。
消息隊列與管道以及有名管道相比,具有更大的靈話性,首先,它提供有格式字節流,有利于
減少開發人員的工作量:其次,消息具有類型,在實際應用中,可作為優先級使用。這兩點是管道以及有名管道所不能比的。同樣,消息隊列可以在幾個進程間復用,而不管這幾個進程是否具有親緣關系,這一點與有名管道很相似:但消息隊列是隨內核持續的,與有名管道(隨進程持續)相比,生命力更強,應用空間更大。
5.4、共享內存映射
采用共享內存通信的一個顯而易見的好處是效率高,因為進程可以直接讀寫內存,而不需要任何數據的拷貝。對于像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據[1]: 一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。
實際上,進程之間在共享內存時,并不總是讀寫少量數據后就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢為止,這樣,數據內容直保存在共享內存中,并沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此采用共李內存的通信方式效率是非常高的。
Linux的2.2.x內核支持多種共享內存方式,如mmap()系統調用,Posix 共享內存,以及系統V共享內存。linux發行版本如Redhat 8.0支持mmap()系統調用及系統V共享內存,但還沒實現Posix共享內存。
mmap/munmap
mmap可以把磁盤文件的一部分直接映射到內存,這樣文件中的位置直接就有對應的內存地址,對文件的讀寫可以直接用指針來做而不需要read/write函數。
#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);參數:addr:為NULL,內核會自己在進程地址空間中選擇合適的地址建立映射。不為NULL,則給內核一個提示,應該從什么地址開始映射,內核會選擇addr之上的某個合適的地址開始映射len:需要映射的那一部分文件的長度。off:參從文件的什么位置開始映射,必須是頁大小的整數倍(在32位體系統結構上通常是4K)。filedes:代表該文件的描述符。prot:PROT_EXEC表示映射的這一段可執行,例如映射共享庫PROT_READ表示映射的這一段可讀PROT_WRITE表示映射的這一段可寫PROT_NONE表示映射的這一段不可訪問flag:(這里只寫了兩種)MAP_SHARED多個進程對同一個文件的映射是共享的,一個進程對映射的內存做了修改,另一個進程也會看到這種變化。MAP_PRIVATE多個進程對同一個文件的映射不是共享的,一個進程對映射的內存做了修改,另一個進程并不會看到這種變化,也不會真的寫到文件中去。返回值:成功則返回映射首地址,如果出錯則返回常數MAP_FAILED當進程終止時,該進程的映射內存會自動解除,也可以調用munmap解除映射。munmap成功返回0,出錯返回 -1。
示例:
#include <stdlib.h> #include <sys/mman.h> #include <fcntl.h> int main(void) {int *p;int fd = open("file.txt", O_RDWR);if (fd < 0) {perror("open hello");exit(1);}p = mmap(NULL, 6, PROT_WRITE, MAP_SHARED, fd, 0);if (p == MAP_FAILED) {perror("mmap");exit(1);}close(fd);munmap(p, 6);return 0; }注意:
- 用于進程間通信時,一般設計成結構體,來傳輸通信的數據
- 進程間通信的文件,應該設計成臨時文件
- 當報總線錯誤時,優先查看共享文件是否有存儲空間(即文件里是否有數據)
六、守護進程
6.1、概念
守護進程,也就是通常所說的 daemom(精靈) 進程,是Linux中的后臺服務進程,生存期較長的進程,通常獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。
守護進程是在后臺運行不受終端控制的進程。
守護進程能自動轉到后臺并且脫離與終端的聯系。
Linux系統中一般有很多守護進程在后臺運行,執行不同的管理任務。
6.2、 模型(守護進程編程步驟)
1. 創建子進程,父進程退出所有工作在子進程中進行形式上脫離了控制終端 2. 在子進程中創建新會話setsid()函數使子進程完全獨立出來,脫離控制 3. 改變當前目錄為根目錄chdir()函數防止占用可卸載的文件系統也可以換成其它路徑 4. 重設文件權限掩碼umask()函數防止繼承的文件創建屏蔽字拒絕某些權限增加守護進程靈活性 5. 關閉文件描述符繼承的打開文件不會用到,浪費系統資源,無法卸載 6. 開始執行守護進程核心工作 7. 守護進程退出處理代碼模型
#include <stdlib.h> #include <stdio.h> #include <fcntl.h>void daemonize(void) {pid_t pid;/** 成為一個新會話的首進程,失去控制終端*/if ((pid = fork()) < 0) {perror("fork");exit(1);} else if (pid != 0) /* parent */exit(0);setsid();/** 改變當前工作目錄到/目錄下.*/if (chdir("/") < 0) {perror("chdir");exit(1);}/* 設置umask為0 */umask(0);/** 重定向0,1,2文件描述符到 /dev/null,因為已經失去控制終端,再操作0,1,2沒有意義.*/close(0);open("/dev/null", O_RDWR);dup2(0, 1);dup2(0, 2); } int main(void) {daemonize();while(1); /* 在此循環中可以實現守護進程的核心工作 */ }注意:運行這個程序,它變成一個守護進程,不再和當前終端關聯。用ps命令看不到,必須運行帶x參數的ps命令才能看到。另外還可以看到,用戶關閉終端窗口或注銷也不會影響守護進程的運行。
總結
以上是生活随笔為你收集整理的复习(三)—— 进程管理详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习笔记——pygame最常用的15个模
- 下一篇: 学习笔记——itertools模块