linux多进程知识汇总
文章目錄
- 前言
- 一、進程的創(chuàng)建與結束
- 1.背景知識
- 2.相關接口
- 3、進程退出方式
- 二、Linux進程控制
- 1、進程地址空間(地址空間)
- 2、進程控制塊(處理機)
- 3、上下文切換
- 三、進程調度算法
- 四、Linux下進程間通信方式?(TX)
- 五、 Linux下同步機制?
- 六、進程同步的四種方法?
- 七、進程狀態(tài)的切換
- 八、守護進程、僵尸進程和孤兒進程
前言
1、每一個進程是資源分配的基本單位。
2、進程結構由以下幾個部分組成:代碼段、堆棧段、數(shù)據(jù)段。代碼段是靜態(tài)的二進制代碼,多個程序可以共享。
3、父進程創(chuàng)建子進程之后,父、子進程除了pid外,所有的部分幾乎一樣。
4、父、子進程共享全部數(shù)據(jù),但并不是說他們就是對同一塊數(shù)據(jù)進行操作,子進程在讀寫數(shù)據(jù)時會通過寫時復制機制將公共的數(shù)據(jù)重新拷貝一份,之后在拷貝出的數(shù)據(jù)上進行操作。
5、如果子進程想要運行自己的代碼段,還可以通過調用execv()函數(shù)重新加載新的代碼段,之后就和父進程獨立開了。(我們在shell中執(zhí)行程序就是通過shell進程先fork()一個子進程再通過execv()重新加載新的代碼段的過程。)
一、進程的創(chuàng)建與結束
1.背景知識
進程有兩種創(chuàng)建方式,一種是操作系統(tǒng)創(chuàng)建的;一種是父進程創(chuàng)建的。從計算機啟動到終端執(zhí)行程序的過程為:0號進程-> 1號內核進程-> 1號用戶進程(init進程) -> getty進程-> shell進程->命令行執(zhí)行進程。所以我們在命令行中通過./program執(zhí)行可執(zhí)行文件時,所有創(chuàng)建的進程都是shell進程的子進程,這也就是為什么shell一關閉,在shell中執(zhí)行的進程都自動被關閉的原因。從shell進程到創(chuàng)建其他子進程需要通過以下接口。
2.相關接口
創(chuàng)建進程:pid_t fork(void);
返回值:出錯返回-1;父進程中返回pid > 0;子進程中pid == 0
結束進程:void exit(int status);
status是退出狀態(tài),保存在全局變量S中,通常0表示正常退出。
獲得PID:pid_t getpid(void);返回調用者pid。
獲得父進程PID:pid_t getppid(void);返回父進程pid。
3、進程退出方式
正常退出方式:exit()、_exit()、return(在main中)。
exit()和_exit()區(qū)別:exit()是對_exit()的封裝,都會終止進程并做相關收尾工作,最主要的區(qū)別是_exit()函數(shù)關閉全部描述符和清理函數(shù)后不會刷新數(shù)據(jù)流,但是exit()會在調用_exit()函數(shù)前刷新數(shù)據(jù)流。
return和exit()區(qū)別:exit()是函數(shù),但有參數(shù),執(zhí)行完之后控制權交給系統(tǒng)。return若是在調用函數(shù)中,執(zhí)行完之后控制權交給調用進程,若是在main函數(shù)中,控制權交給系統(tǒng)。
異常退出方式:abort()、終止信號。
二、Linux進程控制
1、進程地址空間(地址空間)
虛擬存儲器為每個進程提供了獨占系統(tǒng)地址空間的假象。
盡管每個進程地址空間內容不盡相同,但是他們都有相似的結構。X86 Linux進程的地址空間底部是保留給用戶程序的,包括文本、數(shù)據(jù)、堆、棧等,其中文本區(qū)和數(shù)據(jù)區(qū)是通過存儲器映射方式將磁盤中可執(zhí)行文件的相應段映射至虛擬存儲器地址空間中。
有一些"敏感"的地址需要注意下,對于32位進程來說,代碼段從0x08048000開始。從0xC0000000開始到0xFFFFFFFF是內核地址空間,通常情況下代碼運行在用戶態(tài)(使用0x00000000 ~ 0xC00000000的用戶地址空間),當發(fā)生系統(tǒng)調用、進程切換等操作時CPU控制寄存器設置模式位,進入內和模式,在該狀態(tài)(超級用戶模式)下進程可以訪問全部存儲器位置和執(zhí)行全部指令。
也就說32位進程的地址空間都是4G,但用戶態(tài)下只能訪問低3G的地址空間,若要訪問3G ~ 4G的地址空間則只有進入內核態(tài)才行。
2、進程控制塊(處理機)
進程的調度實際就是內核選擇相應的進程控制塊,被選擇的進程控制塊中包含了一個進程基本的信息。
3、上下文切換
內核管理所有進程控制塊,而進程控制塊記錄了進程全部狀態(tài)信息。每一次進程調度就是一次上下文切換,所謂的上下文本質上就是當前運行狀態(tài),主要包括通用寄存器、浮點寄存器、狀態(tài)寄存器、程序計數(shù)器、用戶棧和內核數(shù)據(jù)結構(頁表、進程表、文件表)等。
進程執(zhí)行時刻,內核可以決定搶占當前進程并開始新的進程,這個過程由內核調度器完成,當調度器選擇了某個進程時稱為該進程被調度,該過程通過上下文切換來改變當前狀態(tài)。
一次完整的上下文切換通常是進程原先運行于用戶態(tài),之后因系統(tǒng)調用或時間片用盡切換到內核態(tài)執(zhí)行內核指令,完成上下文切換后回到用戶態(tài),此時已經切換到進程B。
三、進程調度算法
1、先來先服務(FCFS)非搶占式的調度算法,按照請求的順序進行調度。
有利于長作業(yè),但不利于短作業(yè),因為短作業(yè)必須一直等待前面的長作業(yè)執(zhí)行完畢才能執(zhí)行,而長作業(yè)又需要執(zhí)行很長時間,造成了短作業(yè)等待時間過長。
2、短作業(yè)優(yōu)先(SJF)
非搶占式的調度算法,按估計運行時間最短的順序進行調度。
長作業(yè)有可能會餓死,處于一直等待短作業(yè)執(zhí)行完畢的狀態(tài)。因為如果一直有短作業(yè)到來,那么長作業(yè)永遠得不到調度。
3、最短剩余時間優(yōu)先(SRTN)
最短作業(yè)優(yōu)先的搶占式版本,按剩余運行時間的順序進行調度。當一個新的作業(yè)到達時,其整個運行時間與當前進程的剩余時間作比較。
如果新的進程需要的時間更少,則掛起當前進程,運行新的進程。否則新的進程等待。
4、時間片輪轉
將所有就緒進程按FCFS的原則排成一個隊列,每次調度時,把CPU時間分配給隊首進程,該進程可以執(zhí)行一個時間片。
當時間片用完時,由計時器發(fā)出時鐘中斷,調度程序便停止該進程的執(zhí)行,并將它送往就緒隊列的末尾,同時繼續(xù)把CPU時間分配給隊首的進程。
時間片輪轉算法的效率和時間片的大小有很大關系。
因為進程切換都要保存進程的信息并且載入新進程的信息,如果時間片太小,會導致進程切換得太頻繁,在進程切換上就會花過多時間。
而如果時間片過長,那么實時性就不能得到保證。
5、優(yōu)先級調度
為每個進程分配一個優(yōu)先級,按優(yōu)先級進行調度。為了防止低優(yōu)先級的進程永遠等不到調度,可以隨著時間的推移增加等待進程的優(yōu)先級。
6、多級反饋隊列
一個進程需要執(zhí)行100個時間片,如果采用時間片輪轉調度算法,那么需要交換100次。
多級隊列是為這種需要連續(xù)執(zhí)行多個時間片的進程考慮,它設置了多個隊列,每個隊列時間片大小都不同,例如1,2,4,8,…。進程在第一個隊列沒執(zhí)行完,就會被移到下一個隊列。
這種方式下,之前的進程只需要交換7次。每個隊列優(yōu)先權也不同,最上面的優(yōu)先權最高。因此只有上一個隊列沒有進程在排隊,才能調度當前隊列上的進程。
可以將這種調度算法看成是時間片輪轉調度算法和優(yōu)先級調度算法的結合。
四、Linux下進程間通信方式?(TX)
1、管道
無名管道(pipe,一種特殊的只存在于內存中的文件)
1、管道是一種半雙工的通信方式,數(shù)據(jù)只能單向流動,而且只能在具有親緣關系的進程之間使用。進程的親緣關系通常是指父子進程關系。
2、相關接口:int pipe(int fd[2]);
fd[2]:管道兩端用fd[0]和fd[1]來描述,讀的一端用fd[0]表示,寫的一端用fd[1]表示。通信雙方的進程中寫數(shù)據(jù)的一方需要把fd[0]先close掉,讀的一方需要先把fd[1]給close掉。
命名管道(FIFO文件,存在于文件系統(tǒng),可通過文件路徑來指出)
1、命名管道也是半雙工的通信方式,但是允許在沒有親緣關系的進程之間使用,它是先進先出的通信方式。命名管道在文件系統(tǒng)中有對應的文件名
2、命名管道通過命令mk?fo或系統(tǒng)調用mk?fo來創(chuàng)建
3、相關接口:
int mk?fo(const char *pathname, mode_t mode);
pathname:即將創(chuàng)建的FIFO文件路徑,如果文件存在需要先刪除。 mode:和open()中的參數(shù)相同。
2、消息隊列(MQ)
消息隊列是有消息的鏈表,包括POSIX消息隊列和System V消息隊列
1、存放在內核中并由消息隊列標識符標識。
2、有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀取隊列中的消息。
3、消息隊列克服了信號傳遞信息少、管道只能承載無格式字節(jié)流以及緩沖區(qū)大小受限等缺點。
相比于FIFO,消息隊列具有以下優(yōu)點: 消息隊列可以獨立于讀寫進程存在,從而避免了FIFO中同步管道的打開和關閉時可能產生的困難。
避免了FIFO的同步阻塞問題,不需要進程自己提供同步方法; 讀進程可以根據(jù)消息類型有選擇地接收消息,而不像FIFO那樣只能默認地接收。
3、信號(signal)
信號用于通知其它進程有某種事件發(fā)生。除了用于進程間通信外,進程還可以發(fā)送信號給進程本身。
4、共享內存(shared memory)
1、 共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創(chuàng)建,但多個進程都可以訪問。
2、 共享內存是最快的IPC方式,它是針對其他進程間通信方式運行效率低而專門設計的。
3、 它往往與信號量配合使用來實現(xiàn)進程間的同步、互斥和通信。
4、相關接口:
創(chuàng)建共享內存:int shmget(key_t key, int size, int ?ag);
成功時返回一個和key相關的共享內存標識符,失敗-1。 key:為共享內存段命名,多個共享同一片內存的進程使用同一個key。
size:共享內存容量。 ?ag:權限標志位,和open的mode參數(shù)一樣。
連接到共享內存地址空間:void *shmat(int shmid, void *addr, int ?ag);
返回值即共享內存實際地址。
shmid:shmget()返回的標識。 addr:決定以什么方式連接地址。 ?ag:訪問模式。
從共享內存分離:int shmdt(const void *shmaddr);
調用成功返回0,失敗返回-1。
shmaddr:是shmat()返回的地址指針。
拓展
共享內存的方式像極了多線程中線程對全局變量的訪問,大家都對等地有權去修改這塊內存的值。
這就導致在多進程并發(fā)下,最終結果是不可預期的。所以對這塊臨界區(qū)的訪問需要通過信號量來進行進程同步。
但共享內存的優(yōu)勢也很明顯,首先可以通過共享內存進行通信的進程不需要像無名管道一樣需要通信的進程間有親緣關系。其次內存共享的速度也比較快,不存在讀取文件、消息傳遞等過程,只需要到相應映射到的內存地址直接讀寫數(shù)據(jù)即可。
5、信號量(semaphore)/sem?f?/
信號量是一個計數(shù)器,可以用來控制多個進程對共享資源的訪問。
它常作為一種鎖機制,實現(xiàn)進程、線程對臨界區(qū)的同步及互斥訪問。
多線程同步的信號量是POSIX信號量,而在進程里使用SYSTEM V信號量。
相關接口:
創(chuàng)建信號量:int semget(key_t key, int nsems, int sem?ag);
創(chuàng)建成功返回信號量標識符,失敗返回-1。 key:進程pid。 nsems:創(chuàng)建信號量的個數(shù)。 sem?ag:指定信號量讀寫權限。
改變信號量值:int semop(int semid, struct sembuf *sops, unsigned nsops);
我們所需要做的主要工作就是sembuf變量并設置其值,然后調用semop,把設置好的sembuf變量傳遞進去。
struct sembuf結構體定義如下:
struct sembuf{
short sem_num;
short sem_op;
short sem_flg;
};
成功返回信號量標識符,失敗返回-1。 semid:信號量集標識符,由semget()函數(shù)返回。
sops:指向struct sembuf結構的指針,先設置好sembuf值再通過指針傳遞。
nsops:進行操作信號量的個數(shù),即sops結構變量的個數(shù),需大于或等于1。最常見設置此值等于1,只完成對一個信號量的操作。
直接控制信號量信息:int semctl(int semid, int semnum, int cmd, union semun arg);
semid:信號量集標識符。 semnum:信號量集數(shù)組上的下標,表示某一個信號量。arg:union semun類型。
6、套接字(socket)
適用于不同機器間進程通信,在本地也可作為兩個進程通信的方式。
7、內存映射(mapped memory)
內存映射允許任何多個進程間通信,每一個使用該機制的進程通過把一個共享的文件映射到自己的進程地址空間來實現(xiàn)它
8、輔助命令
ipcs命令用于報告共享內存、信號量和消息隊列信息。
ipcs -a:列出共享內存、信號量和消息隊列信息。
ipcs-l:列出系統(tǒng)限額。ipcs -u:列出當前使用情況。
五、 Linux下同步機制?
POSIX信號量:可用于進程同步,也可用于線程同步。
POSIX互斥鎖+條件變量:只能用于線程同步。
六、進程同步的四種方法?
1、臨界區(qū):
對臨界資源進行訪問的那段代碼稱為臨界區(qū)。
為了互斥訪問臨界資源,每個進程在進入臨界區(qū)之前,需要先進行檢查。
2、同步與互斥
同步:多個進程因為合作產生的直接制約關系,使得進程有一定的先后執(zhí)行關系。互斥:多個進程在同一時刻只有一個進程能進入臨界區(qū)。
3、信號量
信號量(Semaphore)是一個整型變量,可以對其執(zhí)行down和up操作,也就是常見的P和V操作。
down:如果信號量大于0,執(zhí)行-1操作;如果信號量等于0,進程睡眠,等待信號量大于0;
up:對信號量執(zhí)行+1操作,喚醒睡眠的進程讓其完成down操作。
down和up操作需要被設計成原語,不可分割,通常的做法是在執(zhí)行這些操作的時候屏蔽中斷。
如果信號量的取值只能為0或者1,那么就成為了互斥量(Mutex),0表示臨界區(qū)已經加鎖,1表示臨界區(qū)解鎖。
使用信號量實現(xiàn)生產者-消費者問題
問題描述:使用一個緩沖區(qū)來保存物品,只有緩沖區(qū)沒有滿,生產者才可以放入物品;只有緩沖區(qū)不為空,消費者才可以拿走物品。
因為緩沖區(qū)屬于臨界資源,因此需要使用一個互斥量mutex來控制對緩沖區(qū)的互斥訪問。
為了同步生產者和消費者的行為,需要記錄緩沖區(qū)中物品的數(shù)量。數(shù)量可以使用信號量來進行統(tǒng)計,這里需要使用兩個信號量:empty記錄空緩沖區(qū)的數(shù)量,full記錄滿緩沖區(qū)的數(shù)量。
其中,empty信號量是在生產者進程中使用,當empty不為0時,生產者才可以放入物品;full信號量是在消費者進程中使用,當full信號量不為0時,消費者才可以取走物品。
注意,不能先對緩沖區(qū)進行加鎖,再測試信號量。也就是說,不能先執(zhí)行down(mutex)再執(zhí)行down(empty)。如果這么做了,那么可能會出現(xiàn)這種情況:生產者對緩沖區(qū)加鎖后,執(zhí)行down(empty)操作,發(fā)現(xiàn)empty = 0,此時生產者睡眠。
消費者不能進入臨界區(qū),因為生產者對緩沖區(qū)加鎖了,消費者就無法執(zhí)行up(empty)操作,empty永遠都為0,導致生產者永遠等待下,不會釋放鎖,消費者因此也會永遠等待下去。
#define N 100 typedef int semaphore; semaphore mutex=1; semaphore empty=N; semaphorefull=0; void producer() { while(TRUE) {int item=produce_item();down(&empty);down(&mutex);insert_item(item);up(&mutex);up(&full);} } void consumer() {while(TRUE) {down(&full);down(&mutex);int item=remove_item();consume_item(item);up(&mutex);up(&empty);} }4、管程
使用信號量機制實現(xiàn)的生產者消費者問題需要客戶端代碼做很多控制,而管程把控制的代碼獨立出來,不僅不容易出錯,也使得客戶端代碼調用更容易。
c語言不支持管程,下面的示例代碼使用了類Pascal語言來描述管程。示例代碼的管程提供了insert()和remove()方法,客戶端代碼通過調用這兩個方法來解決生產者-消費者問題。
管程有一個重要特性:在一個時刻只能有一個進程使用管程。進程在無法繼續(xù)執(zhí)行的時候不能一直占用管程,否則其它進程永遠不能使用管程。
管程引入了條件變量以及相關的操作:wait()和signal()來實現(xiàn)同步操作。對條件變量執(zhí)行wait()操作會導致調用進程阻塞,把管程讓出來給另一個進程持有。signal()操作用于喚醒被阻塞的進程。
使用管程實現(xiàn)生產者-消費者問題
七、進程狀態(tài)的切換
就緒狀態(tài)(ready):等待被調度
運行狀態(tài)(running)
阻塞狀態(tài)(waiting):等待資源應該注意以下內容:
只有就緒態(tài)和運行態(tài)可以相互轉換,其它的都是單向轉換。就緒狀態(tài)的進程通過調度算法從而獲得CPU時間,轉為運行狀態(tài);而運行狀態(tài)的進程,在分配給它的CPU時間片用完之后就會轉為就緒狀態(tài),等待下一次調度。
阻塞狀態(tài)是缺少需要的資源從而由運行狀態(tài)轉換而來,但是該資源不包括CPU時間,缺少CPU時間會從運行態(tài)轉換為就緒態(tài)。
八、守護進程、僵尸進程和孤兒進程
守護進程
指在后臺運行的,沒有控制終端與之相連的進程。它獨立于控制終端,周期性地執(zhí)行某種任務。Linux的大多數(shù)服務器就是用守護進程的方式實現(xiàn)的,如web服務器進程http等
創(chuàng)建守護進程要點:
(1)讓程序在后臺執(zhí)行。方法是調用fork()產生一個子進程,然后使父進程退出。
(2)調用setsid()創(chuàng)建一個新對話期。控制終端、登錄會話和進程組通常是從父進程繼承下來的,守護進程要擺脫它們,不受它們的影響,方法是調用setsid()使進程成為一個會話組長。setsid()調用成功后,進程成為新的會話組長和進程組長,并與原來的登錄會話、進程組和控制終端脫離。
(3)禁止進程重新打開控制終端。經過以上步驟,進程已經成為一個無終端的會話組長,但是它可以重新申請打開一個終端。為了避免這種情況發(fā)生,可以通過使進程不再是會話組長來實現(xiàn)。再一次通過fork()創(chuàng)建新的子進程,使調用fork的進程退出。
(4)關閉不再需要的文件描述符。子進程從父進程繼承打開的文件描述符。如不關閉,將會浪費系統(tǒng)資源,造成進程所在的文件系統(tǒng)無法卸下以及引起無法預料的錯誤。首先獲得最高文件描述符值,然后用一個循環(huán)程序,關閉0到最高文件描述符值的所有文件描述符。
(5)將當前目錄更改為根目錄。
(6)子進程從父進程繼承的文件創(chuàng)建屏蔽字可能會拒絕某些許可權。為防止這一點,使用unmask(0)將屏蔽字清零。
(7)處理SIGCHLD信號。對于服務器進程,在請求到來時往往生成子進程處理請求。如果子進程等待父進程捕獲狀態(tài),則子進程將成為僵尸進程(zombie),從而占用系統(tǒng)資源。如果父進程等待子進程結束,將增加父進程的負擔,影響服務器進程的并發(fā)性能。在Linux下可以簡單地將SIGCHLD信號的操作設為SIG_IGN。這樣,子進程結束時不會產生僵尸進程。
孤兒進程
如果父進程先退出,子進程還沒退出,那么子進程的父進程將變?yōu)閕nit進程。(注:任何一個進程都必須有父進程)。
一個父進程退出,而它的一個或多個子進程還在運行,那么這些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養(yǎng),并由init進程對它們完成狀態(tài)收集工作。
僵尸進程
當子進程比父進程先結束,而父進程又沒有回收子進程,釋放子進程占用的資源,此時子進程將成為一個僵尸進程。如果父進程先退出,子進程被init接管,子進程退出后init會回收其占用的相關資源
設置僵尸進程的目的是維護子進程的信息,以便父進程在以后某個時候獲取。這些信息至少包括進程ID,進程的終止狀態(tài),以及該進程使用的CPU時間,所以當終止子進程的父進程調用wait或waitpid時就可以得到這些信息。如果一個進程終止,而該進程有子進程處于僵尸狀態(tài),那么它的所有僵尸子進程的父進程ID將被重置為1(init進程)。繼承這些子進程的init進程將清理它們(也就是說init進程將wait它們,從而去除它們的僵尸狀態(tài))。
如何解決僵尸進程
1、子進程退出之前,會向父進程發(fā)送一個信號,父進程調用wait(會阻塞)、waitpid函數(shù)(程序不會阻塞)等待這個信號,只要等到了,就不會產生僵尸進程。但這在并發(fā)的服務程序中這是不可能的,因為父進程要做其它的事,例如等待客戶端的新連接,不可能去等待子進程的退出信號。
2、父進程直接忽略子進程的退出信號signal(SIGCHLD,SIG_IGN)
總結
以上是生活随笔為你收集整理的linux多进程知识汇总的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: strcpy和memcpy的区别?
- 下一篇: new / delete与malloc