【Linux从入门到精通】信号量(信号量的原理与相关操作接口)详解

这篇具有很好参考价值的文章主要介绍了【Linux从入门到精通】信号量(信号量的原理与相关操作接口)详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  本篇文章重点对信号量的概念,信号量的申请、初始化、释放、销毁等操作进行讲解。同时举例把信号量应用到生产者消费者模型来理解。希望本篇文章会对你有所帮助。

目录

一、信号量概念

1、1 什么是信号量

1、2 为什么要有信号量

1、3 信号量的PV操作

二、信号量的相关接口

2、1 sem_t

2、2 sem_init

2、3 sem_wait

2、4 sem_post

2、5 sem_destory

三、基于信号量的生产者消费者模型

3、1 信号量控制环形队列

3、1、1 空间资源和数据资源

3、1、2 保护原理(二元信号量)

3、2 demo代码

3、2、1 单生产与单消费

 3、2、2 多生产多消费


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:Linux从入门到精通  👀

💥 标题:信号量💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️

一、信号量概念

1、1 什么是信号量

  我们之前学了互斥锁和体哦阿健变量可以实现线程的互斥与同步。那么还有其他方法吗?信号量也可以做到!

  信号量(Semaphore)是操作系统中一种用于实现线程间同步与互斥的机制。它本质就是一个计数器,用于控制多个线程对共享资源的访问信号量可以被视为一个简单的整数变量,并且可以进行原子操作,包括等待(wait)和释放(signal)

1、2 为什么要有信号量

  信号量(Semaphore)是一种多线程同步的机制,用于解决并发环境中的资源竞争问题。在并发编程中,多个线程可能同时访问共享资源,如果不对资源进行合理的管理,就会导致数据不一致或错误的结果。

  我们在学习互斥锁时,一个线程在操作临界资源的时候,必须临界资源是满足条件的!可是公共资源是否满足生产或者消费条件,我们无法直接得知。因为你要检测,本质也是在访问临界资源。所以只能先加锁,再检测,再操作,再解锁。只要我们对资源进行整体加锁,就默认了我们对这个资源整体使用。但是,有时候会是一份临界资源同时访问不同的区域。这时互斥锁并不能很好的满足对临界资源的充分利用。在这种情况下就可以引入信号量来很好的解决。具体如下图:

【Linux从入门到精通】信号量(信号量的原理与相关操作接口)详解,Linux从入门到精通,linux,c++,多线程,信号量,生产者消费者问题

  现在我们有一个共享资源,不当做一个整体,而让不同的执行流访问不同的区域的话,那么不就可以继续并发了。

1、3 信号量的PV操作

  这里会有一个疑问,我们怎么知道临界资源内部一共有少个区域资源呢?我们又怎么知道内部一定还有资源呢?实际上,一般都是外部就会提供有多少资源。同时,信号量一定会保证內部是否还有资源

  信号量本质是一个计数器。一个线程在申请信号量,本质就是在对信号量的 -- 操作(对剩余资源数量的减减操作)。只要拥有信号量,就在未来一定能够拥有临界资源的一部分。申请信号量的本质:对临界资源中特定小块资源的预订机制。信号量因此保证了只要你申请成功,就代表一定还有资源。如果申请失败,就会进入等待。这不就是我们在访问真正的临界资源之前,我们其实就可以提前知道临界资源的使用情况!!!就不用再进行复杂的加锁、判断、解锁等操作了

  此时发现,线程要访问临界资源中的某一区域,就得先申请信号量。所有人必须的先看到统一信号量。信号量本身必须是公共资源。

  对信号量的操作就是申请和释放操作。信号量本质就是一个计数器,也就是在对信号量进行++和-- 操作。申请资源,可以看成对sem--,同时必须保证操作的原子性,我们也称之为 P 操作。释放资源,可以看成sem++,也必须保证操作的原子性,称之为 V 操作 。信号量核心操作:PV操作

二、信号量的相关接口

