Linux進程間通信

進程間通信

一般簡稱爲IPC----InterProcess Communication。是指在不同進程之間傳播或交換信息。

進程間通信有這麼幾個方式:管道(命名管道/匿名管道),共享隊列,消息隊列,信號量

進程間爲什麼要通信呢?

因爲進程的獨立性,因此想要通信必須能夠共同訪問一個相同的媒介。

進程間通信的目的:數據傳輸,數據共享,進程間的訪問控制。

也正因爲通信的目的不同,使用場景不同,因此操作系統提供了多種進程間通信方式:

管道----傳輸數據

共享內存----共享數據

消息隊列----傳輸數據

信號量----進程間的訪問控制

管道

管道是半雙工通信,雙向選擇的單向通信(即數據只能在一個方向上流動),具有固定的讀端和寫端

它是進程間的數據資料傳輸。在通信過程中,先將數據放到buf中,在將數據拷貝到自己的buf中在進行操作

通信1

管道生命週期隨進程,如果進程消亡了,那麼通信也就結束了

匿名管道
int pipe(int pipefd[2]);
//pipefd輸出型參數
//數組pipefd用於返回引用結尾的兩個文件描述符。

pipefd[0] 從管道讀數據
pipefd[1] 從管道寫數據

如果成功,則返回 0;不成功,則返回-1

只能用於具有親緣關係的進程間通信

#include <stdio.h>    
#include <stdlib.h>    
#include <fcntl.h>    
#include <errno.h>    
    
int main(){    
  int pipefd[2];    
  int ret = pipe(pipefd);    
  if(ret < 0){    
    perror("pipe error");    
    return -1;    
  }    
  int pid = fork();    
  if(pid < 0){    
    perror("fork error");    
    return -1;    
  }else if(pid == 0){    
    close(pipefd[1]);    
  }else{    
    sleep(1);    
    close(pipefd[1]);    
    char buf[1024] = {0};    
    int ret = read(pipefd[0], buf, 1023);    
    printf("read buf:[%d - %s]\n",ret, buf);    
  }    
  return 0;    
} 

先建立管道,之後再創建子進程。這個時候就要考慮到管道的讀寫特性了

若管道中沒有數據,則read會阻塞,直到讀到數據返回
若管道中數據寫滿了,則write會阻塞,直到數據被讀取,管道中有空閒位置,寫入數據後返回

若管道中所有的讀端都被關閉,則write會觸發異常----SIGPIPE(信號標誌)----導致進程退出
若管道中的所有寫有寫端都被關閉,則read返回0----通知用戶沒人寫了

父子進程兩端都要進行關閉

所以代碼中將管道中寫端關閉了,所以讀端返回的是0.

通信2

雖然管道提供了雙向選擇,但是如果我們沒有用到某一端,就把這一端關閉掉

管道同步與互斥特性

當讀寫數據的大小<管道pipe_buf ,是保證操作原子性-----這時操作不可被打斷

互斥:保證對一個臨界資源(公共資源,比如全局變量)同一時間的唯一訪問性(我操作的時候你不能操作)

同步:保證對一個臨界資源訪問的時序可控性(我操作完了你才能操作)

|管道符就是匿名訪問

int main(){                               
  int pipefd[2];                          
  int ret = pipe(pipefd);                 
  if(ret < 0){                            
    perror("pipe error");                 
    return -1;                            
  }                                       
  int pid1 = fork();                      
  if(pid1 == 0){                          
    close(pipefd[0]);//關閉從管道讀數據    
    dup2(pipefd[1],1);//將文件描述符表中 向管道寫數據 替換 標準輸出    
    execlp("ps","ps","-ef",NULL);//程序替換    
  }                                       
  int pid2 = fork();                      
  if(pid2 == 0){                          
    close(pipefd[1]);                     
    dup2(pipefd[0],0);                    
    execlp("grep","grep","ssh",NULL);     
  }                                       
  close(pipefd[0]);//不用的時候將讀端和寫端都關閉    
  close(pipefd[1]);    
  waitpid(pid1,NULL,0);    
  waitpid(pid2,NULL,0);    
  return 0;    
} 

grep讀數據時不知道自己需要多少數據,過濾之後再次讀取。

