【Linux】POSIX信号量详解,从原理认识到代码实现线程间通信

1.POSIX信号量:

POSIX提供了两种信号量:有名信号量和无名信号量。 这两种信号量的本质都是一样的。
在这里插入图片描述

1.1信号量的本质是什么

本质上为计数器 + PCB等待队列 + 一堆接口(等待接口,唤醒接口)
计数器:本质上是对资源的计数

  1. 当执行流获取信号量成功之后,信号量当中的计数器会进行减1操作,当获取失败后,该执行流就会被放到PCB等待队列中去。
  2. 当执行流释放信号量成功之后,信号量当中的计数器会进行加1操作。

又称为基于内存的信号量, 由于其没有名字, 没法通过open操作直接找到对应的信号量, 所以很难直接用于没有关联的两个进程之间。 无名信号量多用于线程之间的同步

2.有名信号量

有名信号量由于其有名字, 多个独立的进程可以通过名字来打开同一个信号量, 从而完成同步操作, 所以有名信号量的操作要方便一些, 适用范围也比无名信号量更广。

2.1有名信号量创建

#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);
参数说明
oflag可取O_CREAT和O_EXCL,取O_CREAT时,表示要创建信号量
mode创建新的信号量的权限,标志位和open函数一样,mode的参数也会根据进程的umask来取掩码
value新创建信号量的初始值

当sem_open函数失败时, 返回SEM_FAILED, 并且设置errno。

2.2有名信号量的关闭

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

当一个进程打开有名信号量时, 系统会记录进程与信号的关联关系。 调用sem_close时, 会终止这种关联关系, 同时信号量的进程数的引用计数减1。

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

注意:关闭不等于删除。

2.3有名信号量的删除

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

将有名信号量的名字作为参数, 传递给sem_unlink, 该函数会负责将该有名信号量删除。 由于系统为信号量维护了引用计数, 所以只有当打开信号量的所有进程都关闭了之后, 才会真正地删除。

3.无名信号量

无名信号量, 由于其没有名字, 所以适用范围要小于有名信号量。 只有将无名信号量放在多个进程或线程都共同可见的内存区域时才有意义, 否则协作的进程无法操作信号量, 达不到同步或互斥的目的。 所以一般而言, 无名信号量多用于线程之间。 因为线程会共享地址空间, 所以访问共同的无名信号量是很容易办到的事情。 或者将信号量创建在共享内存内, 多个进程通过操作共享内存的信号量达到同步或互斥的目的。

3.1无名信号量的创建

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明
sem传入信号量的地址
pshared0:用于线程间、全局变量,非0:用于进程间,信号量所用资源在共享内存中开辟
value资源的个数,本质上是初始化信号量计数器的

无名信号量的生命周期是有限的, 对于线程间共享的信号量, 线程组退出了,无名信号量也就不复存在了。 对于进程间共享的信号量, 信号量的持久性与所在的共享内存的持久性一样。

无名信号量初始化以后, 就可以像操作有名信号量一样操作无名信号量了。

3.2无名信号量的销毁

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

sem_destroy用于销毁sem_init函数初始化的无名信号量。 只有在所有进程都不会再等待一个信号量时, 它才能被安全销毁。

4.信号量的使用

信号量的使用, 总是和某种可用资源联系在一起的。 创建信号量时的value值,其实指定了对应资源的初始个数。 当申请该资源时, 需要先调用sem_wait函数; 当发布该资源或使用完毕释放该资源时, 则调用sem_post函数。

4.1等待信号量

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

注意:调用该接口的执行流会对计数器进行减1。

两种情况:

  1. 如果减1操作执行完之后,计数器的值是大于0的,表示可以访问临界资源,意味着sem_wait可以返回。
  2. 如果减1操作执行完之后,计数器的值是小于0的,该接口的执行流被阻塞,该执行流被放入到PCB等待队列当中。
int sem_trywait(sem_t *sem);

sem_trywait会尝试将信号量的值减1, 如果信号量的值大于0, 那么该函数将信号量的值减1之后会立刻返回。 如果信号量的当前值为0, 那么sem_trywait也不会陷入阻塞, 而是立刻返回失败, 并置errno为EAGAIN。
EAGAIN:当前操作不会执行,但不会阻塞。

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

如果超过了等待时间, 信号量的值仍然为0, 那么返回-1, 并置errno为ETIMEOUT。
ETIMEOUT:超出等待时间限制。

4.2发布信号量

sem_post函数用于发布信号量, 表示资源已经使用完毕, 可以归还资源了。 该函数会使信号量的值加1。

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

如果发布信号量之前, 信号量的值是0, 并且已经有进程或线程正等待在信号量上, 此时会有一个进程被唤醒, 被唤醒的进程会继续sem_wait函数的减1操作。如果有多个进程正等待在信号量上, 那么将无法确认哪个进程会被唤醒。

当函数调用成功时, 返回0; 失败时, 返回-1, 并置errno。 当参数sem并不指向合法的信号量时, 置errno为EINVAL; 当信号量的值超过上限时, 置errno为EOVERFLOW。

