深剖 Linux 信号量

news2024/11/16 4:20:25

目录

    • 传统艺能😎
    • POSIX信号量😍
      • 概念😂
    • 信号量函数😍
      • 初始化信号量👌
      • 销毁信号量👌
      • 等待(申请)信号量👌
      • 发布(释放)信号量🤣
    • 二元信号量模拟实现互斥功能😊
    • 基于环形队列的生产消费模型😍
    • 两大规则😍
    • 环形队列模型代码实现🤣
    • 原理😘

传统艺能😎

小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
在这里插入图片描述
1319365055

🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我


在这里插入图片描述

POSIX信号量😍

我们将可能会被多个执行流同时访问的资源叫做临界资源,临界资源需要进行保护否则会出现数据不一致等问题。

当我们仅用一个互斥锁对临界资源进行保护时,相当于我们将这块临界资源看作一个整体,同一时刻只允许一个执行流对这块临界资源进行访问。

但实际可以将这块临界资源再分割多个区域,当多个执行流访问临界资源时,如果执行流访问的是临界资源的不同区域,那么我们可以让这些执行流同时访问这些不同区域,此时不会出现数据不一致等问题

概念😂

信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。

每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特点的临界资源的权限,当操作完毕后就应该释放信号量。

在这里插入图片描述
信号量的PV操作:

P操作:申请信号量称为P操作,操作本质是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器 -1。
V操作:释放信号量称为V操作,操作本质是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加 +1。

P V 操作必须是原子操作 \color{red} {PV操作必须是原子操作} PV操作必须是原子操作
多个执行流操作是竞争式的在临界资源中申请信号量,因为信号量本来就是临界资源,但信号量本质又是来保护临界资源的,因此这里矛盾就是我不能使用临界资源来保护临界资源,所以PV操作必须是原子的

内存当中变量的 ++、-- 操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行 ++、-- 操作

申请信号量失败被挂起等待

当执行流在申请信号量时,临界资源可能已经全部被申请了,此时信号量的值就是 0,也就是说该执行流就应该在该信号量的等待队列当中进行等待,直到有信号量被释放时再被唤醒( 信号量虽然本质是计数器,但不意味着只有计数器,还包括一个等待队列!)

信号量函数😍

初始化信号量👌

我们使用 sem_init ,函数原型如下:

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

这里 sem 即需要初始化的信号量;pshared 即传入0值表示线程间共享,传入非零值表示进程间共享;value 即信号量初始值(计数器的初始值)。初始化成功返回 0,失败后返回 -1。

POSIX 信号量和 System V 信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的,但 POSIX 信号量可以用于线程间同步。

销毁信号量👌

我们使用 sem_destroy,函数原型如下:

int sem_destroy(sem_t *sem);

这里 sem 即需要销毁的信号量,销毁成功返回 0,失败返回 -1。

等待(申请)信号量👌

我们使用 sem_destroy,函数原型如下:

int sem_wait(sem_t *sem);

sem 即需要等待的信号量。等待成功返回0,信号量的值 -1; 等待失败返回 -1,信号量的值保持不变。

发布(释放)信号量🤣

我们使用 sem_post,函数原型如下:

int sem_post(sem_t *sem);

sem 即需要发布的信号量。发布成功返回0,信号量的值 +1; 等待失败返回 -1,信号量的值保持不变。

二元信号量模拟实现互斥功能😊

信号量本质是一个计数器,二元信号量其实就是将信号量的初始值设置为1

信号量的初始值为1,说明信号量所描述的临界资源只有一份,此时信号量的作用基本等价于互斥锁。例如,我们还是实现一个多线程抢票系统,并用二元信号量模拟实现多线程互斥。

我们在主线程当中创建四个新线程,让这四个新线程执行抢票逻辑,其中我们用全局变量 tickets 记录当前剩余的票数,此时 tickets 是会被多个执行流同时访问的临界资源,我们在逻辑当中加入二元信号量,让每个线程在访问全局变量 tickets 之前先申请信号量,访问完毕后再释放信号量,此时二元信号量达到了互斥的效果避免了负数票数的情况:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

