【Linux】系統編程——進程基礎知識/創建/終止/等待

目錄

基礎概念

程序和進程

進程的狀態

如何創建一個進程

進程控制編程

獲取ID

進程創建 fork()

vfork()  (比較少使用)

exec函數族

execl ()

execlp ()

execv ()

system ()

進程終止 exit()  _exit()

 exit()

 _exit()

孤兒進程

殭屍進程

守護進程

進程等待  wait()  waitpid()

wait() 

waitpid()


基礎概念

搜索文件“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會佔用很多內存直至溢出。

產生過程(看看就行):

  1. 父進程調用fork創建子進程後,子進程運行直至其終止,它立即從內存中移除,但進程描述符仍然保留在內存中(進程描述符佔有極少的內存空間)。
  2. 子進程的狀態變成EXIT_ZOMBIE,並且向父進程發送SIGCHLD 信號,父進程此時應該調用 wait() 系統調用來獲取子進程的退出狀態以及其它的信息。在 wait 調用之後,殭屍進程就完全從內存中移除。
  3. 因此一個殭屍存在於其終止到父進程調用 wait 等函數這個時間的間隙,一般很快就消失,但如果編程不合理,父進程從不調用 wait 等系統調用來收集殭屍進程,那麼這些進程會一直存在內存中。

 怎麼回收殭屍進程

除了wait和waitpid函數,用戶用kill命令其實回收不了,因爲它本身已經死了。還有一個辦法就是殺死父進程,這樣殭屍進程變爲孤兒進程,被init領養,最後都由init回收。
有init領養的進程不會稱爲僵死進程,因爲只要init的子進程終止,init就會調用一個wait函數取得其終止狀態。這樣也就防止了在系統中有很多僵死進程。

守護進程

守護進程(daemon)詳解與創建:下面代碼具體的函數信息以及爲什麼要這麼做都在這裏面

linux之創建守護進程(稍微簡單一點,但還是推薦詳細的)

#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等待

—————————————————————————

進程調度分爲搶佔式和協同式,搶佔式的目的是根據優先級使操作系統的策略更具實時性;協同式根據時間片輪轉,一個一個分配對等的時間片讓它去執行

進程間通信

爲什麼需要進程間通信?進程有獨立的地址空間,每個進程沒有交集,沒有交集就不能通信。所有進程有個最大的交集的操作系統,也就是內核空間,所以通過內核空間進行.......

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章