主頁 > 軟體設計 > 行程間通信

行程間通信

2021-11-02 17:37:09 軟體設計

文章目錄

  • 一.行程間通信介紹
    • 行程間通信目的
    • 行程間通信發展
    • 行程間通信分類
  • 二.管道
    • 匿名管道
    • 命名管道
  • 三.system V 共享記憶體
  • 四.system V訊息佇列
  • 五. 信號量概念

一.行程間通信介紹

行程間通信目的

(1). 資料傳輸:一個行程需要將它的資料發送給另一個行程(如A行程要把資料傳輸給B行程,讓B行程進行一些業務處理)
舉例 :
log.txt 中有一些內容

cat log.txt | grep hello

cat 行程將log.txt中的資料通過管道交給grep行程

(2). 資源共享:多個行程之間共享同樣的資源,
(3). 通知事件:一個行程需要向另一個或一組行程發送訊息,通知它(它們)發生了某種事件(如行程終止時要通知父行程),
(4). 行程控制:有些行程希望完全控制另一個行程的執行(如Debug行程),此時控制行程希望能夠攔截另一個行程的所有陷入和例外,并能夠及時知道它的狀態改變

補充知識 :

(1). 前面的文章說過行程之間是具有獨立性的,那就說明兩個行程要想完成資料共享成本是比較高的,因為行程的獨立性就體現在資料各自私有,所以行程間通信,一般一定要借助第三方(OS)資源

(2). 通信的本質就是"資料的拷貝"
行程A->資料拷貝給OS->OS把資料拷貝給行程B
OS一定要提供一段記憶體區域,能夠被雙方行程看到

(3). 行程間通信本質 : 讓不同的行程,看到同一份資源(記憶體,檔案內核緩沖等),資源由作業系統哪些模塊提供,就有了不同的行程間通信方式

(4). 行程間通信是有標準的(system V標準/POSIX標準)

行程間通信發展

(1). 管道

(2). System V行程間通信
實作一臺機器上的若干行程通信

(3). POSIX行程間通信
實作行程間可以跨網路通信(QQ聊天時就實作了跨主機通信)

行程間通信分類

管道 :
(1). 匿名管道pipe
(2). 命名管道

System V IPC :
(1). System V 共享記憶體
(2). System V 訊息佇列
(3). System V 信號量

POSIX IPC :
(1). 訊息佇列
(2). 共享記憶體
(3). 信號量
(4).互斥量
(5).條件變數
(6). 讀寫鎖

二.管道

匿名管道

(1). 管道是Unix中最古老的行程間通信的形式,
(2). 我們把從一個行程連接到另一個行程的一個資料流稱為一個“管道”
(3). 管道只能進行單向通信

管道通信 : 管道本質上就是一個檔案,前面的行程以寫方式打開檔案,后面的行程以讀方式打開,這樣前面寫完后面讀,于是就實作了通信,實際上管道的設計也是遵循UNIX的“一切皆檔案”設計原則的,它本質上就是一個檔案,雖然實作形態上是檔案,但是管道本身并不占用磁盤或者其他外部存盤的空間,在Linux的實作上,它占用的是記憶體空間,所以,Linux上的管道就是一個操作方式為檔案的記憶體緩沖區,

pipe函式介紹

#include <unistd.h>
功能:創建一無名管道
原型
int pipe(int fd[2]);
引數
fd:檔案描述符陣列,其中fd[0]表示讀端, fd[1]表示寫端
// 以讀方式和寫方式打開同一個檔案,回傳的兩個檔案描述符填入fd陣列中
回傳值:成功回傳0,失敗回傳錯誤代碼

匿名管道實作步驟 :
(1). 呼叫pipe函式創建匿名管道,傳入的輸出型引數fd[2]呼叫后fd[0]為讀端,fd[1]為寫端
(2). fork創建子行程,子行程fd[0]同樣為讀端,fd[1]為寫端
(3). 關閉父行程讀端,子行程寫端(或關閉父行程寫端,子行程讀端)
(4). 父行程向管道寫,子行程從管道讀,從而實作行程間通信

在這里插入圖片描述

