Linux---Linux系統下的進程間通信詳解

進程間通信(IPC)介紹


進程間通信的本質就是讓兩個毫不相關的進程看到一份共同的資源,大概意思就是實現不同進程間的傳播或交換信息

進程間通信的主要方式有管道,消息隊列,共享內存,信號量,Socket,Streams等,這篇博客主要詳細講解前四種通信方式。(因爲後面兩個還沒學,嘿嘿)

一、管道

管道是UNIX中最古老的進程間通信方式
我們把從一個進程連接到另一個進程的一個數據流稱爲“管道”
管道又分爲匿名管道和命名管道兩種

匿名管道

1、特點

  • 管道是半雙工的,只支持單向通信,如果需要全雙工通信,就需要建立起兩個管道
  • 管道只支持有血緣關係的進程間通信,如父子進程和兄弟進程
  • 管道的生命週期隨進程,進程終止管道就會被釋放
  • 管道提供面向字節流的通信

2、原型

#include <unistd.h>
int pipe(int fd[2]);
管道創建成功後返回兩個文件描述符,用fd[2]數組接收,fd[0]爲讀,fd[1]爲寫
返回值:成功返回0,失敗返回錯誤代碼

3、圖解

這裏寫圖片描述

父進程剛創建出子進程時,調用pipe函數返回的文件描述符都是打開的,父子進程通信時就要關閉對應的描述符,如父進程從管道讀數據,用的是fd[0],就要關掉它的fd[1],子進程則相反

這裏寫圖片描述

代碼演示

    int main()
    {        
        pid_t pid;
        int file[2];//用於接收文件描述符
        pipe(file);//創建管道
        char buf[256] = {0};
        pid = fork();
        if(pid < 0)
        {    
            perror("fork");
        }else if(pid == 0)
        {    
            //子進程進行寫操作,關閉文件描述符fd[0]
            close(file[0]);
            write(file[1],"hello father",strlen("hello father"));
        }else
        {    
            //父進程進行讀操作,關閉文件描述符fd[1]                                                                                 
            close(file[1]);
            read(file[0],buf,sizeof(buf));
        }
        printf("%s\n",buf);
    }

因爲在運行a.out文件時是敲了回車的,所以會自動換一次行
這裏寫圖片描述

命名管道

也成爲FIFO,是一種文件類型

1、特點

  • 支持任意兩個進程間的通信
  • FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中

2、與命名管道的區別

  • 匿名管道由pipe函數創建並打開。
  • 命名管道由mkfifo函數創建並打開
  • 唯一的區別就是打開和創建的方式不同,一但這些工作完成之後,它們具有相同的語義

3、原型

#include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode);
成功返回0,出錯返回-1

其中的 mode 參數與open函數中的 mode 相同。一旦創建了一個 FIFO,就可以用一般的文件I/O函數操作它。

當 open 一個FIFO時,是否設置非阻塞標誌(O_NONBLOCK)的區別:

  • 若沒有指定O_NONBLOCK(默認),只讀 open 要阻塞到某個其他進程爲寫而打開此 FIFO。類似的,只寫 open 要阻塞到某個其他進程爲讀而打開它。

  • 若指定了O_NONBLOCK,則只讀 open 立即返回。而只寫 open 將出錯返回 -1 如果沒有進程已經爲讀而打開該 FIFO,其errno置ENXIO。

4、例子:用命名管道實現文件拷貝

writer.c

  int main()
  {
      //創建命名管道,權限是644
      mkfifo("pipe",0644);
      //test 文件已在當前目錄下創建,以只讀方式打開
      int input = open("test",O_RDONLY);
      if(input == -1)
      {
          perror("open");
      }
      //以只寫方式打開命名管道
      int output = open("pipe",O_WRONLY);
      if(output == -1)
      {
          perror("open");
      }
      char out[1024];
      int n;
      while((n = read(input,out,sizeof(out))) > 0)
      {
          //向管道寫數據
          write(output,out,n);                                    
      }
      close(input);
      close(output);
      return 0;
  }

