Posix信号量

c/c++

浏览数:131

2019-9-17

AD:资源代下载服务

1. Posix IPC

概述

以下三种类型的IPC合称为Posix IPC:

  • Posix信号量
  • Posix消息队列
  • Posix共享内存

Posix IPC在访问它们的函数和描述它们的信息上有一些类似点,主要包括:

  • IPC名字
  • 创建或打开时指定的读写权限、创建标志以及用户访问权限

下表汇总了所有Posix IPC函数。

  信号量 消息队列 共享内存
头文件 semaphore.h mqueue.h sys/mman.h
创建、打开或删除IPC的函数
 
 
 
 
 
sem_open
sem_close
sem_unlink
 
sem_init
sem_destroy
mq_open
mq_close
mq_unlink
 
 
 
shm_open
shm_unlink
 
 
 
 
控制IPC操作的函数
 
 
 
mq_getattr
mq_setattr
ftruncate
fstat
IPC操作函数
 
 
 
sem_wait
sem_trywait
sem_post
sem_getvalue
mq_send
mq_receive
mq_notify
 
mmap
munmap
 
 

IPC名字

除了Posix无名信号量,其余三种类型的Posix IPC都使用”Posix IPC”名字进行标识,它可能是文件系统中真实存在的一个路径名,也可能不是。Posix.1是这么描述的:

  • 它必须符合系统规定的路径名规则
  • 如果它以斜杠符开头,那么Posix IPC函数的不同调用将访问同一个IPC对象;否则,具体效果取决于系统实现
  • 对IPC名字中额外斜杠符的解释取决于系统实现

因此,为了便于代码移植,通常在实际项目中遵循下面两条规则:

  • Posix IPC名字必须以一个斜杠符开头,且不能再含有任何其他斜杠符
  • 把所有Posix IPC名字的宏定义统一放在一个便于修改的头文件中

创建与打开IPC

以下是三种Posix IPC的创建与打开函数:

  • sem_open用于创建或打开一个Posix有名信号量
  • mq_open用于创建或打开一个Posix消息队列
  • shm_open用于创建或打开一个Posix共享内存

读写权限与创建标志

这三个函数的第二个参数都是oflag,作用是指定IPC的读写权限与创建标志,下表给出了可组合构成该参数的所有常值。

说 明 sem_open mq_open shm_open
只读
只写
读写

O_RDONLY
O_WRONLY
O_RDWR
O_RDONLY

O_RDWR

若不存在则创建
排他性创建
O_CREAT
O_EXCL
O_CREAT
O_EXCL
O_CREAT
O_EXCL
非阻塞模式
若已存在则截短
O_NONBLOCK
O_EXCL
O_TRUNC

前三行指定读写权限:只读、只写、读写,从表中可以看出:

  • 有名信号量不指定该标志
  • 消息队列可指定任意模式
  • 共享内存不能以只写方式打开

后面四行指定创建标志:

  • O_CREAT若函数第一个参数指定的IPC不存在,则进行创建,此时至少需要第三个参数mode指定用户访问权限(详见后续)
  • O_EXCL:如果和O_CREAT一起指定,那么当IPC已存在且指定了O_CREAT | O_EXCL标志时,会出错返回EEXIST
  • O_NONBLOCK:仅适用于Posix消息队列,作用是队列为空时的读操作队列为满时的写操作不会阻塞
  • O_TRUNC:仅适用于Posix共享内存,作用是当共享内存对象已存在时,将其长度截为0

用户访问权限

创建一个新的Posix IPC时,需要使用第三个参数指定用户访问权限,它是由下表所示常值按位或构成的,常值的格式为S_IRXXX和S_IWXXX,其中XXX代表访问用户。

常 值 说 明
S_IRUSR
S_IWUSR
用户读
用户写
S_IRGRP
S_IWGRP
组成员读
组成员写
S_IROTH
S_IWOTH
其他用户读
其他用户写

IPC对象的持续性

IPC对象的持续性,指的是该类型的一个对象一直存在多长时间,IPC的持续性有三类:

  • 随进程持续:IPC对象一直存在到打开该对象的最后一个进程关闭该对象
  • 随内核持续:IPC对象一直存在到内核重新自举显式删除该对象为止
  • 随文件系统持续:IPC对象一直存在到显式删除该对象为止,即使内核重新自举,该对象依然存在

