UNIX進程間通信(IPC) —— 管道、消息隊列、信號量、共享內(nèi)存

Unix進程間通信(IPC)

IPC概念:

進程間通信IPCInter-Process Communication),指至少兩個進程線程間傳送數(shù)據(jù)或信號的一些技術或方法。進程是計算機系統(tǒng)分配資源的最小單位(嚴格說來是線程)。每個進程都有自己的一部分獨立的系統(tǒng)資源,彼此是隔離的。為了能使不同的進程互相訪問資源并進行協(xié)調(diào)工作,才有了進程間通信。舉一個典型的例子,使用進程間通信的兩個應用可以被分類為客戶端和服務器(見主從式架構(gòu)),客戶端進程請求數(shù)據(jù),服務端回復客戶端的數(shù)據(jù)請求。有一些應用本身既是服務器又是客戶端,這在分布式計算中,時常可以見到。這些進程可以運行在同一計算機上或網(wǎng)絡連接的不同計算機上。

進程間通信技術包括消息傳遞、同步、共享內(nèi)存和遠程過程調(diào)用。IPC是一種標準的Unix通信機制。

使用IPC 的理由:

  • 信息共享:Web服務器,通過網(wǎng)頁瀏覽器使用進程間通信來共享web文件(網(wǎng)頁等)和多媒體;
  • 加速:維基百科使用通過進程間通信進行交流的多服務器來滿足用戶的請求;
  • 模塊化;
  • 私有權分離.

與直接共享內(nèi)存地址空間的多線程編程相比,IPC的缺點:[1]

  • 采用了某種形式的內(nèi)核開銷,降低了性能;
  • 幾乎大部分IPC都不是程序設計的自然擴展,往往會大大地增加程序的復雜度。

一、管道

1、特點:

  1. 管道是一種半雙工的通信方式(即數(shù)據(jù)只能單向流動),也有部分系統(tǒng)上實現(xiàn)了全雙工的管道,出于程序可移植性考慮,建議使用半雙工管道,全雙工的通信可由其它方式實現(xiàn),例如:消息隊列,Unix域套接字。

  2. 管道分為兩種,無名管道和有名管道。

    • 無名管道:

    最早出現(xiàn)的管道是沒有名字的,因此只能用于父子進程間通信,父進程通過fork()系統(tǒng)調(diào)用創(chuàng)建一個子進程,然后通過管道通信。

    • 有名管道(FIFO):

    有名管道也叫FIFO,由于磁盤中存在實際的管道文件,前者沒有,所以叫有名管道。FIFO的意思是(first in ,first out),先進先出。FIFO是一個(單向的)半雙工數(shù)據(jù)流,不同于普通管道的是,每個FIFO都有一個對應文件的路徑名與之關聯(lián),因此它能完成多個無親緣關系進程之間的通信。

  3. FIFO和無名管道的數(shù)據(jù)都存在內(nèi)核的內(nèi)存緩沖區(qū)中,大小一般為一頁(4K)。不同的是,F(xiàn)IFO將內(nèi)核緩沖區(qū)的數(shù)據(jù)映射到了實際的文件節(jié)點,可以在磁盤中看到對應的文件,所以叫有名管道,而無名管道在磁盤中沒有對應文件,因此稱無名管道。

  4. 無名管道通過<unistd.h>頭文件中的pipe()創(chuàng)建,有名管道(FIFO)通過<sys/stat.h>中的mkfifo()創(chuàng)建。

  5. 管道通過read()write()進行讀寫操作,管道內(nèi)核緩沖區(qū)中的數(shù)據(jù)一旦被取走,管道中將不存在。當內(nèi)核緩沖區(qū)滿的時候,write()寫操作將被阻塞,直到緩沖區(qū)有空閑再繼續(xù)。同理,當緩沖區(qū)數(shù)據(jù)為空時,read()操作將阻塞,直到有新數(shù)據(jù)時再返回。

  6. 當進程終止時,管道就完全被刪除了。

  • 無名管道

