第十一章 进程间通信IPC(二),信号量、mmap和共享内存

接续前面一篇《 第十一章 进程间通信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 
#include 
#include 

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 
int sem_close(sem_t *sem);

进程终止的时,打开的信号量会自动关闭。当执行exec系列的函数时,进程打开的有名信号量会自动关闭。

关闭不等同删除,如果要删除信号量则需要调用sem_unlink函数,接口定义如下:

#include 
int sem_unlink(const char *name)

系统为了维护引用计数,只有当所有打开信号量的进程都关闭了之后才会真正的删除。

2.信号量的使用

信号量的使用,总是和资源联系在一起使用。创建信号量时的value值是资源的初始值。申请资源时,需要调用wait系列函数。使用完,释放资源时,调用sem_post函数。

  1. 等待信号量
#include 
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 
int sem_post(sem_t *sem);

调用发布信号量之前,如果信号量值是0,并且已有进程或者线程在sem_wait阻塞等待,此时会有一个被唤醒,不能确定具体是那个。

成功返回0,失败返回-1,并设置errno。如果sem不合法,errno被设置为EINVAL;如果信号量的值超过上限,errno被设置为EOVERFLOW。

  1. 获取信号量的值

返回当前信号量的值,并将值写入 sval 指向的变量

#include 
int sem_getvalue(sem_t *sem, int *sval);

如果信号量的值等于 0 ,同时又有很多进程或线程阻
塞在信号上,返回 0。

当 sem_getvalue 返回时,其返回的值可能已经过时了。从这个意义上讲,该接口的意义并不大。

3.无名信号量的创建和销毁

无名信号量在进程间使用,由于进程间地址空间是独立的,所以需要将信号量放在共享内存区。一般用于线程间的同步操作。

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

pshared用于指明用于进程同步还是线程同步,0代表线程间,非0进程间。
返回0成功,-1失败并设置errno。

  1. 销毁无名信号量
#include 
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 
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 
    long sysconf(int name);
    //调用实例
    long pagesize = sysconf(_SC_PAGESIZE);
    

mmap映射到进程中,映射区总是页的整数倍。参数lenght不是页的整数倍时,向上取整多余部分填充0。调用成功返回映射区域起始地址,失败返回MAP_FAILED,并设置errno。

如果不再需要对应的内存映射了,可以调用 munmap 函数,解除该内存映射:

#include 
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和共享内存_第1张图片
访问超出映射区的内容,触发SIGSEGV信号


第十一章 进程间通信IPC(二),信号量、mmap和共享内存_第2张图片

系统会对非页的整数倍的映射做向上取整,在访问填充部分,不会出错。


第十一章 进程间通信IPC(二),信号量、mmap和共享内存_第3张图片
访问灰色部分不会有问题


第十一章 进程间通信IPC(二),信号量、mmap和共享内存_第4张图片
映射区大于文件,访问出错

  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 
#include 
#include 
int shm_open(const char *name, int oflag, mode_t mode);

内核会自动设置FD_CLOEXEC标志位,即如果进程执行了exec函数,该文件描述符会自动关闭。

应为共享内存是文件,所以可以调用文件相关函数,如 fstat 函数、 fchmod 函数和 fchwon 函数。其
中最重要常用的函数要属 ftruncate 函数。因为新创建的共享内存,默认大小总是 0 。所以在调用 mmap 之
前,需要先调用 ftruncate 函数,以调整文件的大小。

//调整共享内存大小
#include 
int ftruncate(int fd, off_t length);

//获取共享内存大小
#include 
#include 
#include 
int fstat(int fd, struct stat *buf);

对这块内存的所有修改,其他进程都是可见的。

结束通信后可以通过munmap函数解除映射。如果彻底不需要这块内存,可以通过shm_unlink函数来删除。

int shm_unlink(const char *name);

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中的内容全部丢失。

你可能感兴趣的:(Linux环境编程,linux,多进程,posix)