// 父子行程通過匿名管道實作通信的測驗代碼

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]); 3
        //printf("fd[1] : %d\n",fd[1]); 4
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                int count = 10;
                while(count)
                {
                        write(fd[1],msg,strlen(msg));
                        count--;
                        sleep(1);
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                                                                                                                             

關于這段代碼的幾點分析和擴展

(1). 為什么子行程休眠1秒,運行結果父行程也休眠1秒呢 ?

臨界資源 : 被多個行程共享的資源
臨界區 : 訪問臨界資源的代碼
行程互斥 : 在使用系統資源時,一個行程正在使用,另一個行程必須阻塞等待,不能同時使用
管道自帶同步與互斥機制,行程互斥可以解決資料混亂的問題,如果沒有互斥機制,子行程在寫入的時候,父行程就有可能來讀取,就會發生意料之外的結果

管道內部已經自動提供了互斥與同步機制,當子行程向管道寫入資料,然后sleep,父行程去管道讀取資料列印,然后read 識別到管道為空,父行程不再進行讀取,阻塞式的等待子行程寫入,所以并非是父行程sleep了,而是因為子行程寫的慢,導致父行程阻塞式等待

(2). 子行程一直寫,父行程sleep不去讀,寫滿緩沖區之后,子行程會等待父行程讀取之后再寫入,子行程被掛起

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]);
        //printf("fd[1] : %d\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                while(1)
                {
                        write(fd[1],msg,strlen(msg));
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
        		sleep(100);
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                                  

(3). 父行程一直在讀取,子行程寫入5行后不再寫入,父行程會等待子行程寫入之后再讀取,父行程被掛起

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]);
        //printf("fd[1] : %d\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                int count = 10;
                while(count)
                {
                        if(count == 5)
                        {
                        	sleep(1000);
                        }
                        else
                        {
                        	sleep(1);
                         	write(fd[1],msg,strlen(msg));
                        	count--;
                        }
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                      

(4). 父行程讀端關閉,子行程寫端再寫入就無意義,作業系統會殺掉子行程,子行程會收到13號信號(SIGPIPE)

運行結果 :

child send to father : I am a child
child exit sigal : 13
// kill -l 查看信號
13) SIGPIPE
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]);
        //printf("fd[1] : %d\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                int count = 5;
                while(count)
                {
                        if(count == 2)
                        {
                        	sleep(1000);
                        }
                        else
                        {
                        	sleep(1);
                         	write(fd[1],msg,strlen(msg));
                        	count--;
                        }
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
                close(fd[0]);
                break;
        }
        int status;
        waitpid(id,&status,0);
        
        printf("child exit,sign : %d\n",status & 0x7F);
        return 0;
}                      

(5). 對掛起的理解 :
所謂掛起是將行程的PCB由R狀態設定成非R,然后將行程的PCB鏈入等待佇列.
行程被喚醒(將行程的PCB由非R狀態設定為R狀態,將PCB鏈入運行佇列中)

(6). 為什么子行程退出,父行程也退出了呢 ?
如果子行程寫端關閉,父行程讀端read回傳值為0,代表檔案結束

(7). 如果打開檔案的行程退出了,檔案也會被釋放掉,所以管道的生命周期是隨行程的

(8). 管道是半雙工,資料只能向一個方向流動;需要雙方通信時,需要建立起兩個管道
全雙工 : 指可以同時(瞬時)進行信號的雙向傳輸(A→B且B→A)
半雙工 : 只允許甲方向乙方傳送資訊,而乙方不能向甲方傳送

(9). 匿名管道只能用于具有共同祖先的行程(具有親緣關系的行程)之間進行通信;通常,一個管道由一個行程創建,然后該行程呼叫fork,此后父、子行程之間就可應用該管道

(10). 管道的大小是多大呢?

ulimit 是一條查看系統資源的命令,可以看到管道的大小

ulimit -a

在這里插入圖片描述

// 代碼測驗管道的大小

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                char c = 'a';
                int count = 0;
                while(1)
                {
                        write(fd[1],&c,1);
                        count++;
                        printf("%d\n",count);
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                sleep(1000);
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
                close(fd[0]);
                break;
        }

        waitpid(id,NULL,0);

        return 0;
}                                                                                                      

在這里插入圖片描述

讓父行程一直不讀取,子行程一直在寫入,就可以得出管道的大小為 65536 位元組

命名管道

匿名管道可以讓具有親緣關系的行程完成通信,那毫無關系的行程怎么完成通信呢? 我們可以借助命名管道來完成,不同行程可以通過檔案名打開同一個檔案,就可以讓不同行程看到同一份資源,但普通檔案是很難做到通信的,實際上這個檔案在磁盤上只是一個識別符號,沒有任何內容

命令創建命名管道

mkfifo fifo