reader.c

  int main()
  {
      int output;
      //創建一個文件,並以只寫方式打開它
      //目的是將test文件中的內容拷貝到test.bak中
      output = open("test.bak",O_WRONLY|O_CREAT|O_TRUNC,0644);
      if(output == -1)
      {
          perror("open");
      }
      int input;
      input = open("pipe",O_RDONLY);
      if(input == -1)
      {
          perror("open");
      }
      char buf[1024];
      int n;
      while((n = read(input,buf,sizeof(buf))) > 0)
      {
          write(output,buf,n);
      }
      close(input);
      close(output);
      unlink("pipe");
      return 0;
  }

test文件內容
這裏寫圖片描述

test.bak 文件內容顯示拷貝成功
這裏寫圖片描述


二、消息隊列

消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。

1、特點

  • 消息隊列提供了一個從一個進程向另外一個進程發送一個數據塊(有類型數據塊)的方法
  • 消息隊裏的生命週期隨內核,進程終止後消息隊列也不會銷燬

       ipcs -q                  查看當前的消息隊列
       ipcrm -q + 消息隊列ID    銷燬消息隊列,也可以通過在函數內調用msgctl函數清除消息隊列
    
  • 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
  • 消息隊列也有管道一樣的不足,即每個消息隊列的最大長度是有上限的(MSGMAX),每個消息隊列的總的字節數是由上線的(MSGMNB),系統上消息隊列的總數也有一個上限(MSGMNI)

2、原型

       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
       #include <sys/msg.h>
       // 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
       int msgget(key_t key, int flag);
       // key:消息隊列的名字,下面的例子中將具體體現使用方法
       // 添加消息:成功返回0,失敗返回-1
       int msgsnd(int msqid, const void *ptr, size_t size, int flag);
       // msqid:消息隊列的標識符,即隊列ID
       // 讀取消息:成功返回消息數據的長度,失敗返回-1
       int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
       // 控制消息隊列:成功返回0,失敗返回-1
       int msgctl(int msqid, int cmd, struct msqid_ds *buf);

3、例子:用兩個終端實現兩個前臺進程的通信

common.h

    #pragma once

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

    #define PATHNAME "."
    #define PROJ_ID 0X6667

    #define SERVER_TYPE 1
    #define CLIENT_TYPE 2

    struct msgbuf{
        long mtype;
        char mtext[1024];
    };


    int CreateMsgQueue();

    int GetMsgQueue();

    int DestroyMsgQueue(int msgid);

    int SendMsg(int msgid, int who, char *msg);

    int RecvMsg(int msgid, int recvType, char out[]);

common.c

#include "common.h"                                                                                                                     
int CommonMsgQueue(int flags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);//ftok函數可以生成key
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int msgid = msgget(key,flags);
    if(msgid < 0)
    {
        perror("msgget");
        return -1;
    }
    return msgid;
}
int CreateMsgQueue()//創建一個消息隊列
{
    //flag由9個權限標誌構成
    //IPC_CREAT|IPC_EXCL一起用表示如果沒有該消息隊列,則創建一個新的,並返回隊列ID
    return CommonMsgQueue(IPC_CREAT|IPC_EXCL|0666);
}
int GetMsgQueue()//獲得一個消息隊列
{
    //返回已創建的消息隊列ID
    return CommonMsgQueue(IPC_CREAT);
}
int DestroyMsgQueue(int msgid)//銷燬一個消息隊列
{
    if (msgctl(msgid,IPC_RMID,NULL) < 0 )
    {
        perror("msgctl");
        return -1;
    }
    return 0;
}
int SendMsg(int msgid, int who, char *msg)//往消息隊列中發送數據
{
    struct msgbuf buf;
    buf.mtype = who;
    strcpy(buf.mtext,msg);

    if(msgsnd(msgid,(void*)&buf,sizeof(buf.mtext),0) < 0)
    {
        perror("msgsnd");
        return -1;
    }
    return 0;
}
int RecvMsg(int msgid, int recvType, char out[])//從消息隊列中讀取數據
{
    struct msgbuf buf;
    if((msgrcv(msgid,(void *)&buf,sizeof(buf.mtext),recvType,0)) < 0)
    {
        perror("msgrcv");
        return -1;
    }
    strcpy(out,buf.mtext);
    return 0;
}      

server.c

#include "common.h"

