Linux程序設計——進程和信號

進程和信號在Linux系統中無處不在,它掌控着Linxu的活動,確保系統的正常運行,其重要性不言而喻。

1、進程

進程是指運行着一個或多個線程的地址空間和這些線程所需要的系統資源。在Linux中每個正在運行的程序實例都可以被看作是進程,這些進程之間可以共享程序代碼和系統函數庫,因此在任何時刻系統內存中只有代碼的一份副本。

系統中的每個進程在創建時都會被分配一個唯一的數字編號,稱之爲進程標識符或PID。在Linux系統中,PID的取值範圍是2~32768。之所以是從2開始,是因爲Linux將1保留分配給特殊進程init,這個進程主要用來管理其他進程,可以通過運行ps命令查看系統當前正在運行的進程信息,包括進程的ID、進程的狀態、命令字符串等。

上面提到在系統中正在運行的程序實例都可以被看作進程,意味着同一時刻系統中維持多個進程的運行。但是系統硬件資源是有限的,因此需要合理地進程調度,實現這個功能的是LInux內核的進程調度器。進程調度器根據進程的優先級決定下一個時間片分配給哪個進程。在Linux中,任何進程的運行時間不可能超過分配給它們的時間片,它們採用的是搶先式多任務處理,因此進程的掛起和繼續運行無需彼此之間的協作。在系統中,長期不間斷運行的程序的優先級一般會比較低,相反等待時間越長的進程的優先級越高,繼續運行的概率也高。但是進程的優先級並不是完全由系統控制的,可以通過程序調整程序的優先級。比如需要立即運行的進程,我們可以提高其優先級,保證它能夠獲取下一個時間片。

1)新建進程

創建新進程可以調用系統方法system(const char *string),參數代表啓動進程的命令。但是system對shell的依賴性比較大,因爲它必須用一個shell來啓動需要的程序,所以使用比較多的方法是exec和fork。exec比system函數更有效在於它啓動新的程序後,原來的程序就不再運行,而且由exec啓動的新進程繼承了原進程的很多特性。另外一個方法就是fork,這個系統調用複製當前進程,在進程表中創建一個新的表項,新表項中的許多屬性與當前進程是相同的,但新進程有自己的數據空間、環境和文件描述符。調用fork創建新進程時,在父進程中的fork調用返回的是新的子進程的PID,子進程中的fork調用返回0。父進程可以通過返回值判斷哪些是子進程。

#include <stdio.h>
#include <stdlib.h>

int main()
{
	pid_t pid;
	char *message;
	int n;
	printf("fork program starting\n");
	pid = fork();
	switch(pid)
	{
	case -1:
		perror("fork failed\n");
		break;
	case 0:
		message = "This is the child";
		n = 5;
		break;
	default:
		message = "This is the parent";
		n = 3;
		break;
	}
	for(; n > 0; n--)
	{
		puts(message);
		sleep(1);
	}

	return 0;
}

2)等待進程

當用fork啓動一個子進程時,子進程有自己的生命週期並將獨立運行。如果父進程需要知道子進程何時運行結束,就可以通過在父進程中調用wait函數讓父進程等待子進程結束。wait系統調用將暫停父進程直到它的子進程結束爲止,返回子進程的PID,通常是已經運行結束的進程的PID。通過wait系統調用還可以獲取子進程的退出狀態,即子進程的main函數返回的值或子進程中exit函數的退出碼。

#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>

int main()
{
	pid_t pid;
	char *message;
	int n;
	int exit_code;
	printf("fork programming starting\n");
	pid = fork();
	switch(pid)
	{
	case -1:
		perror("fork failed\n");
		exit(1);
	case 0:
		message = "This is child";
		n = 5;
		exit_code = 37;
		break;
	default:
		message = "This is parent";
		n = 3;
		exit_code = 0;
		break;
	}
	for(; n > 0; n--)
	{
		puts(message);
		sleep(1);
	}
	if(pid != 0)
	{
		int start_val;
		pid_t child_pid;
		child_pid = wait(&start_val);
		printf("Child has finished: PID = %d\n", child_pid);
		if(WIFEXITED(start_val))
		{
			printf("Child exited with code: %d\n", WEXITSTATUS(start_val));
		}
		else
		{
			printf("Child terminated abnormally\n");
		}
	}
	exit(exit_code);
}

3)殭屍進程

子進程終止時,它與父進程之間的關聯還會保持,直到父進程也正常終止或父進程調用wait才結束。因此進程表中代表子進程的表項不會立即釋放。雖然子進程已經不再運行,但它仍然存在於系統中,因此它的退出碼還需要保存起來,以備父進程今後的wait調用使用。這時子進程將成爲一個死(defunct)進程或殭屍(zombie)進程。殭屍進程會帶來系統資源的浪費,可以通過下面幾種方法避免殭屍進程:

a、父進程通過wait和waitpid等函數等待子進程結束,這會導致父進程掛起。

b. 如果父進程很忙,那麼可以用signal函數爲SIGCHLD安裝handler,因爲子進程結束後, 父進程會收到該信號,可以在handler中調用wait回收。

c、如果父進程不關心子進程什麼時候結束,那麼可以用signal(SIGCHLD, SIG_IGN) 通知內核,自己對子進程的結束不感興趣,那麼子進程結束後,內核會回收, 並不再給父進程發送信號。

d、fork兩次,父進程fork一個子進程,然後繼續工作,子進程fork一 個孫進程後退出,那麼孫進程被init接管,孫進程結束後,init會回收。不過子進程的回收還要自己做。

2、信號

信號是UNIX和Linux系統響應某些條件而產生的一個事件。信號是由於某些錯誤條件如內存段衝突、浮點運算錯誤或非法指令等造成的。它們由shell和終端處理器生成來引起中斷,還可以作爲在進程間傳遞消息或修改行爲的一種方式,明確地由一個進程發送給另一個進程。信號可以被生成、捕獲、響應或忽略。信號的名稱定義在signal.h中,以SIG開頭。如果進程接收到某個信號,但事先沒有安排捕獲它,進程將會立刻終止。

對信號處理的函數爲signal,但是這個函數在用戶想要保留信號處理函數時存在問題。程序第一次接收到信號到信號處理函數重建這段時間內如果有程序接收到第二個信號,第二次的信號是得不到處理的。sigaction可以解決這個問題,它可以連續處理到來的信號。

另外使用信號並掛起程序的執行是Linux程序設計很重要的一部分,可以保證程序不需要問題在執行,它可以被掛起等待直到某個事件發生後再繼續運行。常用的程序掛起方法有pause系統調用和sigsuspend系統調用,進程可以通過調用sigsuspend函數掛起自己的執行,直到信號集中的一個信號到達爲止。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

void ouch(int sig) // 信號處理函數
{
	printf("OUCH! - I got signal %d\n", sig);
}

int main()
{
	struct sigaction act;
	act.sa_handler = ouch;
	act.sa_flags = 0;
	sigemptyset(&act.sa_mask);

	sigaction(SIGINT, &act, 0); // 截獲SIGINT信號
	while(1)  // 程序將一直打印Hello world直到按下Ctrl+\組合鍵
	{
		printf("Hello world\n");
		sleep(1);
	}

	return 0;
}

和進程、信號相關的另外一個重要的概念是線程,線程比進程更加複雜,在後面的博文中將會詳細介紹。

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