深刻理解Linux進程間通信(IPC)

深刻理解Linux進程間通信(IPC)

分類: Linux環境編程 1941人閱讀 評論(0) 收藏 舉報

目錄(?)[+]

  1. 管道
    1. 管道概述及相關API應用
      1. 管道相關的關鍵概念
      2. 管道的創建
      3. 管道的讀寫規則
      4. 管道應用實例
      5. 管道的侷限性
    2. 有名管道概述及相關API應用
      1. 有名管道相關的關鍵概念
      2. 有名管道的創建
      3. 有名管道的打開規則
      4. 有名管道的讀寫規則
      5. 有名管道應用實例
    3. 小結
    4. 參考資料
  2. 信號上
    1. 信號及信號來源
      1. 信號本質
      2. 信號來源
    2. 信號的種類
      1. 可靠信號與不可靠信號
      2. 實時信號與非實時信號
    3. 進程對信號的響應
    4. 信號的發送
    5. 信號的安裝設置信號關聯動作
    6. 信號集及信號集操作函數
    7. 信號阻塞與信號未決
    8. 參考資料
  3. 信號下
    1. 信號生命週期
    2. 信號編程注意事項
    3. 深入淺出信號應用實例
    4. 結束語
    5. 參考資料
  4. 消息隊列
    1. 消息隊列基本概念
    2. 操作消息隊列
    3. 消息隊列的限制
    4. 消息隊列應用實例
    5. 小結
    6. 參考資料
  5. 信號燈
    1. 信號燈概述
    2. Linux信號燈
    3. 信號燈與內核
    4. 操作信號燈
    5. 信號燈的限制
    6. 競爭問題
    7. 信號燈應用實例
    8. 參考資料
  6. 共享內存上
    1. 內核怎樣保證各個進程尋址到同一個共享內存區域的內存頁面
    2. mmap及其相關係統調用
    3. mmap範例
    4. 對mmap返回地址的訪問
    5. 參考資料
  7. 共享內存下
    1. 系統V共享內存原理
    2. 系統V共享內存API
    3. 系統V共享內存限制
    4. 系統V共享內存範例
    5. 結論
    6. 參考資料
  8. 套接口
    1. 背景知識
    2. 重要數據結構
    3. 套接口編程的幾個重要步驟
    4. 典型調用代碼
    5. 網絡編程中的其他重要概念
    6. 參考資料
深刻理解Linux進程間通信(IPC)

0. 序

1. 管道

1.1. 管道概述及相關API應用

1.2. 有名管道概述及相關API應用

1.3. 小結

1.4. 參考資料

2. 信號(上)

2.1. 信號及信號來源

2.2. 信號的種類

2.3. 進程對信號的響應

2.4. 信號的發送

2.5. 信號的安裝(設置信號關聯動作)

2.6. 信號集及信號集操作函數

2.7. 信號阻塞與信號未決

2.8. 參考資料

3. 信號(下)

3.1. 信號生命週期

3.2. 信號編程注意事項

3.3. 深入淺出:信號應用實例

3.4. 結束語

3.5. 參考資料

4. 消息隊列

4.1. 消息隊列基本概念

4.2. 操作消息隊列

4.3. 消息隊列的限制

4.4. 消息隊列應用實例

4.5. 小結

4.6. 參考資料

5. 信號燈

5.1. 信號燈概述

5.2. Linux信號燈

5.3. 信號燈與內核

5.4. 操作信號燈

5.5. 信號燈的限制

5.6. 競爭問題

5.7. 信號燈應用實例

5.8. 參考資料

6. 共享內存(上)

6.1. 內核怎樣保證各個進程尋址到同一個共享內存區域的內存頁面

6.2. mmap()及其相關係統調用

6.3. mmap()範例

6.4. 對mmap()返回地址的訪問

6.5. 參考資料

7. 共享內存(下)

7.1. 系統V共享內存原理

7.2. 系統V共享內存API

7.3. 系統V共享內存限制

7.4. 系統V共享內存範例

7.5. 結論

7.6. 參考資料

8. 套接口

8.1. 背景知識

8.2. 重要數據結構

8.3. 套接口編程的幾個重要步驟

8.4. 典型調用代碼

8.5. 網絡編程中的其他重要概念

8.6. 參考資料

 

  本文系整理自網絡http://www.ibm.com/developerworks/cn/linux/l-ipc/的系列文章。

  作者鄭彥興,國防科大計算機學院

0. 序

linux下的進程通信手段基本上是從Unix平臺上的進程通信手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校的伯克利軟件發佈中心)在進程間通信方面的側重點有所不同。前者對Unix早期的進程間通信手段進行了系統的改進和擴充,形成了“system V IPC”,通信進程侷限在單個計算機內;後者則跳過了該限制,形成了基於套接口(socket)的進程間通信機制。Linux則把兩者繼承了下來,如圖示:

 

其中,最初Unix IPC包括:管道、FIFO、信號;System V IPC包括:System V消息隊列、System V信號燈、System V共享內存區;Posix IPC包括: Posix消息隊列、Posix信號燈、Posix共享內存區。有兩點需要簡單說明一下:1)由於Unix版本的多樣性,電子電氣工程協會(IEEE)開發了一個獨立的Unix標準,這個新的ANSI Unix標準被稱爲計算機環境的可移植性操作系統界面(PSOIX)。現有大部分Unix和流行版本都是遵循POSIX標準的,而Linux從一開始就遵循POSIX標準;2)BSD並不是沒有涉足單機內的進程間通信(socket本身就可以用於單機內的進程間通信)。事實上,很多Unix版本的單機IPC留有BSD的痕跡,如4.4BSD支持的匿名內存映射、4.3+BSD對可靠信號語義的實現等等。

圖一給出了linux 所支持的各種IPC手段,在本文接下來的討論中,爲了避免概念上的混淆,在儘可能少提及Unix的各個版本的情況下,所有問題的討論最終都會歸結到Linux環境下的進程間通信上來。並且,對於Linux所支持通信手段的不同實現版本(如對於共享內存來說,有Posix共享內存區以及System V共享內存區兩個實現版本),將主要介紹Posix API。

linux下進程間通信的幾種主要手段簡介:

  1. 管道(Pipe)及有名管道(named pipe):管道可用於具有親緣關係進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通信;
  2. 信號(Signal):信號是比較複雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身;linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD爲了實現可靠信號機制,又能夠統一對外接口,用sigaction函數重新實現了signal函數);
  3. 報文(Message)隊列(消息隊列):消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺點。
  4. 共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
  5. 信號量(semaphore):主要作爲進程間以及同一進程不同線程之間的同步手段。
  6. 套接口(Socket):更爲一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支持套接字。

下面將對上述通信機制做具體闡述。

附1:參考文獻[2]中對linux環境下的進程進行了概括說明:

一般來說,linux下的進程包含以下幾個關鍵要素:

  • 有一段可執行程序;
  • 有專用的系統堆棧空間;
  • 內核中有它的控制塊(進程控制塊),描述進程所佔用的資源,這樣,進程才能接受內核的調度;
  • 具有獨立的存儲空間

進程和線程有時候並不完全區分,而往往根據上下文理解其含義。

參考資料

  • UNIX環境高級編程,作者:W.Richard Stevens,譯者:尤晉元等,機械工業出版社。具有豐富的編程實例,以及關鍵函數伴隨Unix的發展歷程。
  • linux內核源代碼情景分析(上、下),毛德操、胡希明著,浙江大學出版社,提供了對linux內核非常好的分析,同時,對一些關鍵概念的背景進行了詳細的說明。
  • UNIX網絡編程第二卷:進程間通信,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。一本比較全面闡述Unix環境下進程間通信的書(沒有信號和套接口,套接口在第一卷中)。

1. 管道

在本系列序中作者概述了linux 進程間通信的幾種主要手段。其中管道和有名管道是最早的進程間通信機制之一,管道可用於具有親緣關係進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通信。認清管道和有名管道的讀寫規則是在程序中應用它們的關鍵,本文在詳細討論了管道和有名管道的通信機制的基礎上,用實例對其讀寫規則進行了程序驗證,這樣做有利於增強讀者對讀寫規則的感性認識,同時也提供了應用範例。

1.1. 管道概述及相關API應用

1.1.1 管道相關的關鍵概念

管道是Linux支持的最初Unix IPC形式之一,具有以下特點:

  • 管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;
  • 只能用於父子進程或者兄弟進程之間(具有親緣關係的進程);
  • 單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在與內存中。
  • 數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出數據。

1.1.2管道的創建

 

#include <unistd.h>

int pipe(int fd[2])

 

該函數創建的管道的兩端處於一個進程中間,在實際應用中沒有太大意義,因此,一個進程在由pipe()創建管道後,一般再fork一個子進程,然後通過管道實現父子進程間的通信(因此也不難推出,只要兩個進程中存在親緣關係,這裏的親緣關係指的是具有共同的祖先,都可以採用管道方式來進行通信)。

1.1.3管道的讀寫規則

管道兩端可分別用描述字fd[0]以及fd[1]來描述,需要注意的是,管道的兩端是固定了任務的。即一端只能用於讀,由描述字fd[0]表示,稱其爲管道讀端;另一端則只能用於寫,由描述字fd[1]來表示,稱其爲管道寫端。如果試圖從管道寫端讀取數據,或者向管道讀端寫入數據都將導致錯誤發生。一般文件的I/O函數都可以用於管道,如close、read、write等等。

從管道中讀取數據:

  • 如果管道的寫端不存在,則認爲已經讀到了數據的末尾,讀函數返回的讀出字節數爲0;
  • 當管道的寫端存在時,如果請求的字節數目大於PIPE_BUF,則返回管道中現有的數據字節數,如果請求的字節數目不大於PIPE_BUF,則返回管道中現有數據字節數(此時,管道中數據量小於請求的數據量);或者返回請求的字節數(此時,管道中數據量不小於請求的數據量)。注:(PIPE_BUF在include/linux/limits.h中定義,不同的內核版本可能會有所不同。Posix.1要求PIPE_BUF至少爲512字節,red hat 7.2中爲4096)。

關於管道的讀規則驗證:

 

 /**************

 * readtest.c *

 **************/

#include <unistd.h>

#include <sys/types.h>

#include <errno.h>

main()

{

       int pipe_fd[2];

       pid_t pid;

       char r_buf[100];

       char w_buf[4];

       char* p_wbuf;

       int r_num;

       int cmd;

 

       memset(r_buf,0,sizeof(r_buf));

       memset(w_buf,0,sizeof(r_buf));

       p_wbuf=w_buf;

       if(pipe(pipe_fd)<0)

       {

              printf("pipe create error\n");

              return -1;

       }

 

       if((pid=fork())==0)

       {

              printf("\n");

              close(pipe_fd[1]);

              sleep(3);//確保父進程關閉寫端

           r_num=read(pipe_fd[0],r_buf,100);

printf(     "read num is %d   the data read from the pipe is %d\n",r_num,atoi(r_buf));

 

              close(pipe_fd[0]);

              exit();

       }

       else if(pid>0)

       {

       close(pipe_fd[0]);//read

       strcpy(w_buf,"111");

       if(write(pipe_fd[1],w_buf,4)!=-1)

              printf("parent write over\n");

       close(pipe_fd[1]);//write

              printf("parent close fd[1] over\n");

       sleep(10);

       }    

}

 /**************************************************

 * 程序輸出結果:

 * parent write over

 * parent close fd[1] over

 * read num is 4   the data read from the pipe is 111

 * 附加結論:

 * 管道寫端關閉後,寫入的數據將一直存在,直到讀出爲止.

 ****************************************************/

 

向管道中寫入數據:

  • 向管道中寫入數據時,linux將不保證寫入的原子性,管道緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據。如果讀進程不讀走管道緩衝區中的數據,那麼寫操作將一直阻塞。 
    注:只有在管道的讀端存在時,向管道中寫入數據纔有意義。否則,向管道中寫入數據的進程將收到內核傳來的SIFPIPE信號,應用程序可以處理該信號,也可以忽略(默認動作則是應用程序終止)。

對管道的寫規則的驗證1:寫端對讀端存在的依賴性

 

#include <unistd.h>

#include <sys/types.h>

main()

{

       int pipe_fd[2];

       pid_t pid;

       char r_buf[4];

       char* w_buf;

       int writenum;

       int cmd;

 

       memset(r_buf,0,sizeof(r_buf));

       if(pipe(pipe_fd)<0)

       {

              printf("pipe create error\n");

              return -1;

       }

 

       if((pid=fork())==0)

       {

              close(pipe_fd[0]);

              close(pipe_fd[1]);

              sleep(10);      

              exit();

       }

       else if(pid>0)

       {

       sleep(1);  //等待子進程完成關閉讀端的操作

       close(pipe_fd[0]);//write

       w_buf="111";

       if((writenum=write(pipe_fd[1],w_buf,4))==-1)

              printf("write to pipe error\n");

       else 

              printf("the bytes write to pipe is %d \n", writenum);

 

       close(pipe_fd[1]);

       }    

}

 

則輸出結果爲: Brokenpipe,原因就是該管道以及它的所有fork()產物的讀端都已經被關閉。如果在父進程中保留讀端,即在寫完pipe後,再關閉父進程的讀端,也會正常寫入pipe,讀者可自己驗證一下該結論。因此,在向管道寫入數據時,至少應該存在某一個進程,其中管道讀端沒有被關閉,否則就會出現上述錯誤(管道斷裂,進程收到了SIGPIPE信號,默認動作是進程終止)

對管道的寫規則的驗證2:linux不保證寫管道的原子性驗證

 

#include <unistd.h>

#include <sys/types.h>

#include <errno.h>

main(int argc,char**argv)

{

       int pipe_fd[2];

       pid_t pid;

       char r_buf[4096];

       char w_buf[4096*2];

       int writenum;

       int rnum;

       memset(r_buf,0,sizeof(r_buf));    

       if(pipe(pipe_fd)<0)

       {

              printf("pipe create error\n");

              return -1;

       }

 

       if((pid=fork())==0)

       {

              close(pipe_fd[1]);

              while(1)

              {

              sleep(1); 

              rnum=read(pipe_fd[0],r_buf,1000);

              printf("child: readnum is %d\n",rnum);

              }

              close(pipe_fd[0]);

 

              exit();

       }

       else if(pid>0)

       {

       close(pipe_fd[0]);//write

       memset(r_buf,0,sizeof(r_buf));    

       if((writenum=write(pipe_fd[1],w_buf,1024))==-1)

              printf("write to pipe error\n");

       else 

              printf("the bytes write to pipe is %d \n", writenum);

       writenum=write(pipe_fd[1],w_buf,4096);

       close(pipe_fd[1]);

       }    

}

 

輸出結果:

the bytes write to pipe 1000

the bytes write to pipe 1000  //注意,此行輸出說明了寫入的非原子性

the bytes write to pipe 1000

the bytes write to pipe 1000

the bytes write to pipe 1000

the bytes write to pipe 120  //注意,此行輸出說明了寫入的非原子性

the bytes write to pipe 0

the bytes write to pipe 0

......

 

結論:

寫入數目小於4096時寫入是非原子的! 
如果把父進程中的兩次寫入字節數都改爲5000,則很容易得出下面結論: 
寫入管道的數據量大於4096字節時,緩衝區的空閒空間將被寫入數據(補齊),直到寫完所有數據爲止,如果沒有進程讀數據,則一直阻塞。

1.1.4管道應用實例

實例一:用於shell

管道可用於輸入輸出重定向,它將一個命令的輸出直接定向到另一個命令的輸入。比如,當在某個shell程序(Bourneshell或C shell等)鍵入who│wc -l後,相應shell程序將創建who以及wc兩個進程和這兩個進程間的管道。考慮下面的命令行:

$kill -l 運行結果見 附一

$kill -l | grep SIGRTMIN 運行結果如下:

 

30) SIGPWR  31) SIGSYS   32) SIGRTMIN      33) SIGRTMIN+1

34) SIGRTMIN+2  35) SIGRTMIN+3  36) SIGRTMIN+4  37) SIGRTMIN+5

38) SIGRTMIN+6  39) SIGRTMIN+7  40) SIGRTMIN+8  41) SIGRTMIN+9

42) SIGRTMIN+10       43) SIGRTMIN+11       44) SIGRTMIN+12       45) SIGRTMIN+13

46) SIGRTMIN+14       47) SIGRTMIN+15       48) SIGRTMAX-15       49) SIGRTMAX-14

 

實例二:用於具有親緣關係的進程間通信

下面例子給出了管道的具體應用,父進程通過管道發送一些命令給子進程,子進程解析命令,並根據命令作相應處理。

 

#include <unistd.h>

#include <sys/types.h>

main()

{

       int pipe_fd[2];

       pid_t pid;

       char r_buf[4];

       char** w_buf[256];

       int childexit=0;

       int i;

       int cmd;

 

       memset(r_buf,0,sizeof(r_buf));

 

       if(pipe(pipe_fd)<0)

       {

              printf("pipe create error\n");

              return -1;

       }

       if((pid=fork())==0)

       //子進程:解析從管道中獲取的命令,並作相應的處理

       {

              printf("\n");

              close(pipe_fd[1]);

              sleep(2);

 

              while(!childexit)

              {    

                     read(pipe_fd[0],r_buf,4);

                     cmd=atoi(r_buf);

                     if(cmd==0)

                     {

printf("child: receive command from parent over\n now child process exit\n");

                            childexit=1;

                     }

 

                     else if(handle_cmd(cmd)!=0)

                            return;

                     sleep(1);

              }

              close(pipe_fd[0]);

              exit();

       }

       else if(pid>0)

       //parent: send commands to child

       {

       close(pipe_fd[0]);

 

       w_buf[0]="003";

       w_buf[1]="005";

       w_buf[2]="777";

       w_buf[3]="000";

       for(i=0;i<4;i++)

              write(pipe_fd[1],w_buf[i],4);

       close(pipe_fd[1]);

       }    

}

//下面是子進程的命令處理函數(特定於應用):

int handle_cmd(int cmd)

{

if((cmd<0)||(cmd>256))

//suppose child only support 256 commands

       {

       printf("child: invalid command \n");

       return -1;

       }

printf("child: the cmd from parent is %d\n", cmd);

return 0;

}

 

1.1.5管道的侷限性

管道的主要侷限性正體現在它的特點上:

  • 只支持單向數據流;
  • 只能用於具有親緣關係的進程之間;
  • 沒有名字;
  • 管道的緩衝區是有限的(管道制存在於內存中,在管道創建時,爲緩衝區分配一個頁面大小);
  • 管道所傳送的是無格式字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式,比如多少字節算作一個消息(或命令、或記錄)等等;

 

1.2. 有名管道概述及相關API應用

1.2.1 有名管道相關的關鍵概念

管道應用的一個重大限制是它沒有名字,因此,只能用於具有親緣關係的進程間通信,在有名管道(named pipe或FIFO)提出後,該限制得到了克服。FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中。這樣,即使與FIFO的創建進程不存在親緣關係的進程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信(能夠訪問該路徑的進程以及FIFO的創建進程之間),因此,通過FIFO不相關的進程也能交換數據。值得注意的是,FIFO嚴格遵循先進先出(first in first out),對管道及FIFO的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如lseek()等文件定位操作。

1.2.2有名管道的創建

 

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char * pathname, mode_t mode)

 

該函數的第一個參數是一個普通的路徑名,也就是創建後FIFO的名字。第二個參數與打開普通文件的open()函數中的mode 參數相同。如果mkfifo的第一個參數是一個已經存在的路徑名時,會返回EEXIST錯誤,所以一般典型的調用代碼首先會檢查是否返回該錯誤,如果確實返回該錯誤,那麼只要調用打開FIFO的函數就可以了。一般文件的I/O函數都可以用於FIFO,如close、read、write等等。

1.2.3有名管道的打開規則

有名管道比管道多了一個打開操作:open。

FIFO的打開規則:

如果當前打開操作是爲讀而打開FIFO時,若已經有相應進程爲寫而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲寫而打開該FIFO(當前打開操作設置了阻塞標誌);或者,成功返回(當前打開操作沒有設置阻塞標誌)。

如果當前打開操作是爲寫而打開FIFO時,如果已經有相應進程爲讀而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲讀而打開該FIFO(當前打開操作設置了阻塞標誌);或者,返回ENXIO錯誤(當前打開操作沒有設置阻塞標誌)。

對打開規則的驗證參見 附2

1.2.4有名管道的讀寫規則

從FIFO中讀取數據:

約定:如果一個進程爲了從FIFO中讀取數據而阻塞打開FIFO,那麼稱該進程內的讀操作爲設置了阻塞標誌的讀操作。

  • 如果有進程寫打開FIFO,且當前FIFO內沒有數據,則對於設置了阻塞標誌的讀操作來說,將一直阻塞。對於沒有設置阻塞標誌讀操作來說則返回-1,當前errno值爲EAGAIN,提醒以後再試。
  • 對於設置了阻塞標誌的讀操作說,造成阻塞的原因有兩種:當前FIFO內有數據,但有其它進程在讀這些數據;另外就是FIFO內沒有數據。解阻塞的原因則是FIFO中有新的數據寫入,不論信寫入數據量的大小,也不論讀操作請求多少數據量。
  • 讀打開的阻塞標誌只對本進程第一個讀操作施加作用,如果本進程內有多個讀操作序列,則在第一個讀操作被喚醒並完成讀操作後,其它將要執行的讀操作將不再阻塞,即使在執行讀操作時,FIFO中沒有數據也一樣(此時,讀操作返回0)。
  • 如果沒有進程寫打開FIFO,則設置了阻塞標誌的讀操作會阻塞。

注:如果FIFO中有數據,則設置了阻塞標誌的讀操作不會因爲FIFO中的字節數小於請求讀的字節數而阻塞,此時,讀操作會返回FIFO中現有的數據量。

向FIFO中寫入數據:

約定:如果一個進程爲了向FIFO中寫入數據而阻塞打開FIFO,那麼稱該進程內的寫操作爲設置了阻塞標誌的寫操作。

對於設置了阻塞標誌的寫操作:

  • 當要寫入的數據量不大於PIPE_BUF時,linux將保證寫入的原子性。如果此時管道空閒緩衝區不足以容納要寫入的字節數,則進入睡眠,直到當緩衝區中能夠容納要寫入的字節數時,纔開始進行一次性寫操作。
  • 當要寫入的數據量大於PIPE_BUF時,linux將不再保證寫入的原子性。FIFO緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據,寫操作在寫完所有請求寫的數據後返回。

對於沒有設置阻塞標誌的寫操作:

  • 當要寫入的數據量大於PIPE_BUF時,linux將不再保證寫入的原子性。在寫滿所有FIFO空閒緩衝區後,寫操作返回。
  • 當要寫入的數據量不大於PIPE_BUF時,linux將保證寫入的原子性。如果當前FIFO空閒緩衝區能夠容納請求寫入的字節數,寫完後成功返回;如果當前FIFO空閒緩衝區不能夠容納請求寫入的字節數,則返回EAGAIN錯誤,提醒以後再寫;

對FIFO讀寫規則的驗證:

下面提供了兩個對FIFO的讀寫程序,適當調節程序中的很少地方或者程序的命令行參數就可以對各種FIFO讀寫規則進行驗證。
程序1:寫FIFO的程序

 

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#define FIFO_SERVER "/tmp/fifoserver"

 

main(int argc,char** argv)

//參數爲即將寫入的字節數

{

       int fd;

       char w_buf[4096*2];

       int real_wnum;

       memset(w_buf,0,4096*2);

       if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))

              printf("cannot create fifoserver\n");

 

       if(fd==-1)

              if(errno==ENXIO)

                     printf("open error; no reading process\n");

 

          fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);

       //設置非阻塞標誌

       //fd=open(FIFO_SERVER,O_WRONLY,0);

       //設置阻塞標誌

       real_wnum=write(fd,w_buf,2048);

       if(real_wnum==-1)

       {

              if(errno==EAGAIN)

                     printf("write to fifo error; try later\n");

       }

       else

              printf("real write num is %d\n",real_wnum);

       real_wnum=write(fd,w_buf,5000);

       //5000用於測試寫入字節大於4096時的非原子性

       //real_wnum=write(fd,w_buf,4096);

       //4096用於測試寫入字節不大於4096時的原子性

 

       if(real_wnum==-1)

              if(errno==EAGAIN)

                     printf("try later\n");

}


程序2:與程序1一起測試寫FIFO的規則,第一個命令行參數是請求從FIFO讀出的字節數

 

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#define FIFO_SERVER "/tmp/fifoserver"

 

main(int argc,char** argv)

{

       char r_buf[4096*2];

       int  fd;

       int  r_size;

       int  ret_size;

       r_size=atoi(argv[1]);

       printf("requred real read bytes %d\n",r_size);

       memset(r_buf,0,sizeof(r_buf));

       fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0);

       //fd=open(FIFO_SERVER,O_RDONLY,0);

       //在此處可以把讀程序編譯成兩個不同版本:阻塞版本及非阻塞版本

       if(fd==-1)

       {

              printf("open %s for read error\n");

              exit();     

       }

       while(1)

       {

 

              memset(r_buf,0,sizeof(r_buf));

              ret_size=read(fd,r_buf,r_size);

              if(ret_size==-1)

                     if(errno==EAGAIN)

                            printf("no data avlaible\n");

              printf("real read bytes %d\n",ret_size);

              sleep(1);

       }    

       pause();

       unlink(FIFO_SERVER);

}

 

程序應用說明:

把讀程序編譯成兩個不同版本:

  • 阻塞讀版本:br
  • 以及非阻塞讀版本nbr

把寫程序編譯成兩個四個版本:

  • 非阻塞且請求寫的字節數大於PIPE_BUF版本:nbwg
  • 非阻塞且請求寫的字節數不大於PIPE_BUF版本:版本nbw
  • 阻塞且請求寫的字節數大於PIPE_BUF版本:bwg
  • 阻塞且請求寫的字節數不大於PIPE_BUF版本:版本bw

下面將使用br、nbr、w代替相應程序中的阻塞讀、非阻塞讀

驗證阻塞寫操作:

  1. 當請求寫入的數據量大於PIPE_BUF時的非原子性:
    • nbr 1000
    • bwg
  2. 當請求寫入的數據量不大於PIPE_BUF時的原子性:
    • nbr 1000
    • bw

驗證非阻塞寫操作:

  1. 當請求寫入的數據量大於PIPE_BUF時的非原子性:
    • nbr 1000
    • nbwg
  2. 請求寫入的數據量不大於PIPE_BUF時的原子性:
    • nbr 1000
    • nbw

不管寫打開的阻塞標誌是否設置,在請求寫入的字節數大於4096時,都不保證寫入的原子性。但二者有本質區別:

對於阻塞寫來說,寫操作在寫滿FIFO的空閒區域後,會一直等待,直到寫完所有數據爲止,請求寫入的數據最終都會寫入FIFO;

而非阻塞寫則在寫滿FIFO的空閒區域後,就返回(實際寫入的字節數),所以有些數據最終不能夠寫入。

對於讀操作的驗證則比較簡單,不再討論。

1.2.5有名管道應用實例

在驗證了相應的讀寫規則後,應用實例似乎就沒有必要了。

1.3.小結

管道常用於兩個方面:(1)在shell中時常會用到管道(作爲輸入輸入的重定向),在這種應用方式下,管道的創建對於用戶來說是透明的;(2)用於具有親緣關係的進程間通信,用戶自己創建管道,並完成讀寫操作。

FIFO可以說是管道的推廣,克服了管道無名字的限制,使得無親緣關係的進程同樣可以採用先進先出的通信機制進行通信。

管道和FIFO的數據是字節流,應用程序之間必須事先確定特定的傳輸"協議",採用傳播具有特定意義的消息。

要靈活應用管道及FIFO,理解它們的讀寫規則是關鍵。

附1:kill -l 的運行結果,顯示了當前系統支持的所有信號:

 

1) SIGHUP     2) SIGINT    3) SIGQUIT 4) SIGILL

5) SIGTRAP   6) SIGABRT 7) SIGBUS   8) SIGFPE

9) SIGKILL    10) SIGUSR1 11) SIGSEGV 12) SIGUSR2

13) SIGPIPE  14) SIGALRM       15) SIGTERM       17) SIGCHLD

18) SIGCONT       19) SIGSTOP 20) SIGTSTP 21) SIGTTIN

22) SIGTTOU       23) SIGURG   24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM   27) SIGPROF 28) SIGWINCH     29) SIGIO

30) SIGPWR  31) SIGSYS   32) SIGRTMIN      33) SIGRTMIN+1

34) SIGRTMIN+2  35) SIGRTMIN+3  36) SIGRTMIN+4  37) SIGRTMIN+5

38) SIGRTMIN+6  39) SIGRTMIN+7  40) SIGRTMIN+8  41) SIGRTMIN+9

42) SIGRTMIN+10       43) SIGRTMIN+11       44) SIGRTMIN+12       45) SIGRTMIN+13

46) SIGRTMIN+14       47) SIGRTMIN+15       48) SIGRTMAX-15       49) SIGRTMAX-14

50) SIGRTMAX-13       51) SIGRTMAX-12       52) SIGRTMAX-11       53) SIGRTMAX-10

54) SIGRTMAX-9  55) SIGRTMAX-8  56) SIGRTMAX-7  57) SIGRTMAX-6

58) SIGRTMAX-5  59) SIGRTMAX-4  60) SIGRTMAX-3  61) SIGRTMAX-2

62) SIGRTMAX-1  63) SIGRTMAX    

 

除了在此處用來說明管道應用外,接下來的專題還要對這些信號分類討論。

附2:對FIFO打開規則的驗證(主要驗證寫打開對讀打開的依賴性)

 

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#define FIFO_SERVER "/tmp/fifoserver"

 

int handle_client(char*);

main(int argc,char** argv)

{

       int r_rd;

       int w_fd;

       pid_t pid;

 

       if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))

              printf("cannot create fifoserver\n");

       handle_client(FIFO_SERVER);

 

}

 

int handle_client(char* arg)

{

int ret;

ret=w_open(arg);

switch(ret)

{

       case 0:

       {    

       printf("open %s error\n",arg);

       printf("no process has the fifo open for reading\n");

       return -1;

       }

       case -1:

       {

              printf("something wrong with open the fifo except for ENXIO");

              return -1;

       }

       case 1:

       {

              printf("open server ok\n");

              return 1;

       }

       default:

       {

              printf("w_no_r return ----\n");

              return 0;

       }

}           

unlink(FIFO_SERVER);

}

 

int w_open(char*arg)

//0  open error for no reading

//-1 open error for other reasons

//1  open ok

{

       if(open(arg,O_WRONLY|O_NONBLOCK,0)==-1)

       {     if(errno==ENXIO)

              {

                     return 0;

              }

              else

              return -1;

       }

       return 1;

 

}

1.4. 參考資料

  • UNIX網絡編程第二卷:進程間通信,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。豐富的UNIX進程間通信實例及分析,對Linux環境下的程序開發有極大的啓發意義。
  • linux內核源代碼情景分析(上、下),毛德操、胡希明著,浙江大學出版社,當要驗證某個結論、想法時,最好的參考資料;
  • UNIX環境高級編程,作者:W.Richard Stevens,譯者:尤晉元等,機械工業出版社。具有豐富的編程實例,以及關鍵函數伴隨Unix的發展歷程。
  • http://www.linux.org.tw/CLDP/gb/Secure-Programs-HOWTO/x346.html 點明linux下sigaction的實現基礎,linux源碼../kernel/signal.c更說明了問題; 
  • pipe手冊,最直接而可靠的參考資料
  • fifo手冊,最直接而可靠的參考資料

2. 信號(上)

2.1. 信號及信號來源

2.1.1 信號本質

信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什麼時候到達。

信號是進程間通信機制中唯一的異步通信機制,可以看作是異步通知,通知接收信號的進程有哪些事情發生了。信號機制經過POSIX實時擴展後,功能更加強大,除了基本通知功能外,還可以傳遞附加信息。

2.1.2 信號來源

信號事件的發生有兩個來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);軟件來源,最常用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。

2.2. 信號的種類

可以從兩個不同的分類角度對信號進行分類:(1)可靠性方面:可靠信號與不可靠信號;(2)與時間的關係上:實時信號與非實時信號。在《Linux環境進程間通信(一):管道及有名管道》的附1中列出了系統所支持的所有信號。

2.2.1 可靠信號與不可靠信號

"不可靠信號"

Linux信號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的信號機制比較簡單和原始,後來在實踐中暴露出一些問題,因此,把那些建立在早期機制上的信號叫做"不可靠信號",信號值小於SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信號都是不可靠信號。這就是"不可靠信號"的來源。它的主要問題是:

  • 進程每次處理信號後,就將對信號的響應設置爲默認動作。在某些情況下,將導致對信號的錯誤處理;因此,用戶如果不希望這樣的操作,那麼就要在信號處理函數結尾再一次調用signal(),重新安裝該信號。
  • 信號可能丟失,後面將對此詳細闡述。 
    因此,早期unix下的不可靠信號主要指的是進程可能對信號做出錯誤的反應以及信號可能丟失。

Linux支持不可靠信號,但是對不可靠信號機制做了改進:在調用完信號處理函數後,不必重新調用該信號的安裝函數(信號安裝函數是在可靠機制上的實現)。因此,Linux下的不可靠信號問題主要指的是信號可能丟失。

"可靠信號"

隨着時間的發展,實踐證明了有必要對信號的原始機制加以改進和擴充。所以,後來出現的各種Unix版本分別在這方面進行了研究,力圖實現"可靠信號"。由於原來定義的信號已有許多應用,不好再做改動,最終只好又新增加了一些信號,並在一開始就把它們定義爲可靠信號,這些信號支持排隊,不會丟失。同時,信號的發送和安裝也出現了新版本:信號發送函數sigqueue()及信號安裝函數sigaction()。POSIX.4對可靠信號機制做了標準化。但是,POSIX只對可靠信號機制應具有的功能以及信號機制的對外接口做了標準化,對信號機制的實現沒有作具體的規定。

信號值位於SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問題。Linux在支持新版本的信號安裝函數sigation()以及信號發送函數sigqueue()的同時,仍然支持早期的signal()信號安裝函數,支持信號發送函數kill()。

注:不要有這樣的誤解:由sigqueue()發送、sigaction安裝的信號就是可靠的。事實上,可靠信號是指後來添加的新信號(信號值位於SIGRTMIN及SIGRTMAX之間);不可靠信號是信號值小於SIGRTMIN的信號。信號的可靠與不可靠只與信號值有關,與信號的發送及安裝函數無關。目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾也不必再調用一次信號安裝函數。同時,由signal()安裝的實時信號支持排隊,同樣不會丟失。

對於目前linux的兩個信號安裝函數:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),而且對SIGRTMIN以後的信號都支持排隊。這兩個函數的最大區別在於,經過sigaction安裝的信號都能傳遞信息給信號處理函數(對所有信號這一點都成立),而經過signal安裝的信號卻不能向信號處理函數傳遞信息。對於信號發送函數來說也是一樣的。

2.2.2 實時信號與非實時信號

早期Unix系統只定義了32種信號,Ret hat7.2支持64種信號,編號0-63(SIGRTMIN=31,SIGRTMAX=63),將來可能進一步增加,這需要得到內核的支持。前32種信號已經有了預定義值,每個信號有了確定的用途及含義,並且每種信號都有各自的缺省動作。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。後32個信號表示實時信號,等同於前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。實時信號是POSIX標準的一部分,可用於應用進程。

非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。

2.3. 進程對信號的響應

進程可以通過三種方式來響應一個信號:(1)忽略信號,即對信號不做任何處理,其中,有兩個信號不能忽略:SIGKILL及SIGSTOP;(2)捕捉信號。定義信號處理函數,當信號發生時,執行相應的處理函數;(3)執行缺省操作,Linux對每種信號都規定了默認操作,詳細情況請參考[2]以及其它資料。注意,進程對實時信號的缺省反應是進程終止。

Linux究竟採用上述三種方式的哪一個來響應信號,取決於傳遞給相應API函數的參數。

2.4. 信號的發送

發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

1、kill() 
#include <sys/types.h> 
#include <signal.h> 
int kill(pid_t pid,int signo) 

參數pid的值

信號的接收進程

pid>0

進程ID爲pid的進程

pid=0

同一個進程組的進程

pid<0 pid!=-1

進程組ID爲 -pid的所有進程

pid=-1

除發送進程自身外,所有進程ID大於1的進程

Sinno是信號值,當爲0時(即空信號),實際不發送任何信號,但照常進行錯誤檢查,因此,可用於檢查目標進程是否存在,以及當前進程是否具有向目標發送信號的權限(root權限的進程可以向任何進程發送信號,非root權限的進程只能向屬於同一個session或者同一個用戶的進程發送信號)。

