2二、進程間通訊

 

 

  進程間通訊(Interprocess Communication,IPC)是一個描述兩個進程彼此交換信息的通用術語。通常狀況下,通訊的兩個進程便可以運行在同一臺機器上,也能夠運行在不一樣的機器上。進程間的通訊是數據的交換,兩個或多個進程合做處理數據或同步信息,以幫助兩個彼此獨立但相關聯的進程調度工做,避免重複工做。進程間通訊方式有不少種,好比可使用socket、使用管道、消息隊列、文件、共享內存等。php

一、管道

   管道是進程間通訊中最古老的方式,他使得數據以一種數據流的方式在多個進程之間流動。管道至關於文件系統上的一個文件,用來緩存所要傳輸的數據,可是在某些特性上又不一樣於文件,例如,當數據讀出後,管道中的數據就沒有了, 單文件就沒有這個特性。綜合來講,管道具備如下特色:html

1) 管道是半雙工的,數據只能向一個方向流動;須要雙方通訊時,須要創建起兩個管道node

2) 匿名管道只能用於父子進程或者兄弟進程之間(具備親緣關係的進程);linux

3) 單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,而且只存在與內存中。程序員

 

管道分爲pipe(無名管道)和fifo(命名管道)兩種,除了創建、打開、刪除的方式不一樣外,這兩種管道幾乎是同樣的。他們都是經過內核緩衝區實現數據傳輸。前者用於父進程和子進程間的通訊,後者用於運行於同一臺機器上的任意兩個進程間的通訊編程

  • pipe用於相關進程之間的通訊,例如父進程和子進程,它經過pipe()系統調用來建立並打開,當最後一個使用它的進程關閉對他的引用時,pipe將自動撤銷。
  • FIFO即命名管道,在磁盤上有對應的節點,但沒有數據塊——換言之,只是擁有一個名字和相應的訪問權限,經過mknode()系統調用或者mkfifo()函數來創建的。一旦創建,任何進程均可以經過文件名將其打開和進行讀寫,而不侷限於父子進程,固然前提是進程對FIFO有適當的訪問權。當再也不被進程使用時,FIFO在內存中釋放,但磁盤節點仍然存在。

pipe(無名管道)

   Linux下使用pipe()建立一個匿名半雙工管道,其函數原型以下:數組

1 #include <unistd.h>
2 int pipe(int fd[2]);    // 返回值:若成功返回0,失敗返回-1

  參數fd是一個長度爲2的文件描述符數組,fd[0]是讀出端,fd[1]是寫入端,函數返回0表示成功,返回-1則表示失敗。緩存

  但函數返回成功,則代表自動維護了一個從fd[1]到fd[0]的數據通道。session

  

要關閉管道只需將這兩個文件描述符關閉便可。數據結構

  單獨操做一個進程管道是沒有意義的,管道的應用通常體如今父子進程或者兄弟進程之間的通訊上。若是要創建一個父進程到子進程的數據通道,須要先調用函數pipe(),緊接着調用函數fork(),因爲子程序自動繼承父進程的數據段,則子進程同時擁有管道的操做權,此時管道的方向取決於用戶怎麼維護該管道。
  當用戶想要一個父進程的數據管道是,須要先在父進程中關閉管道的獨處端,而後相應的在子進程中關閉管道的輸出端,相反,當維護子進程到父進程的數據通道時,則須要在父進程中關閉輸出端,在子進程中關閉讀入端便可。總之,使用函數pipe()和fork()建立子進程,維護父子進程中管道的數據方法是:在父進程中向子進程發送消息,在子進程接受消息。

若要數據流從父進程流向子進程,則關閉父進程的讀端(fd[0])與子進程的寫端(fd[1]);反之,則可使數據流從子進程流向父進程。

#include<stdio.h>
#include<unistd.h>

int main()
{
    int fd[2];  // 兩個文件描述符
    pid_t pid;
    char buff[20];

    if(pipe(fd) < 0)  // 建立管道
        printf("Create Pipe Error!\n");

    if((pid = fork()) < 0)  // 建立子進程
        printf("Fork Error!\n");
    else if(pid > 0)  // 父進程
    {
        close(fd[0]); // 關閉讀端
        write(fd[1], "hello world\n", 12);
    }
    else
    {
        close(fd[1]); // 關閉寫端
        read(fd[0], buff, 20);
        printf("%s", buff);
    }

    return 0;
}

程序運行結果以下:

FIFO(有名管道)

   FIFO(First Input First Output)是一種文件類型,在文件系統中能夠看到。經過FIFO,不相關的進程也能交換數據。

   FIFO的通訊方式相似於在進程中使用文件類傳輸數據,只不過FIFO類型的文件同時具備管道的特性,在數據讀出時,FIFO中同時清除了數據。

  建立FIFO相似於建立文件,FIFO就像普通文件同樣,也能夠經過路徑名進行訪問。Linux系統提供了函數mkfifo(),用於建立FIFO,函數原型以下:

#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
int mkfifoat(int dirfd, const char *pathname, mode_t mode);

  函數mkfifo()中參數mode的規格說明與函數open()中的參數mode的規格說明相同。函數mkfifoat()與函數mkfifo()類似,可是函數mkfifoat()能夠被用來在文件描述符dirfd表示的目錄相關位置建立一個有名管道,有如下三種情形:

   (1)若是參數pathname指定的是絕對路徑名,則參數dirfd會被忽略,而且函數mikfifoat()的行爲和函數mkfifo()的行爲相似。

  (2)若是參數pathname指定是相對相對路徑名,則參數dirfd是一個打開目錄的有效文件描述符,路徑名和目錄相關。

   (3)若是參數pathname 制定的是相對路徑名,則參數dirfd是一個特殊值AT_FDCWD,則路徑名以當前目錄開始,函數mkfifoat()於mkfifo()相似。

  當使用函數open()打開一個有名管道時,非阻塞標識(O_NONBLOCK)會產生下列影響:

  (1)在通常狀況下(沒有制定O_NONBLOCK),只讀open()要阻塞到某個進程爲寫而打開這個FIFO爲止;相似的,只寫open()要阻塞到某個其餘進程爲讀而打開它爲止。

  (2)若是指定了O_NONBLOCK,則只讀當即返回;可是若是沒有進程爲讀而打開一個FIFO,那麼只寫open()將返回-1,同時errno設置爲ENXIO。

  相似於管道,若寫一個尚無進程爲讀而打開的FIFO,將產生信號SIGPIPE,若某個FIFO的最後一個寫進程關閉了該FIFO,則將爲該FIFO的讀進程將產生一個文件結束標誌。

示例:

編寫fifo_write.c和fifo_read.c以下

fifo_write.c

#include<stdio.h>
#include<stdlib.h>   // exit
#include<fcntl.h>    // O_WRONLY
#include<sys/stat.h>
#include<time.h>     // time

int main()
{
    int fd;
    int n, i;
    char buf[1024];
    time_t tp;

    printf("I am %d process.\n", getpid()); // 說明進程ID
    
    if((fd = open("fifo1", O_WRONLY)) < 0) // 以寫打開一個FIFO 
    {
        perror("Open FIFO Failed");
        exit(1);
    }

    for(i=0; i<10; ++i)
    {
        time(&tp);  // 取系統當前時間
        n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
        printf("Send message: %s", buf); // 打印
        if(write(fd, buf, n+1) < 0)  // 寫入到FIFO中
        {
            perror("Write FIFO Failed");
            close(fd);
            exit(1);
        }
        sleep(1);  // 休眠1秒
    }

    close(fd);  // 關閉FIFO文件
    return 0;
}

