信号量机制和生产者消费者问题

信号量机制和生产者消费者问题

文章目录

一、前言

先前理解了竞态条件下的软件思路的解决方法------忙等,当然存在一定问题:没有遵循让权等待 。当遇到长时间事件时,这种方法特别消耗CPU资源,因此有另一种方法专门解决这种问题------睡眠与唤醒

二、信号量机制

2.1 优先级反转问题

2.1.1 概述

有两个进程,基于优先级的调度算法:优先级高的先抢占CPU

概述:当B在执行时,优先级更高的A来了,此时CPU会放弃B,先执行A,执行完A才进行B。

  • 饿死:当一直执行高优先级队列中的任务,使得低优先级的任务无法执行,就会发生饿死

    FreeRTOS实时操作系统支持的调度算法就是先读高再读低(只要有高优先级,低优先级就一定不会执行)

  • 非饿死:对于高优先级队列设置一定的调度限制,到达限制,就执行低优先级的任务

    Linux就是采用非饿死的调度机制

A进程进入scanf函数之后,开始读取键盘,此时用户没有敲,A会放到磁盘驱动的等待队列里(scanf会读取键盘的驱动),高优先级就没有任务了,调度算法只能从低优先级里面取了,取到B。B开始执行,如果执行了加锁操作(把门锁了,在里面干活------执行临界区代码),干活过程中B时间片到了,CPU就把B调到就绪队列中,B再响应其他进程(加锁干活),刚好键盘来了,A从驱动被唤醒到优先级队列里。A也想加锁临界资源(从B那里拿锁------B先干完活,再解锁),但是A优先级高,就无法使得B继续运行。最终导致:B的锁永远释放不了,A也加不了锁。

2.1.2 原因剖析

加锁是轮询方式。加锁一直在耗CPU,A不可能放弃CPU,B没有机会再执行(临界区代码)

这就会出现优先级反转。(按正常说,B应该先运行,但是运行不了了)

2.1.3 解决方法

A加不了锁就不要老是抢CPU了,而是应该置于睡眠状态(加睡眠锁,即互斥锁),放弃CPU

加锁:

  • 轮询方式(自旋锁)
  • 睡眠方式(互斥锁)

睡眠锁也有一个等待队列(lock)。当B执行完,就解锁(设置一个状态)。A也解锁------唤醒,就是唤醒锁的队列,A就又被放到高优先级队列中了。

2.2 睡眠与唤醒

目的:

  • 优先级反转问题
  • 防止轮询耗费CPU
2.2.1 概述
  • 睡觉:放弃CPU
  • 唤醒:防止睡眠状态的调度体永远得不到CPU响应
2.2.2 具体做法

不能纯软件来做,要靠硬件的原语操作(因为睡眠与唤醒一定是不能被打断的):

  • 睡眠原语
  • 唤醒原语

目的:保证程序执行的正确性

整型信号量:轮询(不太用)

记录型信号量 :睡眠和唤醒队列,即等待队列。sleep(调度体),当前这个调度体就没有机会了,只能靠别人调用wake-up原语唤醒对应的等待队列

三、生产者和消费者问题

3.1 场景

两个进程争苹果。有一个进程专门生产苹果(生产者),消费者(另一个进程)不断吃苹果,此时就会出现一个问题,装苹果的仓库大小有限------一次只能放10个苹果,这就导致生产苹果是有限制的。有两个矛盾:生产者什么时候生产,消费者什么时候能吃。这就称为"有界缓存区问题",即生产者消费者问题

3.2 代码模拟

伪代码,看看就好,主要是理解

生产者消费者本身的行为

c 复制代码
#include <stdio.h>
#define FALSE   0
#define TRUE    1

// 缓存区slot(槽)的数量,即存储苹果的地方
#define N 100

// 缓存区数据的数量,使用volatile关键字防止编译器优化掉不必要的读取
volatile int count = 0;
extern void consumer(void);

// 模拟生成数据项的函数(示例),即生产者
int produce_item(void)
{
    // 这里应该实现生产数据项的逻辑,这里简单返回静态值作为示例
    static int item = 0;
    return ++item;
}
// 模拟消费数据项的函数(示例),即消费者
void consumer_item(int item)
{
    // 这里应该实现消费者数据项的逻辑,这里仅打印作为示例
    printf("Consumed: %d\n", item);
}
// 假设的插入数据项到缓存区的函数(需具体实现),即生产完苹果放入仓库
void insert_item(int item)
{
    // 这里应实现将数据项放入缓存区的逻辑
    // 注意:实现实际中可能需要考虑缓存区同步和互斥访问
}
// 假设的从缓存区移除数据项的函数(需具体实现),即消费者从仓库取苹果
int remove_item(void)
{
    // 这里应实现从缓存区取出数据项的逻辑
    // 注意:实际实现中同样需要考虑同步和互斥
    return 0;   // 示例返回
}
// 使用两个原语
// 模拟系统调用sleep,这里使用忙等待(实际应使用系统提供的阻塞机制)
void sleep(void)	// 两种:轮询、睡眠
{
    // 这里仅为示例,实际应使用系统提供的sleep函数
    // 在这里,我们简单让出CPU时间片,但不是真正的sleep
    // 实际应用中,应使用如POSIX的sleep()或Windows的Sleep()
}
// 模拟系统调用wakeup,这里假设有某种机制可以唤醒其他线程或进程
void wakeup(void (*func)(void))
{
    // 这里仅为示例,实际应使用如POSIX的pthread_cond_signal或Windows的SetEvent
    // 这里我们不做任何操作,因为真正的唤醒机制依赖于操作系统
}

