天天看點

第十一章 程序間通信IPC(二),信号量、mmap和共享記憶體一、信号量二、記憶體映射mmap三、POSIX共享記憶體

接續前面一篇《 第十一章 程序間通信IPC(一))》。

目錄

  • 一、信号量
      • 1.建立、打開、關閉和删除有名信号量
      • 2.信号量的使用
      • 3.無名信号量的建立和銷毀
  • 二、記憶體映射mmap
      • 1.概述
      • 2.相關接口
      • 3.共享檔案映射
      • 4.私有檔案映射
      • 5.共享匿名映射
      • 6.私有匿名映射
  • 三、POSIX共享記憶體
      • 1.共享記憶體的建立、使用和删除
      • 2.共享記憶體與tmpfs

提示:這一章主要介紹POSIX IPC中的信号量和記憶體映射

一、信号量

信号量的主要作用是同步程序之間和線程之間的操作,以達到無沖突的通路共享資源的目的。

POSIX中對信号量的操作有兩種,wait和post。

  • 信号量講建立和初始化合二為一,避免可能出現競争條件問題。
  • 修改信号量值的接口(sem_post和sem_wait),一次隻能修改一個信号量
  • 修改信号量值的接口(sem_post和sem_wait),一次隻能将信号量的值加1或者減1.
  • 信号量沒有提供一個等待信号量變為0的接口
  • 信号量并沒有提供UNDO操作。

POSIX信号量在操作的時候,隻要不存在真正的兩個線程争奪一把鎖的情況,那麼修改信号量就隻是使用者态的操作,并不牽扯到核心,是以新能比較好。

信号量分為有名信号量和無名信号量。這兩種信号量的本質一樣。無名信号量由于沒有名字,沒辦法通過open操作直接找到對應的信号量,很難用于沒有關聯的兩個程序之間,是以一般用于線程之間的同步。有名信号量,可以有多個不相幹的程序通過名字打開同一個信号量,進而完成同步操作,是以有名信号量的操作更友善一些,适用範圍更廣。

1.建立、打開、關閉和删除有名信号量

建立或者打開有名信号量,需要調用sem_open函數,接口如下:

#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

sem_t *sem_open (const char *name, int oflag);
sem_t *sem_open (const char *name, int oflag, mode_t mode, unsigned int value);
           

參數name、oflag和mode在前面已經介紹過了,現在說一下value。value是建立信号量的初始值,建立和初始化都是一個接口完成。value的值在最小值0和最大值SEM_VALUE_MAX之間。SUSv3要求最大值至少等于32767,對于Linux而言,這個限制為INT_MAX(Linux/x86該值是2147483647)。

當sem_open失敗時,傳回SEM_FAILED,并且設定errno。

注意,不要嘗試建立sem_t結構的副本,錯誤示範如下:

sem_t *sem_p, sem_dup;
sem_p = sem_open(.....);
sem_dup = *sem_p; //錯誤的
           

當一個程序打開有名信号量的時候,系統會記錄程序與信号量的關聯關系。調用sem_close時,會終止這種關聯關系,同僚信号量的引用計數減1。關閉信号量的接口定義如下:

#include <semaphore.h>
int sem_close(sem_t *sem);
           

程序終止的時,打開的信号量會自動關閉。當執行exec系列的函數時,程序打開的有名信号量會自動關閉。

關閉不等同删除,如果要删除信号量則需要調用sem_unlink函數,接口定義如下:

#include <semaphore.h>
int sem_unlink(const char *name);
           

系統為了維護引用計數,隻有當所有打開信号量的程序都關閉了之後才會真正的删除。

2.信号量的使用

信号量的使用,總是和資源聯系在一起使用。建立信号量時的value值是資源的初始值。申請資源時,需要調用wait系列函數。使用完,釋放資源時,調用sem_post函數。

  1. 等待信号量