Kill()最常用於pid>0時的信號發送,調用成功返回 0; 否則,返回 -1。 注:對於pid<0時的情況,對於哪些進程將接受信號,各種版本說法不一,其實很簡單,參閱內核源碼kernal/signal.c即可,上表中的規則是參考red hat 7.2。

2、raise() 
#include <signal.h> 
int raise(int signo) 
向進程本身發送信號,參數爲即將發送的信號值。調用成功返回 0;否則,返回 -1。

3、sigqueue() 
#include <sys/types.h> 
#include <signal.h> 
int sigqueue(pid_t pid, int sig, const union sigval val) 
調用成功返回 0;否則,返回 -1。

sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。

sigqueue的第一個參數是指定接收信號的進程ID,第二個參數確定即將發送的信號,第三個參數是一個聯合數據結構union sigval,指定了信號傳遞的參數,即通常所說的4字節值。

      typedef union sigval {

             int  sival_int;

             void *sival_ptr;

      }sigval_t;

 

sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號,而不能發送信號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用於檢查pid的有效性以及當前進程是否有權限向目標進程發送信號。

在調用sigqueue時,sigval_t指定的信息會拷貝到3參數信號處理函數(3參數信號處理函數指的是信號處理函數由sigaction安裝,並設定了sa_sigaction指針,稍後將闡述)的siginfo_t結構中,這樣信號處理函數就可以處理這些信息了。由於sigqueue系統調用支持發送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。

注:sigqueue()發送非實時信號時,第三個參數包含的信息仍然能夠傳遞給信號處理函數;sigqueue()發送非實時信號時,仍然不支持排隊,即在信號處理函數執行過程中到來的所有相同信號,都被合併爲一個信號。

4、alarm() 
#include <unistd.h> 
unsigned int alarm(unsigned int seconds) 
專門爲SIGALRM信號而設,在指定的時間seconds秒後,將向進程本身發送SIGALRM信號,又稱爲鬧鐘時間。進程調用alarm後,任何以前的alarm()調用都將無效。如果參數seconds爲零,那麼進程內將不再包含任何鬧鐘時間。 
返回值,如果調用alarm()前,進程中已經設置了鬧鐘時間,則返回上一個鬧鐘時間的剩餘時間,否則返回0。

5、setitimer() 
#include <sys/time.h> 
int setitimer(int which, const struct itimerval *value, struct itimerval*ovalue)); 
setitimer()比alarm功能強大,支持3種類型的定時器:

  • ITIMER_REAL: 設定絕對時間;經過指定的時間後,內核將發送SIGALRM信號給本進程;
  • ITIMER_VIRTUAL 設定程序執行時間;經過指定的時間後,內核將發送SIGVTALRM信號給本進程;
  • ITIMER_PROF 設定進程執行以及內核因本進程而消耗的時間和,經過指定的時間後,內核將發送ITIMER_VIRTUAL信號給本進程;

Setitimer()第一個參數which指定定時器類型(上面三種之一);第二個參數是結構itimerval的一個實例,結構itimerval形式見附錄1。第三個參數可不做處理。

Setitimer()調用成功返回0,否則返回-1。

6、abort() 
#include <stdlib.h> 
void abort(void);

向進程發送SIGABORT信號,默認情況下進程會異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進程設置爲阻塞信號,調用abort()後,SIGABORT仍然能被進程接收。該函數無返回值。

2.5. 信號的安裝(設置信號關聯動作)

如果進程要處理某一信號,那麼就要在進程中安裝該信號。安裝信號主要用來確定信號值及進程針對該信號值的動作之間的映射關係,即進程將要處理哪個信號;該信號被傳遞給進程時,將執行何種操作。

linux主要有兩個函數實現信號的安裝:signal()、sigaction()。其中signal()在可靠信號系統調用的基礎上實現, 是庫函數。它只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支持非實時信號的安裝。sigaction()優於signal()主要體現在支持信號帶有參數。

1、signal() 
#include <signal.h> 
void (*signal(int signum, void (*handler))(int)))(int); 
如果該函數原型不容易理解的話,可以參考下面的分解方式來理解: 
typedef void (*sighandler_t)(int); 
sighandler_t signal(int signum, sighandler_t handler)); 
第一個參數指定信號的值,第二個參數指定針對前面信號值的處理,可以忽略該信號(參數設爲SIG_IGN);可以採用系統默認方式處理信號(參數設爲SIG_DFL);也可以自己實現處理方式(參數指定一個函數地址)。 
如果signal()調用成功,返回最後一次爲安裝信號signum而調用signal()時的handler值;失敗則返回SIG_ERR。

2、sigaction() 
#include <signal.h> 
int sigaction(int signum,const struct sigaction *act,struct sigaction*oldact));

sigaction函數用於改變進程接收到特定信號後的行爲。該函數的第一個參數爲信號的值,可以爲除SIGKILL及SIGSTOP外的任何一個特定有效的信號(爲這兩個信號定義自己的處理函數,將導致信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指針,在結構sigaction的實例中,指定了對特定信號的處理,可以爲空,進程會以缺省方式對信號處理;第三個參數oldact指向的對象用來保存原來對相應信號的處理,可指定oldact爲NULL。如果把第二、第三個參數都設爲NULL,那麼該函數可用於檢查信號的有效性。

第二個參數最爲重要,其中包含了對指定信號的處理、信號所傳遞的信息、信號處理函數執行過程中應屏蔽掉哪些函數等等。

sigaction結構定義如下:

 struct sigaction {

          union{

            __sighandler_t _sa_handler;

            void (*_sa_sigaction)(int,struct siginfo *, void *);

            }_u

                     sigset_t sa_mask;

                    unsigned long sa_flags;

                  void (*sa_restorer)(void);

                  }

 

 

其中,sa_restorer,已過時,POSIX不支持它,不應再被使用。

1、聯合數據結構中的兩個元素_sa_handler以及*_sa_sigaction指定信號關聯函數,即用戶指定的信號處理函數。除了可以是用戶自定義的處理函數外,還可以爲SIG_DFL(採用缺省的處理方式),也可以爲SIG_IGN(忽略信號)。

2、由_sa_handler指定的處理函數只有一個參數,即信號值,所以信號不能傳遞除信號值之外的任何信息;由_sa_sigaction是指定的信號處理函數帶有三個參數,是爲實時信號而設的(當然同樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數爲信號值,第三個參數沒有使用(posix沒有規範使用該參數的標準),第二個參數是指向siginfo_t結構的指針,結構中包含信號攜帶的數據值,參數所指向的結構如下:

 siginfo_t {

                  int      si_signo;  /* 信號值,對所有信號有意義*/

                  int      si_errno;  /* errno值,對所有信號有意義*/

                  int      si_code;   /* 信號產生的原因,對所有信號有意義*/

        union{          /* 聯合數據結構,不同成員適應不同信號 */ 

          //確保分配足夠大的存儲空間

          int _pad[SI_PAD_SIZE];

          //對SIGKILL有意義的結構

          struct{

              ...

              }...

            ... ...

            ... ...         

          //對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構

              struct{

              ...

              }...

            ... ...

            }

      }

 

 

注:爲了更便於閱讀,在說明問題時常把該結構表示爲附錄2所表示的形式。

siginfo_t結構中的聯合數據成員確保該結構適應所有的信號,比如對於實時信號來說,則實際採用下面的結構形式:

       typedef struct {

              int si_signo;

              int si_errno;                 

              int si_code;                  

              union sigval si_value;    

              } siginfo_t;

 

 

結構的第四個域同樣爲一個聯合數據結構:

       union sigval {

              int sival_int;          

              void *sival_ptr;     

              }

 

採用聯合數據結構,說明siginfo_t結構中的si_value要麼持有一個4字節的整數值,要麼持有一個指針,這就構成了與信號相關的數據。在信號的處理函數中,包含這樣的信號相關數據指針,但沒有規定具體如何對這些數據進行操作,操作方法應該由程序開發人員根據具體任務事先約定。

前面在討論系統調用sigqueue發送信號時,sigqueue的第三個參數就是sigval聯合數據結構,當調用sigqueue時,該數據結構中的數據就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就可以讓信號傳遞一些附加信息。信號可以傳遞信息對程序開發是非常有意義的。

信號參數的傳遞過程可圖示如下:


3、sa_mask指定在信號處理程序執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標誌位。

注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程中由sa_mask指定的信號才被阻塞。

4、sa_flags中包含了許多標誌位,包括剛剛提到的SA_NODEFER及SA_NOMASK標誌位。另一個比較重要的標誌位是SA_SIGINFO,當設定了該標誌位時,表示信號附帶的參數可以被傳遞到信號處理函數中,因此,應該爲sigaction結構中的sa_sigaction指定處理函數,而不應該爲sa_handler指定信號處理函數,否則,設置該標誌變得毫無意義。即使爲sa_sigaction指定了信號處理函數,如果不設置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過來的數據,在信號處理函數中對這些信息的訪問都將導致段錯誤(Segmentation fault)。

注:很多文獻在闡述該標誌位時都認爲,如果設置了該標誌位,就必須定義三參數信號處理函數。實際不是這樣的,驗證方法很簡單:自己實現一個單一參數信號處理函數,並在程序中設置該標誌位,可以察看程序的運行結果。實際上,可以把該標誌位看成信號是否傳遞參數的開關,如果設置該位,則傳遞參數;否則,不傳遞參數。

 

2.6. 信號集及信號集操作函數

信號集被定義爲一種數據類型:

       typedef struct {

                     unsigned long sig[_NSIG_WORDS];

                     } sigset_t

 

信號集用來描述信號的集合,linux所支持的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關函數配合使用。下面是爲信號集操作定義的相關函數:

       #include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum)

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

sigemptyset(sigset_t *set)初始化由set指定的信號集,信號集裏面的所有信號被清空;

sigfillset(sigset_t *set)調用該函數後,set指向的信號集中將包含linux支持的64種信號;

sigaddset(sigset_t *set, int signum)在set指向的信號集中加入signum信號;

sigdelset(sigset_t *set, int signum)在set指向的信號集中刪除signum信號;

sigismember(const sigset_t *set, int signum)判定信號signum是否在set指向的信號集中。

 

2.7. 信號阻塞與信號未決

每個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的所有信號在遞送到進程後都將被阻塞。下面是與信號阻塞相關的幾個函數:

#include <signal.h>

int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));

 

sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種:

參數how

進程當前信號集

SIG_BLOCK

在進程當前阻塞信號集中添加set指向信號集中的信號

SIG_UNBLOCK

如果進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞

SIG_SETMASK

更新進程阻塞信號集爲set指向的信號集

sigpending(sigset_t *set))獲得當前已遞送到進程,卻被阻塞的所有信號,在set指向的信號集中返回結果。

sigsuspend(const sigset_t *mask))用於在接收到某個信號之前, 臨時用mask替換進程的信號掩碼, 並暫停進程執行,直到收到信號爲止。sigsuspend 返回後將恢復調用之前的信號掩碼。信號處理函數完成後,進程將繼續執行。該系統調用始終返回-1,並將errno設置爲EINTR。

附錄1:結構itimerval:

            struct itimerval {

                struct timeval it_interval; /* next value */

                struct timeval it_value;    /* current value */

            };

            struct timeval {

                long tv_sec;                /* seconds */

                long tv_usec;               /* microseconds */

            };

 

附錄2:三參數信號處理函數中第二個參數的說明性描述:

siginfo_t {

int      si_signo;  /* 信號值,對所有信號有意義*/

int      si_errno;  /* errno值,對所有信號有意義*/

int      si_code;   /* 信號產生的原因,對所有信號有意義*/

pid_t    si_pid;    /* 發送信號的進程ID,對kill(2),實時信號以及SIGCHLD有意義 */

uid_t    si_uid;    /* 發送信號進程的真實用戶ID,對kill(2),實時信號以及SIGCHLD有意義 */

int      si_status; /* 退出狀態,對SIGCHLD有意義*/

clock_t  si_utime;  /* 用戶消耗的時間,對SIGCHLD有意義 */

clock_t  si_stime;  /* 內核消耗的時間,對SIGCHLD有意義 */

sigval_t si_value;  /* 信號值,對所有實時有意義,是一個聯合數據結構,

                          /*可以爲一個整數(由si_int標示,也可以爲一個指針,由si_ptr標示)*/

 

void *   si_addr;   /* 觸發fault的內存地址,對SIGILL,SIGFPE,SIGSEGV,SIGBUS 信號有意義*/

int      si_band;   /* 對SIGPOLL信號有意義 */

int      si_fd;     /* 對SIGPOLL信號有意義 */

}

 

實際上,除了前三個元素外,其他元素組織在一個聯合結構中,在聯合數據結構中,又根據不同的信號組織成不同的結構。註釋中提到的對某種信號有意義指的是,在該信號的處理函數中可以訪問這些域來獲得與信號相關的有意義的信息,只不過特定信號只對特定信息感興趣而已。

2.8. 參考資料

  1. linux內核源代碼情景分析(上),毛德操、胡希明著,浙江大學出版社,當要驗證某個結論、想法時,最好的參考資料;
  2. UNIX環境高級編程,作者:W.Richard Stevens,譯者:尤晉元等,機械工業出版社。對信號機制的發展過程闡述的比較詳細。
  3. signal、sigaction、kill等手冊,最直接而可靠的參考資料。
  4. http://www.linuxjournal.com/modules.php?op=modload&name=NS-help&file=man提供了許多系統調用、庫函數等的在線指南。
  5. http://www.opengroup.org/onlinepubs/007904975/可以在這裏對許多關鍵函數(包括系統調用)進行查詢,非常好的一個網址。
  6. http://unix.org/whitepapers/reentrant.html對函數可重入進行了闡述。
  7. http://www.uccs.edu/~compsvcs/doc-cdrom/DOCS/HTML/APS33DTE/DOCU_006.HTM對實時信號給出了相當好的描述。

3. 信號(下)

3.1. 信號生命週期

從信號發送到信號處理函數的執行完畢

對於一個完整的信號生命週期(從信號發送到相應的處理函數執行完畢)來說,可以分爲三個重要的階段,這三個階段由四個重要事件來刻畫:信號誕生;信號在進程中註冊完畢;信號在進程中的註銷完畢;信號處理函數執行完畢。相鄰兩個事件的時間間隔構成信號生命週期的一個階段。


下面闡述四個事件的實際意義:

  1. 信號"誕生"。信號的誕生指的是觸發信號的事件發生(如檢測到硬件異常、定時器超時以及調用信號發送函數kill()或sigqueue()等)。
  2. 信號在目標進程中"註冊";進程的task_struct結構中有關於本進程中未決信號的數據成員:

struct sigpending pending:

struct sigpending{

       struct sigqueue *head, **tail;

       sigset_t signal;

};

第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之爲"未決信號信息鏈")的首尾,信息鏈中的每個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:

struct sigqueue{

       struct sigqueue *next;

       siginfo_t info;

}


信號在進程中註冊指的就是信號值加入到進程的未決信號集中(sigpending結構的第二個成員sigset_t signal),並且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。 只要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

注: 
當一個實時信號發送給一個進程時,不管該信號是否已經在進程中註冊,都會被再註冊一次,因此,信號不會丟失,因此,實時信號又叫做"可靠信號"。這意味着同一個實時信號可以在同一個進程的未決信號信息鏈中佔有多個sigqueue結構(進程每收到一個實時信號,都會爲它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即所有誕生的實時信號都會在目標進程中註冊); 
當一個非實時信號發送給一個進程時,如果該信號已經在進程中註冊,則該信號將被丟棄,造成信號丟失。因此,非實時信號又叫做"不可靠信號"。這意味着同一個非實時信號在進程的未決信號信息鏈中,至多佔有一個sigqueue結構(一個非實時信號誕生後,(1)、如果發現相同的信號已經在目標結構中註冊,則不再註冊,對於進程來說,相當於不知道本次信號發生,信號丟失;(2)、如果進程的未決信號中沒有相同信號,則在進程中註冊自己)。

  1. 信號在進程中的註銷。在目標進程執行過程中,會檢測是否有信號等待處理(每次從系統空間返回到用戶空間時都做這樣的檢查)。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中佔有的結構卸掉。是否將信號從進程未決信號集中刪除對於實時與非實時信號是不同的。對於非實時信號來說,由於在未決信號信息鏈中最多隻佔用一個sigqueue結構,因此該結構被釋放後,應該把信號在進程未決信號集中刪除(信號註銷完畢);而對於實時信號來說,可能在未決信號信息鏈中佔用多個sigqueue結構,因此應該針對佔用sigqueue結構的數目區別對待:如果只佔用一個sigqueue結構(進程只收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號註銷完畢)。否則,不應該在進程的未決信號集中刪除該信號(信號註銷完畢)。 
    進程在執行信號相應處理函數之前,首先要把信號在進程中註銷。
  2. 信號生命終止。進程註銷信號後,立即執行相應的信號處理函數,執行完畢後,信號的本次發送對進程的影響徹底結束。

注: 
1)信號註冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多隻註冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被註冊)。 
2)在信號被註銷到相應的信號處理函數執行完畢這段時間內,如果進程又收到同一信號多次,則對實時信號來說,每一次都會在進程中註冊;而對於非實時信號來說,無論收到多少次信號,都會視爲只收到一個信號,只在進程中註冊一次。

3.2. 信號編程注意事項

  1. 防止不該丟失的信號丟失。如果對八中所提到的信號生命週期理解深刻的話,很容易知道信號會不會丟失,以及在哪裏丟失。
  2. 程序的可移植性 
    考慮到程序的可移植性,應該儘量採用POSIX信號函數,POSIX信號函數主要分爲兩類:
    • POSIX 1003.1信號函數: Kill()、sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、sigpending()、sigprocmask()、sigsuspend()。
    • POSIX 1003.1b信號函數。POSIX 1003.1b在信號的實時性方面對POSIX 1003.1做了擴展,包括以下三個函數: sigqueue()、sigtimedwait()、sigwaitinfo()。 其中,sigqueue主要針對信號發送,而sigtimedwait及sigwaitinfo()主要用於取代sigsuspend()函數,後面有相應實例。

#include <signal.h>

int sigwaitinfo(sigset_t *set, siginfo_t *info).