舉例 :
一個行程下執行如下命令

while :; do echo "hello world" ;sleep 1;done > fifo

一個行程下執行如下命令

cat < fifo

在這里插入圖片描述

可以看到兩個行程通過 fifo 命名管道完成了通信,當把讀端關閉的時候,寫端就無意義了,作業系統將寫端殺掉了

函式創建命名管道

int mkfifo(const char *filename,mode_t mode);
創建成功回傳0,失敗回傳-1

下面來寫代碼完成兩個不相關的行程間的通信

// commit.h

#pragma once
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#define FILE_NAME "myfifo"

// colient.c

#include"comm.h"
int main()
{
        int fd = open(FILE_NAME,O_WRONLY);
        if(fd < 0)
        {
                perror("open\n");
                return 1;
        }
        char buf[128];
        while(1)
        {
                printf("Please Enter# :\n");
                ssize_t s = read(0,buf,sizeof(buf) - 1);
                if(s > 0)
                {
                        buf[s - 1] = 0;
                        write(fd,buf,strlen(buf));
                }
        }

        close(fd);
        return 0;
}

// server.c

#include "comm.h"
int main()
{
        if(mkfifo(FILE_NAME,0664) < 0)
        {
                perror("mkfifo\n");
                return 1;
        }

        int fd = open(FILE_NAME,O_RDONLY);
        if(fd < 0)
        {
                perror("open\n");
                return 2;
        }
        char msg[128];
        while(1)
        {
                msg[0] = 0;
                ssize_t s = read(fd,msg,sizeof(msg) - 1);
                if(s > 0)
                {
                        msg[s] = 0;
                        printf("%s\n",msg);
                }
                else if(s == 0)
                {
                        printf("client quit!\n");
                        break;
                }
                else
                {
                        printf("read error!\n");
                        break;
                }

        }
        close(fd);
        return 0;
}				

// Makefile

.PHONY:all
all:client server

client:client.c
        gcc -o $@ $^

server:server.c
        gcc -o $@ $^

.PHONY:clean
clean:
        rm -f client server

在這里插入圖片描述

命名管道相關補充 :

(1). 我們讓服務端不再讀取,當我們的客戶端在寫入的時候,我們的管道檔案的大小并沒有發生變化,說明資料并沒有被刷到磁盤當中,也就意味著雙方通信依然在記憶體當中通信
在這里插入圖片描述

(2). 應用場景一 : 我們在客戶端輸入指令,服務端決議我們的指令
將服務端的代碼用行程程式替換函式 execlp 修改一下即可

// server.c

if(s > 0)
{
      msg[s] = 0;
      printf("%s\n",msg);
      if(fork() == 0)
      {
           execlp(msg,msg,NULL);
           exit(1);
      }
      waitpid(-1,NULL,0);
}

應用場景二 : 客戶端給服務端派發計算任務
// server.c

if (s > 0)
{
    msg[s] = 0;
    printf("%s\n", msg);
    char* p = msg;
    int flag = 0;
    while (*p)
    {
        switch (*p)
        {
        case '+':
            flag = 0;
            break;
        case '-':
            flag = 1;
            break;
        case '*':
            flag = 2;
            break;
        case '/':
            flag = 3;
            break;
        case '%':
            flag = 4;
            break;
        }
        p++;
    }
    char* data1 = strtok(msg, "+-*/%");
    char* data2 = strtok(NULL, "+-*/%");

    int x = atoi(data1);
    int y = atoi(data2);
    int z;
    switch (flag)
    {
    case 0:
        z = x + y;
        break;
    case 1:
        z = x - y;
        break;
    case 2:
        z = x * y;
        break;
    case 3:
        z = x / y;
        break;
    case 4:
        z = x % y;
        break;
    }
    printf("%d\n", z);
}

(3). 行程間通信的意義 : 讓多個行程進行協同完成某種事情,如執行命令,發送字串,完成計算任務等等

(4). 命令列中使用的 | 是匿名管道,因為 | 兩側的行程都是由共同的bash行程創建的

三.system V 共享記憶體

管道通信的本質是基于檔案的,OS沒有做過多的設計作業,而system V行程間通信是OS特地設計的通信方式,system V行程間通信有3種 : 共享記憶體,訊息佇列,信號量,前兩種以傳送資料為目的,最后一種是為了保證行程的同步與互斥