2、1 sem_t

  sem_t 是在 POSIX 系统中用来实现信号量机制的类型。它是一个不透明的数据结构,用于控制多个进程或线程对共享资源的访问。

  sem_t 提供了三个主要的函数接口:

  1. sem_init:用于初始化一个信号量。该函数接受三个参数,分别是指向 sem_t 对象的指针、信号量的共享标志和初始值。共享标志指定信号量的共享方式,根据具体需求可以选择在进程间共享(设置为0)或者在同一进程内的线程间共享(设置为非0)。初始值表示信号量的初始计数值。

  2. sem_wait:该函数使调用线程等待信号量。如果信号量的计数值大于0,则将计数值减一,并立即返回。如果计数值为0,则线程将阻塞,直到信号量的计数值大于0。

  3. sem_post:该函数用于释放信号量。它将信号量的计数值加一,并唤醒因等待该信号量而阻塞的线程。

  4. sem_destroy:该函数是用于销毁一个已经初始化的信号量的函数,在使用完信号量后,通过调用该函数可以释放相关资源。

  下面我们来看这几个函数的详细解释。

2、2 sem_init

  sem_init函数是用于初始化一个信号量的函数,它在程序中创建一个新的信号量,并为其分配必要的资源。函数原型如下:

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数: sem_init函数有三个参数:

  • sem:一个指向sem_t类型的指针,用于存储初始化后的信号量对象。
  • pshared:表示信号量的共享方式。
    • 如果pshared的值为0,表示信号量只能在调用它的进程内的线程之间共享。
    • 如果pshared的值为非零,表示信号量可以在多个进程之间共享。
  • value:表示信号量的初始值(初始临界资源內部有多少个小块资源)。

返回值: sem_init函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。

  以下是一个示例代码,演示了如何使用sem_init函数来初始化一个信号量:

#include <stdio.h>
#include <semaphore.h>

int main() {
    sem_t mySem;
    
    // 初始化一个非共享的信号量,初始值为1
    int ret = sem_init(&mySem, 0, 1);
    if (ret == -1) {
        perror("Failed to initialize semaphore");
        return 1;
    }

    // 进行其他操作...

    // 销毁信号量
    sem_destroy(&mySem);

    return 0;
}

2、3 sem_wait

  sem_wait函数用于对信号量进行等待操作,同时会将信号量减1。以实现对临界资源的互斥访问。函数原型如下:

int sem_wait(sem_t *sem);

参数: sem_wait函数只有一个参数:

  • sem:一个指向已经初始化的信号量对象。

返回值: sem_wait函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功,信号量的值被成功减一。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。常见的错误码包括EINTR(被信号中断)和EDEADLK(死锁)等。

2、4 sem_post

  sem_post函数用于对信号量进行发布操作,以增加信号量的值(对信号量加1)。它通常与sem_wait函数一起使用,用于在对共享资源的访问结束后释放信号量,以便其他线程可以获取到该资源。函数原型如下:

int sem_post(sem_t *sem);

参数: sem_post函数只有一个参数:

  • sem:一个指向已经初始化的信号量对象。

返回值: sem_post函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功,信号量的值被成功增加。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。常见的错误码包括EINVAL(信号量未初始化)和EOVERFLOW(信号量值达到上限)等。

  需要注意的是,sem_post函数并不处理过度发布的情况,即如果信号量的值已经达到了其上限,再调用sem_post函数也无法将其继续增加。因此,在使用信号量时,必须正确地控制信号量的值以避免出现竞态条件或死锁等问题。

2、5 sem_destory

  sem_destory函数用于销毁一个已经初始化的信号量对象,并释放相关的资源。当不再需要使用信号量时,应该调用sem_destroy函数进行清理操作。函数原型如下:

int sem_destroy(sem_t *sem);

参数: sem_destroy函数只有一个参数:

  • sem:一个指向已经初始化的信号量对象。

返回值: sem_destroy函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功,信号量对象被成功销毁并释放了相关的资源。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。常见的错误码包括EINVAL(信号量未初始化)和EBUSY(仍有线程在等待该信号量)等。

三、基于信号量的生产者消费者模型