fifo_read.c

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>

int main()
{
    int fd;
    int len;
    char buf[1024];

    if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 建立FIFO管道
        perror("Create FIFO Failed");

    if((fd = open("fifo1", O_RDONLY)) < 0)  // 以讀打開FIFO
    {
        perror("Open FIFO Failed");
        exit(1);
    }
    
    while((len = read(fd, buf, 1024)) > 0) // 讀取FIFO管道
        printf("Read message: %s", buf);

    close(fd);  // 關閉FIFO文件
    return 0;
}

 建立一個空的fifo1的文件後,先運行fifo_write,再運行fifo_read可獲得以下運行結果

 

二、信號

   信號是Linux系統響應某些條件而產生的一個事件,是進程間通訊的經典方法。Linux有不少種信號,每一個信號都有一個名字,這些名字都以三個字符SIG開頭,經常使用的信號量以下表所示,可使用Shell命令kill -l查看當前系統提供的信號。 

Signal
Description
SIGABRT
由調用abort函數產生,進程非正常退出
SIGALRM
用alarm函數設置的timer超時或setitimer函數設置的interval timer超時
SIGBUS
某種特定的硬件異常,一般由內存訪問引發
SIGCANCEL
由Solaris Thread Library內部使用,一般不會使用
SIGCHLD
進程Terminate或Stop的時候,SIGCHLD會發送給它的父進程。缺省狀況下該Signal會被忽略
SIGCONT
當被stop的進程恢復運行的時候,自動發送
SIGEMT
和實現相關的硬件異常
SIGFPE
數學相關的異常,如被0除,浮點溢出,等等
SIGFREEZE
Solaris專用,Hiberate或者Suspended時候發送
SIGHUP
發送給具備Terminal的Controlling Process,當terminal被disconnect時候發送
SIGILL
非法指令異常
SIGINFO
BSD signal。由Status Key產生,一般是CTRL+T。發送給全部Foreground Group的進程
SIGINT
由Interrupt Key產生,一般是CTRL+C或者DELETE。發送給全部ForeGround Group的進程
SIGIO
異步IO事件
SIGIOT
實現相關的硬件異常,通常對應SIGABRT
SIGKILL
沒法處理和忽略。停止某個進程
SIGLWP
由Solaris Thread Libray內部使用
SIGPIPE
在reader停止以後寫Pipe的時候發送
SIGPOLL
當某個事件發送給Pollable Device的時候發送
SIGPROF
Setitimer指定的Profiling Interval Timer所產生
SIGPWR
和系統相關。和UPS相關。
SIGQUIT
輸入Quit Key的時候(CTRL+\)發送給全部Foreground Group的進程
SIGSEGV
非法內存訪問
SIGSTKFLT
Linux專用,數學協處理器的棧異常
SIGSTOP
停止進程。沒法處理和忽略。
SIGSYS
非法系統調用
SIGTERM
請求停止進程,kill命令缺省發送
SIGTHAW
Solaris專用,從Suspend恢復時候發送
SIGTRAP
實現相關的硬件異常。通常是調試異常
SIGTSTP
Suspend Key,通常是Ctrl+Z。發送給全部Foreground Group的進程
SIGTTIN
當Background Group的進程嘗試讀取Terminal的時候發送
SIGTTOU
當Background Group的進程嘗試寫Terminal的時候發送
SIGURG
當out-of-band data接收的時候可能發送
SIGUSR1
用戶自定義signal 1
SIGUSR2
用戶自定義signal 2
SIGVTALRM
setitimer函數設置的Virtual Interval Timer超時的時候
SIGWAITING
Solaris Thread Library內部實現專用
SIGWINCH
當Terminal的窗口大小改變的時候,發送給Foreground Group的全部進程
SIGXCPU
當CPU時間限制超時的時候
SIGXFSZ
進程超過文件大小限制
SIGXRES
Solaris專用,進程超過資源限制的時候發送

   當引起信號的時間發生時,爲進程產生一個信號,有如下兩種狀況:

 (1)硬件狀況:例如按下鍵盤或其餘硬件故障

  (2)軟件產生:例如除0操做或者執行kill()函數、raise()函數等。

   一個完整的信號週期包括信號的產生、信號在進程內的註冊與註銷以及執行信號處理的三個階段。進程收到信號後有三種處理方式:

  (1)捕捉信號:當信號發生時,進程可執行相應的自處理函數。

  (2)忽略信號:對該信號不作任何處理,但SIGKILL與SIGSTOP信號除外。

  (3)執行默認操做:Linux對每種信號都規定了默認操做。

下面是信號操做中經常使用的函數:

 函數signal

   函數signal()進行信號處理時,須要指出要處理的信號和處理函數信息,其函數原型以下:

#include <signal.h>

typedef void (*sighandler_t)(int)
   
sighandler_t signal(int signum,sighandler_t handler);

  函數執行成功,返回之前的信號處理配置或者處理函數;執行失敗返回SIGERR即-1。

   參數signum用於指定待響應的信號;參數handler爲信號處理函數,有如下三種狀況:

(1)SIG_IGN:忽略該信號。

(2)SIG_DFL:默認方式爲處理該信號。

(3)自定義信號處理函數指針,返回類型爲void。

例子:下面看一個簡單的捕捉SIGUSR1信號的處理函數。

 

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void sig_handler(int signo)
{
  if (signo == SIGINT)
    printf("received SIGINT\n");
}

int main(void)
{
  if (signal(SIGINT, sig_handler) == SIG_ERR)
  printf("\ncan't catch SIGINT\n");
  // A long long wait so that we can easily issue a signal to this process
  while(1) 
    sleep(1);
  return 0;
}

在上面的代碼中,咱們使用無限循環模擬了一個長時間運行的進程。函數sig_handler用做信號處理程序。經過在main()函數中將系統調用'signal'做爲第二個參數傳遞給內核,該函數被註冊到內核。函數'signal'的第一個參數是咱們但願信號處理程序處理的信號,在這種狀況下是SIGINT。

在其後,函數sleep(1)的使用有一個緣由。這個函數已經在while循環中使用,以便while循環在一段時間後執行(在這種狀況下,即1秒)。這變得很重要,不然無限循環運行可能會消耗大部分CPU,會使計算機很是慢。

當進程運行,信號SIGINT由按下Ctrl-C發出,信號SIGQUIT由按下Ctrl-發出,能夠獲得以下結果:

  須要提到的是signal函數是一個比較老的函數,在實際應用中應該避免使用該函數,而使用sigaction函數,後面將詳細介紹這個函數。

函數sigaction

  函數sigaction與函數signal功能相似,主要用於定義在接收到信號後應該採起的處理方式,其函數原型以下:

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函數用於改變進程接收到特定信號後的行爲。該函數的第一個參數爲信號的值,能夠爲除SIGKILL及SIGSTOP外的任何一個特定有效的信號(爲這兩個信號定義本身的處理函數,將致使信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指針,在結構sigaction的實例中,指定了對特定信號的處理,能夠爲空,進程會以缺省方式對信號處理;第三個參數oldact指向的對象用來保存返回的原來對相應信號的處理,可指定oldact爲NULL。若是把第2、第三個參數都設爲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;

}

其中,結構體的關鍵成員含義

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

二、由_sa_sigaction是指定的信號處理函數帶有三個參數,是爲實時信號而設的(固然一樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數爲信號值,第三個參數沒有使用,第二個參數是指向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{

                                                      ...

                                                 }...

                                               ... ...

                                         }

}

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

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

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

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