代碼用圖示來解答一下

通信3

命名管道(FIFO)

FIFO是一種文件類型,可以用於任意進程間通信。

可見於文件系統,因爲創建命名管道會隨之在文件系統中創建一個命名管道文件

類似於在進程中使用文件來傳輸數據,只不過FIFO類型文件同時具有管道的特性

因爲所有的進程都能夠通過打開管道文件,進而獲取管道的操作句柄,因此命名管道可以用於同一主機上任意進程間通信

int mkfifo(const char *pathname, mode_t mode);
pathname:管道文件名
mode:創建權限 0664

fifo_read.c

#include <stdio.h>    
#include <stdlib.h>    
#include <errno.h>    
#include <string.h>    
#include <fcntl.h>    
#include <unistd.h>    
#include <sys/stat.h>    
      
int main(){    
  char *file = "./test.fifo";    
  umask(0);    
  int ret = mkfifo(file, 0664);    
  if(ret < 0){    
    if(errno != EEXIST){    
      perror("mkfifo errno");    
      return -1;    
    }    
  }    
  printf("open file\n");    
  int fd = open(file, O_RDWR);    
  if(fd < 0){    
    perror("open error");                
    return -1;    
  }    
  printf("open success!!\n");    
  while(1){    
    char buf[1024] = {0};    
    int ret = read(fd, buf, 1023);
    if(ret > 0){
      printf("read buf:[%s]\n", buf);
    }else if(ret == 0){
        printf("write closed~~~\n");
    }else{
      perror("read error");
    }
    printf("---------\n");
  }
  close(fd);
  return 0;
} 

fifo_write.c

int main(){
    char *file = "./test.fifo";
    umask(0);
    int ret = mkfifo(file,0664);
    if(ret < 0){
        if(ret != EEXIST){
            perror("mkfifo error");
            return -1;
        }
    }
    printf("open file\n");
    int fd = open(file, O_WRONLY);
    if(fd < 0){
        perror("open error");
        return -1;
    }
    printf("open success!!\n");
    while(1){
        char buf[1024] = {0};
        scanf("%s", buf);
        write(fd, buf, strlen(buf));
    }
    return 0;
}

通信4

這有點類似於服務器–客戶端之間建立連接

fifo_write類似於客服端發送一個請求 fifo_read類似於服務器返回一個請求

命名管道利用了文件系統創建文件進行通信。當write寫入了數據,將數據從自己的buf寫到test.fifo文件中,之後read在將數據從test.fifo中讀取到自己的buf中,最後輸出。

命名管道的讀寫特性

若管道沒有被以寫的方式打開,這時如果只讀打開則會阻塞,直到文件被以寫的方式打開

若管道沒有被以讀的方式打開,這時如果只寫打開則會阻塞,直到文件被以讀的方式打開

若管道以讀寫的方式打開,則不會阻塞

匿名管道和命名管道的區別

匿名管道:速度慢,容量有限,只有父進程進程能通訊

命名管道:任何進程間都能通訊,但速度慢

共享內存

共享內存的定義:在物理上開闢一塊空間,內存直接映射到虛擬內存中,如果一塊內存被多個進程映射,那麼多個進程訪問同一塊內存,則可以實現通信。最快的進程間通信。因爲相較於其他進程間通信方式(將數據從用戶態拷貝到內核態,用的時候,從內核態拷貝到用戶態),共享內存直接將一塊內存映射到用戶空間,用戶可以直接通過地址對內存進行操作,並反饋到其他進程,少了兩步數據拷貝的過程。

共享內存使用流程

1、創建/打開共享內存

int shmget(key_t key, size_t size, int shmflg);
    key:    共享內存標識符
    size:  共享內存大小
    shmflg:打開方式/創建權限
    	IPC_CREAT 共享內存不存在則創建
    	IPC_EXCL 與IPC_CREAT同用,若存在則報錯,不存在則創建
    返回值:操作句柄shmid	失敗:-1
key_t ftok(const char *pathname, int proj_id);
    pathname:   文件名
    proj_id:   數字
    通過文件的 inode節點號 和 proj_id 共同得出一個key值

