目錄
基礎概念
搜索文件“15、系統編程第二天”
增加:
程序和進程
程序運行起來,產生一個進程
程序,是指編譯好的二進制文件,在磁盤上,不佔用系統資源(cpu、內存、打開的文件、設備、鎖....)
進程,是系統資源分配的最小單位。是一個抽象的概念,進程是活躍的程序,佔用系統資源。在內存中執行。
進程的狀態
- 初始態:一般與就緒態歸爲一類
- 就緒態:一切都準備好了,就等待CPU分配時間片了
- 運行態(執行態):佔用CPU中
- 掛起態:等待除了CPU以外的其他資源,主動放棄CPU(遇到缺少資源或發生中斷)
- 終止態
爲什麼要有三態?
多個進程一起進入執行態可能會發生多進程死鎖,動不了了。三態的好處就是有一個緩衝區,可以一個一個去處理它
如何創建一個進程
運行一個可執行文件,fork,vfork,exec函數族,system(clone是啥?)
併發與並行
併發執行:就是CPU輪換的執行,當前進程執行了一個短暫的時間片(ms)後,切換執行另一個進程,如此循環往復,由於時間片很短,
在宏觀上我們會感覺到所有的進程都是在同時運行的,但是在微觀上cpu每次只執行某一個進程的指令。(單核CPU)
並行執行:如果cpu是多核的話,不同的cpu核可以同時獨立的執行不同的進程,這種叫並行運行。所以當cpu是多核時,併發與並行是同時存在的。
進程互斥
進程互斥是指當有若干進程都要使用某一共享資源時,任何時候最多允許一個進程使用,其他要使用該資源的進程必須等待,直到佔用該資源者釋放了該資源爲止。
進程同步
一組併發進程按一定的順序執行的過程稱爲進程間的同步。
進程同步包含(保證)進程互斥。
就像上廁所,如果沒有同步,沒有訪問順序,一個人出來,其他人誰先搶到誰用,有了同步,廁所外可似乎排隊,一個一個按順序用。
具有同步關係的一組併發進程稱爲合作進程,合作進程間互相發送的信號稱爲消息或事件。
臨界資源&臨界區
- 臨界資源:操作系統中將一次只允許一個進程訪問的資源稱爲臨界資源。
- 臨界區:進程中訪問臨界資源的那段程序代碼稱爲臨界區。爲實現對臨界資源的互斥訪問,應保證諸進程互斥的進入各自的臨界區。
爲什麼要保持進程同步——進程死鎖
多個進程因競爭資源而形成一種僵局,若無外力作用,這些進程都將永遠不能再向前推進。
進程調度
操作系統的核心就是任務(進程)管理
- 先來先服務調度算法
- 最短作業優先調度
- 基於優先級調度
- 循環調度或時間片輪轉法
linux進程特點
Linux系統是一個多進程的系統,它的進程之間具有並行性、互不干擾等特點。也就是說,每個進程都是一個獨立的運行單位,擁有各自的權利和責任。其中,各個進程都運行在獨立的虛擬地址空間,因此,即使一個進程發生異常,它也不會影響到系統中的其他進程。
每個進程擁有獨立進程空間的優缺點
優點:
- 對編程人員來說,系統更容易捕獲隨意的內存讀取和寫入操作
- 對用戶來說,操作系統將變得更加健壯,因爲一個應用程序無法破壞另一個進程或操作系統的運行(防止被攻擊)
缺點:
- 多任務實現開銷較大
- 編寫能夠與其他進程進行通信,或者能夠對其他進程進行操作的應用程序將要困難得多
進程控制編程
獲取ID
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //返回當前進程的id
pid_t getppid(void); //返回父進程的id
例.
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = getpid();
printf("pid = %d\n",pid);
while(1);
return 0;
}
進程創建 fork()
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
/*
返回值:
1)正值(新創子進程的進程ID):父進程;
2)0:子進程:;
3)負值:出現錯誤;
*/
- 當fork()順利完成任務時,就會存在兩個進程,每個進程都從fork()返回處開始繼續執行。
- 兩個進程執行相同的代碼(text)段,但是有各自的堆棧(stack)段、數據(data)段以及堆(heap)。
- 子進程的stack、data、heap segments是從父進程拷貝過來的。(讀時共享寫時複製)
- fork()之後,哪一個進程先執行不確定。如果需要確保特定的執行順序,需要採用某種同步(synchronization)技術(semaphores,file locks...)。
- 父子進程共享:文件描述符,mmap建立的映射區(兩個進程間建立一個映射區,完成進程值之間數據傳遞)
- 父子進程相同之處:全局變量(是數據值相同,不是共享!)、.data、.text、棧、堆、環境變量、用戶ID、宿主目錄、進程工作目錄、信號處理方式
- 父子進程不同之處:1.進程ID 2.fork返回值 3.父進程ID 4.進程運行時間 5.鬧鐘(定時器) 6.未決信號集
shell並不知道運行的進程創建了子進程,所以shell進程在進程結束之後就開始執行自己,如果此時子進程併爲結束運行,shell與子進程共同搶佔CPU,所以會出現以下情況
例.
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
fork();
fork();
pid = fork();
if(pid < 0)
{
perror("fork");
return -1;
}
else if(pid > 0)
{
printf("parent pid is %d\n",getpid());
while(1);
}
else if(0 == pid)
{
printf("child pid is %d\n",getpid());
while(1);
}
return 0;
}
3個fork()調用後,共有8個進程(1生2,2生4,4生8)
vfork() (比較少使用)
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
- 子進程共享父進程的代碼,數據,堆棧資源(父進程與子進程共享空間,變量可互相使用,改了一個變量之後另一個進程變量也改變了。)
- 使用vfork後,直接運行exec,節省了資源拷貝的時間
- 使用vfork,創建子進程後直接運行子進程,父進程被阻塞,直到子進程執行了exec()或者exit()。
- 子進程退出使用return會破壞父進程的堆棧環境(會釋放數據段),產生段錯誤,所以退出使用exit或_exit
目的
vfork是爲子進程立即執行exec的程序而專門設計的:
- 無需爲子進程複製虛擬內存頁或頁表,子進程直接共享父進程的資源,直到其成功執行exec或是調用exit退出。
- 在子進程調用exec之前,將暫停執行父進程
exec函數族
無成功返回值,成功不返回,之後的程序不執行,失敗返回-1,並執行之後的程序。一般exec之後只跟perror與exit兩句就行了(也不用判斷了直接寫就行)
exec函數族和fork的區別
fork創建子進程後執行的是和父進程相同的程序。而子進程可以調用exec函數從而能執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啓動例程開始執行。調用exec並不創建新進程,所以調用exec前後該進程的id並未改變。
exec函數名中英文字母意義(方便記憶)
- l (list) 命令行參數列表(命令寫在".........","........","......",NULL)
- p (path) 在用戶的絕對路徑path下查找可執行文件,該文件必須在用戶路徑下,可以只指定程序文件名
- v (vector) 使用命令行參數數組(命令全都寫在數組中,只傳數組進去)
- e (environment) 爲新進程提供新的環境變量
事實上,只有execve是真正的系統調用,其它五個函數最終都調用execve
一般來說
- execlp:系統可執行程序
- execl:用戶自定義可執行程序
一些可能會用到的命令
- whereis 查看命令的路徑
- pwd 查看文件的路徑
execl ()
#include <unistd.h>
int execl(const char * path, const char* arg1,...)
/*
參數:
path : 被執行程序名(含完整路徑);
arg1 - argn: 被執行程序所需的命令行參數,含程序名。以空指針(NULL)結束.
*/
例.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int ret;
#if 0
ret = execl("/bin/ls","ls","-a","/home",NULL); //第一個參數是ls的絕對路徑,-a表示顯示隱藏文件,/home表示列舉home路徑下的文件
if(ret < 0)
{
perror("execl");
return -1;
}
#endif
ret = execl("/mnt/hgfs/share/2019/0119/exe5_8","exe5_8",NULL); //執行該絕對路徑下的文件
if(ret < 0)
{
perror("execl");
return -1;
}
}
注:execl執行成功後自行結束了程序,所以execl函數之後的它都不會去執行。execl會載入你調用的程序,覆蓋原有代碼段,相當於你本來的程序的代碼段被替換成execl執行的了,所以execl後面的都不會輸出了。正確使用方法應該是fork一個子進程,在子進程中調用execl。
execlp ()
#include <unistd.h>
int execlp(const char * path, const char* arg1,...)
/*
參數:
path : 被執行程序名(不含路徑,將從path環境變量中查找該程序;
arg1 - argn: 被執行程序所需的命令行參數,含程序名。以空指針(NULL)結束.
*/
例.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int ret;
#if 0
ret = execlp("ls","ls","-a","/home",NULL); //第一個參數是ls的相對路徑,-a表示顯示隱藏文件,/home表示列舉home路徑下的文件
if(ret < 0)
{
perror("execlp");
return -1;
}
#endif
#if 1
ret = execlp("../0119/exe5_8","exe5_8",NULL); //執行該相對路徑下的文件
if(ret < 0)
{
perror("execlp");
return -1;
}
#endif
}
execv ()
#include <unistd.h>
int execv(const char * path, const char *argv[])
/*
參數:
path : 被執行程序名(含完整路徑);
argv[]: 被執行程序所需的命令行參數數組。
*/
例.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int ret;
#if 0
char *argv[] = {"ls","-a","/home",NULL};
ret = execv("/bin/ls",argv);
if(ret < 0)
{
perror("execv");
return -1;
}
#endif
#if 1
char *argv1[] = {"exe5_8",NULL};
ret = execv("/mnt/hgfs/share/2019/0119/exe5_8",argv1);
if(ret < 0)
{
perror("execv");
return -1;
}
#endif
}
system ()
#include <stdlib.h>
int system(const char* string)
/*
函數說明:
創建子進程,並加載新程序到子進程空間,運行起來。
調用fork產生子進程,由子進程來調用 /bin/sh -c string來執行參數string所代表的命令。命令行怎
麼輸,string裏面就怎麼寫
*/
例.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
int ret;
system("../0119/exe5_8");
return 0;
}
進程終止 exit() _exit() —— 一個刷緩衝區一個不刷
exit()
#include<stdlib.h>
void exit(int status);
/*
函數說明:
exit()用來正常終結目前進程的執行,並把參數status返回給父進程。
參數:
用於標識進程的退出狀態,shell或父進程可獲取該值
0:表示進程正常退出
-1/1:表示進程退出異常
2-n:用戶可自定義
*/
_exit()
#include<unistd.h>
void _exit(int status);
/*
函數說明:
此函數調用後不會返回,並且會傳遞SIGCHLD信號給父進程,父進程可以由wait函數取得子進程結束狀
態。
*/
正常退出
- main調用return
- 任意地方調用exit庫函數
- 任意地方調用_exit函數
異常退出
- 被信號殺死
- 調用abort函數
孤兒進程
父進程先於子進程結束,則子進程成爲孤兒進程,子進程的父進程成爲init進程,稱爲init進程領養孤兒進程。init進程的id可能爲1也有其他。孤兒進程最終肯定都由init進程回收。
其執行順序大致如下:在一個進程終止時,內核逐個檢查所有活動進程,以判斷它是否是正要終止的進程的子進程,如果是,則該進程的父進程ID就更改爲1(init進程的ID);
殭屍進程
進程終止,父進程尚未回收(獲得終止子進程的有關信息,釋放它仍佔用的資),子進程殘留資源(PCB)存放於內核中,變成殭屍(Zombie)進程
殘留的PCB是爲了讓父進程知道子進程的死亡狀態,如果意外死亡可能需要報仇。如果一個進程在其終止的時候,自己就回收所有分配給它的資源,系統就不會產生所謂的殭屍進程了。如果不好好回收進程,這些殘留的PCB會佔用很多內存直至溢出。
產生過程(看看就行):
- 父進程調用fork創建子進程後,子進程運行直至其終止,它立即從內存中移除,但進程描述符仍然保留在內存中(進程描述符佔有極少的內存空間)。
- 子進程的狀態變成EXIT_ZOMBIE,並且向父進程發送SIGCHLD 信號,父進程此時應該調用 wait() 系統調用來獲取子進程的退出狀態以及其它的信息。在 wait 調用之後,殭屍進程就完全從內存中移除。
- 因此一個殭屍存在於其終止到父進程調用 wait 等函數這個時間的間隙,一般很快就消失,但如果編程不合理,父進程從不調用 wait 等系統調用來收集殭屍進程,那麼這些進程會一直存在內存中。
怎麼回收殭屍進程
除了wait和waitpid函數,用戶用kill命令其實回收不了,因爲它本身已經死了。還有一個辦法就是殺死父進程,這樣殭屍進程變爲孤兒進程,被init領養,最後都由init回收。
有init領養的進程不會稱爲僵死進程,因爲只要init的子進程終止,init就會調用一個wait函數取得其終止狀態。這樣也就防止了在系統中有很多僵死進程。
守護進程
守護進程(daemon)詳解與創建:下面代碼具體的函數信息以及爲什麼要這麼做都在這裏面
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int i;
pid = fork();
if(pid < 0)
return -1;
else if(pid > 0)
exit(0);
setsid();//創建一個會話,把它變成該會話的組長
pid = fork();
if(pid < 0)
return -1;
else if(pid > 0)
exit(0);
chdir("/"); //改變文件目錄
umask(0); //文件掩碼
for(i = 0; i < getdtablesize(); i++) //關掉所有文件描述符
{
close(i);
}
while(1)
{
system("echo test >> /test.log"); //每隔5秒往文件裏寫一個test(echo沒有>>是向屏幕打印的意思,有>>是重定向,向文件裏寫入內容)
sleep(5);
}
return 0;
}
該函數實現了一種後臺進程:每隔5秒相test.log裏寫入一個英文單詞test
echo:相當於輸出。echo 1>1的意思是向文件1輸入1
ps =ef | grep a.out:顯示進程,用以驗證a.out的確是在運行中的
ps aux:顯示所有正在運行的進程及其具體信息?
tail -f | test.log:定時刷新顯示test.log中的內容,以驗證的確是每隔5秒被寫入一個test
kill -9 5344:殺死進程id爲5344的進程,告訴它終止的信號是編號爲9的信號
kill -l:查看信號以及對應編號
進程等待 wait() waitpid()
wait()
#include <sys/wait.h>
pid_t wait(int *status);
/*
返回值:
若成功返回回收的子進程ID,若出錯返回-1(也就是沒有子進程可以回收了)。
*/
功能
- 阻塞等待子進程退出(子進程不退出,父進程就等待,不執行其他程序)
- 回收子進程殘留資源
- 獲取子進程結束狀態(退出原因)(若不想知道原因,直接wait(NULL)即可)
有4個互斥的宏可以用來獲取進程終止的原因:
- WIFEXITED(status) —— Wait IF EXITED
若子進程正常終止,該宏返回true。
此時,可以通過WEXITSTATUS(status)獲取子進程的退出狀態(exit函數的參數或者return的參數,此宏返回一個int)。
- WIFSIGNALED(status)
若子進程由信號殺死,該宏返回true。所有進程異常退出的根本原因是收到了信號。
此時,可以通過WTERMSIG(status)獲取使子進程終止的信號值。
- WIFSTOPPED(status)
若子進程被信號暫停(stopped),該宏返回true。
此時,可以通過WSTOPSIG(status)獲取使子進程暫停的信號值。
- WIFCONTINUED(status)
若子進程通過SIGCONT恢復,該宏返回true。
如果一個子進程已經終止,並且是一個僵死進程,wait立即返回並取得該子進程的狀態,否則wait使其調用者阻塞直到一個子進程終止。如果有多個子進程,只有一個wait,wait只能回收最先終止的進程。
while(wait(NULL)); //可以將子進程全部回收完,再做下面的工作
例.
#include <stdio.h>
#include <stdlib.h> //exit函數要用到
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid, wpid;
int i, status;
pid = fork();
if(pid < 0)
{
perror("fork error:");
exit(1);
}
else if(pid == 0)
{
printf("child:%d,parent:%d\n",getpid(),getppid());
sleep(3);
exit(78);
}
else
{
for(i = 0; i < 10; i++)
{
printf("----parent:%d----\n",getpid());
}
wpid = wait(&status);
if(wpid == -1)
{
perror("wait error:");
return 76;
}
if(WIFEXITED(status))
{
printf("child exit with %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("child kill by %d\n",WTERMSIG(status));
}
}
for(i = 0; i < 10; i++)
{
printf("-----after wait------\n");
}
}
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
int status;
if(pid < 0)
return -1;
else if(0 == pid)
{
printf("child pid is %d\n",getpid());
//while(1);
_exit(0);
}
else
{
printf("parent pid is %d\n",getpid());
wait(&status);
printf("WIFEXITED %d",WIFEXITED(status));
}
return 0;
}
/*
程序結果:
while(1)的話,status那個不會輸出,應爲被阻塞了
_exit(0)的輸出爲1
*/
waitpid()
#include <sys/wait.h>
pid_t waitpid (pid_t pid, int * status, int options)
/*
函數功能:
作用同wait,但可指定pid進程清理,可以不阻塞。
參數:
*status:如果不在意結束狀態值,則參數status可以設成NULL。
pid:欲等待的子進程識別碼:
pid> 0 回收指定ID的子進程
pid=-1 等待任何子進程,相當於wait()。
pid<-1 回收指定進程組爲pid絕對值內的任意子進程
pid=0 回收和當前調用waitpid一個組的所有子進程
option:可以爲 0 或下面的 OR 組合
0:跟wait一樣,子進程沒結束就一直阻塞
WNOHANG: 如果沒有任何已經結束的子進程則馬上返回,不予以等待。
此時返回值爲0表示有子進程正在運行
WUNTRACED :如果子進程進入暫停執行情況則馬上返回,但結束狀態不予以理會。
返回值:
如果執行成功則返回回收的子進程ID,如果有錯誤發生則返回-1(也就是沒有子進程可以回收了)。失敗原因存於errno中。
*/
注意:一次wait或waitpid調用只能清理一個子進程,清理多個子進程應使用循環。
waitpid的不阻塞是指運行到此函數如果有進程還沒結束,它能繼續執行下面的程序,但不代表它還能等這個進程執行完再返回這個函數進行回收。所以不管wait還是waitpid要是想清理所有進程都得使用循環。
進程調度
操作系統的核心就是任務(進程)管理
進程調度器
將有限的CPU資源分配給多個進程
目的:最大化處理器效率,讓多個進程同時運行,互不影響
調度機制分類:
- 協同式(非搶佔性/時間片輪轉):誰先創建誰先執行,按順序執行。一個進程運行完自己的時間片,主動退出,CPU無權訪問。實時性不夠。當一個進程出現異常,產生中斷,或者需要做緊急的事情的時候,不能優先執行,要等到自己的時間片到來才能。
- 搶佔式:實時性好。時間片到了或右更高優先級、調度器搶佔CPU進行任務切換。能及時相應一些異常和突發狀況。每個進程有優先級,先執行優先級更高的。
linux之前是協同式,之後協同式和搶佔式共同工作。
調度器把進程分爲兩類:
- 處理器消耗型:渴望獲取更多的CPU時間,並消耗掉調度器分配的全部時間片。如while死循環,科學計算,影視渲染(很消耗CPU資源)
- I/O消耗型:由於等待某種資源,通常處於阻塞狀態,不需要較長的時間片。如父進程做輸入的時候,不做輸入的時候就會把時間片讓出去
調度器發現你是I/O消耗型,就用優先級調度你,你要用的時候把你的優先級調高,處理器消耗型就用協同式,不讓別人打斷你,讓你把自己的時間片消耗掉,再把使用權讓給別人。
面試小結
談談你對進程的理解
進程是什麼,進程如何創建,創建的方法有哪些,進程如何退出,退出的方法,區別。創建過程中產生的問題殭屍孤兒進程,如何解決。多個進程同時運行需要對進程做調度,哪兩類調度,調度策略。多個進程之間傳輸數據做進程通信
進程是操作系統中分配資源的最小單位。
每個進程都有自己獨立的虛擬內存空間,能達到互不干擾,併發並行運行
創建進程的方法fork,vfork,exec,system,各種的特點
進程的退出又分爲正常退出和異常退出,如何讓進程正常退出?
進程有可能產生殭屍進程和孤兒進程,分別產生的原因?引出init進程
解決殭屍進程,通過進程等待,用wait,waitpid等待
—————————————————————————
進程調度分爲搶佔式和協同式,搶佔式的目的是根據優先級使操作系統的策略更具實時性;協同式根據時間片輪轉,一個一個分配對等的時間片讓它去執行
進程間通信
爲什麼需要進程間通信?進程有獨立的地址空間,每個進程沒有交集,沒有交集就不能通信。所有進程有個最大的交集的操作系統,也就是內核空間,所以通過內核空間進行.......