函數kill

  函數kill()用於向自身或其餘進程發送信號,函數原型以下:

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid,int signo)

  參數sig用於指定要發送的信號。參數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,並設置相應的錯誤代碼errno。下面是一些可能返回的錯誤代碼:

EINVAL:指定的信號sig無效。

ESRCH:參數pid指定的進程或進程組不存在。注意,在進程表項中存在的進程,多是一個尚未被wait收回,但已經終止執行的僵死進程。

EPERM: 進程沒有權力將這個信號發送到指定接收信號的進程。由於,一個進程被容許將信號發送到進程pid時,必須擁有root權力,或者是發出調用的進程的UID 或EUID與指定接收的進程的UID或保存用戶ID(savedset-user-ID)相同。若是參數pid小於-1,即該信號發送給一個組,則該錯誤表示組中有成員進程不能接收該信號。

函數raise

  函數raise()用於進程向自身發送信號,其函數原型以下:

#include <signal.h>

int raise(int signo)

參數signo爲即將發送的信號值。調用成功返回 0;不然,返回 -1。

函數pause

  函數pause()用於將調用進程掛起直至捕捉到信號爲止,一般用於判斷信號是否到達,其函數原型以下:

#include <unistd.h>

int pause(void);

函數sigqueue

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

#include <sys/types.h>

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

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指定的信息會拷貝到對應sig 註冊的3參數信號處理函數的siginfo_t結構中,這樣信號處理函數就能夠處理這些信息了。因爲sigqueue系統調用支持發送帶參數信號,因此比kill()系統調用的功能要靈活和強大得多。

函數alarm

  函數alarm()也稱爲鬧鐘函數,是專門爲信號SIGALARM而設的,用於在指定的時間項進程自己發送SIGALARM信號,其函數原型以下:

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

  若是指定的參數seconds爲0,則再也不發送 SIGALRM信號。後一次設定將取消前一次的設定。該調用返回值爲上次定時調用到發送之間剩餘的時間,或者由於沒有前一次定時調用而返回0。

函數setitimer

  如今的系統中不少程序再也不使用alarm調用,而是使用setitimer調用來設置定時器,用getitimer來獲得定時器的狀態,這兩個調用的聲明格式以下:

#include <sys/time.h>

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

 

該系統調用給進程提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就發送一個相應的信號給進程,並使得計時器從新開始。三個計時器由參數which指定,以下所示:

TIMER_REAL:按實際時間計時,計時到達將給進程發送SIGALRM信號。

ITIMER_VIRTUAL:僅當進程執行時才進行計時。計時到達將發送SIGVTALRM信號給進程。

ITIMER_PROF:當進程執行時和系統爲該進程執行動做時都計時。與ITIMER_VIR-TUAL是一對,該定時器常常用來統計進程在用戶態和內核態花費的時間。計時到達將發送SIGPROF信號給進程。

 

定時器中的參數value用來指明定時器的時間,其結構以下:

struct itimerval {

        struct timeval it_interval; /* 下一次的取值 */

        struct timeval it_value; /* 本次的設定值 */

};

該結構中timeval結構定義以下:

struct timeval {

        long tv_sec; /**/

        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

 

在setitimer 調用中,參數ovalue若是不爲空,則其中保留的是上次調用設定的值。定時器將it_value遞減到0時,產生一個信號,並將it_value的值設定爲it_interval的值,而後從新開始計時,如此往復。當it_value設定爲0時,計時器中止,或者當它計時到期,而it_interval 爲0時中止。調用成功時,返回0;錯誤時,返回-1,並設置相應的錯誤代碼errno:

EFAULT:參數value或ovalue是無效的指針。

EINVAL:參數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。

下面是關於setitimer調用的一個簡單示範,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM信號:

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/time.h>

int sec;

 

void sigroutine(int signo) {

        switch (signo) {

        case SIGALRM:

        printf("Catch a signal -- SIGALRM ");

        break;

        case SIGVTALRM:

        printf("Catch a signal -- SIGVTALRM ");

        break;

        }

        return;

}

 

int main()

{

        struct itimerval value,ovalue,value2;

        sec = 5;

 

        printf("process id is %d ",getpid());

        signal(SIGALRM, sigroutine);

        signal(SIGVTALRM, sigroutine);

 

        value.it_value.tv_sec = 1;

        value.it_value.tv_usec = 0;

        value.it_interval.tv_sec = 1;

        value.it_interval.tv_usec = 0;

        setitimer(ITIMER_REAL, &value, &ovalue);

 

        value2.it_value.tv_sec = 0;

        value2.it_value.tv_usec = 500000;

        value2.it_interval.tv_sec = 0;

        value2.it_interval.tv_usec = 500000;

        setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

 

        for (;;) ;

}

程序運行結果以下:

函數abort

向進程發送SIGABORT信號,默認狀況下進程會異常退出,固然可定義本身的信號處理函數。

#include <stdlib.h>

void abort(void);

即便SIGABORT被進程設置爲阻塞信號,調用abort()後,SIGABORT仍然能被進程接收。該函數無返回值。

 信號集及信號集操做函數:信號集是一個能表示多個信號的數據類型

   信號集就是用來放置多個信號,和select函數中的描述符集類似。系統也提供了一系列的信號集函數,這些函數原型以下:

#include <signal.h>

int sigemptyset(sigset_t *set);//清空信號集set

int sigfillset(sigset_t *set);//將全部信號填充到信號集set,set指向的信號集中將包含linux支持的64種信號;

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

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

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

 

函數sigprocmask

   sigprocmask函數能夠檢測或更改(或二者)進程的信號屏蔽字,函數原型以下:

int sigpromask(int how,const sigset_t* set,sigset_t* oset);

  參數oset,輸出參數,若非空,則返回進程的當前屏蔽字。

  參數set,輸入參數,若非空,則表示須要修改的信號屏蔽字。

  參數how,輸入參數,表示以何種方式修改當前信號屏蔽字。若是set爲空,則how無心義。

 參數how的取值有:

(1)SIGBLOCK  該進程新的信號屏蔽字是其當前信號屏蔽字和set指向信號集的並集。set包含了咱們但願阻塞的附加信號。

(2)SIGUBLOCK  該進程新的心啊後屏蔽字是當前信號除去set所指向的信號集。set包含了咱們但願解除阻塞的信號。

(3)SIGSETMASK   賦值操做,該進程新的信號屏蔽字是set指向的值。

函數sigsuspend

  sigsuspend函數就是在捕捉一個信號或發生了一個會終止該進程的信號以前,將進程投入睡眠,直到該信號來到並從信號處理函數中返回。sigsuspend函數原型以下:

int sigsuspend(const sigset_t *mask));

  參數sigmask,將進程的信號屏蔽字設置爲sigmask,也就是說進程會在睡眠後的信號屏蔽字。

  所以在使用sigsuspend函數時,當該函數返回後,應該將進程原來的屏蔽字再從新設置回去。

函數sigpending

   sigpending函數返回在送往進程的時候被阻塞掛起的信號集合。函數原型爲:

int sigpending(sigset_t *set)

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

 

 

經過一個實例進行理解

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void print_sigset(sigset_t *set)
{
  int i;
  for(i=1;i<64;++i)
  {
    if(sigismember(set,i))
    {
      printf("1");
    }else{
      printf("0");
    }
  }
}

int main()
{
  sigset_t myset;
  sigemptyset(&myset);//清空信號集
  sigaddset(&myset,SIGINT);//向信號集添加
  sigaddset(&myset,SIGQUIT);
  sigaddset(&myset,SIGUSR1);
  print_sigset(&myset);
  return 0;
}

 信號集運行結果如圖

 

 