5.信号量如何实现互斥

  1. 初始信号量当中的计数器为1,表示只有一个资源可以使用。
  2. 当执行流A想要访问临界资源的时候,首先获取信号量,由于计数器当中值为1,表示可以访问,计数器的值从1变为0,从而执行流A去访问临界资源。
  3. 此时当执行流B想要访问临界资源的时候,首先获取信号量,但是计数器当中值为0,表示不能访问当前临界资源,执行流B就被放到了PCB等待队列当中,同时信号量当中的计数器值从0变为-1(0–>-1),-1表示当前还有一个执行流在等待访问临界资源。

6.信号量如何实现同步

  1. 不要求信号量当中的计数器一定为1,也可以为其它整数
  2. 当执行流想要访问临界资源的时候,首先获取信号量
  3. 如果信号量当中计数器值大于0,则表示能够访问临界资源,该执行流不会阻塞,顺序执行临界区代码
  4. 如果信号量当中计数器值小于等于0,则表示不能访问该临界资源,该执行流就会被放到PCB等待队列中去,同时计数器也会进行减一操作
  5. 当发布信号量的时候,会对信号量当中的计数器进行加1操作,是否唤醒PCB等待队列的执行流?
  6. 若加1之后,计数器还为负数或者为0,则需要通知PCB等待队列当中的执行流
  7. 计数器加1之后为整数,则不需要通知PCB等待队列。

注意:计数器值为负,则代表还有多少个执行流在PCB等待队列中等待。

7.无名信号量实现生产者与消费者模型

代码:下述代码,可随意更改资源数量和线程数量,结果都是可行的,附注释

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#include <sys/syscall.h>
#include <iostream>

#define REOURSECOUNT 4 //资源数量
#define PTHREADCOUNT 2 //线程数量

class ModelOfProdConsByPosix
{
  public:
    ModelOfProdConsByPosix()//构造
      :_queue(REOURSECOUNT)
    {
      _capacity =  REOURSECOUNT;
      sem_init(&_lock,0,1);//0代表线程间,1为计数器的初始值

      sem_init(&_prod,0,REOURSECOUNT);//生产者,最开始计数器初始值为4,表示可以有4个资源可用
      sem_init(&_cons,0,0);//消费者

      _write_pos = _read_pos = 0;
    }

    ~ModelOfProdConsByPosix()//析构
    {
      sem_destroy(&_lock);
      sem_destroy(&_prod);
      sem_destroy(&_cons);
    }

    void push(int& data)//插入数据
    {
      sem_wait(&_prod);
      sem_wait(&_lock);
      _queue[_write_pos] = data;//在这里我们不需要循环判断,因为可用资源为RESOURCECOUNT个,每写入数据之后,计数器就会减1
      _write_pos = (_write_pos + 1) % _capacity;
      sem_post(&_lock);
      sem_post(&_cons);
    }
    void pop(int &data)//删除数据
    {
      sem_wait(&_cons);
      sem_wait(&_lock);
      data = _queue[_read_pos];//pop时,也不需要循环判断,生产者给消费者发送信号量之后,其计数器就会加1,当pop之后,其计数器就会减1,就达到了同步和互斥
      _read_pos = (_read_pos + 1) % _capacity;
      sem_post(&_lock);
      sem_post(&_prod);
    }
  private:
    std::vector<int> _queue;
    int _capacity;
    sem_t _lock;
    sem_t _prod;
    sem_t _cons;
    int _write_pos;
    int _read_pos;
};


void *ConsumerStart(void *arg)
{
  ModelOfProdConsByPosix *cp = (ModelOfProdConsByPosix*) arg;
  int data = 0;
  while(1)
  {
    cp->pop(data);
    printf("i am pid : %d,i consume : %d\n",(int)syscall(SYS_gettid),data);
  }
}

void *ProductStart(void *arg)
{
  ModelOfProdConsByPosix *cp = (ModelOfProdConsByPosix*) arg;
  int data = 0;
  while(1)
  {
    cp->push(data);
    printf("i am pid : %d,i product : %d\n",(int)syscall(SYS_gettid),data);
    ++data;
  }
}
int main()
{
  ModelOfProdConsByPosix *cp = new ModelOfProdConsByPosix;
  pthread_t cons[PTHREADCOUNT],prod[PTHREADCOUNT];
  for(int i = 0; i < PTHREADCOUNT;++i)
  {
    int ret = pthread_create(&cons[i],NULL,ConsumerStart,(void*)cp);
    if(ret < 0)
    {
      perror("pthread_create");
      return -1;
    }
    ret = pthread_create(&prod[i],NULL,ProductStart,(void*)cp);
    if(ret < 0)
    {
      perror("pthread_create");
      return -1;
    }
  }

  for(int i = 0;i < PTHREADCOUNT;++i)
  {
    pthread_join(cons[i],NULL);
    pthread_join(prod[i],NULL);
  }

  return 0;
}

结果:

在这里插入图片描述

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值