在默认情况下,除了Posix无名信号量是随进程持续,其余所有Posix IPC和System V IPC都是随内核持续。

2. 信号量概述

信号量定义及分类

信号量是一种用于进程间同步或线程间同步的机制,共有三种类型的信号量IPC:

  • Posix有名信号量
  • Posix无名信号量
  • System V信号量

按信号量值的范围,可分为:

  • 记录信号量:信号量的值可以为负数,负数的绝对值代表当前因等待该信号量的值变为正数而阻塞的进程和线程数
  • 计数信号量:信号量的值必须是非负整数,二值信号量(信号量值只能为0或1)是其特殊情况,Linux采用计数信号量

信号量操作

  • 创建(create):创建信号量时需要指定初始值
  • 等待(wait):也叫P操作,若信号量的值大于0就将它减1并结束操作,否则就阻塞等待
  • 挂出(post):也叫V操作,该操作将信号量的值加1

信号量、互斥锁和条件变量的差异

  • 互斥锁必须由给他上锁的线程解锁,而信号量的等待和挂出没有这种限制
  • 互斥锁只有上锁和解锁两种状态,信号量可以有多个状态,因为信号量的值可以有多个
  • 信号量挂出后的状态是持续的,即使挂出时没有线程阻塞于该信号量,挂出操作也不会丢失
  • 条件变量给线程发信号时,若没有相应的线程阻塞,那么给该信号将会丢失

3. Posix有名信号量

Posix有名信号量由IPC路径名标识,因此它天生既可用于线程同步,又可用于进程同步,相关API在头文件<semaphore.h>中,编译时需要指定链接-lrt-pthread

创建和打开

sem_open用于创建一个新的信号量或打开一个已存在的信号量。

//成功返回信号量指针,失败返回SEM_FAILED,链接时需指定 -lrt or -pthread
sem_t *sem_open(const char *name, int oflag, ... /*mode_t mode, unsigned int value*/);

函数参数说明在概述中基本都有介绍,这里不再赘述,只强调两点:

  • oflag只能指定为0、O_CREAT或O_CREAT | O_EXCL
  • value为信号量的初始值,可设范围为[0, SEM_VALUE_MAX]

在Linux中,创建的Posix有名信号量存放在/dev/shm/目录下,可通过ls命令查看:

#include <semaphore.h>
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <stdio.h>

#define POSIX_SEM_NAME  "sem_test"

int main()
{
    sem_t *sem = sem_open(POSIX_SEM_NAME, O_CREAT, 0666, 1);
    
    if (sem != SEM_FAILED)
    {
        printf("sem_open() success\n");
    } 
    
    return 0;   
}

关闭和删除

//两个函数返回值:成功返回0,失败返回-1
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
  • sem_close用于关闭已经打开的有名信号量
  • sem_unlink用于从系统中删除有名信号量

进程终止时,会自动关闭所有已打开的IPC对象(包括有名信号量、消息队列和共享内存),但关闭不等于删除,因为它们都至少具有随内核的持续性,这一点从上面示例代码的执行结果也可以看出来——进程已终止,但/dev/shm/目录下刚刚创建的信号量依然存在。事实上,所有以路径名标识的Posix IPC都有一个引用计数:

  • close和unlink会使引用计数减1
  • IPC名字本身也占用一个引用计数
  • 当引用计数大于0时,unlink就能够从文件系统中删除IPC对象
  • 如果在引用计数大于1时调用unlink,IPC对象会被删除,但不会被析构
  • 只有当引用计数变为0,即在引用计数为1时调用unlink,内核才会对IPC对象进行析构
#include <semaphore.h>
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <stdio.h>

#define POSIX_SEM_NAME  "sem_test"

int main()
{
    sem_t *sem = sem_open(POSIX_SEM_NAME, O_CREAT, 0666, 1);
    
    if (sem != SEM_FAILED)
    {
        printf("sem_open() success\n");
        
        printf("before sem_unlink()\n");
        system("ls /dev/shm/");
        
        sem_close(sem);
        sem_unlink(POSIX_SEM_NAME);
        
        printf("after sem_unlink()\n");
        system("ls /dev/shm/");
    } 
    
    return 0;   
}

等待和挂出

//两个函数返回值:成功返回0,失败返回-1
int sem_wait(sem_t *sem);
int sem_post(sem_t *name);