三、消息隊列

   消息隊列是消息的連接表,存放在內核中並由消息隊列標識符標識。消息隊列與FIFO有許多類似之處,可是少了管道打開文件和關閉文件的麻煩。它可用於不一樣進程間的通訊,可是其重點仍是線程之間的一種通訊方式。如今首先詳細對進程間的通訊進行講解。

一、msgget()

 msgget用來建立和訪問一個消息隊列,函數原型以下:

#include <sys/types.h>
#include <sys/ipc.h> #include <sys/msg.h> int msgget ( key_t key , int msgflg );

 

與其餘的IPC機制同樣,程序必須提供一個鍵來命名某個特定的消息隊列。msgflg是一個權限標誌,表示消息隊列的訪問權限,它與文件的訪問權限同樣。msgflg能夠與IPC_CREAT作或操做,表示當key所命名的消息隊列不存在時建立一個消息隊列,若是key所命名的消息隊列存在時,IPC_CREAT標誌會被忽略,而只返回一個標識符。

它返回一個以key命名的消息隊列的標識符(非零整數),失敗時返回-1.

二、msgsnd()

  該函數用來把消息添加到消息隊列中。它的原型爲:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>


int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

msgid是由msgget函數返回的消息隊列標識符。

msg_ptr是一個指向準備發送消息的指針,可是消息的數據結構卻有必定的要求,指針msg_ptr所指向的消息結構必定要是以一個長整型成員變量開始的結構體,接收函數將用這個成員來肯定消息的類型。因此消息結構要定義成這樣: 

struct my_message {
    long int message_type;
    /* The data you wish to transfer */
};

msg_sz 是msg_ptr指向的消息的長度,注意是消息的長度,而不是整個結構體的長度,也就是說msg_sz是不包括長整型消息類型成員變量的長度。

msgflg 用於控制當前消息隊列滿或隊列消息到達系統範圍的限制時將要發生的事情。

若是調用成功,消息數據的一分副本將被放到消息隊列中,並返回0,失敗時返回-1.

三、msgrcv()

  從消息隊列中讀取以及刪除一條消息,並將內容複製進MSGP指向的緩衝區中,其函數原型以下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int  msgrcv( int  msgid , struct   msgbuf*  msgp ,  int msgsz ,  long msgtyp, int msgflg);

成功時返回所獲取信息的長度,失敗返回-1,錯誤信息存於error 

 

msgid, msg_ptr, msg_st 的做用也函數msgsnd()函數的同樣。

msgtyp: 信息類型。 取值以下: 
msgtyp = 0 ,不分類型,直接返回消息隊列中的第一項 。
msgtyp > 0 ,返回第一項 msgtyp與 msgbuf結構體中的mtype相同的信息 。
msgtyp <0 , 返回第一項 mtype小於等於msgtyp絕對值的信息。

 

 

msgflg 用於控制當隊列中沒有相應類型的消息能夠接收時將發生的事情。

調用成功時,該函數返回放到接收緩存區中的字節數,消息被複制到由msg_ptr指向的用戶分配的緩存區中,而後刪除消息隊列中的對應消息。失敗時返回-1。

四、msgctl()函數

該函數用來控制消息隊列,它與共享內存的shmctl函數類似,它的原型爲:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msgid, int command, struct msgid_ds *buf);

 

command是將要採起的動做,它能夠取3個值,

  • IPC_STAT:把msgid_ds結構中的數據設置爲消息隊列的當前關聯值,即用消息隊列的當前關聯值覆蓋msgid_ds的值。
  • IPC_SET:若是進程有足夠的權限,就把消息列隊的當前關聯值設置爲msgid_ds結構中給出的值
  • IPC_RMID:刪除消息隊列

buf是指向msgid_ds結構的指針,它指向消息隊列模式和訪問權限的結構。msgid_ds結構至少包括如下成員: 

struct msgid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};

示例:消息隊列進行進程通訊。

msg_client.c

#include <stdio.h>  

#include <string.h>  

#include <stdlib.h>  

#include <errno.h>  

#include <sys/types.h>  

#include <sys/ipc.h>  

#include <sys/msg.h>  

#include <sys/stat.h>  

#define MSG_FILE "msg_server.c"  

#define BUFFER 255  

#define PERM S_IRUSR|S_IWUSR  

struct msgtype {  

    long mtype;  

    char buffer[BUFFER+1];  

};  

int main(int argc,char **argv) {  

    struct msgtype msg;  

    key_t key;  

    int msgid;  

    if(argc!=2) {  

        fprintf(stderr,"Usage:%s string\n\a",argv[0]);  

        exit(1);  

    }  

    if((key=ftok(MSG_FILE,'a'))==-1) {  

        fprintf(stderr,"Creat Key Error:%s\a\n",strerror(errno));  

        exit(1);  

    }  

    if((msgid=msgget(key,PERM))==-1) {  

        fprintf(stderr,"Creat Message Error:%s\a\n",strerror(errno));  

        exit(1);  

    }  

    msg.mtype=1;  

    strncpy(msg.buffer,argv[1],BUFFER);  

    msgsnd(msgid,&msg,sizeof(struct msgtype),0);  

    memset(&msg,'\0',sizeof(struct msgtype));  

    msgrcv(msgid,&msg,sizeof(struct msgtype),2,0);  

    fprintf(stderr,"Client receive:%s\n",msg.buffer);  

    exit(0);  

}

 msg_server.c

#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
#include <errno.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/stat.h>  
#include <sys/msg.h>  
#define MSG_FILE "msg_server.c"  
#define BUFFER 255  
#define PERM S_IRUSR|S_IWUSR  
struct msgtype {  
    long mtype;  
    char buffer[BUFFER+1];  
};  
int main() {  
    struct msgtype msg;  
    key_t key;  
    int msgid;  
    if((key=ftok(MSG_FILE,'a'))==-1){  
        fprintf(stderr,"Creat Key Error:%s\a\n",strerror(errno));  
        exit(1);  
    }  
    if((msgid=msgget(key,PERM|IPC_CREAT|IPC_EXCL))==-1) {  
        fprintf(stderr,"Creat Message Error:%s\a\n",strerror(errno));  
        exit(1);  
    }  
    while(1) {  
        msgrcv(msgid,&msg,sizeof(struct msgtype),1,0);  
        fprintf(stderr,"Server Receive:%s\n",msg.buffer);  
        msg.mtype=2;  
        msgsnd(msgid,&msg,sizeof(struct msgtype),0);  
    }  
    exit(0);  
}

 

程序編譯運行結果以下:

 

四、信號量

  信號量是進程間通訊以前進行進程間同步必需要掌握的內容,信號量(Semaphore)是一種用於提供不一樣進程間或一個給定進程的不一樣線程間同步手段的原語,以防止一個進程在訪問共享內存的同時拎一個進程更新這塊內存的狀況。POSIX有兩種信號量:一種是基於名字的信號量,一種是基於內存的信號量。這兩種信號量均可以用於不一樣進程或者不一樣線程之間的同步。

  基於名字的信號量,是一種自然的適用於不一樣進程的同步。由於這種所謂的名字和文件系統進行了關聯,這樣就使得各進程均可以訪問(其實未必真正的和實際的文件相關聯,只不過對於應用來說,用起來就像文件同樣)。

  基於內存的信號量,若是放置在進程間的共享內存中,既能夠進行進程間的同步。儘管這兩種信號兩均可以用於進程或者線程之間的同步,但原則上基於名字的信號量更多的應用於不一樣進程間的同步,而基於內存的信號量更多的用於線程間的同步。

  爲了得到共享資源,進程須要執行下列操做:

(1)測試控制該資源的信號量。

(2)若信號量的值爲正,則進程可使用該資源,這種狀況下,進程會將信號量的值減1,表示它使用了一個資源單位。

(3)若信號量的值爲負,則進程進入休眠狀態,直至信號量值大於0;進程被喚醒後,返回步驟1.

(4)當進程再也不使用由一個信號量控制的共享資源時,該信號量增1;若是有進程正在休眠等待此信號,則喚醒它們。

  爲了正確實現信號量,信號量值的測試及減1操做應當是原子操做,爲此,信號量一般是在內核中實現的。

  經常使用的信號量形式被稱爲二元信號量,它控制單個資源,其初始值爲1.可是通常而言,信號量的初值能夠是任意一個正值,改值代表有多少個共享資源單位可共享應用。

信號量的等待和掛出:

函數semget

  函數semget()用於建立一個新信號量集或獲取一個既有集合的標識符,其函數原型以下:

#include<sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>

int semget(key_t key,int nsems,int semflg);

  函數semget()執行成功,返回新信號量集或即有信號量集的標識符,後續引用單個信號量的系統調用必需要同時指定信號量集標識符和信號量在集合中的序號,一個集合中的信號量從0開始計數;執行失敗則返回-1.

  參數key用於指定信號量集的名稱,其特殊鍵值IPC_PRIVATE的做用是建立一個僅能由本進程訪問的私用信號量集。

  參數semflg用於指定信號量集的訪問權限,由9個權限標識構成。經過指定的IPC_CREAT標誌來建立一個消息隊列,若由參數標識的信號集已經存在,就返回已有信號量集,忽略IPC_CREAT的標識做用。

參數key

參數sem_flg

semget調用結果

errno信息

IPC_PRIVATE

無要求

成功

不存在相同key

IPC_CREAT|權限

成功

存在相同key

IPC_CREAT|

IPC_EXCL|權限值

失敗

EEXIST

存在相同key

IPC_CREAT|權限

成功

 

函數semctl

  函數semctl用於在一個信號量集或集合中的單個信號量上執行各類操做控制,其函數原型以下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid,int semnum,int cmd,...);

   參數semctl()是操做所施加的信號量集的標識符。對於那些在單個信號量上執行的操做,參數semnum標識出了集合中的具體信號量,對於其餘操做則會忽略這個參數,而且能夠將其設置爲0.參數cmd指定了需執行的控制操做,常規控制操做以下:

  IPC_RMID:當即刪除信號量集及其相關聯的semid_ds數據結構。全部因在函數semop調用中等待這個集合中的信號量而阻塞的進程都會當即被喚醒,函數semop()會報告錯誤EIDRM,這個操做無需參數。

  IPC_STAT:在所指向的緩衝區中放置一份與這個信號量集相關聯的semdi_ds數據結構的副本。

  IPC_SET:使用所指向的緩衝區中的值來更新與這個信號量集相關聯的semid_ds數據結構選中的字段。

  GETVAL:函數返回由semid指定的信號量集中第semnum個信號量的值,這個操做無需參數。

  SETVAL:將有semid指定的信號量集中第semnum個信號量的值初始化爲arg.val.

    GETALL:獲取由semid指向的信號量,集中全部信號量的值並將它們存放在arg.array指向的數組中。

  SETALL:使用arg.array指向的數組中的值初始化semid指向的集合中的全部信號量。這個操做將忽略參數semnum。

   每一個信號量集都有一個關聯的semid_ds數據結構,其形式以下:

struct semid{

unsigned short sem_num;/*semaphore number*/
short  sem_op;
short  sem_flg;
}

函數semop

  函數semop()用於在semid標識的信號量集中的信號量上執行一個或多個操做,其函數原型以下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid,struct sembuf *sops,unsigned int nsops);

  參數sops是一個指向數組的指針,函數中包含了須要執行的操做;參數nsops給出了數組的大小(數組中至少包含一個元素)。操做將會按照在數組中的順序以原子的方式被執行。參數sops數組元素的結構形式以下:

struct sembuf{
unsigned short sem_num;
short sem_op;
short sem_flg;
}

  字段sem_num標識出了要操做的信號量;字段sem_op指定了要執行的操做:

  若sem_op>0,這對應於進程釋放佔用的資源數,將sem_op的值加到信號量上,其結果是其餘等待減少信號量值的進程可能會被喚醒並執行它們的操做。調用進程必需要具有在信號量上的修改權限。(V操做)

  若sem_op<0,這表示要獲取該信號量控制的資源數,將信號量減去sem_op。若是信號量當前的值大於或等於sem_op的絕對值,那麼操做就會當即結束;不然函數semop()會阻塞直到信號量增加到在執行操做以後不會致使出現負值的狀況爲止。調用進程必需要具有在信號量上的修改權限。

  若sem_op=0,這表示調用進程但願等待到該信號量值變成0,那麼就對信號量值進行檢查以肯定它當前是否等於0.若是等於0,那麼操做馬上結束;不然函數semop()就會阻塞直到信號量值變爲0爲止。調用進程必需要具有在信號量上的讀權限。

 

若是信號量值小於sem_op的絕對值(資源不能知足要求),則:

⑴若指定了IPC_NOWAIT,則semop()出錯返回EAGAIN。

⑵若未指定IPC_NOWAIT,則信號量的semncnt值加1(由於調用進程將進入休眠狀態),而後調用進程被掛起直至:①此信號量變成大於或等於sem_op的絕對值;②從系統中刪除了此信號量,返回EIDRM;③進程捕捉到一個信號,並從信號處理程序返回,返回EINTR。

(與消息隊列的阻塞處理方式 很類似)

 

 

  從語義上講,增長信號量值對應於使一種資源變得可用以便其餘進程可使用它,而減小信號量值則對應於預留進程需使用的資源。在減少一個信號量值時,若是信號量的值過低——即其餘一些進程已經預留了這個資源那麼操做就會阻塞。

  當函數semop()阻塞時,進程就會保持阻塞,直到發生下列狀況爲止:

(1)另外一個進程修改了信號量值使得待執行的操做可以繼續向前。

(2)一個信號中斷了semop()調用,這種狀況下會返回錯誤碼EINTR.

(3)另外一個進程刪除了semid引用的信號量,這種狀況下會返回錯誤碼EIDRM。

 示例:進程間通訊——讀取信號量

sem_read.c源碼

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int main(int argc,char *argv[])
{
    int semid=0;
    int count=0;
    pid_t pd=0;
    struct sembuf sops;
    
    semid=semget((key_t)12345,3,0666|IPC_CREAT);//這裏的0666是賦予讀寫權限
    if(semid==-1)
    {
    perror("semget()");
    exit(1);
    }
    
    printf("begin fork()\n");
    for(count=0;count<3;count++)
    {
    pd=fork();
    if(pd<0)
    {
    perror("fork()");
    exit(1);
    }
    
    if(pd==0)
    {
    printf("child[%d]created!\n",getpid());
    sops.sem_num=count;
    sops.sem_op=-1;
    sops.sem_flg=0;
    if(semop(semid,&sops,1)==-1)
    {
    perror("semop()");
    exit(1);
    }
    printf("child[%d]exited!\n",getpid());
    exit(0);
    }
    }
exit(0);
}