class Sem{
public:
	Sem(int num)
	{
		sem_init(&_sem, 0, num);
	}
	~Sem()
	{
		sem_destroy(&_sem);
	}
	void P()
	{
		sem_wait(&_sem);
	}
	void V()
	{
		sem_post(&_sem);
	}
private:
	sem_t _sem;
};

Sem sem(1); //二元信号量
int tickets = 2000;
void* TicketGrabbing(void* arg)
{
	std::string name = (char*)arg;
	while (true){
		sem.P();
		if (tickets > 0){
			usleep(1000);
			std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;
			sem.V();
		}
		else{
			sem.V();
			break;
		}
	}
	std::cout << name << " quit..." << std::endl;
	pthread_exit((void*)0);
}

int main()
{
	pthread_t tid1, tid2, tid3, tid4;
	pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");
	pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");
	pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");
	pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");
	
	pthread_join(tid1, nullptr);
	pthread_join(tid2, nullptr);
	pthread_join(tid3, nullptr);
	pthread_join(tid4, nullptr);
	return 0;
}

基于环形队列的生产消费模型😍

在这里插入图片描述
生产者关注的是环形队列当中是否有空间(blank),消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费。

我们用信号量来描述环形队列当中的空间资源(blank_sem)和数据资源(data_sem),在我们初始信号量时给它们设置的初始值是不同的:blank_sem 的初始值应该设为环形队列的容量,因为刚开始时环形队列当中全是空间;data_sem 的初始值应该设为 0,因为刚开始时环形队列当中没有数据。

对于生产者来说,每次生产数据前都需要先申请 blank_sem:如果 blank_sem 的值不为 0,则信号量申请成功,此时生产者可以进行生产操作;反之则信号量申请失败,此时生产者需要在 blank_sem 的等待队列下进行阻塞等待,直到有新的空间后再被唤醒。

生产完数据后就该释放 data_sem:注意虽然生产前是对 blank_sem 进行的 P 操作,但是现在 V 操作应该对 data_sem 进行而不是 blank_sem。

生产者在生产数据前申请到的是 blank 位置,生产完数据后该位置中存储的是生产的数据,在该数据被消费者消费之前,该位置不再是 blank 位置而是 data 位置。生产者生产完数据后,环形队列当中会多一个 data 位置,因此我们应该对 data_sem 进行 V 操作

消费者同理,消每次消费数据前都需要先申请 data_sem:如果 data_sem 不为 0 则信号量申请成功,此时消费者可以进行消费;反之信号量申请失败,此时消费者需要在 data_sem 的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。

当消费者消费完数据后,应该释放blank_sem:虽然消费者在进行消费前是对 data_sem 进行的 P 操作,但是当消费者消费完数据,V 操作应该对 blank_sem 进行而不是 data_sem。

消费者在消费数据前申请到的是 data 位置,消费完数据后该位置的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,我们应该将该位置算作 blank 位置而不是 data 位置。当消费完数据后,意味着环形队列当中多了一个 blank 位置,因此应该对 blank_sem 进行 V 操作

两大规则😍

环形队列模型中生产者和消费者必须遵守如两个规则:

  1. 生产者和消费者不能对同一个位置进行访问

如果生产者和消费者访问的是环形队列中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问,这当然是不允许的。

在这里插入图片描述

  1. 无论是生产者还是消费者,都不应该将对方套一个圈以上。

生产者从消费者的位置一直按顺时针方向进行生产,如果生产者的速度比消费者的速度快,那么当生产了一圈后再次遇到消费者,此时生产者就不应该再继续生产了,因为再生产就会覆盖还未被消费的数据。同理,如果消费者的速度比生产者的速度快,那么当消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为会消费到缓冲区中保存的废弃数据。

在这里插入图片描述

环形队列模型代码实现🤣

#pragma once

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>

#define NUM 8

