1. 程式人生 > >linux 多程序

linux 多程序

Linux下的多程序程式設計初步

2. 多程序程式設計

什麼是一個程序?程序這個概念是針對系統而不是針對使用者的,對使用者來說,他面對的概念是程式。當用戶敲入命令執行一個程式的時候,對系統而言,它將啟動一個程序。但和程式不同的是,在這個程序中,系統可能需要再啟動一個或多個程序來完成獨立的多個任務。多程序程式設計的主要內容包括程序控制和程序間通訊,在了 解這些之前,我們先要簡單知道程序的結構。

2.1 Linux下程序的結構

   Linux下一個程序在記憶體裡有三部分的資料,就是”程式碼段”、”堆疊段”和”資料段”。其實學過組合語言的人一定知道,一般的CPU都有上述三種段暫存器,以方便作業系統的執行。這三個部分是構成一個完整的執行序列的必要的部分。”程式碼段”,顧名思義,就是存放了程式程式碼的資料,假如機器中有數個程序執行相同的一個程式,那麼它們就可以使用相同的程式碼段。”堆疊段”存放的就是子程 序的返回地址、子程式的引數以及程式的區域性變數。而資料段則存放程式的全域性變數,常數以及動態資料分配的資料空間(比如用malloc之類的函式取得的空間)。這其中有許多細節問題,這裡限於篇幅就不多介紹了。系統如果同時執行數個相同的程式,它們之間就不能使用同一個堆疊段和資料段。

2.2 Linux下的程序控制

在傳統的Unix環境下,有兩個基本的操作用於建立和修改程序:函式fork()用來建立一個新的程序,該程序幾乎是當前程序的一個完全拷貝;函式族exec()用來啟動另外的程序以取代當前執行的程序。Linux的程序控制和傳統的Unix程序控制基本一致,只在一些細節的地方有些區別,例如在Linux系統 中呼叫vfork和fork完全相同,而在有些版本的Unix系統中,vfork呼叫有不同的功能。由於這些差別幾乎不影響我們大多數的程式設計,在這裡我們 不予考慮。

2.2.1 殭屍程序

子程序在執行結束的時候會釋放佔有的記憶體,檔案。但是會保留一定的資訊(如PID,退出狀態,執行時間等),直到父程序wait/waitpid來索取之後才釋放。如果沒有索取會一直保留,這就導致程序號被一直佔用,殘留的狀態叫做殭屍程序。

殭屍程序的避免

  1. 父程序通過wait和waitpid等函式等待子程序結束,這會導致父程序掛起。
  2. 如果父程序很忙,那麼可以用signal函式為SIGCHLD安裝handler,因為子程序結束後,父程序會收到該訊號,可以在handler中呼叫wait回收。
  3. 如果父程序不關心子程序什麼時候結束,那麼可以用signal(SIGCHLD,SIG_IGN) 通知核心,自己對子程序的結束不感興趣,那麼子程序結束後,核心會回收,並不再給父程序傳送訊號。
  4. 還有一些技巧,就是fork兩次,父程序fork一個子程序,然後繼續工作,子程序fork一個孫程序後退出,那麼孫程序被init接管,孫程序結束後,init會回收。不過子程序的回收還要自己做。

2.2.2 fork

fork在英文中是”分叉”的意思。為什麼取這個名字呢?因為一個程序在執行中,如果使用了fork,就產生了另一個程序,於是程序就”分叉”了,所以這個名字取得很形象。fork()有兩個返回值或者一個返回值(出錯時候),在父程序fork返回子程序id,子程序返回0.利用getpid可以檢視當前程序id,getppid檢視父程序id,但在子程序中getppid返回的是1。

2.2.3 exec( )函式族

下面我們來看看一個程序如何來啟動另一個程式的執行。在Linux中要使用exec函式族。系統呼叫execve()對當前程序進行替換,替換者為一個指定的程式,其引數包括檔名(filename)、引數列表(argv)以及環境變數(envp)。exec函式族當然不止一個,但它們大致相同,在Linux中,它們分別是:execl,execlp,execle,execv,execve和execvp,下面我只以execlp為例,其它函式究 竟與execlp有何區別,請通過manexec命令來了解它們的具體情況。

