Posix共享內存


1. 概述

共享內存是可用IPC機制中最快的,一旦共享內存區映射到共享它的進程地址空間:

  • 進程間的數據傳遞就不再執行需進入內核的系統調用
  • 各個進程向共享內存讀寫數據往往需要某種形式的同步
  • 這些進程間的同步通常使用Posix有名信號量或無名信號量

對比下面兩張圖所展示的例子:

  • 不使用共享內存,需要4次內核和進程-用戶空間的數據拷貝
  • 使用共享內存,只需要2次內核空間-用戶空間的數據拷貝


2. mmap、munmap和msync函數

mmap

mmap函數把一個文件或一個Posix共享內存對象映射到調用進程的地址空間,使用該函數有三個目的:

  • 使用普通文件以提供內存映射IO
  • 使用特殊文件以提供匿名內存映射
  • 使用Posix共享內存對象以提供Posix共享內存區
//成功返回映射內存的起始地址,失敗返回MAP_FAILED
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

mmap參數解析:

  • addr指定映射內存的起始地址,通常設為NULL,讓內核自己決定起始地址
  • len是被映射到調用進程地址空間中的字節數,它從被映射文件fd開頭起第offset個字節處開始算,offset通常設為0,下圖展示了這個映射關系
  • prot指定對映射內存區的保護,通常設為PROT_READ | PROT_WRITE
  • flags必須在MAP_SHAREDMAP_PRIVATE這兩個標志中選擇指定一個,進程間共享內存需要使用MAP_SHARED
  • 可移植的代碼應把addr設為NULL,並且flags不指定MAP_FIXED

prot 說明 flags 說明
PROT_READ 數據可讀 MAP_SHARED 變動是共享的
PROT_WRITE 數據可寫 MAP_PRIVATE 變動是私有的
PROT_EXEC 數據可執行 MAP_FIXED 准確地解釋addr參數
PROT_NONE 數據不可訪問

mmap成功返回后,可以關閉fd,這對已建立的映射關系沒有影響。
注意,不是所有文件都能進行內存映射,例如終端和套接字就不可以。

munmap

mmap建立的映射關系通過munmap刪除,其中addr是mmap返回的地址,len是映射區的大小,同mmap的參數len。

//成功返回0,失敗返回-1
int munmap(void *addr, size_len);

msync

默認情況下,內核采用虛擬內存算法保持內存映射文件與內存映射區的同步,前提是指定了MAP_SHARED標志,但這種同步可能不是立即生效的,而是在隨后某個時間進行。
但有時候我們修改完數據並進行下一步操作之前,需要確認數據已經同步完成,這時可調用msync函數。

//成功返回0,失敗返回-1
int msync(void *addr, size_t len, int flags);

其中addr和len含義同munmap,flags使用下表中的常值,其中MS_ASYNCMS_SYNC這兩個常值中必須選擇指定一個。

flags 說明
MS_ASYNC 執行異步寫,msync立即返回
MS_SYNC 執行同步寫,msync等同步完成才返回
MS_INVALIDATE 使高速緩存的數據失效

3. 內存映射IO

內存映射IO是父子進程之間共享內存區的一種方法,父進程fork前以MAP_SHARED方式調用mmap,其建立的內存映射關系會被子進程繼承。
我們使用這個方法,來實現以下功能:

  • 父子進程通過內存映射IO共享一片內存
  • 父子進程共同給共享內存區中的一個計數器持續加1

父子進程同步——Posix有名信號量

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

#define MMAP_FILE   "/home/delphi/mmap_file"
#define SEM_PATH    "/sem_mmap"

struct Shared
{
    int count;
};

