接续前面一篇《 第十一章 进程间通信IPC(一))》。
信号量的主要作用是同步进程之间和线程之间的操作,以达到无冲突的访问共享资源的目的。
POSIX中对信号量的操作有两种,wait和post。
POSIX信号量在操作的时候,只要不存在真正的两个线程争夺一把锁的情况,那么修改信号量就只是用户态的操作,并不牵扯到内核,所以新能比较好。
信号量分为有名信号量和无名信号量。这两种信号量的本质一样。无名信号量由于没有名字,没办法通过open操作直接找到对应的信号量,很难用于没有关联的两个进程之间,所以一般用于线程之间的同步。有名信号量,可以有多个不相干的进程通过名字打开同一个信号量,从而完成同步操作,所以有名信号量的操作更方便一些,适用范围更广。
创建或者打开有名信号量,需要调用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);
系统为了维护引用计数,只有当所有打开信号量的进程都关闭了之后才会真正的删除。
信号量的使用,总是和资源联系在一起使用。创建信号量时的value值是资源的初始值。申请资源时,需要调用wait系列函数。使用完,释放资源时,调用sem_post函数。
#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。
接口定义如下:
#include
int sem_post(sem_t *sem);
调用发布信号量之前,如果信号量值是0,并且已有进程或者线程在sem_wait阻塞等待,此时会有一个被唤醒,不能确定具体是那个。
成功返回0,失败返回-1,并设置errno。如果sem不合法,errno被设置为EINVAL;如果信号量的值超过上限,errno被设置为EOVERFLOW。
返回当前信号量的值,并将值写入 sval 指向的变量
#include
int sem_getvalue(sem_t *sem, int *sval);
如果信号量的值等于 0 ,同时又有很多进程或线程阻
塞在信号上,返回 0。
当 sem_getvalue 返回时,其返回的值可能已经过时了。从这个意义上讲,该接口的意义并不大。
无名信号量在进程间使用,由于进程间地址空间是独立的,所以需要将信号量放在共享内存区。一般用于线程间的同步操作。
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
pshared用于指明用于进程同步还是线程同步,0代表线程间,非0进程间。
返回0成功,-1失败并设置errno。
#include
int sem_destroy(sem_t *sem);
只有在说有的进程都不在等待一个信号量的时,才能被安全销毁。对于Linux省略也是ok的,因为在进程退出或者共享内存销毁的时候,匿名信号量的生命周期自动结束。
mmap系统调用的作用远大于共享内存,在ls命令,pthread_create,malloc等等的背后都有它的存在。
mmap的作用是在调用进程的虚拟地址空间创建一个新的内存映射。更具有无实体文件的关联分为一下两类
多个进程可以共享物理内存,映射到各个进程自己的虚拟地址空间。这种内存映射的共享,会在以下两种情况发生
mmap可以选择私有映射或者共享映射,私有映射是调用进程私有的,在fork后并不能与子进程指向同一物理内存。两种映射的区别如下
更具是否关联实体文件分为文件映射和匿名映射,更具是否进程共享分为私有和共享。他们两两组合会有四种情况
- | 文件映射 | 匿名映射 |
---|---|---|
共享 | 内存映射IO,进程间共享内存 | 进程间共享内存 |
私有 | 更具文件内容初始化内存 | 内存分配 |
#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。
创建共享文件映射的步骤如下:
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,但是在使用的时候会返回各种内存错误。
系统会对非页的整数倍的映射做向上取整,在访问填充部分,不会出错。
有两个作用:1、进程间通信。 2、操作文件
通过mmap映射共享内存来实现进程间通信,但是要考虑同步问题。
在mmap操作文件时,比普通read、write少一次从高速缓存页到用户空间的数据拷贝。但是者并不意味着,mmap比read、write快,mmap会引发缺页中断,缺页中断比内存拷贝更加消耗资源。所以,大部分情况是mmap更慢。
最常见的就是加载动态库,多个进程共享相同的文本段。为了防止恶意篡改,一般会用PROT_READ|PROT_EXEC保护。
和文件映射相对应,匿名映射没有文件对应。一般有两种创建匿名映射的方法。
上面两种方法得到的内存映射中的字节,都被初始化为0。
如果设置了MAP_SHARED标志,就是共享匿名映射,它的作用是让相关进程共享一块内存。比如父子进程,通过fork
flags设置为MAP_PRIVATE,一般的作用是分配内存。在glibc中的malloc实现,当分配的内存大于MMAP_THREASHOLD时,会调用mmap。
共享内存可以在无关的进程间共享一块内存区域。没创建一个POSIX共享内存,挂载在/dev/shm下。共享内存可以通过ftruncate函数动态的调整大小,也可以通过munmap和mmap重映射。
创建的本质上是两个接口,通过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,这块共享内存也会存在。除非重启电脑。
共享内存是建立在tmpfs基础上的,shm_open函数干了三件事情
其实就是给open函数穿了一个马甲
tmpfs是一个内存文件系统,该文件系统可将所有的文件内容保存到内存中,而不会写入到磁盘等持久化存储设备中。一旦umount或者系统重启,tmpfs中的内容全部丢失。