1、過程

  1. 創(chuàng)建管道

    #include <unistd.h>
    pipe(int fd[2]) 
    
  2. 通過fork()創(chuàng)建子進程

    // 返回值 >=0:成功 <0:錯誤
    // 如果是父進程則返回子進程 id,子進程則返回 0
    fork()
    
  3. read()write()讀寫緩沖區(qū)的數(shù)據(jù)

2、例子

  • pipe.c
#include <unistd.h>
#include <stdio.h>

int main()
{
    int fd[2];  // 管道描述符
    pid_t pid;  // 進程id
    char buff[20];  // 緩沖區(qū)長度
    if(pipe(fd) < 0){
        printf("創(chuàng)建管道失敗\n");
    }
    pid = fork();   // 創(chuàng)建子進程
    if(pid < 0){
        printf("fork()失敗\n");
    }else if (pid > 0){ // 大于0為主進程
        close(fd[0]);   // 關閉主進程讀端
        write(fd[1],"hello world\n",12);
    }else{  // 小于0為子進程
        close(fd[1]);   // 關閉子進程寫端
        sleep(2);
        read(fd[0], buff, 20);
        printf("讀到的數(shù)據(jù):%s\n", buff);
    }
    return 0;
}
  • 有名管道FIFO

1、過程

  1. 按以下示例,先運行read_fifo.c,創(chuàng)建管道文件(注意: 此時管道文件必須不存在,否則會出錯)。
  2. 因為此時管道中還沒有數(shù)據(jù),read()處于阻塞狀態(tài),等待數(shù)據(jù)。
  3. 再運行write_fifo.c,向管道中寫入數(shù)據(jù),此時read()打印write_fifo.c寫入的數(shù)據(jù)。

2、例子

  • read_fifo.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <time.h>
#include <sys/stat.h>

int main()
{
    int fd,len;
    char buff[1024];    //管道緩沖區(qū)大小
  if(mkfifo("/Users/meetmax/CWork/fifo1", 0666) < 0)
  {     // 創(chuàng)建FIFO管道,此時`fif01`文件必須不存在,否則報錯
        perror("Create FIFO Failed");
        exit(1);
  } 
  
    if((fd = open("/Users/meetmax/CWork/fifo1", O_RDONLY)) < 0) 
    {   // 以只讀模式打開FIFO,和打開普通文件一樣
        perror("Open FIFO Failed");
        exit(1);
    }
     // 如果管道中有數(shù)據(jù),讀取FIFO管道
    while((len = read(fd, buff, 1024)) > 0)
        printf("Read message: %s", buff);

    close(fd);  // 關閉FIFO文件
    return 0;
}
  • write_fifo.c
#include<stdio.h>
#include<stdlib.h>   // exit
#include<fcntl.h>    // O_WRONLY
#include<sys/stat.h>
#include<time.h>     // time
#include <time.h>

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

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

    for(i=0; i<10; ++i)
    {
        time(&tp);  // 取系統(tǒng)當前時間
        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;
}

XIS IPC(基于System V 的IPC函數(shù))

除管道外,還有3種IPC的進程間的通信,分別為:消息隊列、信號量和共享內(nèi)存。這3個IPC有兩種實現(xiàn)方式,分別為基于System VPOSIX的進程間通信。

  • 維基百科

System V

UNIX系統(tǒng)五[來源請求](英語:UNIX System V),是Unix操作系統(tǒng)眾多版本中的一支。它最初由AT&T開發(fā),在1983年第一次發(fā)布,因此也被稱為AT&T System V

POSIX

可移植操作系統(tǒng)接口(英語:Portable Operating System Interface,縮寫為POSIX),是IEEE為要在各種UNIX操作系統(tǒng)上運行軟件,而定義API的一系列互相關聯(lián)的標準的總稱,其正式稱呼為IEEE Std 1003,而國際標準名稱為ISO/IEC 9945。

System V 出現(xiàn)比 POSIX 要早,可以說POSIX是對System V的改進,POSIX API使用比前者更加簡單高效,但是為什么兩者仍然同時存在呢?還是一個移植性的問題,雖然現(xiàn)在新的程序都基于POSIX標準,但是仍然有很多舊的程序使用了基于System V的IPC,因此兩者都保留了。本文的IPC基于System V的IPC函數(shù)。

二、消息隊列