template<class T>
class RingQueue
{
private:
	//P操作
	void P(sem_t& s)
	{
		sem_wait(&s);
	}
	//V操作
	void V(sem_t& s)
	{
	    sem_post(&s);
	}
public:
	RingQueue(int cap = NUM)
		: _cap(cap), _p_pos(0), _c_pos(0)
	{
		_q.resize(_cap);
		sem_init(&_blank_sem, 0, _cap); //blank_sem初始值设置为环形队列的容量
		sem_init(&_data_sem, 0, 0); //data_sem初始值设置为0
	}
	~RingQueue()
	{
	sem_destroy(&_blank_sem);
	sem_destroy(&_data_sem);
	}
	//向环形队列插入数据(生产者调用)
	void Push(const T& data)
	{
		P(_blank_sem); //生产者关注空间资源
		_q[_p_pos] = data;
		V(_data_sem); //生产

		//更新下一次生产的位置
		_p_pos++;
		_p_pos %= _cap;//取模达到环形效果
	}
	//从环形队列获取数据(消费者调用)
	void Pop(T& data)
	{
		P(_data_sem); //消费者关注数据资源
		data = _q[_c_pos];
		V(_blank_sem);

		//更新下一次消费的位置
		_c_pos++;
		_c_pos %= _cap;//取模达到环形效果
	}
private:
	std::vector<T> _q; //环形队列
	int _cap; //环形队列容量
	int _p_pos; //生产位置
	int _c_pos; //消费位置
	sem_t _blank_sem; //描述空间资源
	sem_t _data_sem; //描述数据资源
};

注意当没有设置环形队列的大小时,默认将容量上限设置为 8

p_pos 只会由生产者线程更新,c_pos 只会由消费者线程更新,所以这两个变量访问时不需要保护,因此代码中将 p_pos 和 c_pos 的更新放到了 V 操作之后,就是为了尽量减少临界区的代码。

为了方便理解,这里实现单生产者、单消费者的生产者消费者模型。于是在主函数就只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费:

#include "RingQueue.hpp"

void* Producer(void* arg)
{
	RingQueue<int>* rq = (RingQueue<int>*)arg;
	while (true){
		sleep(1);
		int data = rand() % 100 + 1;
		rq->Push(data);
		std::cout << "Producer: " << data << std::endl;
	}
}
void* Consumer(void* arg)
{
	RingQueue<int>* rq = (RingQueue<int>*)arg;
	while (true){
		sleep(1);
		int data = 0;
		rq->Pop(data);
		std::cout << "Consumer: " << data << std::endl;
	}
}

int main()
{
	srand((unsigned int)time(nullptr));
	pthread_t producer, consumer;
	RingQueue<int>* rq = new RingQueue<int>;
	pthread_create(&producer, nullptr, Producer, rq);
	pthread_create(&consumer, nullptr, Consumer, rq);
	
	pthread_join(producer, nullptr);
	pthread_join(consumer, nullptr);
	delete rq;
	return 0;
}

环形队列要让生产者线程向队列中 Push 数据,让消费者线程从队列中 Pop 数据,因此就必须要让这两个线程同时看到环形队列,所以我们在创建生产者线程和消费者线程时,需要将环形队列作为线程执行例程的参数进行传入,此时生产者消费者步调是一致的。

我们可以让生产者不停的进行生产,而消费者每隔一秒进行消费,模拟供大于求

void* Producer(void* arg)
{
	RingQueue<int>* rq = (RingQueue<int>*)arg;
	while (true){
		int data = rand() % 100 + 1;
		rq->Push(data);
		std::cout << "Producer: " << data << std::endl;
	}
}
void* Consumer(void* arg)
{
	RingQueue<int>* rq = (RingQueue<int>*)arg;
	while (true){
		sleep(1);
		int data = 0;
		rq->Pop(data);
		std::cout << "Consumer: " << data << std::endl;
	}
}

由于生产者生产的更快,运行后一瞬间生产者就将环形队列就满了,此时生产者想要再进行生产,但空间资源已经为 0了,于是生产者只能在 blank_sem 的等待队列下进行阻塞等待,直到由消费完一个数据后对 blank_sem 进行了 V 操作,生产者才会被唤醒。但由于生产者的生产速度很快,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了:
在这里插入图片描述
当然也可以让生产者每隔一秒进行生产,而消费者不停的进行消费,模拟供不应求

void* Producer(void* arg)
{
	RingQueue<int>* rq = (RingQueue<int>*)arg;
	while (true){
		sleep(1);
		int data = rand() % 100 + 1;
		rq->Push(data);
		std::cout << "Producer: " << data << std::endl;
	}
}