sem_wait用于等待有名信号量:

  • 若信号量的值等于0,调用线程将阻塞,直到该值变为大于0
  • 若信号量的值大于0,就将它减1并立即返回

sem_post用于挂出有名信号量,该函数把信号量的值加1,然后阻塞于sem_wait等待该信号量的线程就能够被唤醒。

#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>

#define POSIX_SEM_NAME  "sem_test"

pthread_t tid[2];
sem_t *sem;

/*thread0先处理自己的工作,之后调用sem_post将信号量的值加1,通知thread1可以执行了*/
void *thread0(void *arg)
{
    int value;
    
    while (1)
    {
        /* do work thread0 */
        
        sem_post(sem);
        sem_getvalue(sem, &value);
        printf("thread 0: sem value is %d\n", value);
        sleep(2);
    }
}

/*thread1等待时间比thread0少,但也必须等待thread0调用sem_post将信号量的值加1,才能继续执行*/
void *thread1(void *arg)
{
    int value;
    
    while (1)
    {
        sem_wait(sem);
        sem_getvalue(sem, &value);
        printf("thread 1: sem value is %d\n", value);
        sleep(1);
    } 
}

int main()
{    
    sem = sem_open(POSIX_SEM_NAME, O_CREAT, 0666, 0);
     
    pthread_create(&tid[0], NULL, thread0, NULL);
    pthread_create(&tid[1], NULL, thread1, NULL);  
    sleep(10);

    pthread_cancel(tid[0]);
    pthread_join(tid[0], NULL);
    
    pthread_cancel(tid[1]);
    pthread_join(tid[1], NULL);
    
    sem_close(sem);
    sem_unlink(POSIX_SEM_NAME);
    
    return 0;
}

获取信号量的值

//成功返回0,失败返回-1
int sem_getvalue(sem_t *sem, int *sval);

sem_getvalue用于获取信号量sem的当前值,该值通过参数sval返回。如果有线程或进程正阻塞于sem_wait,POSIX.1-2001允许通过sval返回两种结果:

  • 返回0,这也是Linux的选择,因为Linux采用计数信号量
  • 返回一个负值,其绝对值代表当前阻塞于sem_wait调用的进程和线程数,对应记录信号量

4. Posix无名信号量

Posix无名信号量是基于内存的信号量,也就是说它没有IPC路径名,而是像普通变量一样创建在内存中。

  • Posix无名信号量由sem_init初始化,由sem_destroy销毁
  • Posix无名信号量没有close和unlink之分,销毁即彻底删除
  • Posix无名信号量等待、挂出、获取信号量的值使用和有名信号量相同的API
//两个函数返回值:成功返回0,失败返回-1
int sem_init(sem_t *sem, int shared, unsigned int value);
int sem_destroy(sem_t *sem);

sem_init的sem参数指向要初始化的信号量,shared参数用于指定信号量在线程间共享还是在进程间共享:

  • shared = 0:在线程间共享,信号量创建在当前进程地址空间中,可用于线程间同步,随进程持续
  • shared ≠ 0:在进程间共享,信号量必须创建在共享内存中,可用于进程间同步,随内核持续

一般来说,线程间同步使用有名信号量和无名信号量都可以,而进程间同步直接使用有名信号量就可以了,除非对通讯速度有特殊需求,才考虑shared ≠ 0的无名信号量。

把第3章的示例代码改为使用shared = 0的无名信号量,只有main函数发生了变动,如下所示:

int main()
{    
    sem = (sem_t *)malloc(sizeof(sem_t)); //这里使用动态分配,也可以使用静态分配sem,然后给sem_init传&sem
    sem_init(sem, 0, 0);
     
    pthread_create(&tid[0], NULL, thread0, NULL);
    pthread_create(&tid[1], NULL, thread1, NULL);  
    sleep(10);

    pthread_cancel(tid[0]);
    pthread_join(tid[0], NULL);
    
    pthread_cancel(tid[1]);
    pthread_join(tid[1], NULL);
    
    free(sem);
    sem_destroy(sem);
    
    return 0;
}

5. Posix信号量限制

Posix定义了两个信号量限制:

  • SEM_NSEMS_MAX:一个进程可同时打开的最大信号量个数,该值至少为256
  • SEM_VALUE_MAX:信号量的最大值,该值至少为32767

作者:原野追逐