// 生产者
void producer(void)
{
    int item;
	// 源源不断生产,有人来消费
    while (TRUE)
    {
        item = produce_item();
        // 如果缓存区是满了,就阻塞(忙等)
        while(count == N)		// 放入缓存区判满
        {
            sleep();
        }
        insert_item(item);
        count++;
        if(count == 1)
        {
            wakeup(consumer);	// 一个状态:表示可以消费东西了
        }
    }
}

// 消费者
void consumer(void)
{
    int item;
	// 不断吃东西
    while(TRUE)
    {
        // 如果缓存区是空的,就阻塞(忙等)
        while(count == 0)
        {
            sleep();        // 睡觉需要有人唤醒
        }
        item = remove_item();	// 从缓存移出东西
        consumer_item(item);	// 和上面一起
        count--;
        if(count == N - 1)		// 只唤醒一次
        {
            wakeup(producer);   // 这是告诉一个状态:可以生产
        }
    }
}

问题

  • 睡眠丢失:程序执行过程的问题。如果生产者先唤醒又去生产,缓存区的影响,=0,消费者sleep了,就再也不会唤醒了。
  • sleepwakeup都是轮询,消耗CPU,状态得不到(没有人唤醒就一直睡眠)

解决

提出了信号量,信号量可以解决同步和互斥问题(基于底层机制)。

尽管信号量可以解决两类问题,但是之后提到信号量 就是解决同步 问题,和互斥 相关的是互斥量

3.3 信号量

3.3.1 概述

Dijkstra提出的。

先前在最短路径中也有提及

信号量的核心就是睡眠和唤醒

信号量到底怎么定义的呢?

教材版本(推荐):

信号量含义:衡量资源数量的一个数据类型,它表示的值定义当前系统的一个状态,在临界资源访问时,有多少进程等待该资源,有多少资源可供其他人使用

c 复制代码
struct semaphore			// 记录型信号量(结构体)
{
    int value;						// 资源的数量
    queue<struct task_struct*> que;	  // 这个资源的等待队列,队列里放的元素就是进程标识
};

睡眠:down(pid)sleepP操作、wait(系统调用)

c 复制代码
void P(semaphore S)
{
    pid = getpid();
    S->value--;			// 吃东西
    if(S->value < 0)	// 因为是先-,因此就会是<0
    {
    	wait(S->que, pid);    
    }
}

唤醒:up()wake_upV操作

c 复制代码
void V(semaphore S)
{
	S->value++;				// 送东西
    if(S->value <= 0)		// <= 0才是有人睡觉
    {
        wake_up(S->que);
    }
}

扩展版本

down不是先--,而是先判断;up也是先判断,有人睡觉就唤醒,直到已经没有人了,才是++

3.3.2 细节

信号量值为1-互斥锁

信号量值为n-临界值资源多个

代码

伪代码,理解为主~

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#define FALSE   0
#define TRUE    1

// 缓存区slot(槽)的数量
#define N 100
struct queue{ pid_t pid; };

// typedef struct
// {
//     int value;
//     struct queue wq;
// } semaphore;
typedef int semaphore;
extern void down(semaphore *);
extern void up(semaphore *);

int produce_item(void)
{
    static int item = 0;
    return ++item;
}

void consumer_item(int item)
{
    printf("Consumed: %d\n", item);
}

void insert_item(int item)
{

}

int remove_item(void)
{
    return 0;
}

// 标明缓存区有空位置的个数-空位置实现
semaphore empty = N;      // 缓存区初始化时有N个空位
// 标明缓存区已经放入资源的数量
semaphore full = 0;         // 缓存区初始化时有0个可用资源
// 控制区临界区互斥访问(有同时写入的行为-互斥)
semaphore mutex = 1;        // 表示互斥锁(0/1两种状态,初始化永远为1)

// 生产者
void producer(void)
{
    int item;

    while(TRUE)	// 大框架: 生产->插入
    {
        item = produce_item();  // 生产
        // 等待缓存区有空位置
        down(&empty);           // 谁down谁等,有down就得有up
        down(&mutex);           // 操作锁
        insert_item(item);      // 插入
        up(&mutex);
        up(&full);              // 通知放了一个
    }
}