該函數與sigsuspend()類似,阻塞一個進程直到特定信號發生,但信號到來時不執行信號處理函數,而是返回信號值。因此爲了避免執行相應的信號處理函數,必須在調用該函數前,使進程屏蔽掉set指向的信號,因此調用該函數的典型代碼是:

sigset_t newmask;

int rcvd_sig;

siginfo_t info;

sigemptyset(&newmask);

sigaddset(&newmask, SIGRTMIN);

sigprocmask(SIG_BLOCK, &newmask, NULL);

rcvd_sig = sigwaitinfo(&newmask, &info)

if (rcvd_sig == -1) {

       ..

}


調用成功返回信號值,否則返回-1。sigtimedwait()功能相似,只不過增加了一個進程等待的時間。

  1. 程序的穩定性。 
    爲了增強程序的穩定性,在信號處理函數中應使用可重入函數。

信號處理程序中應當使用可再入(可重入)函數(注:所謂可重入函數是指一個可以被多個任務調用的過程,任務在調用時不必擔心數據是否會出錯)。因爲進程在收到信號後,就將跳轉到信號處理函數去接着執行。如果信號處理函數中使用了不可重入函數,那麼信號處理函數可能會修改原來進程中不應該被修改的數據,這樣進程從信號處理函數中返回接着執行時,可能會出現不可預料的後果。不可再入函數在信號處理函數中被視爲不安全函數。

滿足下列條件的函數多數是不可再入的:(1)使用靜態的數據結構,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;(2)函數實現時,調用了malloc()或者free()函數;(3)實現時使用了標準I/O函數的。The OpenGroup視下列函數爲可再入的:

_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown() 、close()、creat()、dup()、dup2()、execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、 geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、kill()、link()、lseek()、mkdir()、mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、setgid()、setpgid()、setsid()、setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。

即使信號處理函數使用的都是"安全函數",同樣要注意進入處理函數時,首先要保存errno的值,結束時,再恢復原值。因爲,信號處理過程中,errno值隨時可能被改變。另外,longjmp()以及siglongjmp()沒有被列爲可再入函數,因爲不能保證緊接着兩個函數的其它調用是安全的。

3.3. 深入淺出:信號應用實例

linux下的信號應用並沒有想象的那麼恐怖,程序員所要做的最多隻有三件事情:

  1. 安裝信號(推薦使用sigaction());
  2. 實現三參數信號處理函數,handler(int signal,struct siginfo *info, void *);
  3. 發送信號,推薦使用sigqueue()。

實際上,對有些信號來說,只要安裝信號就足夠了(信號處理方式採用缺省或忽略)。其他可能要做的無非是與信號集相關的幾種操作。

實例一:信號發送及處理 
實現一個信號接收程序sigreceive(其中信號安裝由sigaction())。

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

       struct sigaction act;      

       int sig;

       sig=atoi(argv[1]);

 

       sigemptyset(&act.sa_mask);

       act.sa_flags=SA_SIGINFO;

       act.sa_sigaction=new_op;

 

       if(sigaction(sig,&act,NULL) < 0)

       {

              printf("install sigal error\n");

       }

 

       while(1)

       {

              sleep(2);

              printf("wait for the signal\n");

       }

}

void new_op(int signum,siginfo_t *info,void *myact)

{

       printf("receive signal %d", signum);

       sleep(5);

}

 

說明,命令行參數爲信號值,後臺運行sigreceive signo &,可獲得該進程的ID,假設爲pid,然後再另一終端上運行kill -s signo pid驗證信號的發送接收及處理。同時,可驗證信號的排隊問題。 
注:可以用sigqueue實現一個命令行信號發送程序sigqueuesend,見 附錄1

實例二:信號傳遞附加信息 
主要包括兩個實例:

  1. 向進程本身發送信號,並傳遞指針參數;

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

       struct sigaction act;      

       union sigval mysigval;

       int i;

       int sig;

       pid_t pid;       

       char data[10];

       memset(data,0,sizeof(data));

       for(i=0;i < 5;i++)

              data[i]='2';

       mysigval.sival_ptr=data;

 

       sig=atoi(argv[1]);

       pid=getpid();

 

       sigemptyset(&act.sa_mask);

       act.sa_sigaction=new_op;//三參數信號處理函數

       act.sa_flags=SA_SIGINFO;//信息傳遞開關

       if(sigaction(sig,&act,NULL) < 0)

       {

              printf("install sigal error\n");

       }

       while(1)

       {

              sleep(2);

              printf("wait for the signal\n");

              sigqueue(pid,sig,mysigval);//向本進程發送信號,並傳遞附加信息

       }

}

void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實現

{

       int i;

       for(i=0;i<10;i++)

       {

              printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));

       }

       printf("handle signal %d over;",signum);

}

 

這個例子中,信號實現了附加信息的傳遞,信號究竟如何對這些信息進行處理則取決於具體的應用。

  1. 2、 不同進程間傳遞整型參數:把1中的信號發送和接收放在兩個程序中,並且在發送過程中傳遞整型參數。 
    信號接收程序:

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

       struct sigaction act;

       int sig;

       pid_t pid;       

 

       pid=getpid();

       sig=atoi(argv[1]);  

 

       sigemptyset(&act.sa_mask);

       act.sa_sigaction=new_op;

       act.sa_flags=SA_SIGINFO;

       if(sigaction(sig,&act,NULL)<0)

       {

              printf("install sigal error\n");

       }

       while(1)

       {

              sleep(2);

              printf("wait for the signal\n");

       }

}

void new_op(int signum,siginfo_t *info,void *myact)

{

       printf("the int value is %d \n",info->si_int);

}

 

信號發送程序:命令行第二個參數爲信號值,第三個參數爲接收進程ID。

#include <signal.h>

#include <sys/time.h>

#include <unistd.h>

#include <sys/types.h>

main(int argc,char**argv)

{

       pid_t pid;

       int signum;

       union sigval mysigval;

       signum=atoi(argv[1]);

       pid=(pid_t)atoi(argv[2]);

       mysigval.sival_int=8;//不代表具體含義,只用於說明問題

       if(sigqueue(pid,signum,mysigval)==-1)

              printf("send error\n");

       sleep(2);

}

 

注:實例2的兩個例子側重點在於用信號來傳遞信息,目前關於在linux下通過信號傳遞信息的實例非常少,倒是Unix下有一些,但傳遞的基本上都是關於傳遞一個整數,傳遞指針的我還沒看到。我一直沒有實現不同進程間的指針傳遞(實際上更有意義),也許在實現方法上存在問題吧,請實現者email我。

實例三:信號阻塞及信號集操作

#include "signal.h"

#include "unistd.h"

static void my_op(int);

main()

{

       sigset_t new_mask,old_mask,pending_mask;

       struct sigaction act;

       sigemptyset(&act.sa_mask);

       act.sa_flags=SA_SIGINFO;

       act.sa_sigaction=(void*)my_op;

       if(sigaction(SIGRTMIN+10,&act,NULL))

              printf("install signal SIGRTMIN+10 error\n");

       sigemptyset(&new_mask);

       sigaddset(&new_mask,SIGRTMIN+10);

       if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))

              printf("block signal SIGRTMIN+10 error\n");

       sleep(10);      

       printf("now begin to get pending mask and unblock SIGRTMIN+10\n");

       if(sigpending(&pending_mask)<0)

              printf("get pending mask error\n");

       if(sigismember(&pending_mask,SIGRTMIN+10))

              printf("signal SIGRTMIN+10 is pending\n");

       if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)

              printf("unblock signal error\n");

       printf("signal unblocked\n");

       sleep(10);

}

static void my_op(int signum)

{

       printf("receive signal %d \n",signum);

}

 

編譯該程序,並以後臺方式運行。在另一終端向該進程發送信號(運行kill -s 42pid,SIGRTMIN+10爲42),查看結果可以看出幾個關鍵函數的運行機制,信號集相關操作比較簡單。

注:在上面幾個實例中,使用了printf()函數,只是作爲診斷工具,pringf()函數是不可重入的,不應在信號處理函數中使用。

3.4. 結束語

系統地對linux信號機制進行分析、總結使我受益匪淺!感謝王小樂等網友的支持! 
Comments and suggestions are greatly welcome!

附錄1:

用sigqueue實現的命令行信號發送程序sigqueuesend,命令行第二個參數是發送的信號值,第三個參數是接收該信號的進程ID,可以配合實例一使用:

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

int main(int argc,char**argv)

{

       pid_t pid;

       int sig;

       sig=atoi(argv[1]);

       pid=atoi(argv[2]);

       sigqueue(pid,sig,NULL);

       sleep(2);

}

 

3.5. 參考資料

4. 消息隊列

簡介:消息隊列(也叫做報文隊列)能夠克服早期unix通信機制的一些缺點。作爲早期unix通信機制之一的信號能夠傳送的信息量有限,後來雖然POSIX1003.1b在信號的實時性方面作了拓廣,使得信號在傳遞信息量方面有了相當程度的改進,但是信號這種通信方式更像"即時"的通信方式,它要求接受信號的進程在某個時間範圍內對信號做出反應,因此該信號最多在接受信號進程的生命週期內纔有意義,信號所傳遞的信息是接近於隨進程持續的概念(process-persistent),見 附錄 1;管道及有名管道及有名管道則是典型的隨進程持續IPC,並且,只能傳送無格式的字節流無疑會給應用程序開發帶來不便,另外,它的緩衝區大小也受到限制。

消息隊列就是一個消息的鏈表。可以把消息看作一個記錄,具有特定的格式以及特定的優先級。對消息隊列有寫權限的進程可以向中按照一定的規則添加新消息;對消息隊列有讀權限的進程則可以從消息隊列中讀走消息。消息隊列是隨內核持續的(參見 附錄 1)。

目前主要有兩種類型的消息隊列:POSIX消息隊列以及系統V消息隊列,系統V消息隊列目前被大量使用。考慮到程序的可移植性,新開發的應用程序應儘量使用POSIX消息隊列。

在本系列專題的序(深刻理解Linux進程間通信(IPC))中,提到對於消息隊列、信號燈、以及共享內存區來說,有兩個實現版本:POSIX的以及系統V的。Linux內核(內核2.4.18)支持POSIX信號燈、POSIX共享內存區以及POSIX消息隊列,但對於主流Linux發行版本之一redhad8.0(內核2.4.18),還沒有提供對POSIX進程間通信API的支持,不過應該只是時間上的事。

因此,本文將主要介紹系統V消息隊列及其相應API。 在沒有聲明的情況下,以下討論中指的都是系統V消息隊列。

4.1. 消息隊列基本概念

  1. 系統V消息隊列是隨內核持續的,只有在內核重起或者顯示刪除一個消息隊列時,該消息隊列纔會真正被刪除。因此係統中記錄消息隊列的數據結構(struct ipc_ids msg_ids)位於內核中,系統中的所有消息隊列都可以在結構msg_ids中找到訪問入口。
  2. 消息隊列就是一個消息的鏈表。每個消息隊列都有一個隊列頭,用結構struct msg_queue來描述(參見 附錄 2)。隊列頭中包含了該消息隊列的大量信息,包括消息隊列鍵值、用戶ID、組ID、消息隊列中消息數目等等,甚至記錄了最近對消息隊列讀寫進程的ID。讀者可以訪問這些信息,也可以設置其中的某些信息。
  1. 下圖說明了內核與消息隊列是怎樣建立起聯繫的: 
    其中:structipc_ids msg_ids是內核中記錄消息隊列的全局數據結構;struct msg_queue是每個消息隊列的隊列頭。 

從上圖可以看出,全局數據結構struct ipc_ids msg_ids 可以訪問到每個消息隊列頭的第一個成員:struct kern_ipc_perm;而每個struct kern_ipc_perm能夠與具體的消息隊列對應起來是因爲在該結構中,有一個key_t類型成員key,而key則唯一確定一個消息隊列。kern_ipc_perm結構如下:

struct kern_ipc_perm{   //內核中記錄消息隊列的全局數據結構msg_ids能夠訪問到該結構;

            key_t   key;    //該鍵值則唯一對應一個消息隊列

            uid_t   uid;

            gid_t   gid;

uid_t   cuid;

gid_t   cgid;

mode_t  mode;

unsigned long seq;

}

 

4.2. 操作消息隊列

對消息隊列的操作無非有下面三種類型:

1、打開或創建消息隊列 
消息隊列的內核持續性要求每個消息隊列都在系統範圍內對應唯一的鍵值,所以,要獲得一個消息隊列的描述字,只需提供該消息隊列的鍵值即可;

注:消息隊列描述字是由在系統範圍內唯一的鍵值生成的,而鍵值可以看作對應系統內的一條路經。

2、讀寫操作

消息讀寫操作非常簡單,對開發人員來說,每個消息都類似如下的數據結構:

struct msgbuf{

long mtype;

char mtext[1];

};

mtype成員代表消息類型,從消息隊列中讀取消息的一個重要依據就是消息的類型;mtext是消息內容,當然長度不一定爲1。因此,對於發送消息來說,首先預置一個msgbuf緩衝區並寫入消息類型和內容,調用相應的發送函數即可;對讀取消息來說,首先分配這樣一個msgbuf緩衝區,然後把消息讀入該緩衝區即可。

3、獲得或設置消息隊列屬性:

消息隊列的信息基本上都保存在消息隊列頭中,因此,可以分配一個類似於消息隊列頭的結構(struct msqid_ds,見 附錄 2),來返回消息隊列的屬性;同樣可以設置該數據結構。

消息隊列API

1、文件名到鍵值

#include <sys/types.h>

#include <sys/ipc.h>

key_t ftok (char*pathname, char proj);

 

它返回與路徑pathname相對應的一個鍵值。該函數不直接對消息隊列操作,但在調用ipc(MSGGET,…)或msgget()來獲得消息隊列描述字前,往往要調用該函數。典型的調用代碼是:

key=ftok(path_ptr, 'a');

    ipc_id=ipc(MSGGET, (int)key, flags,0,NULL,0);

    …

 

2、linux爲操作系統V進程間通信的三種方式(消息隊列、信號燈、共享內存區)提供了一個統一的用戶界面:
int ipc
(unsigned int call, int first, int second,int third, void * ptr, long fifth);

第一個參數指明對IPC對象的操作方式,對消息隊列而言共有四種操作:MSGSND、MSGRCV、MSGGET以及MSGCTL,分別代表向消息隊列發送消息、從消息隊列讀取消息、打開或創建消息隊列、控制消息隊列;first參數代表唯一的IPC對象;下面將介紹四種操作。

  • int ipcMSGGET, intfirst, intsecond, intthird, void*ptr, longfifth); 
    與該操作對應的系統V調用爲:int msgget( (key_t)first,second)。
  • int ipcMSGCTL, intfirst, intsecond, intthird, void*ptr, longfifth) 
    與該操作對應的系統V調用爲:int msgctl( first,second, (struct msqid_ds*) ptr)。
  • int ipcMSGSND, intfirst, intsecond, intthird, void*ptr, longfifth); 
    與該操作對應的系統V調用爲:int msgsnd( first, (struct msgbuf*)ptr, second, third)。
  • int ipcMSGRCV, intfirst, intsecond, intthird, void*ptr, longfifth); 
    與該操作對應的系統V調用爲:int msgrcv( first,(struct msgbuf*)ptr, second, fifth,third),

 

注:本人不主張採用系統調用ipc(),而更傾向於採用系統V或者POSIX進程間通信API。原因如下:

  • 雖然該系統調用提供了統一的用戶界面,但正是由於這個特性,它的參數幾乎不能給出特定的實際意義(如以first、second來命名參數),在一定程度上造成開發不便。
  • 正如ipc手冊所說的:ipc()是linux所特有的,編寫程序時應注意程序的移植性問題;
  • 該系統調用的實現不過是把系統V IPC函數進行了封裝,沒有任何效率上的優勢;
  • 系統V在IPC方面的API數量不多,形式也較簡潔。

 

3.系統V消息隊列API
系統V消息隊列API共有四個,使用時需要包括幾個頭文件:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

1)int msgget(key_t key, int msgflg)

參數key是一個鍵值,由ftok獲得;msgflg參數是一些標誌位。該調用返回與健值key相對應的消息隊列描述字。

在以下兩種情況下,該調用將創建一個新的消息隊列:

  • 如果沒有消息隊列與健值key相對應,並且msgflg中包含了IPC_CREAT標誌位;
  • key參數爲IPC_PRIVATE;

 

參數msgflg可以爲以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或結果。

調用返回:成功返回消息隊列描述字,否則返回-1。

注:參數key設置成常數IPC_PRIVATE並不意味着其他進程不能訪問該消息隊列,只意味着即將創建新的消息隊列。

2)int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp,int msgflg);
該系統調用從msgid代表的消息隊列中讀取一個消息,並把消息存儲在msgp指向的msgbuf結構中。

msqid爲消息隊列描述字;消息返回後存儲在msgp指向的地址,msgsz指定msgbuf的mtext成員的長度(即消息內容的長度),msgtyp爲請求讀取的消息類型;讀消息標誌msgflg可以爲以下幾個常值的或:

  • IPC_NOWAIT 如果沒有滿足條件的消息,調用立即返回,此時,errno=ENOMSG
  • IPC_EXCEPT 與msgtyp>0配合使用,返回隊列中第一個類型不爲msgtyp的消息
  • IPC_NOERROR 如果隊列中滿足條件的消息內容大於所請求的msgsz字節,則把該消息截斷,截斷部分將丟失。

 

msgrcv手冊中詳細給出了消息類型取不同值時(>0; <0; =0),調用將返回消息隊列中的哪個消息。

msgrcv()解除阻塞的條件有三個:

  1. 消息隊列中有了滿足條件的消息;
  2. msqid代表的消息隊列被刪除;
  3. 調用msgrcv()的進程被信號中斷;

 

調用返回:成功返回讀出消息的實際字節數,否則返回-1。

3)int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);
向msgid代表的消息隊列發送一個消息,即將發送的消息存儲在msgp指向的msgbuf結構中,消息的大小由msgze指定。

對發送消息來說,有意義的msgflg標誌爲IPC_NOWAIT,指明在消息隊列沒有足夠空間容納要發送的消息時,msgsnd是否等待。造成msgsnd()等待的條件有兩種:

  • 當前消息的大小與當前消息隊列中的字節數之和超過了消息隊列的總容量;
  • 當前消息隊列的消息數(單位"個")不小於消息隊列的總容量(單位"字節數"),此時,雖然消息隊列中的消息數目很多,但基本上都只有一個字節。

 