1、特點

  1. 消息隊列是消息的鏈表,存儲在內(nèi)核中,由消息隊列標識符標識。
  2. 消息隊列是隨內(nèi)核持續(xù)的,進程終止時,消息隊列及其內(nèi)容不會被刪除,除非內(nèi)核重啟或者調(diào)用msgctl()顯式的刪除消息隊列。
  3. 消息隊列沒有維護引用計數(shù)器(打開文件有這種計數(shù)器),所以隊列被刪除后,仍在使用該隊列的進程會出錯返回。

2、過程

  1. 先定義消息隊列結(jié)構(gòu)struct msg_form,每條消息都包含:
    • 消息隊列類型:long類型的mtype
    • 消息數(shù)據(jù):char *類型的字符串
  2. 通過文件的路徑名和項目ID(0~255之間),調(diào)用ftok()獲取IPC,獲取key值。創(chuàng)建XSI IPC結(jié)構(gòu)都應指定一個鍵,這個鍵的數(shù)據(jù)類型是系統(tǒng)數(shù)據(jù)類型key_t,通常在頭文件<sys/types.h>中定義。
  3. 接著調(diào)用msgget()函數(shù),使用key作為其中一個參數(shù),由內(nèi)核將key變成IPC的標識符,在這里就是消息隊列ID。
  4. 拿到IPC標識符后,通過msgsnd()msgrcv()分別發(fā)送和接收消息。

3、示例

  • msg_client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <unistd.h>


#define MSG_FILE "/Users/meetmax/CWork/msg_file"

// 消息結(jié)構(gòu)
struct msg_form {
    long mtype;    
    char mtext[256];
};

int main(){
    int msqid;  // 消息隊列id
    key_t key;  // 鍵值
    struct msg_form msg;

    //獲取key值
    if((key = ftok(MSG_FILE,100)) < 0){
        perror("獲取key值失敗\n");
        exit(0);
    }

    printf("key 值為: %d",key);

    if((msqid = msgget(key,IPC_CREAT|0777)) < 0){
        perror("獲取消息隊列失敗");
        exit(0);
    }
    printf("消息隊列id: %d \n",msqid);
    printf("進程id: %d \n",getpid());

    msg.mtype = 888;    // 設置消息類型
    sprintf(msg.mtext,"hello,I'm client %d\n",getpid());
    msgsnd(msqid,&msg,sizeof(msg.mtext),0);

    // 獲取777類型的消息
    msgrcv(msqid, &msg, 256, 999, 0);
    printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
    printf("Client: receive msg.mtype is: %ld.\n", msg.mtype);
    return 0;
}
  • msg_server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <unistd.h>

#define MSG_FILE "/Users/meetmax/CWork/msg_file"
struct msg_form{
    long mtype;
    char mtext[256];
};
int main()
{
    int msqid;
    key_t key;
    struct msg_form msg;
    //獲取key值
    if((key = ftok(MSG_FILE,100)) < 0){
        perror("獲取key失敗");
        exit(1);
    }
    //打印key值
    printf("key的值為 %d \n",key);
    //根據(jù)key值創(chuàng)建消息隊列
    if((msqid = msgget(key,IPC_CREAT|0777)) < 0){
        perror("創(chuàng)建消息隊列失敗");
        exit(1);
    }
    printf("消息隊列id為 : %d \n",msqid);
    printf("進程id為 : %d \n",getpid());
    while(1)
    {
        //接受888類型的消息        
        msgrcv(msqid,&msg,256,888,0);
        printf("Server:receive msg.mtext: %s \n",msg.mtext);
        printf("Server:receive msg.xxx: %ld \n",msg.mtype);
        msg.mtype = 999;
        sprintf(msg.mtext,"hello I'm server: %d \n",getpid());
        //發(fā)送消息
        msgsnd(msqid,&msg,sizeof(msg.mtext),0);
    }
    return 0;
}

三、信號量