// 消费者
void consumer(void)
{
    int item;

    while(TRUE)	// 大框架: 先取->吃掉
    {
        // 等待缓存区有东西,才能移出
        down(&full);
        down(&mutex);
        item = remove_item();       // 取
        up(&mutex);
        up(&empty);                 // 唤醒(释放)
        consumer_item(item);        // 处理
    }
}

互斥锁是保护临界区的(互斥就是我用的时候你不能用)

核心代码思路:

  • 没加锁的原本思路理清(大框架)
  • 先后问题
  • PV配对写,什么时候写?(有P就有V)
  • 考虑加互斥(出现同时写入的情况)

3.4 互斥量

3.4.1 概述

互斥量就是互斥锁,整个资源数量就是1或0,即:unlocked-解锁,locked-加锁,对应接口解锁-mutex_unlock(),加锁-mutex_lock()

锁的内部也是信号量实现的(包含睡眠唤醒)

四、Futex

自旋锁(短、快)和互斥锁(队列,慢)结合体,实现分为:内核服务、用户库

先自旋锁试探,可以快速转换互斥锁

五、线程库里面的互斥锁

5.1 接口

线程调用 描述
Pthread_mutex_init 创建一个互斥量
Pthread_mutex_destroy 撤销一个已存在的互斥量
Pthread_mutex_lock 获得一个锁或堵塞(唤醒)
Pthread_mutex_trylock 获得一个锁或失败(尝试)
Pthread_mutex_unlock 释放一个锁(睡眠)

5.2 代码

先前在没加锁的时候,运行就会出现死循环(因编译执行过程中时间片的打断),当执行加上锁会怎样呢?

代码1:

c 复制代码
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<bits/pthreadtypes.h>

int value1 = 0;
int value2 = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;		// 全局变量-共享
// task1和task2都必须是同一把锁(竞争)
void task1(int flags)
{
    int cnt = 0;
    while(1)
    {
        cnt++;
        pthread_mutex_lock(&mutex);
        value1 = cnt;			// 要么同时赋值cnt,要么都不加(互斥)
        value2 = cnt;
        pthread_mutex_unlock(&mutex);
    }
}

void task2(int flags)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(value1 != value2)
        {
            printf("value1 = %d, value2 = %d\n", value1, value2);
        }
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t tid1, tid2;

//  pthread_mutex_init(&mutex, NULL);
    int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
    if(ret)
    {
        perror("pthread create");
        exit(-1); 
    }

    // 创建第二个线程
    ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
    if(ret)
    {
        perror("pthread create");
        exit(-1); 
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("main process!\n");
    return 0;
}

运行结果:

先解决互斥,再看先后

代码2:

c 复制代码
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<bits/pthreadtypes.h>

int value1 = 0;     // 一个临界资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void task1(int flags)
{
    for (int i = 0; i < 1000; i++)	// 干活的时候不被打扰
    {
        pthread_mutex_lock(&mutex);
        value1++;
        pthread_mutex_unlock(&mutex);
    }
}

void task2(int flags)
{
    for (int i = 0; i < 1000; i++)
    {
        pthread_mutex_lock(&mutex);
        value1++;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t tid1, tid2;

    int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
    if(ret)
    {
        perror("pthread create");
        exit(-1); 
    }

    // 创建第二个线程
    ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
    if(ret)
    {
        perror("pthread create");
        exit(-1); 
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("main process value1 = %d!\n", value1);
    return 0;
}

运行结果:

线程的资源共享,会有很多临界区,就需要考虑同步和互斥

5.3 和条件变量有关的pthread调用

核心:依然是解决资源数量问题

四、小结

本篇介绍了硬件视角下的解决产时间事件的竞态条件的方案------睡眠与唤醒,其底层就是信号量机制。在同步互斥问题中(如:生产者消费者问题),信号量是解决这种问题的关键

相关推荐
天才奇男子7 小时前
HAProxy高级功能全解析
linux·运维·服务器·微服务·云原生
学嵌入式的小杨同学8 小时前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
酥暮沐8 小时前
iscsi部署网络存储
linux·网络·存储·iscsi
❀͜͡傀儡师9 小时前
centos 7部署dns服务器
linux·服务器·centos·dns
Dying.Light9 小时前
Linux部署问题
linux·运维·服务器
S19019 小时前
Linux的常用指令
linux·运维·服务器
小义_9 小时前
【RH134知识点问答题】第7章 管理基本存储
linux·运维·服务器
梁洪飞10 小时前
内核的schedule和SMP多核处理器启动协议
linux·arm开发·嵌入式硬件·arm
_运维那些事儿10 小时前
VM环境的CI/CD
linux·运维·网络·阿里云·ci/cd·docker·云计算
Y1rong11 小时前
linux之文件IO
linux