2、將共享內存映射到虛擬地址空間(建立映射關係)

void shmat(int shmid, const void *shmaddr, int shmflg);
	shmid:	創建共享內存返回的操作句柄
	shmaddr:用於指定映射在虛擬空間的首地址	通常置NULL
    shmflg:0----可讀可寫
    返回值:映射首地址(通過這個地址對共享內存進行操作)	失敗:(void*)-1

3、對共享內存進行基本的內存操作,memcpy

4、解除映射關係 shmdt

int shmdt(const void *shmaddr)
	shamddr:	映射返回的首地址

5、刪除共享內存 shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:	操作句柄
cmd:	IPC_RMID	刪除共享內存
buf:	設置或者獲取共享內存信息,用不着置NULL
共享內存並不是立即刪除的,只是拒絕後續映射連接,當共享內存
映射連接數爲0時,則刪除共享內存

shm_read.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE  4096
int main(){
  int shmid;    
  //1、創建共享內存    
  shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);    
  if(shmid < 0){    
    perror("shmget error");    
    return -1;    
  }    
  //2、將共享內存映射到虛擬地址空間    
  char *shm_start = (char*)shmat(shmid, NULL, 0);    
  if(shm_start == (void*)-1){    
    perror("shmat error");    
    return -1;    
  }    
  while(1){    
    printf("%s\n", shm_start);    
    sleep(1);    
  }    
  //4、解除映射    
  shmdt(shm_start);    
  //5、刪除共享內存    
  shmctl(shmid, IPC_RMID, NULL); 
    return 0;
}

shm_write.c

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/shm.h>    
#define IPC_KEY 0x12345678    
#define PROJ_ID 12345    
#define SHM_SIZE  4096    
    
int main(){    
  int shmid;    
  //1、創建共享內存    
  shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);    
  if(shmid < 0){    
    perror("shmget error");    
    return -1;    
  }    
  //2、將共享內存映射到虛擬地址空間    
  char *shm_start = (char*)shmat(shmid, NULL, 0);    
  if(shm_start == (void*)-1){    
    perror("shmat error");    
    return -1;    
  }    
  int i = 0;    
  while(1){    
    sprintf(shm_start, "明天又是可以學習的一天!!!+%d\n",i++);    
    sleep(1);    
  }
  //4、解除映射
  shmdt(shm_start);
  //5、刪除共享內存
  shmctl(shmid, IPC_RMID, NULL);
  return 0;
} 

通信5

共享內存雙方都可以修改

共享內存沒有同步與互斥

刪除一塊共享內存,並不會立即刪除,而是判斷映射連接數,若爲0則刪除,不爲0則拒絕後續連接,直到爲0刪除

消息隊列

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

消息隊列的建立過程爲

創建消息隊列---->添加數據節點---->獲取數據節點---->刪除

msgget---->msgsnd---->msgrcv(接收數據)---->msgctl

消息隊列傳輸的是有類型的數據塊,用戶可以根據自己的需要選擇性的獲取某些數據類型

信號量

內核中的一個計數器----具有等待隊列(PCB等待隊列),具有等待和喚醒功能

用於資源計數,若計數小於等於0,表示沒有資源,則需要等待

若計數大於0,表示有資源,則可以獲取資源,然後計數-1

如果放置了資源,則計數+1,並且喚醒等待的進程

實現進程間的同步和互斥(資源計數爲0或1的時候才具有互斥)

小結

消息隊列和信號量現在的使用不是特別多,瞭解一下就可以。重點還是共享內存和管道的學習。

在代碼過程中,我們應該可以感覺到,這種通信方式有點類似於服務器和客戶端之間的處理過程。但是具體的實現是不同的。

匿名管道是通過對讀端和寫端的關閉和開啓,在buf緩存區對數據進行拷貝和使用。

而命名管道是通過文件系統的打開和關閉,將數據進行讀寫。讀寫必須同時打開,否則另一端會被阻塞。

而共享內存是在同一個地址映射的一塊虛擬地址被多個進程訪問,這時也就是多個進程同時訪問同一個內存。此時共享內存直接映射一塊內存到用戶空間,用戶直接通過地址對內存進行操作,並反饋到其他進程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章