sem_write.c源碼

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int main(int argc,char *argv[])
{
    int semid=0;
    struct sembuf sops;
    
    if(argc!=2)
    {
    printf("sem_send usage error;\n");
    exit(1);
    }
    
    semid=semget((key_t)12345,3,0666|IPC_CREAT);
    
    if(semid==-1)
    {
    perror("semget()");
    exit(1);
    }
    
    if(strncmp(argv[1],"0",1)==0)
    {
    sops.sem_num=0;
    }else if(strncmp(argv[1],"1",1)==0)
    {
    sops.sem_num=1;
    }else if(strncmp(argv[1],"2",1)==0)
    {
    sops.sem_num=2;
    }else
    {
    perror("argument :count errro\n");
    exit(1);
    }
    
    sops.sem_op=1;
    sops.sem_flg=SEM_UNDO;
    
    if(semop(semid,&sops,1)==-1)
    {    
    perror("semop()");
    exit(1);
    }
    else{
        printf("semop(%d) over.\n",sops.sem_num);
        }
    exit(0);
}
    

sem_write終端輸入指令:

 ./sem_read終端運行結果

 

四、進程間同步

函數set_wait和sem_post

  信號量的等待和掛出操做的函數分別是sem_wait 和sem_post,這兩個函數的原型以下:

int sem_wait(sem_t sem);
int sem_post(sem_t sem);

  sem_wait 函數測試所指定信號量的值,若是該值大於0,則將它減1並當即返回。若是該值等於0.調用線程就會被投入到睡眠中,直到該值大於0,這時再將它減1,函數隨後返回。這種測試信號量的值並進行減1的操做必須是原子的。

  sem_post函數把所指定的信號量的值加1,而後喚醒正在等待改信號量變爲整數的任意線程。當一個線程使用完某個信號量時,應該調用sem_post釋放該信號量。

  這兩個函數調用成功後返回0,若出錯返回-1.

 另外還有兩個函數有時也會被用到,分別是sem_trywait和sem_getvalue函數,這兩個函數的原型以下:

int sem_trywait(sem_t* sem);
int sem_getvalue(sem_t sem,*valp);

  sem_trywait和sem_wait不一樣的是,當信號量的值爲0時,sem_trywait並不把調用進程投人睡眠。而是返回一個EAGAIN錯誤。

  sem_getvalue獲取信號量的值,存放在valp指向的地址中,若是該信號量當前已上鎖,那麼獲取的值或爲0,或者爲某個負數,該負數的絕對值就是等待該信號量解鎖的線程數。

基於名字的信號量

  基於名字的信號量使用sem_open、sem_close和sem_unlink函數來進行信號的建立、關閉和刪除。

函數sem_open

  sem_open用來建立一個信號量,函數原型以下:

sem_t* sem_open(const char* name,int oflag,.../*mode_t mode,unsigned int value*/);

  參數name:信號量的名字,和消息隊列建立的名字類似,須要以「/」開始。

  參數oflag:打開仍是建立(以什麼樣的方式建立)。取值爲0、O_CREATE或O_CREATE|O_EXECEL.若是取值爲非0時,則須要指定下面兩個參數。

  參數mode:建立信號量時指定的權限爲,以下表所示:

  

  以上這些值定義在<sys/stat.h>中。

  參數value:指定信號量的初始值,該初始值不能操做SEM_VALUE_MAX。一些Linux中,SEM_VALUE_MAX的值爲32767。

  須要注意的是,當以O_CREATE方式建立一個消息隊列時,若是該消息隊列已經存在,那麼並不會返回一個錯誤,但不會用value去初始化該消息隊列。若是該消息隊列不存在,則建立,並用value初始化。若是以O_CREATE|O_EXECEL方式建立一個已經存在的消息隊列,則會返回一個錯誤。

  該函數成功則返回一個指向該信號量的指針,不然返回SEM_FAILED錯誤。

函數sem_close和sem_unlink

  這兩個函數和消息隊列系列函數比較類似,其中sem_close只是僅僅關閉,不從物理上刪除,若是須要從物理上刪除則須要使用sem_unlink。原型以下:

int sem_close(sem_t* sem);
int sem_unlink(const char* name);

  成功返回0,不然返回-1.

基於內存的信號量

   基於內存的信號量使用sem_init、sem_destroy函數來進行信號的建立和銷燬,這兩種函數原型以下:

int sem_init(sem_t* sem, int shared,unsigned int value);
int sem_destory(sem* sem);

  這兩個函數都是成功則返回0,不然返回-1。sem_destroy的參數就是sem_init建立的信號量的指針。sem_init中參數sem是一個指針,用於存放信號量,必須由應用程序來分配。參數shared表示是否共享,若是爲0表示不共享,則只能在同一進程的不一樣線程之間使用;若是不爲0則表示共享,則該信號量能夠在不一樣進程之間共享,但前提是保存信號量的內存是各進程共享的(後面會講到共享內存)。

  在這裏再次說明,基於名字的信號量具備自然的進程間共享屬性,因此並不須要像基於內存的信號量這樣指定共享屬性。

 

五、共享內存

  共享內存是進程間通訊的最快方式。不管使用管道、消息隊列仍是socket等手段,這些方法都須要使用諸如read、write等系統調用,可是用共享內存卻沒必要使用系統調用。共享內存能夠是將實際存在的物理文件映射到一段內存中,也能夠將一個POSIX內存區對象映射到一段內存地址中。共享內存在訪問時每每須要使用一些信號量進行同步。

  共享內存才能這種IPC機制不禁內核控制,意味着一般須要經過某種同步方法使得進程不會出現同時訪問共享內存的狀況(如兩個進程同時執行更新操做或者一個進程在從共享內存中獲取數據的同時另外一個進程正在更新這些數據)。信號量就是用來完成這種同步的一種方法。

  使用共享內存一般須要遵循下述步驟:

(1)調用函數shmget()建立一個新共享內存段或取得一個既有共享內存段的標識符。

(2)調用函數shmat()附上共享內存段,即便該段是調用進程的虛擬內存的一部分。

(3)此刻在程序中能夠像對待其餘可用內存那樣對待這個共享內存段。爲引用這塊共享內存,程序須要使用由shmat()調用返回的addr值,它是一個指向進程的虛擬地址空間中該共享內存段起點的指針。

(4)調用函數shmdt()分離共享內存段。調用以後,進程沒法再引用這段共享內存。這一步是可選的,而且在進程終止時會自動完成這一步。

(5)調用函數shmctl()刪除共享內存段。只有當目前全部附加內存段的進程都與之分離後,內存段才能被銷燬。只有一個進程須要執行這一步。

函數shmget

  函數shmget()用於建立一個新的共享內存段或獲取一個既有段的標識符,新建立的共享內存段的內容會被初始化爲0,其函數原型以下:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key,size_t size,int shmflg);

  參數key用於指定共享內存段的名稱,其特殊值IPC_PRIVATE的做用是建立一個僅能由本進程訪問的私用共享內存段;參數size用於指定共享內存端分配所需的字節數,內核是以系統分頁大小的整數倍來分配共享內存的,所以實際上size會被提高到最近的系列分頁大小的整數倍;參數shmflg執行的任務與在IPC get調用中執行的任務同樣,即指定施加於新共享內存段上的權限或須要檢查的既有內存段的權限。

函數shmat -- at:attach

  函數shmat()將共享內存端附加到調用進程的虛擬地址空間中,其函數原型以下:

#include <sys/ipc.h>
#include <sys/shm.h>

void *shmat(int shmid,const void *shamaddr,int shmflg);

  參數shmaddr和shmflg(位掩碼SHM_RND)的設置控制着共享內存段是如何被附加上去的:

(1)若是shmaddr=NULL,那麼共享內存段附加到內核所選擇的一個合適的地址處,這是最優選擇的方法。

(2)若是shmaddr不是NULL而且沒有設置SHM_RND,那麼段會附加到由shmaddr指定的位置處,它必須是系統分頁得一個倍數(不然會發生EINVAL錯誤)。

(3)若是shmaddr不是NULL而且設置了SHM_RND,而且設置了SHMLBA,那麼段會被映射到shmaddr提供的地址,同時將地址設置爲SHMLBA的倍數,這個常量等於系統分頁大小的某個倍數。將一個段附加到值爲SHMLBA的倍數的地址處,這在一些架構上是有必要的,由於這樣纔可以提高CPU的快速緩衝性能和防止出現同一個段的不一樣附加操做在CPU快速緩衝區中存在不一致的視圖狀況。

  不推薦shmaddr指定一個非NULL值,緣由以下:

(1)它下降了一個應用程序的可移植性。一個在Linux系統上立刻有效的地址,在另外一個系統上可能無效。

(2)試圖將一個共享內存段附加到一個正在使用的特定地址處的操做會失敗。例如,當一個應用程序已經在該地址出附加了另外一個段或建立要給內存映射時,就會發生這種狀況。

  函數shmat()返回的結果是附加共享內存段的地址,開發人員能夠像對待普通的C指針那樣對待這個值,段與進程的虛擬內存的其餘部分毫無差別。一般會將函數shmat()返回值賦給一個由程序員定義的結構指針,以便在該段上設定該結構。

  要附加一個共享內存段以供只讀訪問,那麼就須要在參數shmflg中指定SHM_RONLY標記。試圖更新之毒段中的內容會致使段錯誤(SIGSEGV信號)的發生。若是沒有指定SHM_RDONLY,那麼能夠讀取內存又能夠修改內存。

  一個進程要附加一個共享內存段須要在該段上具有讀和寫的權限,除非指定了SHM_RDONLY標記——這樣的話就只需具有讀權限便可。

函數shmdt-- dt:detach

   一個進程再也不須要訪問一個共享內存段時,能夠調用函數shmdt()將該段分離出器虛擬地址空間,函數原型以下:

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

  參數shmaddr標識出待分離的段,他是以前調用函數shmat()返回的一個值。經過fork()建立的子進程會繼承其父進程附加的共享內存段,所以,共享內存爲父進程和子進程之間的通訊提供了一種簡單的IPC方法。

函數shmctl -- ctl:control

  shmctl 與信號量的semctl()函數同樣,用來控制共享內存,它的原型以下:

#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, 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;
};

 

示例:經過共享內存進行進程間的通訊

shm_read.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc,char *argv)
{
    int fd=0;
    int shmid=0;
    char *buf;
    
    shmid=shmget((key_t)12345,4096,0666|IPC_CREAT);
    if(shmid<0)
    {
    perror("shget()");
    exit(1);
    }
    
    buf=(char*)shmat(shmid,NULL,0);
    if(buf==(void*)-1)
    {
    perror("shmat()");
    exit(1);
    }
    if(strcmp(buf,"")==0)
    {
    printf("read nothing\n");
    }
    else(
    printf("read:%s\n",buf);
    }
    if(shmdt(buf)==-1)
    {
    perror("shmdt()");
    exit(1);
    }
}

shm_write.c

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/shm.h>

int main(int argc,char *argv)
{
    int fd=0;
    int shmid=0;
    char *buf;
        
    fd=open("test.txt",O_RDONLY);
    if(fd<0)
    {
    perror("open()");
    exit(1);
    }
    
    shmid=shmget((key_t)12345,4096,0666|IPC_CREAT);
    
    if(shmid<0)
    {
    perror("shmget()");
    exit(1);
    }
    
    buf=(char*)shmat(shmid,NULL,0);

    if(buf==(void*)-1)
    {
    perror("shmat()");
    exit(1);
    }

    if(read(fd,buf,1024)==-1)
    {
    perror("read()");
    exit(1);
    }else
    {
    printf("write successful.\n");
    }
    
    if(shmdt(buf)==-1)
    {
    perror("shmdt()");
    exit(1);
    }
}

程序運行結果以下:

 

內存映射

  內存映射大體分爲兩種。

(1)文件映射:文件映射將一個文件的一部分直接映射到進程的虛擬內存中。一旦一個文件被映射以後就能夠經過在相應的內存區域操做字節來訪問文件內容,映射的分頁會在須要的時候從文件中(自動)加載,這種映射也被稱爲基於文件的映射或內存映射文件。

(2)匿名映射:一個匿名映射沒有對應的文件,這種映射的分頁會被初始化爲0.

  一個進程的映射中的內存能夠與其餘進程中的映射共享(即各個進程的頁表條目指向RAM中的相同分頁),這會在兩種狀況下發生:

(1)當兩個進程映射了一個文件的同一個區域時,他們會共享物理內存的相同分頁。

(2)經過函數fork()建立的子進程會繼承其父進程的映射的副本,而且這些映射所引用的物理內存分頁與父進程中相應映射所引用的分頁相同。

  當兩個或多個進程共享相同分頁時,每一個進程都有可能會看到其餘進程對分頁內容做出的變動,固然這要取決於映射是私有的仍是共享的。

私有映射(MAP_PRIVATE):在映射內容上發生的變動對其餘進程不可見,對於文件映射來說,變動將不會在底層文件上進行。

共享映射(MAP_SHARE):在映射內容上發生的變動對全部共享同一個映射的其餘進程均可見,對於文件映射來說,變動將會發生在底層文件上。

函數mmap

   mmapz把一個文件或者一個POSIX內存對象區映射到調用進程的地址空間,用於 在調用進程的虛擬地址空間上建立一個新映射,該函數的原型以下:

#include <sys/mman.h>

void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);

  函數執行成功,返回新映射的起始地址;發生錯誤,返回MAP_FAILED。

  參數addr指定了映射放置的虛擬地址;若是addr指定爲NULL,那麼內核會映射選擇一個合適的地址,若是add爲非NULL,內核會在選擇映射放置在何處時將這個參數值做爲一個提示信息來處理。

  參數length指定了映射的字節數,length無須是一個系統分頁大小的倍數,但內核會之內存頁大小爲單位來建立映射,所以實際上length會被向上提高爲分頁大小的下一個倍數。

  參數port是一個位掩碼,它指定了施加於映射之上的保護信息,其取值爲:

PROT_NOT:區域沒法訪問。

PROT_READ:區域內容可讀取。

PROT_WRITE:區域內容可修改。

PROT_EXEC:區域內容可執行。

  參數flags是一個控制映射操做各個方面選項的位掩碼,這個掩碼只能是下列值之一:

MAP_PRIVATE:建立一個私有映射,區域內容上發生的改變對使用同一內存的其餘進程是不可見的。對於文件映射來說,所發生的變動將不會反映在底層文件上。

MAP_SHARED:建立一個共享映射,區域內容上所發生的變動對使用MAP_SHARED特性映射同一區域的進程是可見。對文件映射來說,所發生的變動將直接反映在底層文件上。

  剩餘的參數fd和offset是用於文件映射的。參數fd是一個被映射文件的描述符;參數offset指定了映射在文件中的起點,他必須是系統分頁大小的倍數,要映射整個文件就須要將offset指定爲0,而且將length指定爲文件大小。

函數munmap()

  函數munmap()執行的操做與mmap()相反,即在調用進程的虛擬地址空間中刪除一個映射,其函數原型以下:

#include <sys/munmap.h>

