Linux系統程式設計之命名管道與共享記憶體

烏有先生ii發表於2021-12-02

在上一篇部落格中,我們已經熟悉並使用了匿名管道,這篇部落格我們將講述程式間通訊另外兩種常見方式——命名管道與共享記憶體。

1.命名管道

管道是使用檔案的方式,進行程式之間的通訊。因此對於管道的操作,實際上還是用諸如write,read等介面實現。

匿名管道應用的一個限制就是隻能在具有親緣關係(如父程式與子程式、兄弟程式)之間進行通訊。如果想在不相關的程式間進行資料交換,可以使用FIFO檔案來做這種工作。

這裡的FIFO檔案即我們所說的命名管道。必須強調的是,雖然FIFO是一種檔案,但實際上資料的讀寫都是在作業系統開闢的記憶體緩衝區中進行的,並不會真的寫入磁碟中。如果那樣,程式間通訊的效率將會極大降低!

1.1 建立命名管道

1.1.1 使用命令列建立

在命令列中使用mkfifo 管道名的方式來建立命名管道。如下圖

image-20211202172044467

可以看到檔案型別為p,即管道型別的檔案。檔案大小為0,即便寫入到pipe中的資料沒有被另一個程式讀出,依然是0!因為根本不會將資料寫入到磁碟中。

下例中我們將hello world重定向到pipe中,並且從pipe中讀出資料顯示到螢幕。使用兩個shell,進入到同一個目錄下,一個在命令列輸入echo hello world >pipe,另一個輸入 cat < pipe, 可以看到在另一個shell的螢幕上出現了hello world。

image-20211202173937093

1.1.2 使用介面建立

在命令列輸入man 3 mkfifo 後可以看到如下內容:

image-20211202172345433

該介面一共有兩個引數,其中第一個建立的fifo檔案的路徑,第二個是檔案許可權,與我們之前學習的檔案操作的許可權一模一樣。返回值如果等於0則建立成功,-1則建立失敗。

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

  • 匿名管道由pipe函式建立並開啟。
  • 命名管道由mkfifo函式建立,開啟用open。
  • FIFO(命名管道)與pipe(匿名管道)之間唯一的區別在它們建立與開啟的方式不同,一但這些工作完成之後,它們具有相同的語義。其餘操方式與檔案的操作沒有區別。

2. system V共享記憶體

2.1 什麼是共享記憶體?

共享記憶體是最快的IPC形式,是系統在記憶體中開闢一塊記憶體,用於程式之間共享地進行操作。這片記憶體區域經過頁表對映到通訊程式各自的地址空間中,不同的程式可以通過操作自己的地址空間,來操作共享記憶體。如下圖所示:

image-20211202202832808

2.2 共享記憶體函式

共享記憶體的建立及銷燬一般分為以下幾步:

  1. 作業系統在記憶體中申請一片記憶體
  2. 將共享記憶體掛接到程式地址空間中
  3. 使用完畢後,將共享記憶體和程式地址空間去關聯
  4. 作業系統銷燬共享記憶體

因此,作業系統提供了以下幾組介面:

2.2.1 ftok函式

key_t ftok(const char *pathname, int proj_id);

該函式有兩個引數,第一個是路徑名稱,第二個是專案id。兩者配合用於建立唯一的key值,該值可以在系統內標定唯一的通訊資源。(為什麼這裡不說標定唯一的共享記憶體,是因為system v包含共享記憶體,訊息佇列,訊號量三種通訊方式。三種通訊方式都是使用ftok函式建立唯一的key來標定唯一性的。)

2.2.2 shmget函式

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

該函式用來建立共享記憶體。有三個引數:

key即系統層面上這個共享記憶體段名字,就是我們用ftok獲取的唯一值。