一個程序一旦呼叫exec類函式,它本身就”死亡”了,系統把程式碼段替換成新的程式的程式碼,廢棄原有的資料段和堆疊段,併為新程式分配新的資料段與堆疊段,唯一留下的,就是程序號,也就是說,對系統而言,還是同一個程序,不過已經是另一個程式了。(不過exec類函式中有的還允許繼承環境變數之類的信 息。)

那麼如果我的程式想啟動另一程式的執行但自己仍想繼續執行的話,怎麼辦呢?那就是結合fork與exec的使用。下面一段程式碼顯示如何啟動執行其它程式:
  

char  command[256];   
void  main()   
{   
int  rtn;  /*子程序的返回數值*/    
while (1) {   
/* 從終端讀取要執行的命令 */    
printf(  ">"  );   
fgets( command, 256, 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 );   
}   
}   
}   

char command[256];
void main()
{
int rtn; /*子程序的返回數值*/
while(1) {
/* 從終端讀取要執行的命令 */
printf( ">" );
fgets( command, 256, 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 );
}
}
}

此程式從終端讀入命令並執行之,執行完成後,父程序繼續等待從終端讀入命令。DOS/WINDOWS也有exec類函式,其使用方法是類似的,但DOS/WINDOWS還有spawn類函式,因為DOS是單任務的系統,它只能將”父程序”駐留在機器內再執行”子程序”,這就是spawn類的函式。WIN32已經是多工的系統了,但還保留了spawn類函式,WIN32中實現spawn函式的方法同前述UNIX中的方法差不多,開設子程序後父程序等待子程序結束後才繼續執行。UNIX在其一開始就是多工的系統,所以從核心角度上講不需要spawn類函 數。

在這一節裡,我們還要講講system()和popen()函式。system()函式先呼叫fork(),然後再呼叫exec()來執行使用者的登入shell,通過它來查詢可執行檔案的命令並分析引數,最後它麼使用wait()函式族之一來等待子程序的結束。函式popen()和函式system()相似,不同的是它呼叫pipe()函式建立一個管道,通過它來完成程式的標準輸入和標準輸出。這兩個函式是為那些不太勤快的程式設計師設計 的,在效率和安全方面都有相當的缺陷,在可能的情況下,應該儘量避免。

2.3 Linux下的程序間通訊

詳細的講述程序間通訊在這裡絕對是不可能的事情,而且筆者很難有信心說自己對這一部分內容的認識達到了什麼樣的地步,所以在這一節的開頭首先向大家推薦著名作者RichardStevens的著名作品:《Advanced Programming in the UNIXEnvironment》 ,它的中文譯本《UNIX環境高階程式設計》已有機械工業出版社出版,原文精彩,譯文同樣地道,如果你的確對在Linux下程式設計有濃厚的興趣,那麼趕緊將這本書擺到你的書桌上或計算機旁邊來。說這麼多實在是難抑心中的景仰之情,言歸正傳,在這一節裡,我們將介紹程序間通訊最最初步和最 最簡單的一些知識和概念。

首先,程序間通訊至少可以通過傳送開啟檔案來實現,不同的程序通過一個或多個檔案來傳遞資訊,事實上,在很多應用系統裡,都使用了這種方法。但一般說來,程序間通訊(IPC:InterProcess Communication)不包括這種似乎比較低階的通訊方法。Unix系統中實現程序間通訊的方法很多,而且不幸的是,極少方法能在所有的Unix系統中進行移植(唯一一種是半雙工的管道,這也是最原始的一種通訊方式)。而Linux作為一種新興的作業系統,幾乎支援所有的Unix下常用的程序間通訊 方法:管道、訊息佇列、共享記憶體、訊號量、套介面等等。下面我們將逐一介紹。

2.3.1 管道

無名管道

   管道是程序間通訊中最古老的方式,它包括無名管道和有名管道兩種,前者用於父程序和子程序間的通訊,後者用於運行於同一臺機器上的任意兩個程序間的通訊。

無名管道由pipe()函式建立:

#include “unistd.h”
int pipe(int filedis[2]);
引數filedis返回兩個檔案描述符:filedes[0]為讀而開啟,filedes[1]為寫而開啟。filedes[1]的輸出是filedes[0]的輸入。下面的例子示範瞭如何在父程序和子程序間實現通訊。

#define INPUT 0    
#define OUTPUT 1    

void  main() {   
int  file_descriptors[2];   
/*定義子程序號 */    
pid_t pid;   
char  buf[256];   
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);   
}   
}   