int main(int argvc, char **argv)
{
    struct Shared shared;
    struct Shared *ptr; //mmap返回指針類型結構和映射文件數據結構需要一致
    sem_t *mutex;       //因為是進程間同步,不能用互斥鎖,因此使用二值信號量模擬互斥鎖
    int fd;
    int i;

    fd = open(MMAP_FILE, O_CREAT | O_RDWR, 0666);
    memset(&shared, 0, sizeof(shared));
    write(fd, &shared, sizeof(shared)); //將映射文件內容初始化為0
    ptr = mmap(NULL, sizeof(shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    mutex = sem_open(SEM_PATH, O_CREAT, 0666, 1); //二值信號量模擬互斥鎖,初始值必須為1
    sem_unlink(SEM_PATH);

    setbuf(stdout, NULL); //設置標准輸出為無緩沖

    if (fork() == 0)
    {
        for (i = 0; i < 10; i++)
        {
            sem_wait(mutex);
            printf("child: %d\n", ptr->count);
            ptr->count++;
            sem_post(mutex);
        }

        exit(0);
    }

    for (i = 0; i < 10; i++)
    {
        sem_wait(mutex);
        printf("parent: %d\n", ptr->count);
        ptr->count++;
        sem_post(mutex);
    }

    return 0;
}

該示例代碼使用Posix有名信號量來同步父子進程,共享內存區中僅有一個4字節計數器,信號量不在共享內存區中。

父子進程同步——Posix無名信號量

將上面這份代碼修改一下,改為使用建立在共享內存區中的Posix無名信號量同步父子進程,示意圖如下圖所示。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

#define MMAP_FILE   "/home/delphi/mmap_file"

struct Shared
{
    sem_t mutex;
    int count;
};

int main(int argvc, char **argv)
{
    struct Shared shared;
    struct Shared *ptr; //mmap返回指針類型結構和映射文件數據結構需要一致
    int fd;
    int i;

    fd = open(MMAP_FILE, O_CREAT | O_RDWR, 0666);
    memset(&shared, 0, sizeof(shared));
    write(fd, &shared, sizeof(shared)); //將映射文件內容初始化為0
    ptr = mmap(NULL, sizeof(shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    sem_init(&ptr->mutex, 1, 1);    //Posix無名信號量建立在ptr指向的共享內存中
    setbuf(stdout, NULL);          //設置標准輸出為無緩沖

    if (fork() == 0)
    {
        for (i = 0; i < 10; i++)
        {
            sem_wait(&ptr->mutex);
            printf("child: %d\n", ptr->count);
            ptr->count++;
            sem_post(&ptr->mutex);
        }

        exit(0);
    }

    for (i = 0; i < 10; i++)
    {
        sem_wait(&ptr->mutex);
        printf("parent: %d\n", ptr->count);
        ptr->count++;
        sem_post(&ptr->mutex);
    }

    sem_destroy(&ptr->mutex);

    return 0;
}

4. 匿名內存映射

在上面展示的內存映射IO示例代碼中,可以看出編碼的前兩步都是:

  • 調用open創建一個普通文件
  • 調用write將文件內容初始化為0

如果僅僅是用於父子進程共享內存,可以使用匿名內存映射來避免文件的顯式創建打開以及初始化,其辦法是:

  • mmap調用時,flags設為MAP_SHARED | MAP_ANON,fd設為-1,offset設為0即可
  • 匿名內存映射保證這樣的內存區初始化為0

把基於Posix無名信號量的示例代碼改為匿名內存映射,可以看出,fork前的准備工作明顯簡化了許多。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

struct Shared
{
    sem_t mutex;
    int count;
};

int main(int argvc, char **argv)
{
    struct Shared *ptr;
    int i;

    ptr = mmap(NULL, sizeof(struct Shared), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
    sem_init(&ptr->mutex, 1, 1);
    setbuf(stdout, NULL);

    if (fork() == 0)
    {
        for (i = 0; i < 10; i++)
        {
            sem_wait(&ptr->mutex);
            printf("child: %d\n", ptr->count);
            ptr->count++;
            sem_post(&ptr->mutex);
        }

        exit(0);
    }

    for (i = 0; i < 10; i++)
    {
        sem_wait(&ptr->mutex);
        printf("parent: %d\n", ptr->count);
        ptr->count++;
        sem_post(&ptr->mutex);
    }

    sem_destroy(&ptr->mutex);

    return 0;
}

5. Posix共享內存

前面講的都是在父子進程間使用共享內存的技術,現在把共享內存區的概念擴展到任意進程之間,Posix.1提供了兩種在任意進程間共享內存區的方法。

  • 內存映射IO:該方法其實也可以用於無親緣關系進程間共享內存
  • Posix共享內存:這是Posix IPC的第三種機制

這兩種技術都需要調用mmap,區別在於mmap參數fd的獲取手段:

  • 內存映射IO通過open獲得
  • Posix共享內存通過shm_open獲得

shm_open和shm_unlink函數

shm_open用於創建一個新的Posix共享內存對象或打開一個已存在的Posix共享內存對象。
shm_unlink用於從系統中刪除一個Posix共享內存對象。

//成功返回非負描述符,失敗返回-1
int shm_open(const char *name, int oflag, mode_t mode);

//成功返回0,失敗返回-1
int shun_unlink(const char *name);

shm_open參數說明:

  • oflag參數不能設置O_WRONLY標志
  • 和mq_open、sem_open不同,shm_open的mode參數總是必須指定,當指定了O_CREAT標志時,mode為用戶權限位,否則將mode設為0

shm_open的返回值是一個描述符,它隨后用作mmap的第五個參數fd。

ftruncate和fstat函數

處理mmap的時候,普通文件或Posix共享內存對象的大小都可以通過調用ftruncate設置。

#include <unistd.h>

//成功返回0,失敗返回-1
int ftruncate(int fd, off_t length):
  • 對於普通文件,若文件長度大於length,額外的數據會被丟棄;若文件長度小於length,則擴展文件大小到length
  • 對於Posix共享內存對象,ftruncate把該對象的大小設置成length字節

我們調用ftruncate來指定新創建的Posix共享內存對象大小,或者修改已存在的Posix共享內存對象大小。

  • 創建新的Posix共享內存對象時指定大小是必須的,否則訪問mmap返回的地址會報bus error錯誤
  • 當打開一個已存在的Posix共享內存對象時,可以調用fstat來獲取該對象的信息
#include <sys/stat.h>
#include <sys/types.h>

//成功返回0,失敗返回-1
int fstat(int fd, struct stat *buf);

stat結構有12個或以上的成員,然而當fd指代一個Posix共享內存對象時,只有四個成員含有信息:

struct stat
{
    mode_t st_mode;  //用戶訪問權限
    uid_t  st_uid;   //user id of owner
    gid_t  st_gid;   //group id of owner
    off_t  st_size;  //文件大小
};

6. Posix共享內存示例代碼

示例代碼包括一個.h文件和兩個.c文件:

  • common.h定義server和client共同使用的信息
  • server.c創建並初始化Posix共享內存對象,以及同步需要的信號量
  • client.c打開Posix共享內存對象,然后給共享內存中的計數器加1

進程同步——Posix有名信號量

common.h

#ifndef _COMMON_H_
#define _COMMON_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

#define SHM_FILE    "/shm_file"
#define SEM_PATH    "/sem_mmap"

struct Shared
{
    int count;
};

#endif

server.c

#include "common.h"

int main()
{
    struct Shared *ptr;
    sem_t *mutex;
    int fd;

    shm_unlink(SHM_FILE);
    fd = shm_open(SHM_FILE, O_RDWR | O_CREAT, 0666);
    ftruncate(fd, sizeof(struct Shared));
    ptr = mmap(NULL, sizeof(struct Shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    sem_unlink(SEM_PATH);
    mutex = sem_open(SEM_PATH, O_CREAT, 0666, 1);
    sem_close(mutex);

    pause();

    return 0;
}

client.c

#include "common.h"

int main(int argc, char **argv)
{
    struct Shared *ptr;
    struct stat buf;
    sem_t *mutex;
    int fd;
    int nloop;
    int i;

    fd = shm_open(SHM_FILE, O_RDWR, 0);
    fstat(fd, &buf);
    ptr = mmap(NULL, buf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    mutex = sem_open(SEM_PATH, 0);
    nloop = atoi(argv[1]);

    for (i = 0; i < nloop; i++)
    {
        sem_wait(mutex);
        printf("pid %d: %d\n", getpid(), ptr->count++);
        sem_post(mutex);
    }

    return 0;
}

編譯並啟動server,阻塞在pasue()中。

后台同時運行三個client進程。

截取進程切換時的部分輸出片段,可以看到切換后計數依然是連續的。

進程同步——Posix無名信號量

common.h

#ifndef _COMMON_H_
#define _COMMON_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

#define SHM_FILE    "/shm_file"

struct Shared
{
    sem_t mutex;
    int count;
};

#endif

server.c

#include "common.h"

int main()
{
    struct Shared *ptr;
    int fd;

    shm_unlink(SHM_FILE);
    fd = shm_open(SHM_FILE, O_RDWR | O_CREAT, 0666);
    ftruncate(fd, sizeof(struct Shared));
    ptr = mmap(NULL, sizeof(struct Shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    sem_init(&ptr->mutex, 1, 1);

    pause();

    return 0;
}

client.c

#include "common.h"

int main(int argc, char **argv)
{
    struct Shared *ptr;
    struct stat buf;
    int fd;
    int nloop;
    int i;

    fd = shm_open(SHM_FILE, O_RDWR, 0);
    fstat(fd, &buf);
    ptr = mmap(NULL, buf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    //共享內存中的mutex已由server初始化,client直接使用就可以了,不能重復初始化
    nloop = atoi(argv[1]);

    for (i = 0; i < nloop; i++)
    {
        sem_wait(&ptr->mutex);
        printf("pid %d: %d\n", getpid(), ptr->count++);
        sem_post(&ptr->mutex);
    }

    return 0;
}

編譯並啟動server,阻塞在pasue()中。

后台同時運行三個client進程。

截取進程切換時的部分輸出片段,可以看到切換后計數依然是連續的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM