進程控制與進程通信編程

進程控制與進程通信編程
作者:宋寶華 
1.Linux進程
       Linux進程在內存中包含三部分數據:代碼段、堆棧段和數據段。代碼段存放了程序的代碼。代碼段可以爲機器中運行同一程序的數個進程共享。堆棧段存放的是子程序(函數)的返回地址、子程序的參數及程序的局部變量。而數據段則存放程序的全局變量、常數以及動態數據分配的數據空間(比如用malloc函數申請的內存)。與代碼段不同,如果系統中同時運行多個相同的程序,它們不能使用同一堆棧段和數據段。
Linux進程主要有如下幾種狀態:用戶狀態(進程在用戶狀態下運行的狀態)、內核狀態(進程在內核狀態下運行的狀態)、內存中就緒(進程沒有執行,但處於就緒狀態,只要內核調度它,就可以執行)、內存中睡眠(進程正在睡眠並且處於內存中,沒有被交換到SWAP設備)、就緒且換出(進程處於就緒狀態,但是必須把它換入內存,內核才能再次調度它進行運行)、睡眠且換出(進程正在睡眠,且被換出內存)、被搶先(進程從內核狀態返回用戶狀態時,內核搶先於它,做了上下文切換,調度了另一個進程,原先這個進程就處於被搶先狀態)、創建狀態(進程剛被創建,該進程存在,但既不是就緒狀態,也不是睡眠狀態,這個狀態是除了進程0以外的所有進程的最初狀態)、僵死狀態(進程調用exit結束,進程不再存在,但在進程表項中仍有記錄,該記錄可由父進程收集)。
下面我們來以一個進程從創建到消亡的過程講解Linux進程狀態轉換的“生死因果”。
1)進程被父進程通過系統調用fork創建而處於創建態;
2fork調用爲子進程配置好內核數據結構和子進程私有數據結構後,子進程進入就緒態(或者在內存中就緒,或者因爲內存不夠而在SWAP設備中就緒);
3)若進程在內存中就緒,進程可以被內核調度程序調度到CPU運行;
4)內核調度該進程進入內核狀態,再由內核狀態返回用戶狀態執行。該進程在用戶狀態運行一定時間後,又會被調度程序所調度而進入內核狀態,由此轉入就緒態。有時進程在用戶狀態運行時,也會因爲需要內核服務,使用系統調用而進入內核狀態,服務完畢,會由內核狀態轉回用戶狀態。要注意的是,進程在從內核狀態向用戶狀態返回時可能被搶佔,這是由於有優先級更高的進程急需使用CPU,不能等到下一次調度時機,從而造成搶佔;
5)進程執行exit調用,進入僵死狀態,最終結束。
2.進程控制
進程控制中主要涉及到進程的創建、睡眠和退出等,在Linux中主要提供了forkexecclone的進程創建方法,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函數族,包含多個函數(execlexeclpexecleexecvexecveexecvp),被用於啓動一個指定路徑和文件名的進程。
exec函數族的特點體現在:某進程一旦調用了exec類函數,正在執行的程序就被幹掉了,系統把代碼段替換成新的程序(由exec類函數執行)的代碼,並且原有的數據段和堆棧段也被廢棄,新的數據段與堆棧段被分配,但是進程號卻被保留。也就是說,exec執行的結果爲:系統認爲正在執行的還是原先的進程,但是進程對應的程序被替換了。
fork函數可以創建一個子進程而當前進程不死,如果我們在fork的子進程中調用exec函數族就可以實現既讓父進程的代碼執行又啓動一個新的指定進程,這實在是很妙的。forkexec的搭配巧妙地解決了程序啓動另一程序的執行但自己仍繼續運行的問題,請看下面的例子:
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
cloneLinux2.0以後才具備的新功能,它較fork更強(可認爲forkclone要實現的一部分),可以使得創建的子進程共享父進程的資源,並且要使用此函數必須在編譯內核時設置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_VMCLONE_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的進程間通信(IPCInterProcess 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”的有名管道):
1mkfifo("fifoexample","rw");
2mknod 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); /* 將共享內存連接到自身地址空間中*/
shmidshmget函數返回的共享存儲標識符,addrflag參數決定了以什麼方式來確定連接的地址,函數的返回值即是該進程數據段所連接的實際地址。此後,進程可以對此地址進行讀寫操作訪問共享內存。
本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。一般說來,爲了獲得共享資源,進程需要執行下列操作:
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這個操作系統的關鍵。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章