void* Consumer(void* arg)
{
	RingQueue<int>* rq = (RingQueue<int>*)arg;
	while (true){
		int data = 0;
		rq->Pop(data);
		std::cout << "Consumer: " << data << std::endl;
	}
}

在这里插入图片描述

原理😘

在 b l a n k s e m 和 d a t a s e m 两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题 \color{red} {在 blank_sem 和 data_sem 两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题} blanksemdatasem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题

因为只有当生产者和消费者指向同一个位置时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:

  1. 环形队列为空时
  2. 环形队列为满时

也就是说环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,环形队列中就不会出现数据不一致的问题。并且大部分情况下并不会指向同一个位置,因此大部分情况下可以让生产者和消费者并发执行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/594044.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux下C、C++、和Java程序设计

1.创建用户和之前一样。 RHEL7.2用户名和密码的创建以及DHCP服务的安装_封奚泽优的博客-CSDN博客https://blog.csdn.net/weixin_64066303/article/details/130763469?spm1001.2014.3001.55012.用yum安装&#xff08;分别是c,c,java,javac)(之前我以为是分开安装的&#xff0c…

Sui Builder House首尔站倒计时!

Sui主网上线后的第一场Builder House活动即将在韩国首尔举行&#xff0c;同期将举办首场线下面对面的黑客松。活动历时两天&#xff0c;将为与会者提供独特的学习、交流和娱乐的机会。活动详情请查看&#xff1a;Sui Builder House首尔站&#xff5c;主网上线后首次亮相。 Sui…

2. 虚拟环境

一、为什么要搭建虚拟环境&#xff1f; 在实际开发过程中&#xff0c;多个程序可能需要调试各种版本的不同环境&#xff0c;比如不同的Python解释器&#xff0c;不同的flask版本 二、如何搭建虚拟环境&#xff1f; 什么是虚拟环境&#xff1f; 它就是一个特殊的文件夹&…

一个matlab colorbar的简易代码cmocean

matlab自带的色阶不全&#xff0c;无法满足绘图的需求&#xff0c;而cmocean函数提供了一些常用的色阶。 函数命令&#xff1a;cmocean(ColormapName,varargin)&#xff0c;其中的ColormapName有如下的可选参数&#xff1a; 各个参数的绘图效果如下&#xff1a; 另外的一个参…

基于CAPL版本的CRC32算法

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

UI设计用什么软件做?

1、即时设计 即时设计是一款国内的在线协同设计工具&#xff0c;提供原型设计、UI/UX 设计和设计交付等核心功能。它无需第三方插件&#xff0c;拥有丰富的组件样式、中英文字体库和本地化资源&#xff0c;受到专业设计师的好评。与其他国外的 UI 工具相比&#xff0c;即时设计…

加拿大访问学者博士后签证材料清单指南

加拿大作为一个受欢迎的留学和研究目的地&#xff0c;吸引着许多国际学者和博士后前往交流和深造。作为准备申请加拿大访问学者或博士后签证的申请人&#xff0c;准备充分的材料是至关重要的。下面是知识人网小编整理的个关于加拿大访问学者博士后签证材料清单的指南&#xff0…

mysql-xtrabackup的使用

一、安装 1.下载压缩包 根据当前地址选择对应的版本和系统 wget https://downloads.percona.com/downloads/Percona-XtraBackup-2.4/Percona-XtraBackup-2.4.28/binary/tarball/percona-xtrabackup-2.4.28-Linux-x86_64.glibc2.17.tar.gz2.解压缩 tar xvf percona-xtrabac…

【分布鲁棒和多目标非负矩阵分解】基于DR-NMF的对NMF问题噪声模型的识别鲁棒性研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

A*寻路之旅:用SDL图形化演示

前言 欢迎来到小K的数据结构专栏的第十小节&#xff0c;本节将为大家带来A*寻路算法的图形化详解&#xff0c;学了之后寻路不再迷路&#xff08;✨当然也为大家准备了完整的源码&#xff0c;好像在文章顶部欸~ &#xff09;~希望你看完之后&#xff0c;能对你有所帮助&#xff…