#define INPUT 0
#define OUTPUT 1

void main() {
int file_descriptors[2];
/*定義子程序號 */
pid_t pid;
char buf[256];
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 mkfifo(const char *pathname, mode_t mode)
pathname: FIFO檔名
mode: 屬性
建立成功返回值大於0,一旦建立了了FIFO,就可open去開啟它,可以使用open,read,close等去操作FIFO
當開啟FIFO時,非阻塞標誌(O_NONBLOCK)將會對讀寫產生如下影響:
1. 沒有使用O_NONBLOCK:訪問要求無法滿足時程序將阻塞。如試圖讀取空的FIFO,將導致程序阻塞;
1. 使用O_NONBLOCK:訪問要求無法滿足時不阻塞,立即出錯返回,errno是ENXIO;

讀管道example:

 #include <stdio.h> 
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 #define P_FIFO         "/tmp/p_fifo"
 int main(int argc, char** argv)
{
         char cache[100];
         int fd;
         memset(cache,0, sizeof(cache));                             //初始化記憶體
         if(access(P_FIFO,F_OK)==0){                                 //管道檔案存在
                   execlp("rm","-f", P_FIFO, NULL);                  //刪掉
                   printf("access.\n");
         }
         if(mkfifo(P_FIFO, 0777) < 0){           
                   printf("createnamed pipe failed.\n");
         }
         fd= open(P_FIFO,O_RDONLY|O_NONBLOCK);        //     非阻塞方式開啟,只讀
         while(1){                                                                             //     一直去讀
                   memset(cache,0, sizeof(cache));
                   if((read(fd,cache, 100)) == 0 ){                           //     沒有讀到資料
                            printf("nodata:\n");
                   }
                   else
                            printf("getdata:%s\n", cache);                //     讀到資料,將其列印
                            sleep(1); //休眠1s
         }
         close(fd);
         return0;
}

寫管道example:
 #include <stdio.h>
 #include <fcntl.h>
 #include <unistd.h>
 #define P_FIFO "/tmp/p_fifo"
 int main(int argc, char argv[])
{ 
         intfd;
         if(argc< 2){
                   printf("pleaseinput the write data.\n");
         }
         fd= open(P_FIFO,O_WRONLY|O_NONBLOCK);                //非阻塞方式
         write(fd,argv[1], 100);                                                            //將argv[1]寫道fd裡面去
         close(fd);
}

測試:
root--> ./mkfifo_r
no data:
no data:
get data:linuxdba
no data:
no data:
no data:
no data:
no data:
......

2.3.2 訊息佇列

   訊息佇列用於運行於同一臺機器上的程序間通訊,它和管道很相似,事實上,它是一種正逐漸被淘汰的通訊方式,我們可以用流管道或者套介面的方式來取代它,所以,我們對此方式也不再解釋,也建議讀者忽略這種方式。

2.3.3 共享記憶體

1. shmget函式

共享記憶體是執行在同一臺機器上的程序間通訊最快的方式,因為資料不需要在不同的程序間複製。通常由一個程序建立一塊共享記憶體區,其餘程序對這塊記憶體區進行讀寫。得到共享記憶體有兩種方式:對映/dev/mem裝置和記憶體映像檔案。前一種方式不給系統帶來額外的開銷,但在現實中並不常用,因為它控制存取的將是實際的實體記憶體,在Linux系統下,這隻有通過限制Linux系統存取的記憶體才可以做到,這當然不太實際。常用的方式是通過shmXXX函式族來實現利 用共享記憶體進行儲存的。
首先要用的函式是shmget,它獲得一個共享儲存識別符號。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int flag);
  • 程式需要提供一個引數key(非0整數),它有效地為共享記憶體段命名,shmget函式成功時返回一個與key相關的共享記憶體識別符號(非負整數),用於後續的共享記憶體函式。呼叫失敗返回-1.