1、特點

  1. 信號量類似鎖機制,能夠使臨界區(qū)內(nèi)的資源在某一時刻只能被一個進程訪問。臨界區(qū)是指多個進程或線程共享的內(nèi)存空間,在訪問臨界區(qū)的時候,多個進程操作同一個資源,此時就存在競態(tài)條件,通常在兩個進程對同一個資源寫操作時,會產(chǎn)生結(jié)果不一致的問題,因為我們不知道系統(tǒng)進程何時切換,這種情況也很難復現(xiàn)和調(diào)試。必須有一種機制來保證在同一時刻只能有一個進程訪問臨界區(qū),信號量就提供了這種機制。

  2. 信號量是一種特殊的變量,程序?qū)λ脑L問都是原子操作,所謂原子操作,即是指不可被中斷的操作,要實現(xiàn)原子操作單純軟件是不夠的。雖然也能實現(xiàn),但是效率很低,信號量是一種和硬件緊密結(jié)合的機制,它不會被系統(tǒng)進程切換和中斷操作打斷。本文以二值信號量為例子,二值信號量能實現(xiàn)互斥鎖的功能,保證同一時間只能一個進程訪問資源。

  3. 信號量的P,V操作

    來自維基百科

計數(shù)信號量具備兩種操作動作,之前稱為 V(又稱signal())與 P(wait())。 V操作會增加信號量 S的數(shù)值,P操作會減少它。

運作方式:

  1. 初始化,給與它一個非負數(shù)的整數(shù)值。
  2. 運行 P(wait()),信號量S的值將被減少。企圖進入臨界區(qū)塊的進程,需要先運行 P(wait())。當信號量S減為負值時,進程會被擋住,不能繼續(xù);當信號量S不為負值時,進程可以獲準進入臨界區(qū)塊。
  3. 運行 V(又稱signal()),信號量S的值會被增加。結(jié)束離開臨界區(qū)塊的進程,將會運行 V(又稱signal())。當信號量S不為負值時,先前被擋住的其他進程,將可獲準進入臨界區(qū)塊

2、過程

  1. 獲取key值(同消息隊列)
  2. 獲取信號量ID(同消息隊列)
  3. semctl()函數(shù)初始化信號量
  4. fork()子進程
  5. 執(zhí)行P,V操作

3、例子

sem.c 二值信號量

#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h> // 信號量函數(shù)庫
#include <unistd.h>

#define SEM_FILE "/Users/meetmax/CWork/sem_file" // 信號量文件

union sem_union //信號量聯(lián)合
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

//初始化信號量
int init_sem(int sem_id,int val)
{
    union sem_union tmp;
    tmp.val = val;
    if((semctl(sem_id,0,SETVAL,tmp)) == -1)
    {
        perror("初始化信號量失敗");
        return -1;
    }
    return 0;
}

/**
 * P操作
 * 信號量大于0時執(zhí)行 -1 操作,獲取資源
 * 若信號量 <= 0 則掛起等待
*/
int sem_p(int sem_id)
{
    struct sembuf sbuf;
    sbuf.sem_num = 0;
    sbuf.sem_op = -1;
    sbuf.sem_flg = SEM_UNDO;
    if(semop(sem_id,&sbuf,1) == -1)
    {
        perror("p操作失敗");
        return -1;
    }
    return 0;
}

/**
 * V操作
 * 信號量 <= 0時執(zhí)行,+1操作,釋放資源
 * 若信號量 > 0 時掛起等待
 */
int sem_v(int sem_id)
{
    struct sembuf sbuf;
    sbuf.sem_num = 0;
    sbuf.sem_op = 1;
    sbuf.sem_flg = SEM_UNDO;
    if(semop(sem_id,&sbuf,1) == -1)
    {
        perror("V操作失敗");
        return -1;
    }
    return 0;
}

//刪除信號量
int sem_del(int sem_id)
{
    union sem_union tmp;
    if(semctl(sem_id,0,IPC_RMID,tmp) == -1)
    {
        perror("刪除信號量失敗");
        return -1;
    }
    return 0;
}