msgsnd()解除阻塞的條件有三個:

  1. 不滿足上述兩個條件,即消息隊列中有容納該消息的空間;
  2. msqid代表的消息隊列被刪除;
  3. 調用msgsnd()的進程被信號中斷;

 

調用返回:成功返回0,否則返回-1。

4)int msgctl(int msqid, int cmd, struct msqid_ds *buf);
該系統調用對由msqid標識的消息隊列執行cmd操作,共有三種cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。

  1. IPC_STAT:該命令用來獲取消息隊列信息,返回的信息存貯在buf指向的msqid結構中;
  2. IPC_SET:該命令用來設置消息隊列的屬性,要設置的屬性存儲在buf指向的msqid結構中;可設置屬性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同時,也影響msg_ctime成員。
  3. IPC_RMID:刪除msqid標識的消息隊列;

 

調用返回:成功返回0,否則返回-1。

4.3. 消息隊列的限制

每個消息隊列的容量(所能容納的字節數)都有限制,該值因系統不同而不同。在後面的應用實例中,輸出了redhat 8.0的限制,結果參見 附錄 3

另一個限制是每個消息隊列所能容納的最大消息數:在redhad 8.0中,該限制是受消息隊列容量制約的:消息個數要小於消息隊列的容量(字節數)。

注:上述兩個限制是針對每個消息隊列而言的,系統對消息隊列的限制還有系統範圍內的最大消息隊列個數,以及整個系統範圍內的最大消息數。一般來說,實際開發過程中不會超過這個限制。

4.4. 消息隊列應用實例

消息隊列應用相對較簡單,下面實例基本上覆蓋了對消息隊列的所有操作,同時,程序輸出結果有助於加深對前面所講的某些規則及消息隊列限制的理解。

#include <sys/types.h>

#include <sys/msg.h>

#include <unistd.h>

void msg_stat(int,struct msqid_ds );

main()

{

int gflags,sflags,rflags;

key_t key;

int msgid;

int reval;

struct msgsbuf{

        int mtype;

        char mtext[1];

    }msg_sbuf;

struct msgmbuf

    {

    int mtype;

    char mtext[10];

    }msg_rbuf;

struct msqid_ds msg_ginfo,msg_sinfo;

char* msgpath="/unix/msgqueue";

key=ftok(msgpath,'a');

gflags=IPC_CREAT|IPC_EXCL;

msgid=msgget(key,gflags|00666);

if(msgid==-1)

{

    printf("msg create error\n");

    return;

}

//創建一個消息隊列後,輸出消息隊列缺省屬性

msg_stat(msgid,msg_ginfo);

sflags=IPC_NOWAIT;

msg_sbuf.mtype=10;

msg_sbuf.mtext[0]='a';

reval=msgsnd(msgid,&msg_sbuf,sizeof(msg_sbuf.mtext),sflags);

if(reval==-1)

{

    printf("message send error\n");

}

//發送一個消息後,輸出消息隊列屬性

msg_stat(msgid,msg_ginfo);

rflags=IPC_NOWAIT|MSG_NOERROR;

reval=msgrcv(msgid,&msg_rbuf,4,10,rflags);

if(reval==-1)

    printf("read msg error\n");

else

    printf("read from msg queue %d bytes\n",reval);

//從消息隊列中讀出消息後,輸出消息隊列屬性

msg_stat(msgid,msg_ginfo);

msg_sinfo.msg_perm.uid=8;//just a try

msg_sinfo.msg_perm.gid=8;//

msg_sinfo.msg_qbytes=16388;

//此處驗證超級用戶可以更改消息隊列的缺省msg_qbytes

//注意這裏設置的值大於缺省值

reval=msgctl(msgid,IPC_SET,&msg_sinfo);

if(reval==-1)

{

    printf("msg set info error\n");

    return;

}

msg_stat(msgid,msg_ginfo);

//驗證設置消息隊列屬性

reval=msgctl(msgid,IPC_RMID,NULL);//刪除消息隊列

if(reval==-1)

{

    printf("unlink msg queue error\n");

    return;

}

}

void msg_stat(int msgid,struct msqid_ds msg_info)

{

int reval;

sleep(1);//只是爲了後面輸出時間的方便

reval=msgctl(msgid,IPC_STAT,&msg_info);

if(reval==-1)

{

    printf("get msg info error\n");

    return;

}

printf("\n");

printf("current number of bytes on queue is %d\n",msg_info.msg_cbytes);

printf("number of messages in queue is %d\n",msg_info.msg_qnum);

printf("max number of bytes on queue is %d\n",msg_info.msg_qbytes);

//每個消息隊列的容量(字節數)都有限制MSGMNB,值的大小因系統而異。在創建新的消息隊列時,//msg_qbytes的缺省值就是MSGMNB

printf("pid of last msgsnd is %d\n",msg_info.msg_lspid);

printf("pid of last msgrcv is %d\n",msg_info.msg_lrpid);

printf("last msgsnd time is %s", ctime(&(msg_info.msg_stime)));

printf("last msgrcv time is %s", ctime(&(msg_info.msg_rtime)));

printf("last change time is %s", ctime(&(msg_info.msg_ctime)));

printf("msg uid is %d\n",msg_info.msg_perm.uid);

printf("msg gid is %d\n",msg_info.msg_perm.gid);

}

 

程序輸出結果見 附錄 3

 

4.5. 小結

消息隊列與管道以及有名管道相比,具有更大的靈活性,首先,它提供有格式字節流,有利於減少開發人員的工作量;其次,消息具有類型,在實際應用中,可作爲優先級使用。這兩點是管道以及有名管道所不能比的。同樣,消息隊列可以在幾個進程間複用,而不管這幾個進程是否具有親緣關係,這一點與有名管道很相似;但消息隊列是隨內核持續的,與有名管道(隨進程持續)相比,生命力更強,應用空間更大。

附錄 1: 在參考文獻[1]中,給出了IPC隨進程持續、隨內核持續以及隨文件系統持續的定義:

  1. 隨進程持續:IPC一直存在到打開IPC對象的最後一個進程關閉該對象爲止。如管道和有名管道;
  2. 隨內核持續:IPC一直持續到內核重新自舉或者顯示刪除該對象爲止。如消息隊列、信號燈以及共享內存等;
  3. 隨文件系統持續:IPC一直持續到顯示刪除該對象爲止。

 

附錄 2: 
結構msg_queue用來描述消息隊列頭,存在於系統空間:

struct msg_queue {

    struct kern_ipc_perm q_perm;

    time_t q_stime;         /* last msgsnd time */

    time_t q_rtime;         /* last msgrcv time */

    time_t q_ctime;         /* last change time */

    unsigned long q_cbytes;     /* current number of bytes on queue */

    unsigned long q_qnum;       /* number of messages in queue */

    unsigned long q_qbytes;     /* max number of bytes on queue */

    pid_t q_lspid;          /* pid of last msgsnd */

    pid_t q_lrpid;          /* last receive pid */

    struct list_head q_messages;

    struct list_head q_receivers;

    struct list_head q_senders;

};

 

結構msqid_ds用來設置或返回消息隊列的信息,存在於用戶空間;

struct msqid_ds {

    struct ipc_perm msg_perm;

    struct msg *msg_first;      /* first message on queue,unused  */

    struct msg *msg_last;       /* last message in queue,unused */

    __kernel_time_t msg_stime;  /* last msgsnd time */

    __kernel_time_t msg_rtime;  /* last msgrcv time */

    __kernel_time_t msg_ctime;  /* last change time */

    unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */

    unsigned long  msg_lqbytes; /* ditto */

    unsigned short msg_cbytes;  /* current number of bytes on queue */

    unsigned short msg_qnum;    /* number of messages in queue */

    unsigned short msg_qbytes;  /* max number of bytes on queue */

    __kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */

    __kernel_ipc_pid_t msg_lrpid;   /* last receive pid */

};

 

//可以看出上述兩個結構很相似。

 

附錄 3: 消息隊列實例輸出結果:

current number of bytes on queue is 0

number of messages in queue is 0

max number of bytes on queue is 16384

pid of last msgsnd is 0

pid of last msgrcv is 0

last msgsnd time is Thu Jan  1 08:00:00 1970

last msgrcv time is Thu Jan  1 08:00:00 1970

last change time is Sun Dec 29 18:28:20 2002

msg uid is 0

msg gid is 0

//上面剛剛創建一個新消息隊列時的輸出

current number of bytes on queue is 1

number of messages in queue is 1

max number of bytes on queue is 16384

pid of last msgsnd is 2510

pid of last msgrcv is 0

last msgsnd time is Sun Dec 29 18:28:21 2002

last msgrcv time is Thu Jan  1 08:00:00 1970

last change time is Sun Dec 29 18:28:20 2002

msg uid is 0

msg gid is 0

read from msg queue 1 bytes

//實際讀出的字節數

current number of bytes on queue is 0

number of messages in queue is 0

max number of bytes on queue is 16384   //每個消息隊列最大容量(字節數)

pid of last msgsnd is 2510

pid of last msgrcv is 2510

last msgsnd time is Sun Dec 29 18:28:21 2002

last msgrcv time is Sun Dec 29 18:28:22 2002

last change time is Sun Dec 29 18:28:20 2002

msg uid is 0

msg gid is 0

current number of bytes on queue is 0

number of messages in queue is 0

max number of bytes on queue is 16388   //可看出超級用戶可修改消息隊列最大容量

pid of last msgsnd is 2510

pid of last msgrcv is 2510  //對操作消息隊列進程的跟蹤

last msgsnd time is Sun Dec 29 18:28:21 2002

last msgrcv time is Sun Dec 29 18:28:22 2002

last change time is Sun Dec 29 18:28:23 2002    //msgctl()調用對msg_ctime有影響

msg uid is 8

msg gid is 8

 

4.6. 參考資料

  • UNIX網絡編程第二卷:進程間通信,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。對POSIX以及系統V消息隊列都有闡述,對Linux環境下的程序開發有極大的啓發意義。
  • linux內核源代碼情景分析(上),毛德操、胡希明著,浙江大學出版社,給出了系統V消息隊列相關的源代碼分析。
  • http://www.fanqiang.com/a4/b2/20010508/113315.html,主要闡述linux下對文件的操作,詳細介紹了對文件的存取權限位,對IPC對象的存取權限同樣具有很好的借鑑意義。 
  • msgget、msgsnd、msgrcv、msgctl手冊

 

5. 信號燈

簡介: 信號燈與其他進程間通信方式不大相同,它主要提供對進程間共享資源訪問控制機制。相當於內存中的標誌,進程可以根據它判定是否能夠訪問某些共享資源,同時,進程也可以修改該標誌。除了用於訪問控制外,還可用於進程同步。

5.1. 信號燈概述

信號燈與其他進程間通信方式不大相同,它主要提供對進程間共享資源訪問控制機制。相當於內存中的標誌,進程可以根據它判定是否能夠訪問某些共享資源,同時,進程也可以修改該標誌。除了用於訪問控制外,還可用於進程同步。信號燈有以下兩種類型:

  • 二值信號燈:最簡單的信號燈形式,信號燈的值只能取0或1,類似於互斥鎖。 
    注:二值信號燈能夠實現互斥鎖的功能,但兩者的關注內容不同。信號燈強調共享資源,只要共享資源可用,其他進程同樣可以修改信號燈的值;互斥鎖更強調進程,佔用資源的進程使用完資源後,必須由進程本身來解鎖。
  • 計算信號燈:信號燈的值可以取任意非負值(當然受內核本身的約束)。

5.2. Linux信號燈

linux對信號燈的支持狀況與消息隊列一樣,在red had 8.0發行版本中支持的是系統V的信號燈。因此,本文將主要介紹系統V信號燈及其相應API。在沒有聲明的情況下,以下討論中指的都是系統V信號燈。

注意,通常所說的系統V信號燈指的是計數信號燈集。

5.3. 信號燈與內核

1、系統V信號燈是隨內核持續的,只有在內核重起或者顯示刪除一個信號燈集時,該信號燈集纔會真正被刪除。因此係統中記錄信號燈的數據結構(struct ipc_ids sem_ids)位於內核中,系統中的所有信號燈都可以在結構sem_ids中找到訪問入口。

2、下圖說明了內核與信號燈是怎樣建立起聯繫的:

其中:structipc_ids sem_ids是內核中記錄信號燈的全局數據結構;描述一個具體的信號燈及其相關信息。

 其中,struct sem結構如下:

struct sem{

int semval;             // current value

int sempid              // pid of last operation

}

從上圖可以看出,全局數據結構structipc_ids sem_ids可以訪問到structkern_ipc_perm的第一個成員:structkern_ipc_perm;而每個structkern_ipc_perm能夠與具體的信號燈對應起來是因爲在該結構中,有一個key_t類型成員key,而key則唯一確定一個信號燈集;同時,結構struct kern_ipc_perm的最後一個成員sem_nsems確定了該信號燈在信號燈集中的順序,這樣內核就能夠記錄每個信號燈的信息了。kern_ipc_perm結構參見《Linux環境進程間通信(三):消息隊列》。struct sem_array見附錄1。

5.4. 操作信號燈

對消息隊列的操作無非有下面三種類型:

1、打開或創建信號燈 
與消息隊列的創建及打開基本相同,不再詳述。

2、信號燈值操作 
linux可以增加或減小信號燈的值,相應於對共享資源的釋放和佔有。具體參見後面的semop系統調用。

3、獲得或設置信號燈屬性: 
系統中的每一個信號燈集都對應一個struct sem_array結構,該結構記錄了信號燈集的各種信息,存在於系統空間。爲了設置、獲得該信號燈集的各種信息及屬性,在用戶空間有一個重要的聯合結構與之對應,即union semun。

 

聯合semun數據結構各成員意義參見附錄2

信號燈API

1、文件名到鍵值

#include <sys/types.h>

#include <sys/ipc.h>

key_t ftok (char*pathname, char proj);

 

它返回與路徑pathname相對應的一個鍵值,具體用法請參考《Linux環境進程間通信(三):消息隊列》。

2、 linux特有的ipc()調用:

int ipc(unsigned int call, int first, intsecond, int third, void *ptr, long fifth);

參數call取不同值時,對應信號燈的三個系統調用: 
當call爲SEMOP時,對應int semop(int semid, struct sembuf *sops, unsigned nsops)調用; 
當call爲SEMGET時,對應int semget(key_t key, int nsems,int semflg)調用; 
當call爲SEMCTL時,對應int semctl(int semid,int semnum,int cmd,union semun arg)調用; 
這些調用將在後面闡述。

注:本人不主張採用系統調用ipc(),而更傾向於採用系統V或者POSIX進程間通信API。原因已在Linux環境進程間通信(三):消息隊列中給出。

3、系統V信號燈API

系統V消息隊列API只有三個,使用時需要包括幾個頭文件:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

 

1)int semget(key_t key, int nsems, int semflg) 
參數key是一個鍵值,由ftok獲得,唯一標識一個信號燈集,用法與msgget()中的key相同;參數nsems指定打開或者新創建的信號燈集中將包含信號燈的數目;semflg參數是一些標誌位。參數key和semflg的取值,以及何時打開已有信號燈集或者創建一個新的信號燈集與msgget()中的對應部分相同,不再祥述。 
該調用返回與健值key相對應的信號燈集描述字。 
調用返回:成功返回信號燈集描述字,否則返回-1。 
注:如果key所代表的信號燈已經存在,且semget指定了IPC_CREAT|IPC_EXCL標誌,那麼即使參數nsems與原來信號燈的數目不等,返回的也是EEXIST錯誤;如果semget只指定了IPC_CREAT標誌,那麼參數nsems必須與原來的值一致,在後面程序實例中還要進一步說明。

2)int semop(int semid, struct sembuf *sops, unsigned nsops); 
semid是信號燈集ID,sops指向數組的每一個sembuf結構都刻畫一個在特定信號燈上的操作。nsops爲sops指向數組的大小。 
sembuf結構如下:

struct sembuf {

       unsigned short      sem_num;             /* semaphore index in array */

       short                     sem_op;         /* semaphore operation */

       short                     sem_flg;         /* operation flags */

};

 

sem_num對應信號集中的信號燈,0對應第一個信號燈。sem_flg可取IPC_NOWAIT以及SEM_UNDO兩個標誌。如果設置了SEM_UNDO標誌,那麼在進程結束時,相應的操作將被取消,這是比較重要的一個標誌位。如果設置了該標誌位,那麼在進程沒有釋放共享資源就退出時,內核將代爲釋放。如果爲一個信號燈設置了該標誌,內核都要分配一個sem_undo結構來記錄它,爲的是確保以後資源能夠安全釋放。事實上,如果進程退出了,那麼它所佔用就釋放了,但信號燈值卻沒有改變,此時,信號燈值反映的已經不是資源佔有的實際情況,在這種情況下,問題的解決就靠內核來完成。這有點像殭屍進程,進程雖然退出了,資源也都釋放了,但內核進程表中仍然有它的記錄,此時就需要父進程調用waitpid來解決問題了。 
sem_op的值大於0,等於0以及小於0確定了對sem_num指定的信號燈進行的三種操作。具體請參考linux相應手冊頁。 
這裏需要強調的是semop同時操作多個信號燈,在實際應用中,對應多種資源的申請或釋放。semop保證操作的原子性,這一點尤爲重要。尤其對於多種資源的申請來說,要麼一次性獲得所有資源,要麼放棄申請,要麼在不佔有任何資源情況下繼續等待,這樣,一方面避免了資源的浪費;另一方面,避免了進程之間由於申請共享資源造成死鎖。 
也許從實際含義上更好理解這些操作:信號燈的當前值記錄相應資源目前可用數目;sem_op>0對應相應進程要釋放sem_op數目的共享資源;sem_op=0可以用於對共享資源是否已用完的測試;sem_op<0相當於進程要申請-sem_op個共享資源。再聯想操作的原子性,更不難理解該系統調用何時正常返回,何時睡眠等待。 
調用返回:成功返回0,否則返回-1。

3) int semctl(int semid,int semnum,int cmd,union semun arg) 
該系統調用實現對信號燈的各種控制操作,參數semid指定信號燈集,參數cmd指定具體的操作類型;參數semnum指定對哪個信號燈操作,只對幾個特殊的cmd操作有意義;arg用於設置或返回信號燈信息。 
該系統調用詳細信息請參見其手冊頁,這裏只給出參數cmd所能指定的操作。

IPC_STAT

獲取信號燈信息,信息由arg.buf返回;

IPC_SET