不相關的程序可以通過該函式的返回值訪問同一共享記憶體,它代表程式可能要使用的某個資源,程式對所有共享記憶體的訪問都是間接的,程式先通過呼叫shmget函式並提供一個鍵,再由系統生成一個相應的共享記憶體識別符號(shmget函式的返回值),只有shmget函式才直接使用訊號量鍵,所有其他的訊號量函式使用由semget函式返回的訊號量識別符號。

  • 第二個引數,size以位元組為單位指定需要共享的記憶體容量

  • 第三個引數,shmflg是許可權標誌,它的作用與open函式的mode引數一樣,如果要想在key標識的共享記憶體不存在時,建立它的話,可以與IPC_CREAT做或操作。共享記憶體的許可權標誌與檔案的讀寫許可權一樣,舉例來說,0644,它表示允許一個程序建立的共享記憶體被記憶體建立者所擁有的程序向共享記憶體讀取和寫入資料,同時其他使用者建立的程序只能讀取共享記憶體。

2. shmat函式

第一次建立完共享記憶體時,它還不能被任何程序訪問,shmat函式的作用就是用來啟動對該共享記憶體的訪問,並把共享記憶體連線到當前程序的地址空間。它的原型如下:

    void *shmat(int shm_id, const void *shm_addr, int shmflg);  
  • 第一個引數,shm_id是由shmget函式返回的共享記憶體標識。
  • 第二個引數,shm_addr指定共享記憶體連線到當前程序中的地址位置,通常為空,表示讓系統來選擇共享記憶體的地址。
  • 第三個引數,shm_flg是一組標誌位,通常為0。

呼叫成功時返回一個指向共享記憶體第一個位元組的指標,如果呼叫失敗返回-1.

3. shmdt函式

該函式用於將共享記憶體從當前程序中分離。注意,將共享記憶體分離並不是刪除它,只是使該共享記憶體對當前程序不再可用。它的原型如下:

    int shmdt(const void *shmaddr);  

引數shmaddr是shmat函式返回的地址指標,呼叫成功時返回0,失敗時返回-1.

4. shmctl函式

與訊號量的semctl函式一樣,用來控制共享記憶體,它的原型如下:

    int shmctl(int shm_id, int command, struct shmid_ds *buf);  
  • 第一個引數,shm_id是shmget函式返回的共享記憶體識別符號。

  • 第二個引數,command是要採取的操作,它可以取下面的三個值 :

    • IPC_STAT:把shmid_ds結構中的資料設定為共享記憶體的當前關聯值,即用共享記憶體的當前關聯值覆蓋shmid_ds的值。
    • IPC_SET:如果程序有足夠的許可權,就把共享記憶體的當前關聯值設定為shmid_ds結構中給出的值
    • IPC_RMID:刪除共享記憶體段
  • 第三個引數,buf是一個結構指標,它指向共享記憶體模式和訪問許可權的結構。
    shmid_ds結構至少包括以下成員:

    struct shmid_ds  
    {  
        uid_t shm_perm.uid;  
        uid_t shm_perm.gid;  
        mode_t shm_perm.mode;  
    };  
5. 使用共享記憶體進行程序間通訊

下面就以兩個不相關的程序來說明程序間如何通過共享記憶體來進行通訊。其中一個檔案shmread.c建立共享記憶體,並讀取其中的資訊,另一個檔案shmwrite.c向共享記憶體中寫入資料。為了方便操作和資料結構的統一,為這兩個檔案定義了相同的資料結構,定義在檔案shmdata.c中。結構shared_use_st中的written作為一個可讀或可寫的標誌,非0:表示可讀,0表示可寫,text則是記憶體中的檔案。

shmdata.h的原始碼如下:

#ifndef _SHMDATA_H_HEADER  
#define _SHMDATA_H_HEADER  

#define TEXT_SZ 2048  

struct shared_use_st  
{  
    int written;//作為一個標誌,非0:表示可讀,0表示可寫  
    char text[TEXT_SZ];//記錄寫入和讀取的文字  
};  

#endif  

原始檔shmread.c的原始碼如下:

#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <sys/shm.h>  
#include "shmdata.h"  