共享記憶體,顧名思義就是允許兩個不相關的行程訪問同一個邏輯記憶體,共享記憶體是兩個正在運行的行程之間共享和傳遞資料的一種非常有效的方式,不同行程之間共享的記憶體通常為同一段物理記憶體,行程可以將同一段物理記憶體連接到他們自己的地址空間中,所有的行程都可以訪問共享記憶體中的地址,如果某個行程向共享記憶體寫入資料,所做的改動將立即影響到可以訪問同一段共享記憶體的任何其他行程,

共享記憶體區是最快的IPC形式,一旦這樣的記憶體映射到共享它的行程的地址空間,這些行程間資料傳遞不再涉及到內核,換句話說是行程不再通過執行進入內核的系統呼叫來傳遞彼此的資料

在這里插入圖片描述

共享記憶體建立的程序
(1). 申請共享記憶體
(2). 將共享記憶體關聯到地址空間(修改頁表,建立映射關系)
(3). 去關聯共享記憶體(修改頁表,取消映射關系)
(4). 釋放共享記憶體(記憶體歸還給系統)

行程通過共享記憶體實作通信的相關函式

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

key_t ftok(const char *pathname, int proj_id);
功能 : 由檔案路徑和proj_id生成的用來唯一標識系統的shm資源
回傳值 : 成功回傳一個 key_t ,失敗回傳-1
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
key : 由ftok生成的key標識,標識系統的唯一shm
size : 創建的共享記憶體的大小,要設定為 PAGE_SIZE 的整數倍
shmflg : 創建共享記憶體時的選項
IPC_CREAT : 如果共享記憶體存在,直接回傳共享記憶體,不存在,創建(呼叫成功的情況下,一定會獲得一個shm,但無法確定是否為全新的shm)
IPC_EXCL : 單獨使用無意義
IPC_CREAT|IPC_EXCL : 如果共享記憶體存在,出錯回傳,不存在則創建,即呼叫成功,一定會獲得一個全新的shm
回傳值: 成功時回傳一個新建或已經存在的的共享記憶體識別符號,取決于shmflg的引數,失敗回傳-1并設定錯誤碼

PAGE_SIZE : 4096 位元組(4KB)(一頁資料),在作業系統層面上,進行記憶體申請/釋放,尤其是和外設進行IO的時候,記憶體并不是按位元組去操作的,而是按頁框(頁幀)來操作的

shmget()中的size設定為4096位元組,OS分配一頁空間,設定為4097位元組,OS分配兩頁空間(雖然 ipcs -m bytes為4097位元組)

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

void *shmat(int shmid, const void *shmaddr, int shmflg);
功能 : 建立共享記憶體和行程地址空間的映射關系 ,將共享記憶體映射到行程地址空間的共享區
shmid : 共享記憶體id
shmaddr : 共享記憶體映射到行程地址空間的起始地址,絕大多數情況下,設定為NULL,因為自己沒有辦法知道具體映射到了哪里(映射到共享區),一般由作業系統去設定
shmflg : 設定為0默認為讀寫共享記憶體
回傳值 : 回傳共享記憶體映射到行程地址空間中的起始地址,出錯回傳(void*)-1
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
功能 : 取消共享記憶體和行程地址空間的關聯
shmaddr : 由shmat所回傳的指標
回傳值 : 成功回傳0,失敗回傳-1
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid : 共享記憶體id 
cmd : 可選項
IPC_RMID : 洗掉共享記憶體的選項
shmid_ds : 指向 shmid_ds 結構體的指標
回傳值 : 成功回傳0,出錯回傳-1

幾點補充

(1). 系統中可能存在多個共享記憶體,OS對共享記憶體進行管理依然遵循先描述再組織,建立 struct shmid_ds 結構體后再組織,內核的資料結構在文章下面

(2). ipcs 查看訊息佇列/共享記憶體/信號量
ipcs -m 查看共享記憶體
在這里插入圖片描述

(3). 行程已經退出,但是曾經創建的共享記憶體還存在,說明共享記憶體的周期是隨內核的,行程不主動洗掉或者用命令洗掉,共享記憶體一直存在直到關機重啟,且IPC資源一定是由內核提供并維護的

(4). 使用 ipcrm -m shmid 命令洗掉共享記憶體
使用 shmctl 洗掉共享記憶體

(5). 怎么保證兩個行程通信時映射的物理記憶體是同一個呢?

共享記憶體本身在建立的時候,會有對應的key值,key值會被寫入作業系統內部,作為共享記憶體在系統層面上的唯一識別符號,只要通信雙方拿到同一key值,就能訪問同一物理記憶體