int munmap(void *addr,size_t length);

  參數addr:是由mmap返回的映射區起始地址。

  參數length:映射區的大小。

  函數成功返回0,不然返回-1.

函數msync

  msync函數是強制同步映射區與底層文件一致。若是mmap是以MAP_SHARED方式映射,那麼對於映射去的修改會被內核更新到磁盤文件,但並不必定老是馬上更新。有時候可能須要確信硬盤文件的內容和映射區的內容是否一致,便可以使用該函數進行同步。該函數的原型以下:

void* msync(void* addr,size_t len,int flags);

  參數addr:是由mmap返回的映射區起始地址。

  參數len:映射區大小,一般是整個映射區的大小,但也能夠只是一部分。

  參數flags:表示更新的方式,取值以下:

 MS_ASYNC:執行異步寫。

 MS_SYNC:執行同步寫。

 MS_INVALIDATE:使高速緩存的數據失效數據可執行。

  在取值中,MS_ANYNC和MS_SYNC必須指定一個,但不能同時指定。若是制定MS_ASYNC,則須要等到寫操做完成,即寫入到磁盤後纔會返回。若是同時還指定了MS_INVALIDATE,則與其最終副本不一致的文件數據的全部內存中副本都失效,後續的引用將從文件中取得數據。

 

示例:內存映射方式進程間通訊

mmap_write.c

 

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <fcntl.h>

#include <string.h>

#include <sys/mman.h>

#include <sys/stat.h>

#include <sys/types.h>



typedef struct

{

    char name[4];

    int age;

}people;



int main(int argc,char *argv[])

{

    int fd=0;

    int count=0;

    people *p_map;

    

    char temp='a';

    

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

    

    if(fd<0)

    {

        perror("open()");

        exit(1);

    }

    

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

    

    if(write(fd,"",1)<0)

    {

        perror("write()");

        exit(1);

    }    

    

    p_map=(people*)mmap(NULL,10*sizeof(people),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

    

    if(p_map==(void*)-1)

    {

        perror("mmap()");

        exit(1);

    }

    close(fd);

    

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

    {

    temp+=1;

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

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

    }

    printf("mmap write finished.\n");

    exit(0);

}

 

mmap_read.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

typedef struct
{
    char name[4];
    int age;
}people;
    
int main(int argc,char *argv[])
{
    int fd=0;
    int count=0;
    people *p_map;
    
    fd=open(argv[1],O_CREAT|O_RDWR,0666);
    if(fd<0)
    {
    
        perror("open()");
        exit(1);
    }
    
    p_map=(people*)mmap(NULL,10*sizeof(people),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p_map==(void*)-1)
    {
        perror("mmap()");
        exit(1);
    }
    for(count;count<10;count++)
    {
    printf("name:%s age:%d\n",(*(p_map+count)).name,(*(p_map+count)).age);
    }
    munmap(p_map,10*sizeof(people));
    exit(0);
}

 

 程序運行結果以下:

 

posix共享內存區

  處理將文件映射到共享內存外,POSIX還提供了一種將內存對象映射到共享內存的方法。POSIX內存區對象就是使用shm_open打開一個POSIX的名字,和基於名字的信號量打開相似。可是這種映射的地步支撐對象不必定是物理文件。可能就是在內存中。

函數shm_open

  shm_open函數用來打開或者建立一個POSIX內存區對象,對於mmap函數而言,shm_open與open函數打開一個文件沒有什麼區別,只是shm_open函數是在/dev/shm目錄上生成一個文件,並且會校驗該目錄下是否是掛載了tmpfs文件系統,若是不是也不能正常打開的。因此通常仍是用shm_open函數更規範一些,由於這個文件存在tmpfs文件系統下,在不用的狀況系統會自動刪除掉。該函數的原型以下:  

int shm_open(const char* name,int oflag,mode_t mode);

  參數name:是內存對象的名字,必須以「/」開始。

  參數oflag:打開或者建立方式。oflag必須含有O_RDONLY或O_RDWR標準,還能夠制定O_CREAT、O_EXCL或O_TRUNC。O_CREAT表示建立,O_EXCL表示排他性建立。若是指定O_TRUNC,則若是該內存對象存在,那麼它將被截短成爲0長度。

  參數mode:表示權限。若是須要賦予權限位。

 

 函數shm_unlink

  shm_unlink用來刪除一個POSIX內存區對象,該函數的原型以下:

int shm_unlink(const char* name);

  參數name:使用shm_open打開的POSIX內存去對象名字。

  該函數成功返回0,失敗返回-1.

  須要注意的是,shm_unlink僅僅只是刪除一個名字,防止其餘shm_open再次打開而已,其底層的支撐對象並不會被刪除。直到全部的對於該對象的應用關閉後,該對象將被刪除。

 示例:

shm_open_w.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>

/*封裝打印出錯函數*/
void sys_err(const char *str,int num){
    perror(str);
    exit(num);
}

int main(int argc,char *argv[])
  {
    int fd = shm_open("/hello.txt",O_RDWR|O_CREAT|O_EXCL,0777);
    /*O_EXCL|O_CREAT,若文件已經存在,則報錯*/
    if(fd < 0){
        fd = shm_open("/hello.txt",O_RDWR,0777);
        /*直接打開文件讀寫*/
    }else
        ftruncate(fd,4096);
        /*若爲本身建立的文件,則爲文件分配內存空間大小*/
    void *ptr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

    puts("start writeing  data....");
    /*這裏爲寫文件*/
    strcpy((char*)ptr,"Hello\n");
    puts("write over");
    getchar();
    shm_unlink("/hello.txt");
    close(fd);
    return 0;
  }

shm_open_r.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>

void sys_err(const char *str,int num){
    perror(str);
    exit(num);
}

int main(int argc,char *argv[])
  {
    int fd = shm_open("/hello.txt",O_RDWR|O_CREAT|O_EXCL,0777);
    if(fd < 0){
        fd = shm_open("/hello.txt",O_RDWR,0777);
    }else
        ftruncate(fd,4096);
    void *ptr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

    puts("start reading data....");
    puts((char*)ptr);
    puts("read over");
    shm_unlink("/hello.txt");
    close(fd);
    return 0;
  }

編譯運行結果以下:

 

 

 

 

 

 

參考資料

進程間通訊的方式——信號、管道、消息隊列、共享內存

進程間通訊

Linux C 程序設計大全—進程間通訊

Linux 信號詳解一(signal函數)

Linux信號(signal) 機制分析

Linux信號 - 示例C捕獲信號的程序(SIGINT,SIGKILL,SIGSTOP等)

2.信號

System V 信號量

linux信號量(轉載)

Linux進程間通訊(五):信號量 semget()、semop()、semctl()

linux基礎——linux進程間通訊(IPC)機制總結

Linux進程間通訊(六):共享內存 shmget()、shmat()、shmdt()、shmctl()

Linux進程間通訊(七):消息隊列 msgget()、msgsend()、msgrcv()、msgctl()

msgget();msgsnd();msgrcv();msgctl(); 消息隊列 Linux進程間的通訊方式之消息隊列

linux c學習筆記----消息隊列(ftok,msgget,msgsnd,msgrcv,msgctl)

linux應用編程筆記(14)共享內存編程

共享內存

共享內存函數(shmget、shmat、shmdt、shmctl)及其範例

mmap和shm共享內存的區別和聯繫

linux 共享內存shm_open實現進程間大數據交互

程序報錯 undefined reference to `shm_open'

-lz -lrt -lm -lc都是什麼庫

相關文章
相關標籤/搜索