int main()  
{  
    int running = 1;//程式是否繼續執行的標誌  
    void *shm = NULL;//分配的共享記憶體的原始首地址  
    struct shared_use_st *shared;//指向shm  
    int shmid;//共享記憶體識別符號  
    //建立共享記憶體  
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);  
    if(shmid == -1)  
    {  
        fprintf(stderr, "shmget failed\n");  
        exit(EXIT_FAILURE);  
    }  
    //將共享記憶體連線到當前程序的地址空間  
    shm = shmat(shmid, 0, 0);  
    if(shm == (void*)-1)  
    {  
        fprintf(stderr, "shmat failed\n");  
        exit(EXIT_FAILURE);  
    }  
    printf("\nMemory attached at %X\n", (int)shm);  
    //設定共享記憶體  
    shared = (struct shared_use_st*)shm;  
    shared->written = 0;  
    while(running)//讀取共享記憶體中的資料  
    {  
        //沒有程序向共享記憶體定資料有資料可讀取  
        if(shared->written != 0)  
        {  
            printf("You wrote: %s", shared->text);  
            sleep(rand() % 3);  
            //讀取完資料,設定written使共享記憶體段可寫  
            shared->written = 0;  
            //輸入了end,退出迴圈(程式)  
            if(strncmp(shared->text, "end", 3) == 0)  
                running = 0;  
        }  
        else//有其他程序在寫資料,不能讀取資料  
            sleep(1);  
    }  
    //把共享記憶體從當前程序中分離  
    if(shmdt(shm) == -1)  
    {  
        fprintf(stderr, "shmdt failed\n");  
        exit(EXIT_FAILURE);  
    }  
    //刪除共享記憶體  
    if(shmctl(shmid, IPC_RMID, 0) == -1)  
    {  
        fprintf(stderr, "shmctl(IPC_RMID) failed\n");  
        exit(EXIT_FAILURE);  
    }  
    exit(EXIT_SUCCESS);  
}  

原始檔shmwrite.c的原始碼如下:

#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <string.h>  
#include <sys/shm.h>  
#include "shmdata.h"  

int main()  
{  
    int running = 1;  
    void *shm = NULL;  
    struct shared_use_st *shared = NULL;  
    char buffer[BUFSIZ + 1];//用於儲存輸入的文字  
    int shmid;  
    //建立共享記憶體  
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);  
    if(shmid == -1)  
    {  
        fprintf(stderr, "shmget failed\n");  
        exit(EXIT_FAILURE);  
    }  
    //將共享記憶體連線到當前程序的地址空間  
    shm = shmat(shmid, (void*)0, 0);  
    if(shm == (void*)-1)  
    {  
        fprintf(stderr, "shmat failed\n");  
        exit(EXIT_FAILURE);  
    }  
    printf("Memory attached at %X\n", (int)shm);  
    //設定共享記憶體  
    shared = (struct shared_use_st*)shm;  
    while(running)//向共享記憶體中寫資料  
    {  
        //資料還沒有被讀取,則等待資料被讀取,不能向共享記憶體中寫入文字  
        while(shared->written == 1)  
        {  
            sleep(1);  
            printf("Waiting...\n");  
        }  
        //向共享記憶體中寫入資料  
        printf("Enter some text: ");  
        fgets(buffer, BUFSIZ, stdin);  
        strncpy(shared->text, buffer, TEXT_SZ);  
        //寫完資料,設定written使共享記憶體段可讀  
        shared->written = 1;  
        //輸入了end,退出迴圈(程式)  
        if(strncmp(buffer, "end", 3) == 0)  
            running = 0;  
    }  
    //把共享記憶體從當前程序中分離  
    if(shmdt(shm) == -1)  
    {  
        fprintf(stderr, "shmdt failed\n");  
        exit(EXIT_FAILURE);  
    }  
    sleep(2);  
    exit(EXIT_SUCCESS);  
}  

分析:

1、程式shmread建立共享記憶體,然後將它連線到自己的地址空間。在共享記憶體的開始處使用了一個結構struct_use_st。該結構中有個標誌written,當共享記憶體中有其他程序向它寫入資料時,共享記憶體中的written被設定為0,程式等待。當它不為0時,表示沒有程序對共享記憶體寫入資料,程式就從共享記憶體中讀取資料並輸出,然後重置設定共享記憶體中的written為0,即讓其可被shmwrite程序寫入資料。