3、1 信号量控制环形队列

  我们之前学习了​ 生产者消费者问题(条件变量 & 互斥锁)。之前学习的时由阻塞队列来实现的。通过互斥锁与条件变量很好的维护了生产者与消费者之前的同步与互斥关系。那么我么那接下来看看用信号量来维护生产者和消费者之间的同步与互斥关系的环形队列。

3、1、1 空间资源和数据资源

  我们先看下图:

【Linux从入门到精通】信号量(信号量的原理与相关操作接口)详解,Linux从入门到精通,linux,c++,多线程,信号量,生产者消费者问题

  生产者就是要生产数据放进环形队列中去。那么生产者所需要的就是申请环形队列空间资源。这个空间的大小我们可以自定义,比如环形队列由10个空间资源

  消费者就是去环形队列拿数据。消费者所需要的就是申请数据资源。也就是看环形队列中是否还有数据资源。 

3、1、2 保护原理(二元信号量)

  我们发现:生产和消费在队列为空的时候或者满的时,可能访问同一个位置。那我们必须保证生产者生产的数据个数最多不能超过环形队列的容量,其次消费者在空的时候不能再拿数据

  那我们就可以用两个信号量来很好的维护这两个角色的需求。生产者对应空间资源信号量,消费者对应数据资源信号量。当申请对应的信号量资源失败时,也会进入阻塞式等待。直到有信号量资源才会继续执行。

  当只有单生产单消费时,我们只需要维护好生产与消费的同步与互斥关系。多生产与多消费时,还需维护生产与生产、消费与消费的互斥关系。下面我们直接看代码。

3、2 demo代码

3、2、1 单生产与单消费

ringQueue.hpp

#include<iostream>
#include<semaphore.h>
#include<unistd.h>
#include<vector>
#include<assert.h>
#include<pthread.h>
#include<time.h>
#include<string.h>


using namespace std;

#include"Task.hpp"
#include"LogTest.hpp"

static const int g_cap=5;

template<class T>
class RingQueue
{
private:
    void P(sem_t& sem)
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t& sem)
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

public:
    RingQueue(int cap = g_cap)
        :_cap(cap)
    {
        int n = sem_init(&_spaceSem, 0, _cap);
        assert(n == 0);
        n = sem_init(&_dataSem, 0, 0);
        assert(n == 0);

        _queue.resize(_cap);
        _productorStep = _consumerStep = 0;
    }

    // 生产者
    void push(const T& in)
    {
        P(_spaceSem);
        _queue[_productorStep++] = in;
        _productorStep %= _cap;
        V(_dataSem);
    }

    // 消费者
    void pop(T* out)
    {
        P(_dataSem);
        *out = _queue[_consumerStep++];
        _consumerStep %= _cap;
        V(_spaceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }
private:
    sem_t _spaceSem; // 生产者——空间资源
    sem_t _dataSem;  // 消费者——数据资源
    vector<T> _queue;
    int _cap;
    int _productorStep;
    int _consumerStep;
};

testMain.cpp

#include "ringQueue.hpp"

int myAdd(int x, int y)
{
    return x + y;
}

void* ProductorRoutine(void *arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*) arg;
    while(true)
    {
        int x = rand() % 10 +1;
        int y = rand() % 100 + 1;
        Task t(x, y, myAdd);
        rq->push(t);
        LogMessage(1,"%s:%d + %d = ?","生产者申请了一个空间", x, y);
        //sleep(1);
    }
}

void* ConsumerRoutine(void* arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*)arg;
    while(true)
    {
        Task t;
        rq->pop(&t);
        LogMessage(1,"%s:%d + %d = %d","消费者消费了一个数据", t.x_, t.y_, t());
        sleep(1);
    }
}
int main()
{
    srand((unsigned int)time(nullptr) ^ 0x666888);
    RingQueue<Task> *rq = new RingQueue<Task>();

    pthread_t c, p;
    pthread_create(&p, nullptr, ProductorRoutine, rq);
    pthread_create(&c, nullptr, ConsumerRoutine, rq);

    pthread_join(p, nullptr);
    pthread_join(c, nullptr);
    delete rq;
    return 0;
}

  通过上述代码我们发现:信号量有点类似于互斥锁+条件变量的结合,不还是在竞争资源串行访问吗?实际上刚开始我们并不知道是先生产,还是先消费。如果先消费,则需等待。如果满的情况下,生产也需要等待。其他情况下大部分时间都是在并发执行的生产和消费可同时进行。

 3、2、2 多生产多消费

  当多生产和多消费时,我们还需维护生产与生产、消费与消费的互斥关系。这时候就需要互斥锁来维护了。代码如下:

ringQueue.hpp

#pragma once

#include<iostream>
#include<semaphore.h>
#include<unistd.h>
#include<vector>
#include<assert.h>
#include<pthread.h>
#include<time.h>
#include<string.h>


using namespace std;

#include"Task.hpp"
#include"LogTest.hpp"

static const int g_cap=5;

template<class T>
class RingQueue
{
private:
    void P(sem_t& sem)
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t& sem)
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

public:
    RingQueue(int cap = g_cap)
        :_cap(cap)
    {
        int n = sem_init(&_spaceSem, 0, _cap);
        assert(n == 0);
        n = sem_init(&_dataSem, 0, 0);
        assert(n == 0);

        _queue.resize(_cap);
        _productorStep = _consumerStep = 0;
    }

    // 生产者
    void push(const T& in)
    {
        P(_spaceSem);
        pthread_mutex_lock(&_pmutex);  
        _queue[_productorStep++] = in;
        _productorStep %= _cap;
        pthread_mutex_unlock(&_pmutex);
        V(_dataSem);
    }

    // 消费者
    void pop(T* out)
    {
        P(_dataSem);
        pthread_mutex_lock(&_cmutex);
        *out = _queue[_consumerStep++];
        _consumerStep %= _cap;
        pthread_mutex_unlock(&_cmutex);
        V(_spaceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }
private:
    sem_t _spaceSem; // 生产者——空间资源
    sem_t _dataSem;  // 消费者——数据资源
    vector<T> _queue;
    int _cap;
    int _productorStep;
    int _consumerStep;

    pthread_mutex_t _pmutex;
    pthread_mutex_t _cmutex;
};

testMain.cpp

#include "ringQueue.hpp"

int myAdd(int x, int y)
{
    return x + y;
}

void* ProductorRoutine(void *arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*) arg;
    while(true)
    {
        int x = rand() % 10 +1;
        int y = rand() % 100 + 1;
        Task t(x, y, myAdd);
        rq->push(t);
        LogMessage(1,"%s:%d + %d = ?","生产者申请了一个空间", x, y);
        //sleep(1);
    }
}

void* ConsumerRoutine(void* arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*)arg;
    while(true)
    {
        Task t;
        rq->pop(&t);
        LogMessage(1,"%s:%d + %d = %d","消费者消费了一个数据", t.x_, t.y_, t());
        sleep(1);
    }
}
int main()
{
    srand((unsigned int)time(nullptr) ^ 0x666888);
    RingQueue<Task> *rq = new RingQueue<Task>();

    pthread_t p[4], c[8];
    for(int i = 0; i < 4; i++) pthread_create(p+i, nullptr, ProductorRoutine, rq);
    for(int i = 0; i < 8; i++) pthread_create(c+i, nullptr, ConsumerRoutine, rq);

    for(int i = 0; i < 4; i++) pthread_join(p[i], nullptr);
    for(int i = 0; i < 8; i++) pthread_join(c[i], nullptr);
    delete rq;
    return 0;
}

  这里又有一个小细节:我们在插入或者删除时,是先加锁再申请信号量呢,还是先申请信号量再加锁呢? 首先申请信号量的操作时原子的,这个问题不用担心。关键在于插入和删除的过程。其实先加锁再申请信号量是肯定可行的,就是先申请信号量再加锁可以吗?答案是可以的!申请信号量的本质就是在对资源的预定。只要你申请信号量成功,就一定有资源可用。当我们先把信号量申请完,就是把资源先分配给了不同线程。反而会更好一点。后续各个线程不用竞争信号资源了,只需竞争锁资源就可以了。文章来源地址https://www.toymoban.com/news/detail-719943.html