設置信號燈信息,待設置信息保存在arg.buf中(在manpage中給出了可以設置哪些信息);

GETALL

返回所有信號燈的值,結果保存在arg.array中,參數sennum被忽略;

GETNCNT

返回等待semnum所代表信號燈的值增加的進程數,相當於目前有多少進程在等待semnum代表的信號燈所代表的共享資源;

GETPID

返回最後一個對semnum所代表信號燈執行semop操作的進程ID;

GETVAL

返回semnum所代表信號燈的值;

GETZCNT

返回等待semnum所代表信號燈的值變成0的進程數;

SETALL

通過arg.array更新所有信號燈的值;同時,更新與本信號集相關的semid_ds結構的sem_ctime成員;

SETVAL

設置semnum所代表信號燈的值爲arg.val;

調用返回:調用失敗返回-1,成功返回與cmd相關:

Cmd

return value

GETNCNT

Semncnt

GETPID

Sempid

GETVAL

Semval

GETZCNT

Semzcnt

5.5. 信號燈的限制

1、一次系統調用semop可同時操作的信號燈數目SEMOPM,semop中的參數nsops如果超過了這個數目,將返回E2BIG錯誤。SEMOPM的大小特定與系統,redhat 8.0爲32。

2、信號燈的最大數目:SEMVMX,當設置信號燈值超過這個限制時,會返回ERANGE錯誤。在redhat 8.0中該值爲32767。

3、系統範圍內信號燈集的最大數目SEMMNI以及系統範圍內信號燈的最大數目SEMMNS。超過這兩個限制將返回ENOSPC錯誤。redhat 8.0中該值爲32000。

4、每個信號燈集中的最大信號燈數目SEMMSL,redhat 8.0中爲250。 SEMOPM以及SEMVMX是使用semop調用時應該注意的;SEMMNI以及SEMMNS是調用semget時應該注意的。SEMVMX同時也是semctl調用應該注意的。

5.6. 競爭問題

第一個創建信號燈的進程同時也初始化信號燈,這樣,系統調用semget包含了兩個步驟:創建信號燈;初始化信號燈。由此可能導致一種競爭狀態:第一個創建信號燈的進程在初始化信號燈時,第二個進程又調用semget,並且發現信號燈已經存在,此時,第二個進程必須具有判斷是否有進程正在對信號燈進行初始化的能力。在參考文獻[1]中,給出了繞過這種競爭狀態的方法:當semget創建一個新的信號燈時,信號燈結構semid_ds的sem_otime成員初始化後的值爲0。因此,第二個進程在成功調用semget後,可再次以IPC_STAT命令調用semctl,等待sem_otime變爲非0值,此時可判斷該信號燈已經初始化完畢。下圖描述了競爭狀態產生及解決方法:


實際上,這種解決方法也是基於這樣一個假定:第一個創建信號燈的進程必須調用semop,這樣sem_otime才能變爲非零值。另外,因爲第一個進程可能不調用semop,或者semop操作需要很長時間,第二個進程可能無限期等待下去,或者等待很長時間。

5.7. 信號燈應用實例

本實例有兩個目的:1、獲取各種信號燈信息;2、利用信號燈實現共享資源的申請和釋放。並在程序中給出了詳細註釋。

#include <linux/sem.h>

#include <stdio.h>

#include <errno.h>

#define SEM_PATH "/unix/my_sem"

#define max_tries 3

int semid;

main()

{

int flag1,flag2,key,i,init_ok,tmperrno;

struct semid_ds sem_info;

struct seminfo sem_info2;

union semun arg;       //union semun: 請參考附錄2

struct sembuf askfor_res, free_res;

flag1=IPC_CREAT|IPC_EXCL|00666;

flag2=IPC_CREAT|00666;

key=ftok(SEM_PATH,'a');

//error handling for ftok here;

init_ok=0;

semid=semget(key,1,flag1);

//create a semaphore set that only includes one semphore.

if(semid<0)

{

  tmperrno=errno;

  perror("semget");

if(tmperrno==EEXIST)

//errno is undefined after a successful library call( including perror call)

//so it is saved  in tmperrno.

    {

    semid=semget(key,1,flag2);

//flag2 只包含了IPC_CREAT標誌, 參數nsems(這裏爲1)必須與原來的信號燈數目一致

    arg.buf=&sem_info;

    for(i=0; i<max_tries; i++)

    {

      if(semctl(semid, 0, IPC_STAT, arg)==-1)

      {  perror("semctl error"); i=max_tries;}

      else

      {

        if(arg.buf->sem_otime!=0){ i=max_tries;  init_ok=1;}

        else   sleep(1); 

      }

    }

    if(!init_ok)

  // do some initializing, here we assume that the first process that creates the sem

  //  will finish initialize the sem and run semop in max_tries*1 seconds. else it will 

  // not run semop any more.

    {

      arg.val=1;

      if(semctl(semid,0,SETVAL,arg)==-1) perror("semctl setval error");

    }

  }

  else

  {perror("semget error, process exit");  exit();  }

}

else //semid>=0; do some initializing  

{

  arg.val=1;

  if(semctl(semid,0,SETVAL,arg)==-1)

    perror("semctl setval error");

}

//get some information about the semaphore and the limit of semaphore in redhat8.0

  arg.buf=&sem_info;

  if(semctl(semid, 0, IPC_STAT, arg)==-1)

    perror("semctl IPC STAT");   

  printf("owner's uid is %d\n",   arg.buf->sem_perm.uid);

  printf("owner's gid is %d\n",   arg.buf->sem_perm.gid);

  printf("creater's uid is %d\n",   arg.buf->sem_perm.cuid);

  printf("creater's gid is %d\n",   arg.buf->sem_perm.cgid);

  arg.__buf=&sem_info2;

  if(semctl(semid,0,IPC_INFO,arg)==-1)

    perror("semctl IPC_INFO");

  printf("the number of entries in semaphore map is %d \n",  arg.__buf->semmap);

  printf("max number of semaphore identifiers is %d \n",    arg.__buf->semmni);

  printf("mas number of semaphores in system is %d \n",   arg.__buf->semmns);

  printf("the number of undo structures system wide is %d \n",  arg.__buf->semmnu);

  printf("max number of semaphores per semid is %d \n",   arg.__buf->semmsl);

  printf("max number of ops per semop call is %d \n",  arg.__buf->semopm);

  printf("max number of undo entries per process is %d \n",  arg.__buf->semume);

  printf("the sizeof of struct sem_undo is %d \n",  arg.__buf->semusz);

  printf("the maximum semaphore value is %d \n",  arg.__buf->semvmx);

 

//now ask for available resource: 

  askfor_res.sem_num=0;

  askfor_res.sem_op=-1;

  askfor_res.sem_flg=SEM_UNDO;   

 

    if(semop(semid,&askfor_res,1)==-1)//ask for resource

      perror("semop error");

 

  sleep(3);

  //do some handling on the sharing resource here, just sleep on it 3 seconds

  printf("now free the resource\n"); 

 

//now free resource 

  free_res.sem_num=0;

  free_res.sem_op=1;

  free_res.sem_flg=SEM_UNDO;

  if(semop(semid,&free_res,1)==-1)//free the resource.

    if(errno==EIDRM)

      printf("the semaphore set was removed\n");

//you can comment out the codes below to compile a different version:     

  if(semctl(semid, 0, IPC_RMID)==-1)

    perror("semctl IPC_RMID");

  else printf("remove sem ok\n");

}

 

注:讀者可以嘗試一下注釋掉初始化步驟,進程在運行時會出現何種情況(進程在申請資源時會睡眠),同時可以像程序結尾給出的註釋那樣,把該程序編譯成兩個不同版本。下面是本程序的運行結果(操作系統redhat8.0):

owner's uid is 0

owner's gid is 0

creater's uid is 0

creater's gid is 0

the number of entries in semaphore map is 32000

max number of semaphore identifiers is 128

mas number of semaphores in system is 32000

the number of undo structures system wide is 32000

max number of semaphores per semid is 250

max number of ops per semop call is 32

max number of undo entries per process is 32

the sizeof of struct sem_undo is 20

the maximum semaphore value is 32767

now free the resource

remove sem ok

 

Summary:信號燈與其它進程間通信方式有所不同,它主要用於進程間同步。通常所說的系統V信號燈實際上是一個信號燈的集合,可用於多種共享資源的進程間同步。每個信號燈都有一個值,可以用來表示當前該信號燈代表的共享資源可用(available)數量,如果一個進程要申請共享資源,那麼就從信號燈值中減去要申請的數目,如果當前沒有足夠的可用資源,進程可以睡眠等待,也可以立即返回。當進程要申請多種共享資源時,linux可以保證操作的原子性,即要麼申請到所有的共享資源,要麼放棄所有資源,這樣能夠保證多個進程不會造成互鎖。Linux對信號燈有各種各樣的限制,程序中給出了輸出結果。另外,如果讀者想對信號燈作進一步的理解,建議閱讀sem.h源代碼,該文件不長,但給出了信號燈相關的重要數據結構。

附錄1: struct sem_array如下:

/*系統中的每個信號燈集對應一個sem_array 結構 */

struct sem_array {

  struct kern_ipc_perm  sem_perm;    /* permissions .. see ipc.h */

  time_t      sem_otime;      /* last semop time */

  time_t      sem_ctime;      /* last change time */

  struct sem    *sem_base;      /* ptr to first semaphore in array */

  struct sem_queue  *sem_pending;    /* pending operations to be processed */

  struct sem_queue  **sem_pending_last;   /* last pending operation */

  struct sem_undo    *undo;      /* undo requests on this array */

  unsigned long    sem_nsems;    /* no. of semaphores in array */

};

 

其中,sem_queue結構如下:

/* 系統中每個因爲信號燈而睡眠的進程,都對應一個sem_queue結構*/

 struct sem_queue {

  struct sem_queue *  next;     /* next entry in the queue */

  struct sem_queue **  prev;

  /* previous entry in the queue, *(q->prev) == q */

  struct task_struct*  sleeper;   /* this process */

  struct sem_undo *  undo;     /* undo structure */

  int   pid;             /* process id of requesting process */

  int   status;           /* completion status of operation */

  struct sem_array *  sma;       /* semaphore array for operations */

  int  id;               /* internal sem id */

  struct sembuf *  sops;       /* array of pending operations */

  int  nsops;             /* number of operations */

  int  alter;             /* operation will alter semaphore */

};

 

附錄2:union semun是系統調用semctl中的重要參數:

union semun {

       int val;                                 /* value for SETVAL */

       struct semid_ds *buf;           /* buffer for IPC_STAT & IPC_SET */

       unsigned short *array;          /* array for GETALL & SETALL */

       struct seminfo *__buf;          /* buffer for IPC_INFO */   //test!!

       void *__pad;

};

struct  seminfo {

       int semmap;

       int semmni;

       int semmns;

       int semmnu;

       int semmsl;

       int semopm;

       int semume;

       int semusz;

       int semvmx;

       int semaem;

};

5.8. 參考資料

[1] UNIX網絡編程第二卷:進程間通信,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。對POSIX以及系統V信號燈都有闡述,對Linux環境下的程序開發有極大的啓發意義。

[2] linux內核源代碼情景分析(上),毛德操、胡希明著,浙江大學出版社,給出了系統V信號燈相關的源代碼分析,尤其在闡述保證操作原子性方面,以及闡述undo標誌位時,討論的很深刻。

[3]GNU/Linux編程指南,第二版,Kurt Wall等著,張輝譯

[4]semget、semop、semctl手冊

6. 共享內存(上)

簡介: 共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式。兩個不同進程A、B共享內存的意思是,同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時看到進程B對共享內存中數據的更新,反之亦然。由於多個進程共享同一塊內存區域,必然需要某種同步機制,互斥鎖和信號量都可以。

採用共享內存通信的一個顯而易見的好處是效率高,因爲進程可以直接讀寫內存,而不需要任何數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據[1]:一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據後就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢爲止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此,採用共享內存的通信方式效率是非常高的。

Linux的2.2.x內核支持多種共享內存方式,如mmap()系統調用,Posix共享內存,以及系統V共享內存。linux發行版本如Redhat 8.0支持mmap()系統調用及系統V共享內存,但還沒實現Posix共享內存,本文將主要介紹mmap()系統調用及系統V共享內存API的原理及應用。

6.1. 內核怎樣保證各個進程尋址到同一個共享內存區域的內存頁面

1、page cache及swap cache中頁面的區分:一個被訪問文件的物理頁面都駐留在page cache或swap cache中,一個頁面的所有信息由struct page來描述。struct page中有一個域爲指針mapping ,它指向一個struct address_space類型結構。page cache或swap cache中的所有頁面就是根據address_space結構以及一個偏移量來區分的。

2、文件與address_space結構的對應:一個具體的文件在打開後,內核會在內存中爲之建立一個struct inode結構,其中的i_mapping域指向一個address_space結構。這樣,一個文件就對應一個address_space結構,一個address_space與一個偏移量能夠確定一個page cache 或swap cache中的一個頁面。因此,當要尋址某個數據時,很容易根據給定的文件及數據在文件內的偏移量而找到相應的頁面。

3、進程調用mmap()時,只是在進程空間內新增了一塊相應大小的緩衝區,並設置了相應的訪問標識,但並沒有建立進程空間到物理頁面的映射。因此,第一次訪問該空間時,會引發一個缺頁異常。

4、對於共享內存映射情況,缺頁異常處理程序首先在swap cache中尋找目標頁(符合address_space以及偏移量的物理頁),如果找到,則直接返回地址;如果沒有找到,則判斷該頁是否在交換區(swap area),如果在,則執行一個換入操作;如果上述兩種情況都不滿足,處理程序將分配新的物理頁面,並把它插入到page cache中。進程最終將更新進程頁表。 
注:對於映射普通文件情況(非共享映射),缺頁異常處理程序首先會在page cache中根據address_space以及數據偏移量尋找相應的頁面。如果沒有找到,則說明文件數據還沒有讀入內存,處理程序會從磁盤讀入相應的頁面,並返回相應地址,同時,進程頁表也會更新。

5、所有進程在映射同一個共享內存區域時,情況都一樣,在建立線性地址與物理地址之間的映射之後,不論進程各自的返回地址如何,實際訪問的必然是同一個共享內存區域對應的物理頁面。 
注:一個共享內存區域可以看作是特殊文件系統shm中的一個文件,shm的安裝點在交換區上。

上面涉及到了一些數據結構,圍繞數據結構理解問題會容易一些。

6.2. mmap()及其相關係統調用

mmap()系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以向訪問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。

注:實際上,mmap()系統調用並不是完全爲了用於共享內存而設計的。它本身提供了不同於一般對普通文件的訪問方式,進程可以像讀寫內存一樣對普通文件的操作。而Posix或系統V的共享內存IPC則純粹用於共享目的,當然mmap()實現共享內存也是其主要應用之一。

1、mmap()系統調用形式如下:

void* mmap ( void * addr , size_t len , intprot , int flags , int fd , off_t offset ) 
參數fd爲即將映射到進程空間的文件描述字,一般由open()返回,同時,fd可以指定爲-1,此時須指定flags參數中的MAP_ANON,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用於具有親緣關係的進程間通信)。len是映射到調用進程地址空間的字節數,它從被映射文件開頭offset個字節開始算起。prot 參數指定共享內存的訪問權限。可取如下幾個值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執行), PROT_NONE(不可訪問)。flags由以下幾個常值指定:MAP_SHARED , MAP_PRIVATE ,MAP_FIXED,其中,MAP_SHARED, MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用。offset參數一般設爲0,表示從文件頭開始映射。參數addr指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函數的返回值爲最後文件映射到進程空間的地址,進程可直接操作起始地址爲該值的有效地址。這裏不再詳細介紹mmap()的參數,讀者可參考mmap()手冊頁獲得進一步的信息。

2、系統調用mmap()用於共享內存的兩種方式:

(1)使用普通文件提供的內存映射:適用於任何進程之間; 此時,需要打開或創建一個文件,然後再調用mmap();典型調用代碼如下:

       fd=open(name, flag, mode);

if(fd<0)

       ...

 

 

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE,MAP_SHARED , fd , 0); 通過mmap()實現共享內存的通信方式有許多特點和要注意的地方,我們將在範例中進行具體說明。

(2)使用特殊文件提供匿名內存映射:適用於具有親緣關係的進程之間; 由於父子進程特殊的親緣關係,在父進程中先調用mmap(),然後調用fork()。那麼在調用fork()之後,子進程繼承父進程匿名映射後的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區域進行通信了。注意,這裏不是一般的繼承關係。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。 
對於具有親緣關係的進程實現共享內存最好的方式應該是採用匿名內存映射的方式。此時,不必指定具體的文件,只要設置相應的標誌即可,參見範例2。

3、系統調用munmap()

int munmap( void * addr, size_t len ) 
該調用在進程地址空間中解除一個映射關係,addr是調用mmap()時返回的地址,len是映射區的大小。當映射關係解除後,對原來映射地址的訪問將導致段錯誤發生。

4、系統調用msync()

int msync ( void * addr , size_t len, intflags) 
一般說來,進程在映射空間的對共享內容的改變並不直接寫回到磁盤文件中,往往在調用munmap()後才執行該操作。可以通過調用msync()實現磁盤上文件內容與共享內存區的內容一致。

6.3. mmap()範例

下面將給出使用mmap()的兩個範例:範例1給出兩個進程通過映射普通文件實現共享內存通信;範例2給出父子進程通過匿名映射實現共享內存。系統調用mmap()有許多有趣的地方,下面是通過mmap()映射普通文件實現進程間的通信的範例,我們通過該範例來說明mmap()實現共享內存的特點及注意事項。

範例1:兩個進程通過映射普通文件實現共享內存通信

範例1包含兩個子程序:map_normalfile1.c及map_normalfile2.c。編譯兩個程序,可執行文件分別爲map_normalfile1及map_normalfile2。兩個程序通過命令行參數指定同一個文件來實現共享內存方式的進程間通信。map_normalfile2試圖打開命令行參數指定的一個普通文件,把該文件映射到進程的地址空間,並對映射後的地址空間進行寫操作。map_normalfile1把命令行參數指定的文件映射到進程地址空間,然後對映射後的地址空間執行讀操作。這樣,兩個進程通過命令行參數指定同一個文件來實現共享內存方式的進程間通信。

下面是兩個程序代碼:

/*-------------map_normalfile1.c-----------*/

#include <sys/mman.h>

#include <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

typedef struct{

  char name[4];

  int  age;

}people;

main(int argc, char** argv) // map a normal file as shared mem:

{

  int fd,i;

  people *p_map;

  char temp;

 

  fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);

  lseek(fd,sizeof(people)*5-1,SEEK_SET);

  write(fd,"",1);

 

  p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,

        MAP_SHARED,fd,0 );

  close( fd );

  temp = 'a';

  for(i=0; i<10; i++)

  {

    temp += 1;

    memcpy( ( *(p_map+i) ).name, &temp,2 );

    ( *(p_map+i) ).age = 20+i;

  }

  printf(" initialize over \n ");

  sleep(10);

  munmap( p_map, sizeof(people)*10 );

  printf( "umap ok \n" );

}

/*-------------map_normalfile2.c-----------*/

#include <sys/mman.h>

#include <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

typedef struct{

  char name[4];

  int  age;

}people;

main(int argc, char** argv)  // map a normal file as shared mem:

{

  int fd,i;

  people *p_map;

  fd=open( argv[1],O_CREAT|O_RDWR,00777 );

  p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,

       MAP_SHARED,fd,0);

  for(i = 0;i<10;i++)

  {

  printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );

  }

  munmap( p_map,sizeof(people)*10 );

}

 

map_normalfile1.c首先定義了一個people數據結構,(在這裏採用數據結構的方式是因爲,共享內存區的數據往往是有固定格式的,這由通信的各個進程決定,採用結構的方式有普遍代表性)。map_normfile1首先打開或創建一個文件,並把文件的長度設置爲5個people結構大小。然後從mmap()的返回地址開始,設置了10個people結構。然後,進程睡眠10秒鐘,等待其他進程映射同一個文件,最後解除映射。

map_normfile2.c只是簡單的映射一個文件,並以people數據結構的格式從mmap()返回的地址處讀取10個people結構,並輸出讀取的值,然後解除映射。

分別把兩個程序編譯成可執行文件map_normalfile1和map_normalfile2後,在一個終端上先運行./map_normalfile2 /tmp/test_shm,程序輸出結果如下:

initialize over

umap ok

 

在map_normalfile1輸出initialize over 之後,輸出umap ok之前,在另一個終端上運行map_normalfile2/tmp/test_shm,將會產生如下輸出(爲了節省空間,輸出結果爲稍作整理後的結果):

name: b   age 20;    name: c   age 21;    name: d   age 22;    name: e   age 23;    name: f    age 24;

name: g   age 25;    name: h   age 26;    name: I    age 27;    name: j    age 28;    name: k   age 29;

 

在map_normalfile1輸出umap ok後,運行map_normalfile2則輸出如下結果:

name: b   age 20;    name: c   age 21;    name: d   age 22;    name: e   age 23;    name: f    age 24;

name:      age 0;      name:      age 0;      name:      age 0;      name:      age 0;      name:      age 0;

 

從程序的運行結果中可以得出的結論

1、最終被映射文件的內容的長度不會超過文件本身的初始大小,即映射不能改變文件的大小;

2、可以用於進程通信的有效地址空間大小大體上受限於被映射文件的大小,但不完全受限於文件大小。打開文件被截短爲5個people結構大小,而在map_normalfile1中初始化了10個people數據結構,在恰當時候(map_normalfile1輸出initialize over 之後,輸出umap ok之前)調用map_normalfile2會發現map_normalfile2將輸出全部10個people結構的值,後面將給出詳細討論。 
注:在linux中,內存的保護是以頁爲基本單位的,即使被映射文件只有一個字節大小,內核也會爲映射分配一個頁面大小的內存。當被映射文件小於一個頁面大小時,進程可以對從mmap()返回地址開始的一個頁面大小進行訪問,而不會出錯;但是,如果對一個頁面以外的地址空間進行訪問,則導致錯誤發生,後面將進一步描述。因此,可用於進程間通信的有效地址空間大小不會超過文件大小及一個頁面大小的和。

3、文件一旦被映射後,調用mmap()的進程對返回地址的訪問是對某一內存區域的訪問,暫時脫離了磁盤上文件的影響。所有對mmap()返回地址空間的操作只在內存中有意義,只有在調用了munmap()後或者msync()時,才把內存中的相應內容寫回磁盤文件,所寫內容仍然不能超過文件的大小。

範例2:父子進程通過匿名映射實現共享內存

#include <sys/mman.h>

#include <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

typedef struct{

  char name[4];

  int  age;

}people;

main(int argc, char** argv)

{

  int i;

  people *p_map;

  char temp;

  p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,

       MAP_SHARED|MAP_ANONYMOUS,-1,0);

  if(fork() == 0)

  {

    sleep(2);

    for(i = 0;i<5;i++)

      printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age);

    (*p_map).age = 100;

    munmap(p_map,sizeof(people)*10); //實際上,進程終止時,會自動解除映射。

    exit();

  }

  temp = 'a';

  for(i = 0;i<5;i++)

  {

    temp += 1;

    memcpy((*(p_map+i)).name, &temp,2);

    (*(p_map+i)).age=20+i;

  }

  sleep(5);

  printf( "parent read: the first people,s age is %d\n",(*p_map).age );

  printf("umap\n");

  munmap( p_map,sizeof(people)*10 );

  printf( "umap ok\n" );

}

 

考察程序的輸出結果,體會父子進程匿名共享內存:

child read: the 1 people's age is 20

child read: the 2 people's age is 21

child read: the 3 people's age is 22

child read: the 4 people's age is 23

child read: the 5 people's age is 24

parent read: the first people,s age is 100

umap

umap ok

6.4. 對mmap()返回地址的訪問

前面對範例運行結構的討論中已經提到,linux採用的是頁式管理機制。對於用mmap()映射普通文件來說,進程會在自己的地址空間新增一塊空間,空間大小由mmap()的len參數指定,注意,進程並不一定能夠對全部新增空間都能進行有效訪問。進程能夠訪問的有效地址大小取決於文件被映射部分的大小。簡單的說,能夠容納文件被映射部分大小的最少頁面個數決定了進程從mmap()返回的地址開始,能夠有效訪問的地址空間大小。超過這個空間大小,內核會根據超過的嚴重程度返回發送不同的信號給進程。可用如下圖示說明:

 

注意:文件被映射部分而不是整個文件決定了進程能夠訪問的空間大小,另外,如果指定文件的偏移部分,一定要注意爲頁面大小的整數倍。下面是對進程映射地址空間的訪問範例:

#include <sys/mman.h>

#include <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

typedef struct{

       char name[4];

       int  age;

}people;

main(int argc, char** argv)

{

       int fd,i;

       int pagesize,offset;

       people *p_map;

 

       pagesize = sysconf(_SC_PAGESIZE);

       printf("pagesize is %d\n",pagesize);

       fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);

       lseek(fd,pagesize*2-100,SEEK_SET);

       write(fd,"",1);

       offset = 0;      //此處offset = 0編譯成版本1;offset = pagesize編譯成版本2

       p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);

       close(fd);

 

       for(i = 1; i<10; i++)

       {

              (*(p_map+pagesize/sizeof(people)*i-2)).age = 100;

              printf("access page %d over\n",i);

              (*(p_map+pagesize/sizeof(people)*i-1)).age = 100;

              printf("access page %d edge over, now begin to access page %d\n",i, i+1);

              (*(p_map+pagesize/sizeof(people)*i)).age = 100;

              printf("access page %d over\n",i+1);

       }

       munmap(p_map,sizeof(people)*10);

}

如程序中所註釋的那樣,把程序編譯成兩個版本,兩個版本主要體現在文件被映射部分的大小不同。文件的大小介於一個頁面與兩個頁面之間(大小爲:pagesize*2-99),版本1的被映射部分是整個文件,版本2的文件被映射部分是文件大小減去一個頁面後的剩餘部分,不到一個頁面大小(大小爲:pagesize-99)。程序中試圖訪問每一個頁面邊界,兩個版本都試圖在進程空間中映射pagesize*3的字節數。

版本1的輸出結果如下:

pagesize is 4096

access page 1 over

access page 1 edge over, now begin to access page 2

access page 2 over

access page 2 over

access page 2 edge over, now begin to access page 3

Bus error        //被映射文件在進程空間中覆蓋了兩個頁面,此時,進程試圖訪問第三個頁面

 

版本2的輸出結果如下:

pagesize is 4096

access page 1 over

access page 1 edge over, now begin to access page 2

Bus error        //被映射文件在進程空間中覆蓋了一個頁面,此時,進程試圖訪問第二個頁面

 

結論:採用系統調用mmap()實現進程間通信是很方便的,在應用層上接口非常簡潔。內部實現機制區涉及到了linux存儲管理以及文件系統等方面的內容,可以參考一下相關重要數據結構來加深理解。在本專題的後面部分,將介紹系統v共享內存的實現。

6.5. 參考資料

[1] Understanding the Linux Kernel, 2ndEdition, By Daniel P. Bovet, Marco Cesati , 對各主題闡述得重點突出,脈絡清晰。

[2] UNIX網絡編程第二卷:進程間通信,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。對mmap()有詳細闡述。

[3] Linux內核源代碼情景分析(上),毛德操、胡希明著,浙江大學出版社,給出了mmap()相關的源代碼分析。

[4]mmap()手冊

 

7. 共享內存(下)

簡介:在共享內存(上)中,主要圍繞着系統調用mmap()進行討論的,本部分將討論系統V共享內存,並通過實驗結果對比來闡述兩者的異同。系統V共享內存指的是把所有共享數據放在共享內存區域(IPC sharedmemory region),任何想要訪問該數據的進程都必須在本進程的地址空間新增一塊內存區域,用來映射存放共享數據的物理內存頁面。

 

系統調用mmap()通過映射一個普通文件實現共享內存。系統V則是通過映射特殊文件系統shm中的文件實現進程間的共享內存通信。也就是說,每個共享內存區域對應特殊文件系統shm中的一個文件(這是通過shmid_kernel結構聯繫起來的),後面還將闡述。

7.1. 系統V共享內存原理

進程間需要共享的數據被放在一個叫做IPC共享內存區域的地方,所有需要訪問該共享區域的進程都要把該共享區域映射到本進程的地址空間中去。系統V共享內存通過shmget獲得或創建一個IPC共享內存區域,並返回相應的標識符。內核在保證shmget獲得或創建一個共享內存區,初始化該共享內存區相應的shmid_kernel結構注同時,還將在特殊文件系統shm中,創建並打開一個同名文件,並在內存中建立起該文件的相應dentry及inode結構,新打開的文件不屬於任何一個進程(任何進程都可以訪問該共享內存區)。所有這一切都是系統調用shmget完成的。

注:每一個共享內存區都有一個控制結構struct shmid_kernel,shmid_kernel是共享內存區域中非常重要的一個數據結構,它是存儲管理和文件系統結合起來的橋樑,定義如下:

struct shmid_kernel /* private to the kernel */

{    

       struct kern_ipc_perm     shm_perm;

       struct file *            shm_file;

       int                  id;

       unsigned long         shm_nattch;

       unsigned long         shm_segsz;

       time_t                   shm_atim;

       time_t                   shm_dtim;

       time_t                   shm_ctim;

       pid_t                     shm_cprid;

       pid_t                     shm_lprid;

};

 

該結構中最重要的一個域應該是shm_file,它存儲了將被映射文件的地址。每個共享內存區對象都對應特殊文件系統shm中的一個文件,一般情況下,特殊文件系統shm中的文件是不能用read()、write()等方法訪問的,當採取共享內存的方式把其中的文件映射到進程地址空間後,可直接採用訪問內存的方式對其訪問。

這裏我們採用[1]中的圖表給出與系統V共享內存相關數據結構:

 

正如消息隊列和信號燈一樣,內核通過數據結構struct ipc_ids shm_ids維護系統中的所有共享內存區域。上圖中的shm_ids.entries變量指向一個ipc_id結構數組,而每個ipc_id結構數組中有個指向kern_ipc_perm結構的指針。到這裏讀者應該很熟悉了,對於系統V共享內存區來說,kern_ipc_perm的宿主是shmid_kernel結構,shmid_kernel是用來描述一個共享內存區域的,這樣內核就能夠控制系統中所有的共享區域。同時,在shmid_kernel結構的file類型指針shm_file指向文件系統shm中相應的文件,這樣,共享內存區域就與shm文件系統中的文件對應起來。

在創建了一個共享內存區域後,還要將它映射到進程地址空間,系統調用shmat()完成此項功能。由於在調用shmget()時,已經創建了文件系統shm中的一個同名文件與共享內存區域相對應,因此,調用shmat()的過程相當於映射文件系統shm中的同名文件過程,原理與mmap()大同小異。

7.2. 系統V共享內存API

對於系統V共享內存,主要有以下幾個API:shmget()、shmat()、shmdt()及shmctl()。

#include <sys/ipc.h>

#include <sys/shm.h>

shmget()用來獲得共享內存區域的ID,如果不存在指定的共享區域就創建相應的區域。shmat()把共享內存區域映射到調用進程的地址空間中去,這樣,進程就可以方便地對共享區域進行訪問操作。shmdt()調用用來解除進程對共享內存區域的映射。shmctl實現對共享內存區域的控制操作。這裏我們不對這些系統調用作具體的介紹,讀者可參考相應的手冊頁面,後面的範例中將給出它們的調用方法。

注:shmget的內部實現包含了許多重要的系統V共享內存機制;shmat在把共享內存區域映射到進程空間時,並不真正改變進程的頁表。當進程第一次訪問內存映射區域訪問時,會因爲沒有物理頁表的分配而導致一個缺頁異常,然後內核再根據相應的存儲管理機制爲共享內存映射區域分配相應的頁表。

7.3. 系統V共享內存限制

在/proc/sys/kernel/目錄下,記錄着系統V共享內存的一下限制,如一個共享內存區的最大字節數shmmax,系統範圍內最大共享內存區標識符數shmmni等,可以手工對其調整,但不推薦這樣做。

在[2]中,給出了這些限制的測試方法,不再贅述。

7.4. 系統V共享內存範例

本部分將給出系統V共享內存API的使用方法,並對比分析系統V共享內存機制與mmap()映射普通文件實現共享內存之間的差異,首先給出兩個進程通過系統V共享內存通信的範例:

/***** testwrite.c *******/

#include <sys/ipc.h>

#include <sys/shm.h>

#include <sys/types.h>

#include <unistd.h>

typedef struct{

       char name[4];

       int age;

} people;

main(int argc, char** argv)

{

       int shm_id,i;

       key_t key;

       char temp;

       people *p_map;

       char* name = "/dev/shm/myshm2";

       key = ftok(name,0);

       if(key==-1)

              perror("ftok error");

       shm_id=shmget(key,4096,IPC_CREAT);     

       if(shm_id==-1)

       {

              perror("shmget error");

              return;

       }

       p_map=(people*)shmat(shm_id,NULL,0);

       temp='a';

       for(i = 0;i<10;i++)

       {

              temp+=1;

              memcpy((*(p_map+i)).name,&temp,1);

              (*(p_map+i)).age=20+i;

       }

       if(shmdt(p_map)==-1)

              perror(" detach error ");

}

/********** testread.c ************/

#include <sys/ipc.h>

#include <sys/shm.h>

#include <sys/types.h>

#include <unistd.h>

typedef struct{

       char name[4];

       int age;

} people;

main(int argc, char** argv)

{

       int shm_id,i;

       key_t key;

       people *p_map;

       char* name = "/dev/shm/myshm2";

       key = ftok(name,0);

       if(key == -1)

              perror("ftok error");

       shm_id = shmget(key,4096,IPC_CREAT);   

       if(shm_id == -1)

       {

              perror("shmget error");

              return;

       }

       p_map = (people*)shmat(shm_id,NULL,0);

       for(i = 0;i<10;i++)

       {

       printf( "name:%s\n",(*(p_map+i)).name );

       printf( "age %d\n",(*(p_map+i)).age );

       }

       if(shmdt(p_map) == -1)

              perror(" detach error ");

}

testwrite.c創建一個系統V共享內存區,並在其中寫入格式化數據;testread.c訪問同一個系統V共享內存區,讀出其中的格式化數據。分別把兩個程序編譯爲testwrite及testread,先後執行./testwrite及./testread 則./testread輸出結果如下:

name: b   age 20;    name: c   age 21;    name: d   age 22;    name: e   age 23;    name: f    age 24;

name: g   age 25;    name: h   age 26;    name: I    age 27;    name: j    age 28;    name: k   age 29;

通過對試驗結果分析,對比系統V與mmap()映射普通文件實現共享內存通信,可以得出如下結論:

1、系統V共享內存中的數據,從來不寫入到實際磁盤文件中去;而通過mmap()映射普通文件實現的共享內存通信可以指定何時將數據寫入磁盤文件中。注:前面講到,系統V共享內存機制實際是通過映射特殊文件系統shm中的文件實現的,文件系統shm的安裝點在交換分區上,系統重新引導後,所有的內容都丟失。

2、系統V共享內存是隨內核持續的,即使所有訪問共享內存的進程都已經正常終止,共享內存區仍然存在(除非顯式刪除共享內存),在內核重新引導之前,對該共享內存區域的任何改寫操作都將一直保留。

3、通過調用mmap()映射普通文件進行進程間通信時,一定要注意考慮進程何時終止對通信的影響。而通過系統V共享內存實現通信的進程則不然。 注:這裏沒有給出shmctl的使用範例,原理與消息隊列大同小異。

7.5. 結論

共享內存允許兩個或多個進程共享一給定的存儲區,因爲數據不需要來回複製,所以是最快的一種進程間通信機制。共享內存可以通過mmap()映射普通文件(特殊情況下還可以採用匿名映射)機制實現,也可以通過系統V共享內存機制實現。應用接口和原理很簡單,內部機制複雜。爲了實現更安全通信,往往還與信號燈等同步機制共同使用。

共享內存涉及到了存儲管理以及文件系統等方面的知識,深入理解其內部機制有一定的難度,關鍵還要緊緊抓住內核使用的重要數據結構。系統V共享內存是以文件的形式組織在特殊文件系統shm中的。通過shmget可以創建或獲得共享內存的標識符。取得共享內存標識符後,要通過shmat將這個內存區映射到本進程的虛擬地址空間。

7.6. 參考資料

[1] Understanding the Linux Kernel, 2ndEdition, By Daniel P. Bovet, Marco Cesati , 對各主題闡述得重點突出,脈絡清晰。

[2] UNIX網絡編程第二卷:進程間通信,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。對mmap()有詳細闡述。

[3] Linux內核源代碼情景分析(上),毛德操、胡希明著,浙江大學出版社,給出了mmap()相關的源代碼分析。

[4]shmget、shmat、shmctl、shmdt手冊

 

8. 套接口

簡介: 在本專題的前面幾個部分,如消息隊列、信號燈、共享內存等,都是基於SysV的IPC機制進行討論的,它們的應用侷限在單一計算機內的進程間通信;基於BSD套接口不僅可以實現單機內的進程間通信,還可以實現不同計算機進程之間的通信。本文將主要介紹BSD套接口(sockets),以及基於套接口的重要而基本的API。

一個套接口可以看作是進程間通信的端點(endpoint),每個套接口的名字都是唯一的(唯一的含義是不言而喻的),其他進程可以發現、連接並且與之通信。通信域用來說明套接口通信的協議,不同的通信域有不同的通信協議以及套接口的地址結構等等,因此,創建一個套接口時,要指明它的通信域。比較常見的是unix域套接口(採用套接口機制實現單機內的進程間通信)及網際通信域。

8.1. 背景知識

linux目前的網絡內核代碼主要基於伯克利的BSD的unix實現,整個結構採用的是一種面向對象的分層機制。層與層之間有嚴格的接口定義。這裏我們引用[1]中的一個圖表來描述linux支持的一些通信協議:

 

我們這裏只關心IPS,即因特網協議族,也就是通常所說的TCP/IP網絡。我們這裏假設讀者具有網絡方面的一些背景知識,如瞭解網絡的分層結構,通常所說的7層結構;瞭解IP地址以及路由的一些基本知識。

目前linux網絡API是基於BSD套接口的(系統V提供基於流I/O子系統的用戶接口,但是linux內核目前不支持流I/O子系統)。套接口可以說是網絡編程中一個非常重要的概念,linux以文件的形式實現套接口,與套接口相應的文件屬於sockfs特殊文件系統,創建一個套接口就是在sockfs中創建一個特殊文件,並建立起爲實現套接口功能的相關數據結構。換句話說,對每一個新創建的BSD套接口,linux內核都將在sockfs特殊文件系統中創建一個新的inode。描述套接口的數據結構是socket,將在後面給出。

8.2. 重要數據結構

下面是在網絡編程中比較重要的幾個數據結構,讀者可以在後面介紹編程API部分再回過頭來了解它們。

(1)表示套接口的數據結構struct socket

套接口是由socket數據結構代表的,形式如下: 

struct socket

{

socket_state  state;     /* 指明套接口的連接狀態,一個套接口的連接狀態可以有以下幾種

套接口是空閒的,還沒有進行相應的端口及地址的綁定;還沒有連接;正在連接中;已經連接;正在解除連接。 */

  unsigned long    flags;

  struct proto_ops  ops;  /* 指明可對套接口進行的各種操作 */

  struct inode    inode;    /* 指向sockfs文件系統中的相應inode */

  struct fasync_struct  *fasync_list;  /* Asynchronous wake up list  */

  struct file    *file;          /* 指向sockfs文件系統中的相應文件  */

struct sock    sk;  /* 任何協議族都有其特定的套接口特性,該域就指向特定協議族的套接口對

象。 */

  wait_queue_head_t  wait;

  short      type;

  unsigned char    passcred;

};

 

(2)描述套接口通用地址的數據結構struct sockaddr

由於歷史的緣故,在bind、connect等系統調用中,特定於協議的套接口地址結構指針都要強制轉換成該通用的套接口地址結構指針。結構形式如下: 

struct sockaddr {

       sa_family_t     sa_family;       /* address family, AF_xxx     */

       char        sa_data[14];    /* 14 bytes of protocol address     */

};

 

(3)描述因特網地址結構的數據結構struct sockaddr_in(這裏侷限於IP4):

struct sockaddr_in

  {

    __SOCKADDR_COMMON (sin_);      /* 描述協議族 */

    in_port_t sin_port;                /* 端口號 */

    struct in_addr sin_addr;        /* 因特網地址 */

    /* Pad to size of `struct sockaddr'.  */

    unsigned char sin_zero[sizeof (struct sockaddr) -

                        __SOCKADDR_COMMON_SIZE -

                        sizeof (in_port_t) -

                        sizeof (struct in_addr)];

  };

 

一般來說,讀者最關心的是前三個域,即通信協議、端口號及地址。

8.3. 套接口編程的幾個重要步驟

(1)創建套接口,由系統調用socket實現:

int socket( int domain, int type, int ptotocol);

 

參數domain指明通信域,如PF_UNIX(unix域),PF_INET(IPv4),PF_INET6(IPv6)等;type指明通信類型,如SOCK_STREAM(面向連接方式)、SOCK_DGRAM(非面向連接方式)等。一般來說,參數protocol可設置爲0,除非用在原始套接口上(原始套接口有一些特殊功能,後面還將介紹)。

注:socket()系統調用爲套接口在sockfs文件系統中分配一個新的文件和dentry對象,並通過文件描述符把它們與調用進程聯繫起來。進程可以像訪問一個已經打開的文件一樣訪問套接口在sockfs中的對應文件。但進程絕不能調用open()來訪問該文件(sockfs文件系統沒有可視安裝點,其中的文件永遠不會出現在系統目錄樹上),當套接口被關閉時,內核會自動刪除sockfs中的inodes。

(2)綁定地址

根據傳輸層協議(TCP、UDP)的不同,客戶機及服務器的處理方式也有很大不同。但是,不管通信雙方使用何種傳輸協議,都需要一種標識自己的機制。

通信雙方一般由兩個方面標識:地址和端口號(通常,一個IP地址和一個端口號常常被稱爲一個套接口)。根據地址可以尋址到主機,根據端口號則可以尋址到主機提供特定服務的進程,實際上,一個特定的端口號代表了一個提供特定服務的進程。

對於使用TCP傳輸協議通信方式來說,通信雙方需要給自己綁定一個唯一標識自己的套接口,以便建立連接;對於使用UDP傳輸協議,只需要服務器綁定一個標識自己的套接口就可以了,用戶則不需要綁定(在需要時,如調用connect時[注1],內核會自動分配一個本地地址和本地端口號)。綁定操作由系統調用bind()完成:

int bind( int sockfd, const struct sockaddr * my_addr, socklen_t my_addr_len)

 

第二個參數對於Ipv4來說,實際上需要填充的結構是struct sockaddr_in,前面已經介紹了該結構。這裏只想強調該結構的第一個域,它表明該套接口使用的通信協議,如AF_INET。聯繫socket系統調用的第一個參數,讀者可能會想到PF_INET與AF_INET究竟有什麼不同?實際上,原來的想法是每個通信域(如PF_INET)可能對應多個協議(如AF_INET),而事實上支持多個協議的通信域一直沒有實現。因此,在linux內核中,AF_***與PF_***被定義爲同一個常數,因此,在編程時可以不加區分地使用他們。

注1:在採用非面向連接通信方式時,也會用到connect()調用,不過與在面向連接中的connect()調用有本質的區別:在非面向連接通信中,connect調用只是先設置一下對方的地址,內核爲本地套接口記下對方的地址,然後採用send()來發送數據,這樣避免每次發送時都要提供相同的目的地址。其中的connect()調用不涉及握手過程;而在面向連接的通信方式中,connect()要完成一個嚴格的握手過程。

(3)請求建立連接(由TCP客戶發起)

對於採用面向連接的傳輸協議TCP實現通信來說,一個比較重要的步驟就是通信雙方建立連接(如果採用udp傳輸協議則不需要),由系統調用connect()完成:

int connect( int sockfd, const struct sockaddr * servaddr, socklen_t addrlen)

第一個參數爲本地調用socket後返回的描述符,第二個參數爲服務器的地址結構指針。connect()向指定的套接口請求建立連接。

注:與connect()相對應,在服務器端,通過系統調用listen(),指定服務器端的套接口爲監聽套接口,監聽每一個向服務器套接口發出的連接請求,並通過握手機制建立連接。內核爲listen()維護兩個隊列:已完成連接隊列和未完成連接隊列。

(4)接受連接請求(由TCP服務器端發起)

服務器端通過監聽套接口,爲所有連接請求建立了兩個隊列:已完成連接隊列和未完成連接隊列(每個監聽套接口都對應這樣兩個隊列,當然,一般服務器只有一個監聽套接口)。通過accept()調用,服務器將在監聽套接口的已連接隊列頭中,返回用於代表當前連接的套接口描述字。

int accept( int sockfd, struct sockaddr * cliaddr, socklen_t * addrlen)

第一個參數指明哪個監聽套接口,一般是由listen()系統調用指定的(由於每個監聽套接口都對應已連接和未連接兩個隊列,因此它的內部機制實質是通過sockfd指定在哪個已連接隊列頭中返回一個用於當前客戶的連接,如果相應的已連接隊列爲空,accept進入睡眠)。第二個參數指明客戶的地址結構,如果對客戶的身份不感興趣,可指定其爲空。

注:對於採用TCP傳輸協議進行通信的服務器和客戶機來說,一定要經過客戶請求建立連接,服務器接受連接請求這一過程;而對採用UDP傳輸協議的通信雙方則不需要這一步驟。

(5)通信

客戶機可以通過套接口接收服務器傳過來的數據,也可以通過套接口向服務器發送數據。前面所有的準備工作(創建套接口、綁定等操作)都是爲這一步驟準備的。

常用的從套接口中接收數據的調用有:recv、recvfrom、recvmsg等,常用的向套接口中發送數據的調用有send、sendto、sendmsg等。

int recv(int s, void *

        buf, size_t

        len, int

        flags)

int recvfrom(int s,  void *

        buf,  size_t

        len, int

        flags, struct sockaddr *

        from, socklen_t *

        fromlen)

int recvmsg(int s, struct msghdr *

        msg, int

        flags)

int send(int s,const void *

        msg, size_t

        len, int

        flags)

int sendto(int s, const void *

        msg, size_t

        len, int

        flags const struct sockaddr *

        to, socklen_t

        tolen)

int sendmsg(int s, const struct msghdr *

        msg, int

        flags)

 

這裏不再對這些調用作具體的說明,只想強調一下,recvfrom()以及recvmsg()可用於面向連接的套接口,也可用於面向非連接的套接口;而recv()一般用於面向連接的套接口。另外,在調用了connect()之後,就應給調用send()而不是sendto()了,因爲調用了connect之後,目標就已經確定了。

前面講到,socket()系統調用返回套接口描述字,實際上它是一個文件描述符。所以,可以對套接口進行通常的讀寫操作,即使用read()及write()方法。在實際應用中,由於面向連接的通信(採用TCP傳輸協議)是可靠的,同時又保證字節流原有的順序,所以更適合用read及write方法。而非面向連接的通信(採用UDP傳輸協議)是不可靠的,字節流也不一定保持原有的順序,所以一般不宜用read及write方法。

(6)通信的最後一步是關閉套接口

由close()來完成此項功能,它唯一的參數是套接口描述字,不再贅述。

8.4. 典型調用代碼

到處可以發現基於套接口的客戶機及服務器程序,這裏不再給出完整的範例代碼,只是給出它們的典型調用代碼,並給出簡要說明。

(1)典型的TCP服務器代碼:

... ...

int listen_fd, connect_fd;

struct sockaddr_in serv_addr, client_addr;

... ...

listen_fd = socket ( PF_INET, SOCK_STREAM, 0 );

 

/* 創建網際Ipv4域的(由PF_INET指定)面向連接的(由SOCK_STREAM指定,

如果創建非面向連接的套接口則指定爲SOCK_DGRAM)

的套接口。第三個參數0表示由內核確定缺省的傳輸協議,

對於本例,由於創建的是可靠的面向連接的基於流的套接口,

內核將選擇TCP作爲本套接口的傳輸協議) */

 

bzero( &serv_addr, sizeof(serv_addr) );

serv_addr.sin_family = AF_INET ;  /* 指明通信協議族 */

serv_addr.sin_port = htons( 49152 ) ;       /* 分配端口號 */

inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ;

/* 分配地址,把點分十進制IPv4地址轉化爲32位二進制Ipv4地址。 */

bind( listen_fd, (struct sockaddr*) serv_addr, sizeof ( struct sockaddr_in )) ;

/* 實現綁定操作 */

listen( listen_fd, max_num) ;

/* 套接口進入偵聽狀態,max_num規定了內核爲此套接口排隊的最大連接個數 */

for( ; ; ) {

... ...

connect_fd = accept( listen_fd, (struct sockaddr*)client_addr, &len ) ; /* 獲得連接fd. */

... ...                                   /* 發送和接收數據 */

}

注:端口號的分配是有一些慣例的,不同的端口號對應不同的服務或進程。比如一般都把端口號21分配給FTP服務器的TCP/IP實現。端口號一般分爲3段,0-1023(受限的衆所周知的端口,由分配數值的權威機構IANA管理),1024-49151(可以從IANA那裏申請註冊的端口),49152-65535(臨時端口,這就是爲什麼代碼中的端口號爲49152)。

對於多字節整數在內存中有兩種存儲方式:一種是低字節在前,高字節在後,這樣的存儲順序被稱爲低端字節序(little-endian);高字節在前,低字節在後的存儲順序則被稱爲高端字節序(big-endian)。網絡協議在處理多字節整數時,採用的是高端字節序,而不同的主機可能採用不同的字節序。因此在編程時一定要考慮主機字節序與網絡字節序間的相互轉換。這就是程序中使用htons函數的原因,它返回網絡字節序的整數。

(2)典型的TCP客戶代碼:

... ...

int socket_fd;

struct sockaddr_in serv_addr ;

... ...

socket_fd = socket ( PF_INET, SOCK_STREAM, 0 );

bzero( &serv_addr, sizeof(serv_addr) );

serv_addr.sin_family = AF_INET ;  /* 指明通信協議族 */

serv_addr.sin_port = htons( 49152 ) ;       /* 分配端口號 */

inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ;

/* 分配地址,把點分十進制IPv4地址轉化爲32位二進制Ipv4地址。 */

connect( socket_fd, (struct sockaddr*)serv_addr, sizeof( serv_addr ) ) ; /* 向服務器發起連接請求 */

... ...                                                 /* 發送和接收數據 */

... ...

 

對比兩段代碼可以看出,許多調用是服務器或客戶機所特有的。另外,對於非面向連接的傳輸協議,代碼還有簡單些,沒有連接的發起請求和接收請求部分。

8.5. 網絡編程中的其他重要概念

下面列出了網絡編程中的其他重要概念,基本上都是給出這些概念能夠實現的功能,讀者在編程過程中如果需要這些功能,可查閱相關概念。

(1)、I/O複用的概念

I/O複用提供一種能力,這種能力使得當一個I/O條件滿足時,進程能夠及時得到這個信息。I/O複用一般應用在進程需要處理多個描述字的場合。它的一個優勢在於,進程不是阻塞在真正的I/O調用上,而是阻塞在select()調用上,select()可以同時處理多個描述字,如果它所處理的所有描述字的I/O都沒有處於準備好的狀態,那麼將阻塞;如果有一個或多個描述字I/O處於準備好狀態,則select()不阻塞,同時會根據準備好的特定描述字採取相應的I/O操作。

(2)、Unix通信域

前面主要介紹的是PF_INET通信域,實現網際間的進程間通信。基於Unix通信域(調用socket時指定通信域爲PF_LOCAL即可)的套接口可以實現單機之間的進程間通信。採用Unix通信域套接口有幾個好處:Unix通信域套接口通常是TCP套接口速度的兩倍;另一個好處是,通過Unix通信域套接口可以實現在進程間傳遞描述字。所有可用描述字描述的對象,如文件、管道、有名管道及套接口等,在我們以某種方式得到該對象的描述字後,都可以通過基於Unix域的套接口來實現對描述字的傳遞。接收進程收到的描述字值不一定與發送進程傳遞的值一致(描述字是特定於進程的),但是特們指向內核文件表中相同的項。

(3)、原始套接口

原始套接口提供一般套接口所不提供的功能: 

  • 原始套接口可以讀寫一些用於控制的控制協議分組,如ICMPv4等,進而可實現一些特殊功能。
  • 原始套接口可以讀寫特殊的IPv4數據包。內核一般只處理幾個特定協議字段的數據包,那麼一些需要不同協議字段的數據包就需要通過原始套接口對其進行讀寫;
  • 通過原始套接口可以構造自己的Ipv4頭部,也是比較有意思的一點。

創建原始套接口需要root權限。

(4)、對數據鏈路層的訪問

對數據鏈路層的訪問,使得用戶可以偵聽本地電纜上的所有分組,而不需要使用任何特殊的硬件設備,在linux下讀取數據鏈路層分組需要創建SOCK_PACKET類型的套接口,並需要有root權限。

(5)、帶外數據(out-of-band data)

如果有一些重要信息要立刻通過套接口發送(不經過排隊),請查閱與帶外數據相關的文獻。

(6)、多播

linux內核支持多播,但是在默認狀態下,多數linux系統都關閉了對多播的支持。因此,爲了實現多播,可能需要重新配置並編譯內核。具體請參考[4]及[2]。

結論:linux套接口編程的內容可以說是極大豐富,同時它涉及到許多的網絡背景知識,有興趣的讀者可在[2]中找到比較系統而全面的介紹。

至此,本專題系列(linux環境進程間通信)全部結束了。實際上,進程間通信的一般意義通常指的是消息隊列、信號燈和共享內存,可以是posix的,也可以是SYS v的。本系列同時介紹了管道、有名管道、信號以及套接口等,是更爲一般意義上的進程間通信機制。

8.6. 參考資料

  • Understanding the Linux Kernel, 2nd Edition, By Daniel P. Bovet, Marco Cesati , 對各主題闡述得重點突出,脈絡清晰。網絡部分分析集中在TCP/IP協議棧的數據連路層、網絡層以及傳輸層。
  • UNIX網絡編程第一卷:套接口API和X/Open傳輸接口API,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。不僅對套接口網絡編程有極好的描述,而且極爲詳盡的闡述了相關的網絡背景知識。不論是入門還是深入研究,都是不可多得的好資料。
  • Linux內核源代碼情景分析(下),毛德操、胡希明著,浙江大學出版社,給出了unix域套接口部分的內核代碼分析。
  • GNU/Linux編程指南,入門、應用、精通,第二版,Kurt Wall等著,張輝譯
發佈了1 篇原創文章 · 獲贊 9 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章