int main()
{
    int msgid = CreateMsgQueue();

    char buf[1024];
    while(1)                                                                                                                            
    {   
        buf[0] = 0;
        RecvMsg(msgid,CLIENT_TYPE,buf);
        printf("Client say# %s \n",buf);

        printf("Please Enter# ");
        fflush(stdout);//刷新緩衝區
        ssize_t s = read(0,buf,sizeof(buf));//從標準輸出讀取數據到buf中
        if(s > 0)
        {   
            buf[s-1] = 0;
            SendMsg(msgid,SERVER_TYPE,buf);
            printf("send done,wait for client:..\n");
        }   
        if(strcmp(buf,"exit") == 0)
        {   
            break;
        }   
    }   
    DestroyMsgQueue(msgid);
    return 0;
}

client.c

#include "common.h"                                                                                                                     

int main()
{
    int msgid = GetMsgQueue();

    char buf[1024];
    while(1)
    {   
        buf[0] = 0;
        printf("Please Enter# ");
        fflush(stdout);//刷新緩衝區
        ssize_t s = read(0,buf,sizeof(buf));//從標準輸出讀取數據到buf中
        if(s > 0)
        {   
            buf[s-1] = 0;
            SendMsg(msgid,CLIENT_TYPE,buf);
            printf("send done, wait recv...\n");
        }   
        RecvMsg(msgid,SERVER_TYPE,buf);
        printf("Server say# %s \n",buf);
        if(strcmp(buf,"exit") == 0)
        {   
            return 0;
        }   
    }   
    return 0;
}

這裏寫圖片描述


三、共享內存

共享內存(Shared Memory),指兩個或多個進程共享一個給定的存儲區。

1、特點

  • 共享內存是最快的IPC形式,因爲進程是直接對內存進行讀取的
  • 共享內存的生命週期隨內核

    ipcs  -m                         查看共享內存
    ipcrm -m + 共享內存ID             手動銷燬共享內存或者通過調用shmctl函數來銷燬
    
  • 信號量和共享內存通常放在一起使用,信號量用來同步對共享內存的訪問

2、圖解

這裏寫圖片描述

3、原型

 #include <sys/shm.h>
  // 創建或獲取一個共享內存:成功返回共享內存ID,失敗返回-1
 int shmget(key_t key, size_t size, int flag);
 // 連接共享內存到當前進程的地址空間:成功返回指向共享內存的指針,失敗返回-1
 void *shmat(int shm_id, const void *addr, int flag);
 // 斷開與共享內存的連接:成功返回0,失敗返回-1
 int shmdt(void *addr); 
 // 控制共享內存的相關信息:成功返回0,失敗返回-1
 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

當用shmget函數創建一段共享內存時,必須指定其 size;而如果引用一個已存在的共享內存,則將 size 指定爲0 。

當一段共享內存被創建以後,它並不能被任何進程訪問。必須使用shmat函數連接該共享內存到當前進程的地址空間,連接成功後把共享內存區對象映射到調用進程的地址空間,隨後可像本地空間一樣訪問。

shmdt函數是用來斷開shmat建立的連接的。注意,這並不是從系統中刪除該共享內存,只是當前進程不能再訪問該共享內存而已。

shmctl函數可以對共享內存執行多種操作,根據參數 cmd 執行相應的操作。常用的是IPC_RMID(從系統中刪除該共享內存)。

4、例子:一個進程往共享內存中寫數據,一個進程往出讀數據

commo.h

#pragma once                                                      

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

#define PATHNAME "."
#define PROJ_ID 0X6666

int CreateShm(int size);

int DestroyShm(int shmid);

int GetShm(int size);

common.c

#include "common.h"
//與創建消息隊列很類似,就不做介紹了                                                                                                    
int CommonShm(int size, int flags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int shmid = 0;
    if((shmid = shmget(key,size,flags)) < 0)
    {
        perror("shmget");
        return -2;
    }
    return shmid;
}
int CreateShm(int size)
{
    return CommonShm(size,IPC_CREAT|IPC_EXCL|0666);
}
int DestroyShm(int shmid)
{   
    if(shmctl(shmid,IPC_RMID,NULL) < 0)
    {   
        return -1;
    }
    return 0;
}
int GetShm(int size)
{   
    return CommonShm(size,IPC_CREAT);
}