2、程式shmwrite取得共享記憶體並連線到自己的地址空間中。檢查共享記憶體中的written,是否為0,若不是,表示共享記憶體中的資料還沒有被完,則等待其他程序讀取完成,並提示使用者等待。若共享記憶體的written為0,表示沒有其他程序對共享記憶體進行讀取,則提示使用者輸入文字,並再次設定共享記憶體中的written為1,表示寫完成,其他程序可對共享記憶體進行讀操作。

  • 關於前面的例子的安全性討論
    這個程式是不安全的,當有多個程式同時向共享記憶體中讀寫資料時,問題就會出現。可能你會認為,可以改變一下written的使用方式,例如,只有當written為0時程序才可以向共享記憶體寫入資料,而當一個程序只有在written不為0時才能對其進行讀取,同時把written進行加1操作,讀取完後進行減1操作。這就有點像檔案鎖中的讀寫鎖的功能。咋看之下,它似乎能行得通。但是這都不是原子操作,所以這種做法是行不能的。試想當written為0時,如果有兩個程序同時訪問共享記憶體,它們就會發現written為0,於是兩個程序都對其進行寫操作,顯然不行。當written為1時,有兩個程序同時對共享記憶體進行讀操作時也是如些,當這兩個程序都讀取完是,written就變成了-1.

要想讓程式安全地執行,就要有一種程序同步的進位制,保證在進入臨界區的操作是原子操作。例如,可以使用前面所講的訊號量來進行程序的同步。因為訊號量的操作都是原子性的。

6.使用共享記憶體的優缺點

1、優點:我們可以看到使用共享記憶體進行程序間的通訊真的是非常方便,而且函式的介面也簡單,資料的共享還使程序間的資料不用傳送,而是直接訪問記憶體,也加快了程式的效率。同時,它也不像匿名管道那樣要求通訊的程序有一定的父子關係。

2、缺點:共享記憶體沒有提供同步的機制,這使得我們在使用共享記憶體進行程序間通訊時,往往要藉助其他的手段來進行程序間的同步工作。

2.3.4 訊號量

訊號量又稱為訊號燈,它是用來協調不同程序間的資料物件的,而最主要的應用是前一節的共享記憶體方式的程序間通訊。本質上,訊號量是一個計數器,它用來記錄對某個資源(如共享記憶體)的存取狀況。一般說來,為了獲得共享資源,程序需要執行下列操作:

測試控制該資源的訊號量。
1. 若此訊號量的值為正,則允許進行使用該資源。程序將進號量減1。
1. 若此訊號量為0,則該資源目前不可用,程序進入睡眠狀態,直至訊號量值大於0,程序被喚醒,轉入步驟(1)。
1. 當程序不再使用一個訊號量控制的資源時,訊號量值加1。如果此時有程序正在睡眠等待此訊號量,則喚醒此程序。

維護訊號量狀態的是Linux核心作業系統而不是使用者程序。我們可以從標頭檔案”/usr/src/linux/include/linux/sem.h“” 中看到核心用來維護訊號量狀態的各個結構的定義。訊號量是一個數據集合,使用者可以單獨使用這一集合的每個元素。要呼叫的第一個函式是semget,用以獲 得一個訊號量ID。

#include <sys/types.h>    
   #include <sys/ipc.h>   
   #include <sys/sem.h>   
    int  semget(key_t key,  int  nsems,  int  flag);   

#include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/sem.h>
   int semget(key_t key, int nsems, int flag);

key是前面講過的IPC結構的關鍵字,它將來決定是建立新的訊號量集合,還是引用一個現有的訊號量集合。nsems是該集合中的訊號量數。如果是建立新集合(一般在伺服器中),則必須指定nsems;如果是引用一個現有的訊號量集合(一般在客戶機中)則將nsems指定為0。semctl函式用來對訊號量進行操作。

   int semctl(int semid, int semnum, int cmd, union semun arg);

不同的操作是通過cmd引數來實現的,在標頭檔案sem.h中定義了7種不同的操作,實際程式設計時可以參照使用。semop函式自動執行訊號量集合上的運算元組。

int semop(int semid, struct sembuf semoparray[], size_t nops);

semoparray是一個指標,它指向一個訊號量運算元組。nops規定該陣列中操作的數量。

下面,我們看一個具體的例子,它建立一個特定的IPC結構的關鍵字和一個訊號量,建立此訊號量的索引,修改索引指向的訊號量的值,最後我們清除訊號量。在下面的程式碼中,函式ftok生成我們上文所說的唯一的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);   
}   

#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);
}

2.3.5 套介面

套介面(socket)程式設計是實現Linux系統和其他大多數作業系統中程序間通訊的主要方式之一。我們熟知的WWW服務、FTP服務、TELNET服務等都是基於套介面程式設計來實現的。除了在異地的計算機程序間以外,套介面同樣適用於本地同一臺計算機內部的程序間通訊。關於套介面的經典教材同樣是 Richard Stevens編著的《Unix網路程式設計:聯網的API和套接字》,清華大學出版社出版了該書的影印版。它同樣是Linux程式設計師的必備書籍之一。