(6). 創建共享記憶體的步驟(訊息佇列和信號量同理)
(1). 呼叫shmget()函式申請一塊物理記憶體, 創建 struct shmid_ds 內核資料結構,將 key 值填入 struct ipc_perm 中的 key 中
(2). 為其分配一個 shmid 編號(struct ipc_perm* arr[]的下標),將 struct ipc_perm 的地址填入陣列下標對應的內容中
(3). 當我們通過 shmid 編號去找共享記憶體時,通過全域的 struct ipc_ids 結構體找到 struct ipc_perm* arr[]陣列,找到陣列下標為shmid中的 struct ipc_perm 的地址,* (struct shmid_ds*)p 就可以訪問struct shmid_ds 結構體(C++中的切片技術)

在這里插入圖片描述

在這里插入圖片描述

shm_segsz : 共享記憶體大小
shm_atime : 最近 attach 時間
shm_dtime : 最近 detach 時間
shm_ctime : 最近 change 時間
shm_cpid :  共享記憶體創建者的pid
shm_lpid :  最后一個操作共享記憶體的行程pid
shm_nattch : 當前關聯的行程數量 

(7). 通過如下代碼看一下 nattch 的變化

// comm.h

#ifndef _COMM_H
#define _COMM_H

#define PATHNAME "/home/luanyiping/test/10_28/shm"
#define PROJ_ID 0x66
#define SIZE 4096

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


#endif

// server.c

#include"comm.h"
int main()
{
        key_t k = ftok(PATHNAME,PROJ_ID);
        if(k < 0)
        {
                perror("ftok\n");
                return 1;
        }

        printf("k : %x\n",k);

        int shmid = shmget(k,SIZE,IPC_CREAT|IPC_EXCL|0600);
        if(shmid < 0)
        {
                perror("shmget\n");
                return 2;
        }

        printf("shmid : %d\n",shmid);


        printf("attach begin!\n");
        sleep(5);
        char* mem = shmat(shmid,NULL,0);
        sleep(5);
        printf("attach end!\n");


        printf("dettach begin!\n");
        sleep(5);
        shmdt(mem);
        printf("dettach end!\n");
        sleep(5);

        shmctl(shmid,IPC_RMID,NULL);

        return 0;
}

運行如下監控腳本,可以看到共享記憶體鏈接數由0變為1,再由1變為0,最后刪掉共享記憶體

while :; do ipcs -m;sleep 1;echo"########################################";done

(8). 使用共享記憶體完成行程間通信代碼

// server.c

#include"comm.h"
int main()
{
        key_t k = ftok(PATHNAME,PROJ_ID);
        if(k < 0)
        {
                perror("ftok\n");
                return 1;
        }


        int shmid = shmget(k,SIZE,IPC_CREAT|IPC_EXCL|0600);
        if(shmid < 0)
        {
                perror("shmget\n");
                return 2;
        }


        char* mem = shmat(shmid,NULL,0);
        while(1)
        {
                printf("%s\n",mem);
                sleep(1);
        }

        shmdt(mem);


        shmctl(shmid,IPC_RMID,NULL);

        return 0;
}

// client.c

#include"comm.h"
int main()
{
        key_t k = ftok(PATHNAME,PROJ_ID);
        if(k < 0)
        {
                perror("ftok\n");
                return 1;
        }

        int shmid = shmget(k,SIZE,IPC_CREAT);
        if(shmid < 0)
        {
                perror("shmget\n");
                return 2;
        }

        char* mem = shmat(shmid,NULL,0);
        int i = 0;
        while(1)
        {
                mem[i] = 'A' + i;
                sleep(1);
                i++;
                mem[i] = '\0';
        }

        shmdt(mem);
}                            

(9).

1). 讀寫共享記憶體時,并沒有使用系統呼叫介面,因此和管道通信相比拷貝次數少,速度快
2). 不提供任何保護機制(不存在互斥和同步)

將 server 端的 sleep(1) 去掉,讓 server 端一直讀,觀察運行結果我們發現 client 端在寫入時,server 端仍然在讀,會造成資料混亂(即不存在互斥)

// server端一直在讀取
while(1)
{
	printf("%s\n",mem);
}

四.system V訊息佇列

訊息佇列提供了一個從一個行程向另外一個行程發送一塊資料的方法
每個資料塊都被認為是有一個型別,接收者行程接收的資料塊可以有不同的型別值