int main()
{
    int sem_id;
    key_t key;
    pid_t pid;

    //獲取key值
    if((key = ftok(SEM_FILE,100)) == -1)
    {
        perror("獲取key值失敗");
        exit(1);
    }
    //獲取信號量id
    if((sem_id = semget(key,1,IPC_CREAT|0666)) == -1)
    {
        perror("信號量id獲取失敗");
        exit(1);
    }

    //初始化信號量
    init_sem(sem_id,0);

    //fork進程
    if((pid = fork()) == -1)
    {
        perror("進程fork失敗");
        exit(1);
    }else if(pid == 0){ //子進程
        printf("我是子進程:%d \n",getpid());
        sleep(2);
        sem_v(sem_id);
    }else if(pid > 0){ //父進程
        sem_p(sem_id);
        printf("我是父進程:%d \n",getpid());
        sem_v(sem_id);
        sem_del(sem_id);
    }
    return 0;
}

四、共享內(nèi)存

1、特點

概念

顧名思義,共享內(nèi)存就是允許兩個不相關的進程訪問同一個邏輯內(nèi)存。共享內(nèi)存是在兩個正在運行的進程之間共享和傳遞數(shù)據(jù)的一種非常有效的方式。不同進程之間共享的內(nèi)存通常安排為同一段物理內(nèi)存。進程可以將同一段共享內(nèi)存連接到它們自己的地址空間中,所有進程都可以訪問共享內(nèi)存中的地址,就好像它們是由用C語言函數(shù)malloc分配的內(nèi)存一樣。而如果某個進程向共享內(nèi)存寫入數(shù)據(jù),所做的改動將立即影響到可以訪問同一段共享內(nèi)存的任何其他進程。

2、過程

  1. ftko()獲取key值(同消息隊列)
  2. shmget()函數(shù)獲取共享內(nèi)存ID
  3. 進程通過shmat()函數(shù)連接共享內(nèi)存
  4. 訪問共享內(nèi)存

3、示例

shm_server.c 讀數(shù)據(jù)

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

#define SHM_FILE "/Users/meetmax/CWork/shm_file"

int main()
{
    int shm_id;
    key_t key;
    char * shm;
    struct shmid_ds buf;
    // 獲取key
    if((key = ftok(SHM_FILE,100)) == -1)
    {
        perror("獲取key失敗");
        exit(0);
    }
    // 獲取共享內(nèi)存描述符ID
    if((shm_id = shmget(key,512,IPC_CREAT|0666)) == -1)
    {
        perror("獲取共享內(nèi)存id失敗");
        exit(0);
    }
    // 連接共享內(nèi)存
    if((int)(shm = (char *)shmat(shm_id,0,0)) == -1)
    {
        perror("連接共享內(nèi)存失敗");
        exit(1);
    }
    printf("開始接收數(shù)據(jù)\n");
  
    // 開始忙等,接收數(shù)據(jù)
    while(1)
    {
        if(strlen(shm) > 0){
            printf("收到數(shù)據(jù):%s \n",shm);
            sprintf(shm,"");
        }
        if(strcmp(shm,"r") == 0){
            printf("已退出\n");
            break;
        }
    }
    // 刪除共享內(nèi)存
    shmctl(shm_id,IPC_RMID,&buf);
    return 0;

}

sem_client.c 寫入數(shù)據(jù)

#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>

#define SHM_FILE "/Users/meetmax/CWork/shm_file"

int main()
{
    int shm_id;
    key_t key;
    char * shm;
   
    // 獲取key
    if((key = ftok(SHM_FILE,100)) == -1)
    {
        perror("獲取key失敗");
        exit(0);
    }
    // 獲取共享內(nèi)存id
    if((shm_id = shmget(key,512,IPC_CREAT|0666)) == -1)
    {
        perror("獲取共享內(nèi)存id失敗");
        exit(0);
    }
    // 連接共享內(nèi)存,若不存在則創(chuàng)建
    if((int)(shm = (char *)shmat(shm_id,0,0)) == -1)   
    {
        perror("連接共享內(nèi)存失敗");
        exit(1);
    }
    printf("請輸入:");
    scanf("%s",shm); // 寫入數(shù)據(jù)到共享內(nèi)存
    shmdt(shm); // 斷開連接
    return 0;
}

參考

  • 《UNIX環(huán)境高級編程》
  • 《UNIX網(wǎng)絡編程:卷2》
  • 部分來自互聯(lián)網(wǎng)
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容