Linux下的C編程實戰(三)
――進程式控制制與進程通訊編程
1.Linux進程
Linux進程在記憶體中包含三部分資料:程式碼片段、堆棧段和資料區段。程式碼片段存放了程式的代碼。程式碼片段可以為機器中運行同一程式的數個
進程共用。堆棧段存放的是子程式(函數)的返回地址、子程式的參數及程式的局部變數。而資料區段則存放程式的全域變數、常數以及動態資料分配的資料空間(比如用malloc函數申請的記憶體)。與程式碼片段不同,如果系統中同時運行多個相同的程式,它們不能使用同一堆棧段和資料區段。
Linux進程主要有如下幾種狀態:使用者狀態(進程在使用者狀態下啟動並執行狀態)、核心狀態(進程在核心狀態下啟動並執行狀態)、記憶體中就緒(進程沒有執行,但處於就緒狀態,只要核心調度它,就可以執行)、記憶體中睡眠(進程正在睡眠並且處於記憶體中,沒有被交換到SWAP裝置)、就緒且換出(進程處於就緒狀態,但是必須把它換入記憶體,核心才能再次調度它進行運行)、睡眠且換出(進程正在睡眠,且被換出記憶體)、被搶先(進程從核心狀態返回使用者狀態時,核心搶先於它,做了環境切換,調度了另一個進程,原先這個進程就處於被搶先狀態)、建立狀態(進程剛被建立,該進程存在,但既不是就緒狀態,也不是睡眠狀態,這個狀態是除了進程0以外的所有進程的最初狀態)、僵死狀態(進程調用exit結束,進程不再存在,但在進程表項中仍有記錄,該記錄可由父進程收集)。
下面我們來以一個進程從建立到消亡的過程講解Linux進程狀態轉換的“生死因果”。
(1)進程被父進程通過系統調用fork建立而處於建立態;
(2)fork調用為子進程配置好核心資料結構和子進程私人資料結構後,子進程進入就緒態(或者在記憶體中就緒,或者因為記憶體不夠而在SWAP裝置中就緒);
(3)若進程在記憶體中就緒,進程可以被核心發送器調度到CPU運行;
(4)核心調度該進程進入核心狀態,再由核心狀態返回使用者狀態執行。該進程在使用者狀態運行一定時間後,又會被發送器所調度而進入核心狀態,由此轉入就緒態。有時進程在使用者狀態運行時,也會因為需要核心服務,使用系統調用而進入核心狀態,服務完畢,會由核心狀態轉回使用者狀態。要注意的是,進程在從核心狀態向使用者狀態返回時可能被搶佔,這是由於有優先順序更高的進程急需使用CPU,不能等到下一次調度時機,從而造成搶佔;
(5)進程執行exit調用,進入僵死狀態,最終結束。
2.進程式控制制
進程式控制制中主要涉及到進程的建立、睡眠和退出等,在Linux中主要提供了fork、exec、clone的進程建立方法,sleep的進程睡眠和exit的進程退出調用,另外Linux還提供了父進程等待子進程結束的系統調用wait。
fork
對於沒有接觸過Unix/Linux作業系統的人來說,fork是最難理解的概念之一,它執行一次卻返回兩個值,完全“不可思議”。先看下面的程式
:
int main()
{
int i;
if (fork() == 0)
{
for (i = 1; i < 3; i++)
printf("This is child process"n");
}
else
{
for (i = 1; i < 3; i++)
printf("This is parent process"n");
}
}
執行結果為:
This is child process
This is child process
This is parent process
This is parent process
fork在英文中是“分叉”的意思,這個名字取得很形象。一個進程在運行中,如果使用了fork,就產生了另一個進程,於是進程就“分叉”了
。當前進程為父進程,通過fork()會產生一個子進程。對於父進程,fork函數返回子程式的進程號而對於子程式,fork函數則返回零,這就是一個函數返回兩次的本質。可以說,fork函數是Unix系統最傑出的成就之一,它是七十年代Unix早期的開發人員經過理論和實踐上的長期艱苦探
索後取得的成果。
如果我們把上述程式中的迴圈放的大一點:
int main()
{
int i;
if (fork() == 0)
{
for (i = 1; i < 10000; i++)
printf("This is child process"n");
}
else
{
for (i = 1; i < 10000; i++)
printf("This is parent process"n");
}
};
則可以明顯地看到父進程和子進程的並發執行,交替地輸出“This is child process”和“This is parent process”。
此時此刻,我們還沒有完全理解fork()函數,再來看下面的一段程式,看看究竟會產生多少個進程,程式的輸出是什嗎?
int main()
{
int i;
for (i = 0; i < 2; i++)
{
if (fork() == 0)
{
printf("This is child process"n");
}
else
{
printf("This is parent process"n");
}
}
};
exec
在Linux中可使用exec函數族,包含多個函數(execl、execlp、execle、execv、execve和execvp),被用於啟動一個指定路徑和檔案名稱的進程。
exec函數族的特點體現在:某進程一旦調用了exec類函數,正在執行的程式就被幹掉了,系統把程式碼片段替換成新的程式(由exec類函數執行)的代碼,並且原有的資料區段和堆棧段也被廢棄,新的資料區段與堆棧段被分配,但是進程號卻被保留。也就是說,exec執行的結果為:系統認為正在執行的還是原先的進程,但是進程對應的程式被替換了。
fork函數可以建立一個子進程而當前進程不死,如果我們在fork的子進程中調用exec函數族就可以實現既讓父進程的代碼執行又啟動一個新的
指定進程,這實在是很妙的。fork和exec的搭配巧妙地解決了程式啟動另一程式的執行但自己仍繼續啟動並執行問題,請看下面的例子:
char command[MAX_CMD_LEN];
void main()
{
int rtn; /* 子進程的返回數值*/
while (1)
{
/* 從終端讀取要執行的命令*/
printf(">");
fgets(command, MAX_CMD_LEN, stdin);
command[strlen(command) - 1] = 0;
if (fork() == 0)
{
/* 子進程執行此命令*/
execlp(command, command);
/* 如果exec函數返回,表明沒有正常執行命令,列印錯誤資訊*/
perror(command);
exit(errorno);
}
else
{
/* 父進程,等待子進程結束,並列印子進程的傳回值*/
wait(&rtn);
printf(" child process return %d"n", rtn);
}
}
};
這個函數基本上實現了一個shell的功能,它讀取使用者輸入的進程名和參數,並啟動對應的進程。
clone
clone是Linux2.0以後才具備的新功能,它較fork更強(可認為fork是clone要實現的一部分),可以使得建立的子進程共用父進程的資源,並
且要使用此函數必須在編譯核心時設定clone_actually_works_ok選項。
clone函數的原型為:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
此函數返回建立進程的PID,函數中的flags標誌用於設定建立子進程時的相關選項,具體含義如下表:
標誌
含義
CLONE_PARENT
建立的子進程的父進程是調用者的父進程,新進程與建立它的進程成了“兄弟”而不是“父子”
CLONE_FS
子進程與父進程共用相同的檔案系統,包括root、目前的目錄、umask
CLONE_FILES
子進程與父進程共用相同的檔案描述符(file descriptor)表
CLONE_NEWNS
在新的namespace啟動子進程,namespace描述了進程的檔案hierarchy
CLONE_SIGHAND
子進程與父進程共用相同的訊號處理(signal handler)表
CLONE_PTRACE
若父進程被trace,子進程也被trace
CLONE_VFORK
父進程被掛起,直至子進程釋放虛擬記憶體資源
CLONE_VM
子進程與父進程運行於相同的記憶體空間
CLONE_PID
子進程在建立時PID與父進程一致
CLONE_THREAD
Linux 2.4中增加以支援POSIX線程標準,子進程與父進程共用相同的線程群
來看下面的例子:
int variable, fd;
int do_something() {
variable = 42;
close(fd);
_exit(0);
}
int main(int argc, char *argv[]) {
void **child_stack;
char tempch;
variable = 9;
fd = open("test.file", O_RDONLY);
child_stack = (void **) malloc(16384);
printf("The variable was %d"n", variable);
clone(do_something, child_stack, CLONE_VM|CLONE_FILES, NULL);
sleep(1); /* 延時以便子進程完成關閉檔案操作、修改變數 */
printf("The variable is now %d"n", variable);
if (read(fd, &tempch, 1) < 1) {
perror("File Read Error");
exit(1);
}
printf("We could read from the file"n");
return 0;
}
運行輸出:
The variable is now 42
File Read Error
程式的輸出結果告訴我們,子進程將檔案關閉並將變數修改(調用clone時用到的CLONE_VM、CLONE_FILES標誌將使得變數和檔案描述符表被共
享),父進程隨即就感覺到了,這就是clone的特點。
sleep
函數調用sleep可以用來使進程掛起指定的秒數,該函數的原型為:
unsigned int sleep(unsigned int seconds);
該函數調用使得進程掛起一個指定的時間,如果指定掛起的時間到了,該調用返回0;如果該函數調用被訊號所打斷,則返回剩餘掛起的時間數
(指定的時間減去已經掛起的時間)。
exit
系統調用exit的功能是終止本進程,其函數原型為:
void _exit(int status);
_exit會立即終止發出調用的進程,所有屬於該進程的檔案描述符都關閉。參數status作為退出的狀態值返回父進程,在父進程中通過系統調用
wait可獲得此值。
wait
wait系統調用包括:
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait的作用為發出調用的進程只要有子進程,就睡眠到它們中的一個終止為止;waitpid等待由參數pid指定的子進程退出。
3.處理序間通訊
Linux的處理序間通訊(IPC,InterProcess Communication)通訊方法有管道、訊息佇列、共用記憶體、訊號量、套介面等。
管道分為有名管道和無名管道,無名管道只能用於親屬進程之間的通訊,而有名管道則可用於無親屬關係的進程之間。
#define INPUT 0
#define OUTPUT 1
void main()
{
int file_descriptors[2];
/*定義子進程號*/
pid_t pid;
char buf[BUFFER_LEN];
int returned_count;
/*建立無名管道*/
pipe(file_descriptors);
/*建立子進程*/
if ((pid = fork()) == - 1)
{
printf("Error in fork"n");
exit(1);
}
/*執行子進程*/
if (pid == 0)
{
printf("in the spawned (child) process..."n");
/*子進程向父進程寫資料,關閉管道的讀端*/
close(file_descriptors[INPUT]);
write(file_descriptors[OUTPUT], "test data", strlen("test data"));
exit(0);
}
else
{
/*執行父進程*/
printf("in the spawning (parent) process..."n");
/*父進程從管道讀取子進程寫的資料,關閉管道的寫端*/
close(file_descriptors[OUTPUT]);
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
printf("%d bytes of data received from spawned process: %s"n",
returned_count, buf);
}
}
上述程式中,無名管道以
int pipe(int filedis[2]);
方式定義,參數filedis返回兩個檔案描述符filedes[0]為讀而開啟,filedes[1]為寫而開啟,filedes[1]的輸出是filedes[0]的輸入;
在Linux系統下,有名管道可由兩種方式建立(假設建立一個名為“fifoexample”的有名管道):
(1)mkfifo("fifoexample","rw");
(2)mknod fifoexample p
mkfifo是一個函數,mknod是一個系統調用,即我們可以在shell下輸出上述命令。
有名管道建立後,我們可以像讀寫檔案一樣讀寫之:
/* 進程一:讀有名管道*/
void main()
{
FILE *in_file;
int count = 1;
char buf[BUFFER_LEN];
in_file = fopen("pipeexample", "r");
if (in_file == NULL)
{
printf("Error in fdopen."n");
exit(1);
}
while ((count = fread(buf, 1, BUFFER_LEN, in_file)) > 0)
printf("received from pipe: %s"n", buf);
fclose(in_file);
}
/* 進程二:寫有名管道*/
void main()
{
FILE *out_file;
int count = 1;
char buf[BUFFER_LEN];
out_file = fopen("pipeexample", "w");
if (out_file == NULL)
{
printf("Error opening pipe.");
exit(1);
}
sprintf(buf, "this is test data for the named pipe example"n");
fwrite(buf, 1, BUFFER_LEN, out_file);
fclose(out_file);
}
訊息佇列用於運行於同一台機器上的處理序間通訊,與管道相似;
共用記憶體通常由一個進程建立,其餘進程對這塊記憶體區進行讀寫。得到共用記憶體有兩種方式:映射/dev/mem裝置和記憶體映像檔案。前一種方式
不給系統帶來額外的開銷,但在現實中並不常用,因為它控制存取的是實際的實體記憶體;常用的方式是通過shmXXX函數族來實現共用記憶體:
int shmget(key_t key, int size, int flag); /* 獲得一個共用儲存標識符*/
該函數使得系統分配size大小的記憶體用作共用記憶體;
void *shmat(int shmid, void *addr, int flag); /* 將共用記憶體串連到自身地址空間中*/
shmid為shmget函數返回的共用儲存標識符,addr和flag參數決定了以什麼方式來確定串連的地址,函數的傳回值即是該進程資料區段所串連的實
際地址。此後,進程可以對此地址進行讀寫操作訪問共用記憶體。
本質上,訊號量是一個計數器,它用來記錄對某個資源(如共用記憶體)的存取狀況。一般說來,為了獲得共用資源,進程需要執行下列操作:
(1)測試控制該資源的訊號量;
(2)若此訊號量的值為正,則允許進行使用該資源,進程將進號量減1;
(3)若此訊號量為0,則該資源目前不可用,進程進入睡眠狀態,直至訊號量值大於0,進程被喚醒,轉入步驟(1);
(4)當進程不再使用一個訊號量控制的資源時,訊號量值加1,如果此時有進程正在睡眠等待此訊號量,則喚醒此進程。
下面是一個使用訊號量的例子,該程式建立一個特定的IPC結構的關鍵字和一個訊號量,建立此訊號量的索引,修改索引指向的訊號量的值,最
後清除訊號量:
#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
void main()
{
key_t unique_key; /* 定義一個IPC關鍵字*/
int id;
struct sembuf lock_it;
union semun options;
int i;
unique_key = ftok(".", 'a'); /* 產生關鍵字,字元'a'是一個隨機種子*/
/* 建立一個新的訊號量集合*/
id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);
printf("semaphore id=%d"n", id);
options.val = 1; /*設定變數值*/
semctl(id, 0, SETVAL, options); /*設定索引0的訊號量*/
/*列印出訊號量的值*/
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d"n", i);
/*下面重新設定訊號量*/
lock_it.sem_num = 0; /*設定哪個訊號量*/
lock_it.sem_op = - 1; /*定義操作*/
lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/
if (semop(id, &lock_it, 1) == - 1)
{
printf("can not lock semaphore."n");
exit(1);
}
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d"n", i);
/*清除訊號量*/
semctl(id, 0, IPC_RMID, 0);
}
通訊端通訊並不為Linux所專有,在所有提供了TCP/IP協議棧的作業系統中幾乎都提供了socket,而所有這樣作業系統,對通訊端的編程方法幾
乎是完全一樣的。
4.小節
本章講述了Linux進程的概念,並以多個執行個體講解了進程式控制制及處理序間通訊方法,理解這一章的內容可以說是理解Linux這個作業系統的關鍵。