#include <semaphore.h>
int sem_wait(sem_t *sem); //(1)
int sem_trywait(sem_t *sem); //(2)
int sem_timedwait(sem_t *sem, struct *abs_timeout); //(3)
           

成功傳回0,失敗傳回-1。如果傳回成功,信号量會被原子的減1。

- 标号(1) 标号(2) 标号(3)
阻塞 目前value不大于0,等待值大于0 - -
傳回0 value減1 value減1 value減1
傳回-1 在阻塞期間被信号打斷,設定errno為EINTR 目前值不大于0,并設定errno為EAGAIN 等待value大于0的時間已經超過abs_timeout,設定errno為ETIMEOUT
  1. 釋出信号量

表示資源已經使用完畢,可以歸還資源了。該函數會使資源值加1。

接口定義如下:

#include <semaphore.h>
int sem_post(sem_t *sem);
           

調用釋出信号量之前,如果信号量值是0,并且已有程序或者線程在sem_wait阻塞等待,此時會有一個被喚醒,不能确定具體是那個。

成功傳回0,失敗傳回-1,并設定errno。如果sem不合法,errno被設定為EINVAL;如果信号量的值超過上限,errno被設定為EOVERFLOW。

  1. 擷取信号量的值

傳回目前信号量的值,并将值寫入 sval 指向的變量

#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
           

如果信号量的值等于 0 ,同時又有很多程序或線程阻

塞在信号上,傳回 0。

當 sem_getvalue 傳回時,其傳回的值可能已經過時了。從這個意義上講,該接口的意義并不大。

3.無名信号量的建立和銷毀

無名信号量在程序間使用,由于程序間位址空間是獨立的,是以需要将信号量放在共享記憶體區。一般用于線程間的同步操作。

  1. 無名信号量的初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
           

pshared用于指明用于程序同步還是線程同步,0代表線程間,非0程序間。

傳回0成功,-1失敗并設定errno。

  1. 銷毀無名信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
           

隻有在說有的程序都不在等待一個信号量的時,才能被安全銷毀。對于Linux省略也是ok的,因為在程序退出或者共享記憶體銷毀的時候,匿名信号量的生命周期自動結束。

二、記憶體映射mmap

mmap系統調用的作用遠大于共享記憶體,在ls指令,pthread_create,malloc等等的背後都有它的存在。

1.概述

mmap的作用是在調用程序的虛拟位址空間建立一個新的記憶體映射。更具有無實體檔案的關聯分為一下兩類

  • 檔案映射:記憶體映射區有實體檔案與之關聯。mmap将普通檔案的一部分内容直接映射到調用程序的虛拟位址空間。一旦完成映射,就可以通過相應的記憶體區域位址來操作檔案内容。
  • 匿名映射:沒有實體檔案關聯,映射的記憶體區域初始化為0

多個程序可以共享實體記憶體,映射到各個程序自己的虛拟位址空間。這種記憶體映射的共享,會在以下兩種情況發生

  • 通過fork,子程序繼承了父程序通過mmap映射的副本。
  • 多個程序通過mmap映射同一個檔案的同一區域。

mmap可以選擇私有映射或者共享映射,私有映射是調用程序私有的,在fork後并不能與子程序指向同一實體記憶體。兩種映射的差別如下

  • 私有映射(MAP_PRIVATE):映射内容上發生變化對其他程序是不可見的。對于檔案映射,變更不會同步到底層檔案中。
  • 共享映射(MAP_SHARED):在映射内容上不生的變更,對所有共享同一映射的程序都是可見的。對檔案映射而言,變更會同步到底層檔案中。很顯然共享映射是用于程序通信的。

更具是否關聯實體檔案分為檔案映射和匿名映射,更具是否程序共享分為私有和共享。他們兩兩組合會有四種情況

- 檔案映射 匿名映射
共享 記憶體映射IO,程序間共享記憶體 程序間共享記憶體
私有 更具檔案内容初始化記憶體 記憶體配置設定