ctfshow 每周大挑战 RCE极限挑战4、5

看过官方wp之后复现的&#xff0c;用的payload是自己后来写的&#xff0c;可能不如官方的看着清晰 有点强迫症似的在抠细节&#xff08;x 目录 挑战4最初的思路通过HackBar拿flag的写法写法一写法二 挑战5burp中的payload 大佬们也太极限啦 挑战4 最初的思路 第4题的长度限制…

UM2082F08 125k三通道低频无线唤醒ASK接收功能的SOC芯片 汽车PKE钥匙

1产品描述 UM2082F08是基于单周期8051内核的超低功耗8位、具有三通道低频无线唤醒ASK接收功能的SOC芯片。芯片可检测30KHz~300KHz范围的LF (低频)载波频率数据并触发唤醒信号&#xff0c;同时可以调节接收灵敏度&#xff0c;确保在各种应用环境下实现可靠唤醒&#xff0c;其拥…

母婴健康老人护理医护上门陪诊产后恢复预约上门小程序源码

母婴健康老人护理医护上门陪诊产后恢复预约上门小程序 在线预约 上门打针 产后恢复 会员卡 余额充值 优惠券 分销商 unippthinkphp <template> <view class"container" :style"{background:pagebase.base.bc}"> <Pengp…

Django+Vue实现文件上传下载功能

目录 前言 上传功能 后端代码 前端代码 下载功能 后端代码 前端代码 前言 首先我要实现的页面效果是这样的 当点击上传文件按钮&#xff0c;弹出上传文件的弹出框&#xff0c;可以上传多个文件&#xff0c;点击确定后才正式开始上传 点击右侧下载按钮&#xff0c;可以直…

springboot中将logback切换为log4j2

前言 springboot默认使用logback作为日志记录框架&#xff0c;常见的日志记录框架有log4j、logback、log4j2。这篇文章我们来学习怎样将logbak替换为log4j2。 一、为什么使用log4j2&#xff1f; 我们在项目中经常使用一个叫SLF4J的依赖&#xff0c;它是做什么的呢&#xff1f; …

Java 实现在顺序表末尾插入一个元素

一、思路 1.因为我们是用数组实现的顺序表&#xff0c;因此首先要保证数组有足够的空间来进行插入元素. 2.如果数组满了就需要将数组扩容&#xff0c;没满就开始插入. 3.当前数组中的元素个数就是每一次要插入的末尾位置的下标. 4.定义一个 usedSize 来表示当前的元素个数. 5.插…

Pandas+ChatGPT强强结合诞生PandasAI,数据分析师行业要变天了?

大家好&#xff0c;我是千与千寻&#xff0c;可以叫我千寻&#xff0c;我自己主要的编程语言是Python和Java。 说到Python编程语言&#xff0c;使用Python语言主要使用的是数据科学领域的从业者。 Python编程语言之所以在数据科学领域十分火热&#xff0c;源于Python语言的三…

9.Ansible Conditions介绍

Ansible条件语句 1)上面的例子在不同的机器上安装nginx&#xff0c;不同的操作系统风格使用不同的软件包管理器&#xff61;debian使用APT, Red Hat使用Yum, 但这是两个独立的Playbook,您必须为各自的服务器使用正确的剧本&#xff61; 可以使用条件语句&#xff0c;将这两个P…

Mongodb在Linux下载安装及部署

前言 一、下载安装包 Mongodb官网&#xff1a;Download MongoDB Community Server | MongoDB 二、安装及配置 博主下载的安装包是&#xff1a;mongodb-linux-x86_64-rhel70-6.0.6.tgz 新建目录 # 进入 usr 文件夹 cd /usr# 新建 mongodb 文件夹 mkdir mongodb# 进入 mongodb …

如何自学成为黑客

学习路线 不BB&#xff0c;直接上干货。 学完下面的内容&#xff0c;绝对可以进入黑客圈。 文末有福利噢&#xff01; 第一步&#xff1a;计算机基础 了解计算机基本常识&#xff0c;常用软件使用。需要学会基本使用的软件或技术有&#xff1a;Word、VMware、VPN、Visual St…