size是共享記憶體大小,一般是頁的整數倍。(一個page是4096 bytes

shmflg是由九個許可權標誌構成的,它們的用法和建立檔案時使用的mode模式標誌是一樣的。IPC_CREATE 的用法是如果key標定的共享記憶體不存在則建立,若存在則直接使用。IPC_EXCL一般要配合IPC_CREATE使用,即key標定的共享記憶體不存在則建立,若存在則報錯,常用於申請開闢共享記憶體的一端。除此之外,在建立共享記憶體時,還應該標定該共享記憶體的許可權(與檔案中設定許可權如出一轍),如0664等。

返回值:成功返回一個非負整數,即該共享記憶體段的標識碼(注意,該標識碼是使用者層面的標識碼,簡單,方便使用者使用!而key是系統層面的!;失敗返回-1。)

2.2.3 shmat函式

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

該函式用於將共享記憶體掛接到程式地址空間中。

shmid引數是使用shmget獲取的使用者層面的共享記憶體識別符號。

shmaddr引數是掛接到程式地址空間中的起始虛擬地址,用於一般不關心,設定為NULL,讓作業系統自己選擇分配。

shmflg引數:0即對該共享記憶體有讀寫許可權,另一個SHM_RDONLY即對該共享記憶體有隻讀許可權。

返回值:成功返回一個指標,指向共享記憶體起始地址;失敗返回-1。

2.2.4 shmdt函式

int shmdt (const void* shmaddr);

該函式用於將共享記憶體與當前程式去關聯。

唯一的一個引數shmaddr是由shmat所返回的指標。

去關聯成功則返回0,失敗則返回-1。

注意:去關聯與銷燬共享記憶體是完全不同的兩個概念!

2.2.5 shmctl函式

int shmctl(int shmid, int cmd, struct shmid_ds* buf);

功能:用於控制共享記憶體(常用於銷燬共享記憶體)

第一個引數shmid是shmget函式返回的共享記憶體識別符號,cmd有三個可以採取的動作,常使用IPC_RMID來銷燬共享記憶體,buf指向一個儲存著共享記憶體的模式狀態和訪問權的資料結構,如果是為了銷燬該共享記憶體,直接置為NULL。

成功返回0,失敗返回-1。

image-20211202211929262

3.共享記憶體的資料結構

struct shmid_ds {
    struct ipc_perm     shm_perm;  /* operation perms */
    int         shm_segsz;  /* size of segment (bytes) */
    __kernel_time_t     shm_atime;  /* last attach time */
    __kernel_time_t     shm_dtime;  /* last detach time */
    __kernel_time_t     shm_ctime;  /* last change time */
    __kernel_ipc_pid_t  shm_cpid;   /* pid of creator */
    __kernel_ipc_pid_t  shm_lpid;   /* pid of last operator */
    unsigned short      shm_nattch; /* no. of current attaches */
    unsigned short      shm_unused; /* compatibility */
    void            *shm_unused2;   /* ditto - used by DIPC */
    void            *shm_unused3;   /* unused */
};

最前面的 struct ipc_perm shm_perm是system v的三種通訊方式共有的,該結構體內就有key值。

unsigned short shm_nattch;這個欄位即掛接該共享記憶體的程式數。

4.使用共享記憶體的例子

4.1例項程式碼

Makefile:

.PHONY: all
all: server client
client: client.c comm.c
	gcc -o $@ $^
server: server.c comm.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -f client server 

comm.h

#ifndef COMM_H 
#define COMM_H
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0x66
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
#endif

comm.c

#include "comm.h"

static int commShm(int size, int flags)
{
  key_t _key = ftok(PATHNAME, PROJ_ID);
  if(_key < 0)
  {
    perror("ftok");
    return -1;
  }
  int shmid = shmget(_key, size, flags);
  if(shmid < 0)
  {
    perror("shmget");
    return -2;
  }
  return shmid;
}

int destroyShm(int shmid)
{
  if(shmctl(shmid, IPC_RMID, NULL) < 0)
  {
    perror("shmctl");
    return -1;
  }
  return 0;
}

int createShm(int size)
{
  return commShm(size, IPC_CREAT|IPC_EXCL|0666);
}

//creatShm是server端建立shm,getShm是client端獲取建立好的shm
int getShm(int size)
{
  return commShm(size, IPC_CREAT);
}

server.c

#include "comm.h"

int main()
{
  int shmid = createShm(4096);

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

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

client.c

#include "comm.h"
 
int main()
{
    int shmid = getShm(4096);
    sleep(1);
    char *addr = (char*)shmat(shmid, NULL, 0);
    sleep(2);
    int i = 0;
    while(1)
        addr[i] = 'A'+i;
        i++;
        addr[i] = 0;
        sleep(1);
    }
 
    shmdt(addr);
    sleep(2);
    return 0;
}

4.2 效果展示

在./server執行之前,在命令列輸入ipcs -m命令,檢視此時共享記憶體情況。發現系統中此時沒有共享記憶體。

image-20211202220050270

./server執行後,此時可以看到nattach變為1,即有一個程式掛接到該共享記憶體。

image-20211202220322255

執行./client後,此時nattch變為2,即此時有兩個程式掛接到該共享記憶體。

image-20211202220359375

在./client端ctrl+C之後,發現./server端還在列印,此時列印的資料不再改變,因為沒有程式向共享記憶體中寫入資料。此時nattch變為1。

image-20211202220435318

在關閉./server之後,發現nattch變為1。但是,識別符號為5的共享記憶體仍然存在!!也就是說,共享記憶體的生命週期隨核心!!而不是隨程式!!

image-20211202220500631

4.3 共享記憶體的特性

通過4.2中的例子,我們可以得到共享記憶體的以下特性:

1.共享記憶體的生命週期隨記憶體。

2.系統層面是用key來標識共享記憶體的,而使用者層面是通過shmid來進行標識,且shmid比key要簡單得多。

3.可以使用ipcs -m命令來檢視共享記憶體的狀態,使用ipcrm -m +shmid來刪除共享記憶體。

4.刪除共享記憶體是銷燬記憶體中的記憶體空間,而去關聯實際上是刪除程式頁表中程式地址空間和對應共享記憶體的對映關係。

5.如果在刪除的時候,nattch欄位為0,那麼核心中描述共享記憶體的結構體也被釋放了。如果在刪除的時候,nattch欄位不為0,那麼key會變為0x00000000.表示當前共享記憶體不能被其他程式掛接,共享記憶體的status變為destroy。如下圖所示:在client執行過程中使用ipcrm -m 6刪除6號共享記憶體,再使用ipcs -m檢視,此時key為0x00000000,status為dest。

image-20211202222416524

在共享記憶體status為dest時,一旦該共享記憶體的程式掛接數為0,共享記憶體將會被立即銷燬。如下圖所示。在./client被crtl C後,使用ipcs -m命令,此時系統中再無共享記憶體。

image-20211202222509806

6.共享記憶體不提供同步與互斥機制(回想一下,管道是否提供)。

7.共享記憶體是最快的程式通訊方式。因為共享記憶體寫是覆蓋寫的方式,讀是直接訪問地址。而我們之前所學習的管道,需要通過系統呼叫(如write,read)來進行資料的讀寫,相比之下,共享記憶體的資料拷貝次數更少,因此效率也會更高。

3.其他程式通訊方式

除了共享記憶體,system v還提供了訊息佇列和訊號量兩種程式通訊方式,其中訊息佇列還是為了實現程式間的資料交換,而訊號量主要是用於實現程式之間的同步與互斥機制。

3.1 system V訊息佇列

訊息佇列提供了一個從一個程式向另外一個程式傳送一塊資料的方法。

每個資料塊都被認為是有一個型別,接收者程式接收的資料塊可以有不同的型別值。

IPC資源必須刪除,否則不會自動清除,除非重啟,所以system V IPC資源的生命週期隨核心。

3.2 system V訊號量

訊號量主要用於同步和互斥的。

由於各程式要求共享資源,而且有些資源需要互斥使用,因此各程式間競爭使用這些資源,程式的這種關係為程式的互斥。

系統中某些資源一次只允許一個程式使用,稱這樣的資源為臨界資源或互斥資源。在程式中涉及到互斥資源的程式段叫臨界區。

關於同步與互斥,我們將在多執行緒部分重點學習。

相關文章