到了这里,关于【Linux从入门到精通】信号量(信号量的原理与相关操作接口)详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux】浅谈信号量

    tips:system V 是一套标准,共享内存,信号量,消息队列属于system V。 进程A和进程B进行通信时,假如进程A向物理内存的共享区写入\\\"Hello World\\\",但是当进程A写入了\\\"Hello\\\"时,进程B就向内存读取了,所以只读取到了\\\"Hello\\\",这就导致进程A想向进程B发送的信息,进程B读取不完整,

    2024年02月05日
    浏览(45)
  • C语言用信号量进行pv操作

    用semget()函数获取信号量,其中semget()函数中所需传的参数如下: semget(key_t key , int nsems, semflg); 参数解释: 1、其中key可以通过ftok() 函数进行获取,ftok函数所需传入的参数为①文件路径②一个八位的整数 2、nsems是所需创建的信号量的个数,如果只是访问已存在的信号量集合

    2024年02月13日
    浏览(44)
  • 【Linux】进程间通信 -- 信号量

    信号量是什么? 本质是一个计数器,通常用来表示公共资源中,资源数量多少的问题 公共资源:能被多个进程同时可以访问的资源 访问没有保护的公共资源:数据不一致问题(比如我想写abc123,但是我123还没有写入,就读取了abc,可能数据分开会导致数据无意义) 为什么要

    2024年02月16日
    浏览(47)
  • linux进程间通信(信号量)

    信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即 P(信号变量))和发 送(即 V(信号变量))信息操作。最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式, 叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号

    2024年02月07日
    浏览(38)
  • 【Linux篇】第十七篇——信号量

    前言 POSIX信号量 信号量的概念 信号量的工作原理 信号量函数 二元信号量模拟实现互斥功能 基于环形队列的生产消费模型 空间资源和数据资源 生产者和消费者申请和释放资源 必须遵守的两个规则 代码实现 信号量保护环形队列的原理 将可能被多个执行流同时访问的资源叫

    2024年02月06日
    浏览(47)
  • linux中互斥锁,自旋锁,条件变量,信号量,与freeRTOS中的消息队列,信号量,互斥量,事件的区别

    对于目前主流的RTOS的任务,大部分都属于并发的线程。 因为MCU上的资源每个任务都是共享的,可以认为是单进程多线程模型。 【freertos】003-任务基础知识 在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式,但是如果在使用操作系统的应用中用全局变

    2024年02月11日
    浏览(48)
  • 【Linux】多线程 之 POSIX信号量

    信号量又称为 信号灯 本质就是一个计数器,用于描述临界资源数目的 sem: 0 - 1 - 0 若临界资源只有1个,则sem设为1,当要使用临界资源时,sem由1变为0,其他人在想申请,则申请不到挂起排队,等待释放临界资源时 sem由0变为1 ,才可以再申请临界资源 这种信号量称为 二元信号

    2024年02月16日
    浏览(48)
  • linux(system V标准)信号量

    目录:             1.什么是信号量             2.信号量的本质 1.什么是信号量   2.信号量的本质  什么是临界资源呢?? 凡是倍多个执行流同时访问的资源就是临界资源!!! 我们看一个问题,我们fork()之后创建一个子进程,那么我们的全局变量,是不是我们父

    2024年02月07日
    浏览(42)
  • Linux进程间通信【消息队列、信号量】

    ✨个人主页: 北 海 🎉所属专栏: Linux学习之旅 🎃操作环境: CentOS 7.6 阿里云远程服务器 在 System V 通信标准中,还有一种通信方式: 消息队列 ,以及一种实现互斥的工具: 信号量 ;随着时代的发展,这些陈旧的标准都已经较少使用了,但作为 IPC 中的经典知识,我们可

    2024年02月08日
    浏览(62)
  • 嵌入式实时操作系统的设计与开发(信号量学习)

    除了临界点机制、互斥量机制可实现临界资源的互斥访问外,信号量(Semaphore)是另一选择。 信号量与互斥量的区别 对于互斥量来说,主要应用于临界资源的互斥访问,并且能够有效地避免优先级反转问题。 对于信号量而言,它虽然也能用于临界资源的互斥访问,但是不能

    2024年02月08日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包