關於這一部分的內容,可以參照本文作者的另一篇文章《設計自己的網路螞蟻》,那裡由常用的幾個套介面函式的介紹和示例程式。這一部分或許是Linux程序間通訊程式設計中最須關注和最吸引人的一部分,畢竟,Internet 正在我們身邊以不可思議的速度發展著,如果一個程式設計師在設計編寫他下一個程式的時候,根本沒有考慮到網路,考慮到Internet,那麼,可以說,他的設 計很難成功。

3 Linux的程序和Win32的程序/執行緒比較

熟悉WIN32程式設計的人一定知道,WIN32的程序管理方式與Linux上有著很大區別,在UNIX裡,只有程序的概念,但在WIN32裡卻還有一個”執行緒”的概念,那麼Linux和WIN32在這裡究竟有著什麼區別呢?
WIN32裡的程序/執行緒是繼承自OS/2的。在WIN32裡,”程序”是指一個程式,而”執行緒”是一個”程序”裡的一個執行”線索”。從核心上講,WIN32的多程序與Linux並無多大的區別,在WIN32裡的執行緒才相當於Linux的程序,是一個實際正在執行的程式碼。但是,WIN32裡同一個程序裡各個執行緒之間是共享資料段的。這才是與Linux的程序最大的不同。

下面這段程式顯示了WIN32下一個程序如何啟動一個執行緒。

int  g;   
DWORD  WINAPI ChildProcess(  LPVOID  lpParameter ){   
int  i;   
for  ( i = 1; i <1000; i ++) {   
g ++;   
printf(  "This is Child Thread: %d/n" , g );   
}   
ExitThread( 0 );   
};   

void  main()   
{   
int  threadID;   
int  i;   
g = 0;   
CreateThread( NULL, 0, ChildProcess, NULL, 0, &threadID );   
for  ( i = 1; i <1000; i ++) {   
g ++;   
printf(  "This is Parent Thread: %d/n" , g );   
}   
}   

int g;
DWORD WINAPI ChildProcess( LPVOID lpParameter ){
int i;
for ( i = 1; i <1000; i ++) {
g ++;
printf( "This is Child Thread: %d/n", g );
}
ExitThread( 0 );
};

void main()
{
int threadID;
int i;
g = 0;
CreateThread( NULL, 0, ChildProcess, NULL, 0, &threadID );
for ( i = 1; i <1000; i ++) {
g ++;
printf( "This is Parent Thread: %d/n", g );
}
}

在WIN32下,使用CreateThread函式建立執行緒,與Linux下建立程序不同,WIN32執行緒不是從建立處開始執行的,而是由 CreateThread指定一個函式,執行緒就從那個函式處開始執行。此程式同前面的UNIX程式一樣,由兩個執行緒各列印1000條資訊。threadID是子執行緒的執行緒號,另外,全域性變數g是子執行緒與父執行緒共享的,這就是與Linux最大的不同之處。大家可以看出,WIN32的程序/執行緒要比Linux複雜,在Linux要實現類似WIN32的執行緒並不難,只要fork以後,讓子程序呼叫ThreadProc函式,並且為全域性變數開設共享 資料區就行了,但在WIN32下就無法實現類似fork的功能了。所以現在WIN32下的 C語言 編譯器所提供的庫函式雖然已經能相容大多數Linux/UNIX的庫函式,但卻仍無法實現fork。

對於多工系統,共享資料區是必要的,但也是一個容易引起混亂的問題,在WIN32下,一個程式設計師很容易忘記執行緒之間的資料是共享的這一情況,一個執行緒修改過一個變數後,另一個執行緒卻又修改了它,結果引起程式出問題。但在Linux下,由於變數本來並不共享,而由程式設計師來顯式地指定要共享的資料,使程式變 得更清晰與安全。至於WIN32的”程序”概念,其含義則是”應用程式”,也就是相當於UNIX下的exec了。

Linux也有自己的多執行緒函式pthread,它既不同於Linux的程序,也不同於WIN32下的程序,關於pthread的介紹和如何在Linux環境下編寫多執行緒程式我們將在另一篇文章《Linux下的多執行緒程式設計》中講述。