訊息佇列的節點可以簡單的理解為由兩部分組成,一部分是型別,另一部分是傳遞的資訊,用鏈表的方式將節點連接起來,剛開始的時候,訊息佇列為空,兩個行程都可以通過某種方式看到同一個佇列

訊息佇列相關函式

// 創建訊息佇列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
key : 某個訊息佇列的名字
msgflg : 由九個權限標志構成,它們的用法和創建檔案時使用的mode模式標志是一樣的
回傳值 : 成功回傳一個非負整數,即該訊息佇列的標識碼,失敗回傳-1
// 控制訊息佇列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid : 由msgget函式回傳的訊息佇列標志碼
cmd : 將要采取的動作(3個可取值)
回傳值 : 成功回傳0,失敗回傳-1
// 向訊息佇列中發送資料
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid : 由msgget函式回傳的訊息佇列標志碼
msgp : 指標指向要發送的資料
msgsz : msgp所指向的訊息長度,不含保存訊息型別的 long int 型別
msgflg : 控制當前訊息佇列滿或達到系統上限時發生的事情
msgflg = IPC_NOWATT 表示佇列滿不等待,回傳 EAGIN 錯誤
回傳值 : 成功0,失敗-1
// 獲取訊息佇列的資料塊
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgtyp=0 回傳佇列第一條訊息
msgtyp > 0 回傳佇列第一條型別等于msgtyp的資訊
msgtyp < 0 回傳佇列第一條型別小于等于msgtyp絕對值的訊息,并且是滿足條件的訊息型別最小的資訊
msgflg=IPC_NOWATT 佇列沒有可讀資訊不等待,回傳ENOMSG錯誤
msgflg=MSG_NOERROR 訊息大小超過 msgsz 時被截斷
msgtyp > 0 且 msgflg=MSG_EXCEPT 接受型別不等于 msgtyp 的第一條資訊

訊息佇列的內核資料結構 struct msqid_ds
在這里插入圖片描述

msg_first : 指向佇列的頭
msg_last :  指向佇列的尾
msg_stime : 最近一次發送資料的時間
msg_rtime : 最近一次獲取資料的時間
msg_ctime : 最近一次改變的時間
msg_cbytes : 訊息佇列的大小
msg_qnum : 訊息佇列中訊息的數量

補充 :
我們發現共享記憶體,訊息佇列,信號量的內核資料結構第一個欄位都是 struct ipc_perm

五. 信號量概念

信號量內核資料結構

在這里插入圖片描述

截止到現在,行程間通信我們學了匿名管道,命名管道,共享記憶體,訊息佇列,本質都是讓不同的行程看到同一份資源,一旦開始通信后,就會出現一個問題,兩個行程有可能一個正在寫入,另一個就來讀取了,會導致資料混亂的問題,為了解決這個問題,我們需要加鎖或實作同步和互斥,加鎖會導致效率的降低

臨界資源 : 被多個行程共享的資源
臨界區 : 訪問臨界資源的代碼

(1). 信號量的使用主要是用來保護共享資源,使得資源在一個時刻只有一個行程(執行緒)所擁有,
(2). 信號量本質是一個計數器,用來描述臨界資源中資源數目
(3). 信號量分為二元信號量,多元信號量;二元信號量將臨界資源看作一個,實作了互斥的功能

舉例如下 :
行程A和行程B競爭式的使用臨界資源,當行程A想要寫入共享記憶體,申請信號量,將信號量- -,開始寫入共享記憶體,這時行程B就會被掛起,只有當行程A寫入完畢后,釋放信號量,信號量++,行程B才會被喚醒,進行讀取共享記憶體,從而實作互斥
在這里插入圖片描述

補充 :

(1). 原子性 : 一個事務的所有操作要么不間斷地全部被執行,要么一個也沒有執行(一個行程訪問共享記憶體,要么沒有訪問,要么訪問完畢了)

(2). 由上面所說我們可以使用信號量來保護臨界資源,但兩個行程也要共享信號量,信號量也是臨界資源,哪該怎么保護信號量呢? 實際上我們的 PV 操作必須保證原子性

(3). IPC資源必須洗掉,否則不會自動清除,除非重啟,所以system V IPC資源的生命周期隨內核

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/344775.html

標籤:其他

上一篇:如何將角色旋轉到相機的旋轉?

下一篇:第三屆全國大學生演算法設計與編程挑戰賽個人銀首——>金獎

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more