2.相關接口

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
           

fd、offset和length:指定了記憶體映射的源。fd對應檔案描述符,從檔案的offset位置起,長度是lenght的内容映射到虛拟位址空間。對于檔案映射,需要先調用open函數擷取檔案的描述符fd,匿名映射必須為-1。

addr:指定将檔案對應内容映射到程序虛拟位址空間的起始位址。一般用NULL,代表讓核心選擇,友善移植。

prot:設定對記憶體映射區域的保護,分别為PROT_EXEC、PROT_READ、PROT_WRITE和PROT_NONE分别代表可執行、可讀、可寫和不可通路。

flags:指定是共享映射還是私有映射,是檔案映射還是匿名映射。如下

标志位 說明
MAP_SHARED 請求建立共享映射,和MAP_PRIVATE互斥隻能使用一個
MAP_PRIVATE 請求建立私有映射,和MAP_SHARED互斥隻能使用一個
MAP_ANONYMOUS 請求建立匿名映射,fd必須-1
MAP_FIXED 鐵了心的要映射到對應的位址,addr一般要求按頁對其,核心無法完成映射傳回失敗。如果程序的記憶體區域和映射的位址區域有重疊,将覆寫。

參數 addr 和 offset 都必須按頁對齊,Linux一般為4096位元組。也可以通過一下兩種方式擷取

  • 指令行方式
    getconf PAGESIZE
               
  • 代碼方式
    //函數原型
    #include <unistd.h>
    long sysconf(int name);
    //調用執行個體
    long pagesize = sysconf(_SC_PAGESIZE);
               

mmap映射到程序中,映射區總是頁的整數倍。參數lenght不是頁的整數倍時,向上取整多餘部分填充0。調用成功傳回映射區域起始位址,失敗傳回MAP_FAILED,并設定errno。

如果不再需要對應的記憶體映射了,可以調用 munmap 函數,解除該記憶體映射:

#include <sys/mman.h>
int munmap(void *addr, size_t length);
           

addr是mmap傳回的起始位址,length是映射區的大小。關閉對應檔案描述符不會引發munmap。

3.共享檔案映射

  1. 共享檔案映射的建立和使用

建立共享檔案映射的步驟如下:

a. 打開檔案,擷取檔案描述符。這一步通過open來完成。

b. 将檔案描述符fd作為參數,傳入mmap函數

僞代碼

fd = open(...);
addr = mmap(..., MAP_SHARED, fd, ...);
close(fd); //更具以後是否用到檔案描述符來确定是否close,close後不會解除記憶體映射
           

open函數中的權限指定要與mmap中的一緻,open至少要有讀權限。如果在mmap中prot指定了PROT_WRITE,flags指定MAP_SHARED,open中需要O_RDWR标志。

不是所有的檔案都支援mmap,比如管道檔案。

調用mmap隻是建立一種聯系,并不會把檔案的内容加載到映射區,隻有在對映射區做讀寫操作的時候引發缺頁中斷,才會加載檔案中的資料。

offset應小于檔案長度,offset+length也要小于檔案長度。

mmap會檢查offset是否為系統分頁的整數倍,如果不是傳回MAP_FAILED。不會檢查offset和offset+length是否有效,即使無效也不會傳回MAP_FAILED,但是在使用的時候會傳回各種記憶體錯誤。

第十一章 程式間通信IPC(二),信号量、mmap和共享記憶體一、信号量二、記憶體映射mmap三、POSIX共享記憶體

通路超出映射區的内容,觸發SIGSEGV信号

第十一章 程式間通信IPC(二),信号量、mmap和共享記憶體一、信号量二、記憶體映射mmap三、POSIX共享記憶體

系統會對非頁的整數倍的映射做向上取整,在通路填充部分,不會出錯。

第十一章 程式間通信IPC(二),信号量、mmap和共享記憶體一、信号量二、記憶體映射mmap三、POSIX共享記憶體