server.c

  #include "common.h"

  int main()
  {
      int shmid = CreateShm(4096);
      char *addr = shmat(shmid, NULL, 0);
      sleep(2);
      int i = 0;                                                                                                                        
      while(i++<26)
      {
          printf("client# %s\n",addr);
          sleep(1);
      }
      //將共享內存段與當前進程脫離
      shmdt(addr);
      sleep(2);
      DestroyShm(shmid);
      return 0;
  }    

client.c

  #include "common.h"

  int main()
  {
      int shmid = GetShm(4096);
      sleep(1);
      char *addr =shmat(shmid, NULL, 0);
      sleep(2);
      int i = 0;
      while(i<26)
      {   
          //每隔1秒寫一個字母進去                                                                                                       
          addr[i] = 'A'+i;
          i++;
          addr[i] = 0;
          sleep(1);
      }   

      shmdt(addr);
      sleep(2);
      DestroyShm(shmid);
      return 0;
  }

這裏寫圖片描述
client只負責寫數據,所以沒有輸出結果
這裏寫圖片描述


信號量

信號量主要作用於同步和互斥,而不是存儲進程間通信數據

1、特點

  • 信號量本質上是一個計數器,記錄了臨界資源的數目

    臨界資源:系統中有些資源一次只允許一個進程訪問,被稱爲臨界資源,也可被當做互斥資源
    臨界區: 在進程中涉及到互斥資源的程序段叫臨界區
    
  • 信號量基於操作系統的P,V操作,且P,V操作是原子性的
  • 每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。

2、原型

簡單的信號量是隻能取 0 和 1 的變量,這也是信號量最常見的一種形式,叫做二值信號量(Binary Semaphore)。而可以取多個正整數的信號量被稱爲通用信號量。

Linux 下的信號量函數都是在通用的信號量數組上進行操作,而不是在一個單一的二值信號量上進行操作。

 #include <sys/sem.h>
 // 創建或獲取一個信號量組:若成功返回信號量集ID,失敗返回-1
 int semget(key_t key, int num_sems, int sem_flags);
 // 對信號量組進行操作,改變信號量的值:成功返回0,失敗返回-1
 int semop(int semid, struct sembuf semoparray[], size_t numops);  
 // 控制信號量的相關信息
 int semctl(int semid, int sem_num, int cmd, ...);

當semget創建新的信號量集合時,必須指定集合中信號量的個數(即num_sems),通常爲1; 如果是引用一個現有的集合,則將num_sems指定爲 0 。

在semop函數中,sembuf結構的定義如下:

 struct sembuf 
 {
     short sem_num; // 信號量組中對應的序號,0~sem_nums-1
     short sem_op;  // 信號量值在一次操作中的改變量
     short sem_flg; // IPC_NOWAIT, SEM_UNDO
 }

其中 sem_op 是一次操作中的信號量的改變量:

  • sem_op > 0,表示進程釋放相應的資源數,將 sem_op 的值加到信號量的值上。如果有進程正在休眠等待此信號量,則換行它們。
  • sem_op < 0,請求 sem_op 的絕對值的資源。

    • 如果相應的資源數可以滿足請求,則將該信號量的值減去sem_op的絕對值,函數成功返回。

    • 當相應的資源數不能滿足請求時,這個操作與sem_flg有關。

      • sem_flg 指定IPC_NOWAIT,則semop函數出錯返回EAGAIN。
      • sem_flg 沒有指定IPC_NOWAIT,則將該信號量的semncnt值加1,然後進程掛起直到下述情況發生:
        • 當相應的資源數可以滿足請求,此信號量的semncnt值減1,該信號量的值減去sem_op的絕對值。成功返回;
        • 此信號量被刪除,函數smeop出錯返回EIDRM;
        • 進程捕捉到信號,並從信號處理函數返回,此情況下將此信號量的semncnt值減1,函數semop出錯返回EINTR
  • sem_op == 0,進程阻塞直到信號量的相應值爲0:
    • 當信號量已經爲0,函數立即返回。
    • 如果信號量的值不爲0,則依據sem_flg決定函數動作:
      • sem_flg指定IPC_NOWAIT,則出錯返回EAGAIN。
      • sem_flg沒有指定IPC_NOWAIT,則將該信號量的semncnt值加1,然後進程掛起直到下述情況發生:
        • 信號量值爲0,將信號量的semzcnt的值減1,函數semop成功返回;
        • 此信號量被刪除,函數smeop出錯返回EIDRM;
        • 進程捕捉到信號,並從信號處理函數返回,在此情況將此信號量的semncnt值減1,函數semop出錯返回EINTR

semctl函數中的命令有多種,這裏就說兩個常用的:

  • SETVAL:用於初始化信號量爲一個已知的值。所需要的值作爲聯合semun的val成員來傳遞。在信號量第一次使用之前需要設置信號量。
  • IPC_RMID:刪除一個信號量集合。如果不刪除信號量,它將繼續在系統中存在,即使程序已經退出,它可能在你下次運行此程序時引發問題,而且信號量是一種有限的資源。

    3、例子:父進程與子進程之間同步打印字母,使其成對出現

    commn.h

#pragma once                                                                                                                            

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

#define PATHNAME "."
#define PROJ_ID 0X6666

union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *_buf;
};

int CreateSem(int nums);
int InitSem(int semid, int nums, int initVal);
int GetSem(int nums);
int P(int semid, int who);
int V(int semid, int who);
int DestroySem(int semid);

common.c

#include "common.h"
static int CommonSemSet(int nums, int flags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int semid = semget(key,nums,flags);
    if(semid < 0)
    {
        perror("semget");
        return -2;
    }
    return semid;
}
 //創建信號量集                                                                                                                                       
int CreateSem(int nums)
{
    return CommonSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}   
//初始化信號量集
int InitSem(int semid, int nums, int initVal)
{
    union semun un;
    un.val = initVal;
    if(semctl(semid,nums,SETVAL,un) < 0)
    {
        perror("semctl");
        return -1;
    }   
    return 0;
} 
//獲取已創建好的信號量集  
int GetSem(int nums)
{
    return CommonSemSet(nums,IPC_CREAT);
}
static int CommPV(int semid, int who, int op)
{
    struct sembuf sf;
    sf.sem_flg = 0;
    sf.sem_op = op;
    sf.sem_num = who;
    if(semop(semid,&sf,1)<0)
    {
        perror("semop");
        return -1;
    }
    return 0;
}
//申請資源,相當於對信號量進行減操作
int P(int semid, int who)
{
    return CommPV(semid,who,-1);
}
//釋放資源,相當於對信號量進行加操作
int V(int semid, int who)
{
    return CommPV(semid,who,1);
}
//銷燬剛創建的信號量集
int DestroySem(int semid)
{
    if(semctl(semid, 0 ,IPC_RMID) < 0)
    {
        perror("semctl");
        return -1;
    }
    return 0
}

test_sem.c

#include "common.h"
#include <unistd.h>
#include <sys/wait.h>
int main()                                                                                                                              
{
    int semid = CreateSem(1);
    InitSem(semid, 0 ,1);
    pid_t pid = fork();
    int i = 20; 
    if(pid < 0) 
    {
        perror("fork");
        return 0;
    }else if(pid == 0)
    {
        //子進程
        int _semid = GetSem(0);
        //通過P,V操作實現兩個進程的互斥
        //在屏幕上打印成對出現的AB
        while(i--)
        {
            P(_semid, 0);
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");
            fflush(stdout);
            usleep(321456);
            V(_semid,0);
        }   
    }   
    else{
            //父進程
        while(i--)
        {        
            P(semid,0);
            printf("B");
            fflush(stdout);
            usleep(223456);
            printf("B ");
            fflush(stdout);
            usleep(121456);
            V(semid,0);
        }
        wait(NULL);
    }
    DestroySem(semid);
    return 0;
}   

運行結果(加上PV操作),AB成對出現,父子進程實現同步,那去掉PV之後呢
這裏寫圖片描述

去掉PV之後現象如下,可以看出父子進程沒有實現同步
這裏寫圖片描述


五種進程間通信方式總結

  1. 管道:只支持有血緣關係的進程間通信,且生命週期隨進程
  2. FIFO:支持任意進程間通信,創建和打開方式與管道不同,其他同管道一樣
  3. 消息隊列:存在系統限制,而且有時候需要手動刪除,在讀一條消息的時候還要注意上一條消息沒有讀完的情況
  4. 共享內存:速度最快的IPC方式,但要注意保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當於線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存
  5. 信號量:不能用來傳遞複雜消息,只能用來互斥與同步
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章