通路灰色部分不會有問題

第十一章 程式間通信IPC(二),信号量、mmap和共享記憶體一、信号量二、記憶體映射mmap三、POSIX共享記憶體

映射區大于檔案,通路出錯

  1. 共享檔案映射的用途

有兩個作用:1、程序間通信。 2、操作檔案

通過mmap映射共享記憶體來實作程序間通信,但是要考慮同步問題。

在mmap操作檔案時,比普通read、write少一次從高速緩存頁到使用者空間的資料拷貝。但是者并不意味着,mmap比read、write快,mmap會引發缺頁中斷,缺頁中斷比記憶體拷貝更加消耗資源。是以,大部分情況是mmap更慢。

4.私有檔案映射

最常見的就是加載動态庫,多個程序共享相同的文本段。為了防止惡意篡改,一般會用PROT_READ|PROT_EXEC保護。

5.共享匿名映射

和檔案映射相對應,匿名映射沒有檔案對應。一般有兩種建立匿名映射的方法。

  • 在mmap中的flags中指定MAP_ANONYMOUS,fd設定為-1。
  • 打開/dev/zero裝置,并将得到的fd傳給mmap。

上面兩種方法得到的記憶體映射中的位元組,都被初始化為0。

如果設定了MAP_SHARED标志,就是共享匿名映射,它的作用是讓相關程序共享一塊記憶體。比如父子程序,通過fork

6.私有匿名映射

flags設定為MAP_PRIVATE,一般的作用是配置設定記憶體。在glibc中的malloc實作,當配置設定的記憶體大于MMAP_THREASHOLD時,會調用mmap。

三、POSIX共享記憶體

共享記憶體可以在無關的程序間共享一塊記憶體區域。沒建立一個POSIX共享記憶體,挂載在/dev/shm下。共享記憶體可以通過ftruncate函數動态的調整大小,也可以通過munmap和mmap重映射。

1.共享記憶體的建立、使用和删除

建立的本質上是兩個接口,通過shm_open傳回檔案描述符,通過mmap映射到程序的位址空間。

接口定義如下:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open(const char *name, int oflag, mode_t mode);
           

核心會自動設定FD_CLOEXEC标志位,即如果程序執行了exec函數,該檔案描述符會自動關閉。

應為共享記憶體是檔案,是以可以調用檔案相關函數,如 fstat 函數、 fchmod 函數和 fchwon 函數。其

中最重要常用的函數要屬 ftruncate 函數。因為新建立的共享記憶體,預設大小總是 0 。是以在調用 mmap 之

前,需要先調用 ftruncate 函數,以調整檔案的大小。

//調整共享記憶體大小
#include <unistd.h>
int ftruncate(int fd, off_t length);

//擷取共享記憶體大小
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);
           

對這塊記憶體的所有修改,其他程序都是可見的。

結束通信後可以通過munmap函數解除映射。如果徹底不需要這塊記憶體,可以通過shm_unlink函數來删除。

shm_unlink不影響既有的映射,不可以重新mmap。當使用者都調用了munmap後,引用計數為0,才會真正的删除。

如果不調用shm_unlink即使所有的程序都munmap,這塊共享記憶體也會存在。除非重新開機電腦。

2.共享記憶體與tmpfs

共享記憶體是建立在tmpfs基礎上的,shm_open函數幹了三件事情

  1. 處理傳入的字元串參數,獲得全路徑的name: /dev/shm/name
  2. 建立或者打開/dev/shm/name檔案
  3. 為打開的檔案設定FD_CLOEXEC标志

其實就是給open函數穿了一個馬甲

tmpfs是一個記憶體檔案系統,該檔案系統可将所有的檔案内容儲存到記憶體中,而不會寫入到磁盤等持久化儲存設備中。一旦umount或者系統重新開機,tmpfs中的内容全部丢失。

繼續閱讀