首页 > 其他分享 >《操作系统真相还原》实验记录2.7——生产者与消费者问题

《操作系统真相还原》实验记录2.7——生产者与消费者问题

时间:2025-01-21 21:54:19浏览次数:1  
标签:操作系统 ioqueue 真相 环形 ioq 线程 缓冲区 2.7 struct

一、生产者与消费者问题简述

  1. 我们知道,在计算机中可以并行多个线程,当它们之间相互合作时,必然会存在共享资源的问题,这是通过“线程同步”来解决的,而诠释“线程同步”最典型的例子就是著名的“生产者与消费者问题”。
    1. “同步”:指多个线程相互协作,共同完成一个任务,属于线程间工作步调的相互制约。
    2. “互斥”:指多个线程“分时”访问共享资源。
  2. 生产者与消费者问题是描述多个线程协同工作的模型,而信号量解决了协同工作中的“同步”和“互斥”。
    生产者与消费者问题模型
  3. 生产者与消费者问题描述如下:
    1. 有一个或多个生产者、一个或多个消费者和一个固定大小的缓冲区,所有生产者和消费者共享这同一个缓冲区。生产者生产某种类型的数据,每次放一个到缓冲区中,消费者消费这种数据,每次从缓冲区中消费一个。同一时刻,缓冲区只能被一个生产者或消费者使用。当缓冲区已满时,生产者不能继续往缓冲区中添加数据,当缓冲区为空时,消费者不能在缓冲区中消费数据。
  4. 生产者与消费者问题强调的是:对于有限大小的公共缓冲区,如何同步生产者与消费者的运行,以达到对共享缓冲区的互斥访问,并且保证生产者不会过度生产,消费者不会过度消费,缓冲区不会被破坏。
    1. 对这种缓冲区的破坏,要么是对缓冲区访问溢出,也就是数据存取的地址超过了缓冲区的范围;要么是缓冲区中的数据被破坏,也就是新数据把尚未读取的老数据覆盖。

二、环形缓冲区的实现

  1. 缓冲区是多个线程共同使用的共享内存,因此需要保证各线程对缓冲区是互斥访问,并且不会对其过度使用,从而确保不会使缓冲区遭到破坏。也就是说,只要我们能够设计出合理的缓冲区操作方式,就能够解决生产者与消费者问题
  2. 环形缓冲区
    1. 环形缓冲区本质上依然是线性缓冲区,但其使用方式像环一样,没有固定的起始地址和终止地址,环内任何地址都可以作为起始和结束。
    2. 对于缓冲区的访问,我们提供两个指针,一个是头指针,用于往缓冲区中写数据,另一个是尾指针,用于从缓冲区中读数据。每次通过头指针往缓冲区中写入一个数据后,使头指针加 1 指向缓冲区中下一个可写入数据的地址,每次通过尾指针从缓冲区中读取一个数据后,使尾指针加1 指向缓冲区中下一个可读入数据的地址,也就是说,缓冲区相当于一个队列,数据在队列头被写入,在队尾处被读出。
    3. 用线性空间实现这种逻辑上的环形空间,只要我们控制好头指针和尾指针的位置就好了,无论它们怎样变化,始终让它们落在缓冲区空间之内,当指针到达缓冲区的上边界后,想办法将指针置为缓冲区的下边界(通常是对缓冲区大小取模),从而使头尾指针形成回路,逻辑上实现环形缓冲区。这两个指针相当于缓冲区的游标,在缓冲区空间内来回滑动。
    4. 我们的环形缓冲区是个线性队列,队列可以用线性数据结构来实现,比如数组和链表,为了简单,我们用数组来定义队列,实现环形缓冲区。
      环形缓冲区

三、添加键盘输入缓冲区

  1. 虽然我们的环形缓冲区支持多个生产者和消费者,但目前我们应用的场合非常简单,只是用在单一生产者和单一消费者的环境中,即生产者是键盘驱动,消费者是将来的shell
  2. 本节的任务是:将“在键盘驱动中处理的字符”存入环形缓冲区当中。

3.1 代码详情

ioqueue.h:头文件中定义了队列结构体

#ifndef __DEVICE_IOQUEUE_H
#define __DEVICE_IOQUEUE_H
#include "stdint.h"
#include "stdbool.h"
#include "thread.h"
#include "sync.h"

#define bufsize 64

struct ioqueue {
    /*the lock of buffer*/
    struct lock lock;

    /*if buffer is full, "producer" record which thread become sleeping.*/
    struct task_struct* producer;

    /*if buffer is empty, "consumer" record which thread become sleeping.*/
    struct task_struct* consumer;

    char buf[bufsize];  //buffer
    int32_t head;      //the head of queue
    int32_t tail;      //the tail of queue
};

void ioqueue_init(struct ioqueue* ioq);
bool ioq_full(struct ioqueue* ioq);
char ioq_getchar(struct ioqueue* ioq);
void ioq_putchar(struct ioqueue* ioq, char byte);

#endif

ioqueue.c:文件中定义了对环形键盘缓冲区的各类操作函数

#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"
#include "stdbool.h"

void ioqueue_init(struct ioqueue* ioq) {
    lock_init(&ioq->lock);
    ioq->producer = ioq->consumer = NULL;
    ioq->head = ioq->tail = 0;
}

static int32_t next_pos(int32_t pos) {  //next position in ioqueue.
    return (pos + 1) % bufsize;
}

bool ioq_full(struct ioqueue* ioq) {
    ASSERT(intr_get_status() == INTR_OFF);  //ioqueue is public space, so we must make sure the interrupt is close when we operating.
    return next_pos(ioq->head) == ioq->tail;
}

static bool ioq_empty(struct ioqueue* ioq) {
    ASSERT(intr_get_status() == INTR_OFF);
    return ioq->head == ioq->tail;
}

/*make producer or consumer to waite*/
/*pay attention: "struct task_struct** waiter" is pointed to ioqueue->producer or ioqueue->consumer.*/
static void ioq_wait(struct task_struct** waiter) {
    ASSERT(*waiter == NULL && waiter != NULL);
    *waiter = running_thread();
    thread_block(TASK_BLOCKED);
}

/*wakeup the waiter*/
static void wakeup(struct task_struct** waiter) {
    ASSERT(*waiter != NULL);
    thread_unblock(*waiter);
    *waiter = NULL;
}

/*consumer get one character from ioqueue*/
char ioq_getchar(struct ioqueue* ioq) {
    ASSERT(intr_get_status() == INTR_OFF);

    while(ioq_empty(ioq)) {
        /*using lock will have effect in situation of more consumers and more producers, 
        not only one consumer and producer.*/
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq->consumer);
        lock_release(&ioq->lock);
    }

    char byte = ioq->buf[ioq->tail];
    ioq->tail = next_pos(ioq->tail);

    if(ioq->producer != NULL) {
        wakeup(&ioq->producer);
    }

    return byte;
}

/*producer write one Byte to ioqueue*/
void ioq_putchar(struct ioqueue* ioq, char byte) {
    ASSERT(intr_get_status() == INTR_OFF);

    while(ioq_full(ioq)) {
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq->producer);
        lock_release(&ioq->lock);
    }

    ioq->buf[ioq->head] = byte;
    ioq->head = next_pos(ioq->head);

    if(ioq->consumer != NULL) {
        wakeup(&ioq->consumer);
    }
}

keyboard.c:ioq_putchar()函数将处理后的键盘输入字符存入环形键盘缓冲区

//略
if(cur_char != 0) {  //we use keymap[0] to occupy place.
            if(ioq_full(&kbd_buf) != true) {
                put_char(cur_char);
                ioq_putchar(&kbd_buf, cur_char);
            }
            return;
        }
//略

四、生产者与消费者实例测试

  1. 实例测试设计内容大致如下:
    1. 一位生产者
      1. 由键盘中断驱动,且生产者位于主线程中。
      2. 键盘中断使得输入的字符存储到 “环形键盘缓冲区kbd_buf[]” 中。
      3. 当缓冲区存满后,再产生键盘中断将不做任何处理直接返回。
    2. 两位消费者
      1. 由时钟中断驱动,消费者位于新创建的两个独立线程中。
      2. 消费者访问 “环形键盘缓冲区kbd_buf[]” 。
        1. 如果 “环形键盘缓冲区kbd_buf[]” 不为空,则输出信息(包含该独立线程标识);
        2. 如果 “环形键盘缓冲区kbd_buf[]” 为空,则进入等待;
          1. 第一个发现 “环形键盘缓冲区kbd_buf[]” 为空的线程得到 “环形键盘缓冲区kbd_buf[]” 的锁,同时将自身PCB指针保存在waiter中,并将自身换下进程;
          2. 第二个发现 “环形键盘缓冲区kbd_buf[]” 为空的线程会由于无法得到 “环形键盘缓冲区kbd_buf[]” 的锁,因此只好将自己加入到 “环形键盘缓冲区kbd_buf[]” 的锁的阻塞队列中等待锁的持有者释放锁后唤醒自己并将自己换下处理器。
          3. 此时主线程上处理器运行while(1);,等待键盘输入,且由于两个独立线程此时都不在thread_ready_list中,因此主线程始终被换上换下处理器。
  2. 具体测试代码请读者自行编程实现练习,以串联和巩固自线程创建以来的知识点。

标签:操作系统,ioqueue,真相,环形,ioq,线程,缓冲区,2.7,struct
From: https://www.cnblogs.com/Yu-Xing-Hai/p/18684512/Producer_and_Consumer

相关文章

  • 操作系统1.1
    入门:计算机系统的层次结构一、操作系统的定义操作系统(OS)是指控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配;以提供给用户和其他软件方便的接口和环境;它是计算机系统中最基本的系统软件二、操作系统的功能操作系统是系统资源的管理者......
  • 《操作系统真象还原》第九章 线程(一) 在内核中实现线程
    第九章线程(一)在内核中实现线程本文是对《操作系统真象还原》第九章(一)学习的笔记,欢迎大家一起交流。我们在本节的任务:创建并初始化PCB模拟pthread_create函数创建线程并执行线程函数首先我们要明确内核级线程的优势,内核级线程是cpu的一个调度单位,当一个进程中的线程越多,享......
  • 30天自制操作系统day1&day2
    day1:  二进制编辑器bz:https://www.vcraft.jp/soft/bz.html  初识机器语言和汇编语言,并分别用其实现了软盘映像文件(完全用作者的复制粘贴)。  二进制编辑器中输入内容如下:  只有图中部分有非0内容,其余部分均为0,最末行首地址是001440。  保存为helloos.img,即为一个......
  • 30天开发操作系统 第 16 天 -- 多任务 v2.0
    前言大家好!昨天我们已经实践了很多关于多任务的内容,不过今天我们还得继续讲多任务。可“老是讲多任务都听腻了啊!”,但多任务真的非常重要(当然,如果你不想做一个多任务的操作系统那就不重要啦)。从咱们制作的操作系统角度来说,希望大家能够在充分做好多任务机制的基础上,再......
  • 面试必会(嵌入式)操作系统面试高频(三)线程与进程
    目录1.请你说说CPU工作原理⭐⭐2.死锁的原因、条件?以及如何预防⭐⭐⭐3.死锁与活锁⭐⭐死锁:活锁:解决活锁问题的一般策略包括:4.说说sleep和wait的区别?⭐⭐⭐sleep和wait的区别:5.简述epoll和select的区别,epoll为什么高效?⭐⭐⭐⭐epoll:Select:epoll为什么高效?拷贝开......
  • 面试必会(嵌入式)操作系统面试高频(一)线程与进程
    目录1.什么是线程?进程,线程,彼此有什么区别?⭐⭐⭐进程线程线程和进程区别:2.什么时候用进程,什么时候用线程?⭐⭐使用进程的情况:使用线程的情况:3.一个线程占多大内存?⭐⭐⭐4.说说什么是信号量,有什么作用?⭐⭐5.多进程内存共享可能存在什么问题?如何处理?⭐⭐⭐⭐⭐多进程内......
  • 编程语言“鄙视链”背后的真相
    在编程世界里,各种编程语言的使用者之间似乎存在着一条无形的“鄙视链”。从古老神秘的C到灵动便捷的Python,从严谨规范的Java到天马行空的Ruby,从C++与Python的效率之争,到Java与JavaScript的应用场景差异,不同语言的拥趸似乎总在暗自较量。然而,这条所谓的“鄙视......
  • 车载软件小结 --- 什么是实时操作系统?
    我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师:简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身边有这样灵性的人,一定要......
  • 硬件虚拟化(KVM)和操作系统虚拟化(Docker)
    硬件虚拟化分为I型虚拟化和II型虚拟化。I型虚拟化直接在硬件上虚拟出多个硬件,然后在虚拟出的硬件运行上操作系统;II型虚拟化作为软件在已有的操作系统上虚拟出多个硬件,然后在虚拟出的硬件上运行操作系统。操作系统虚拟化操作系统虚拟化是一种概念,目的是为了让应用和服务运行在......
  • 操作系统之进程管理
    进程管理1、进程进程简单点来说就是正在运行的程序就算是一个进程,它是一个动态的运行的过程,没有运行的程序只能算是一段代码,不能称为进程。进程是计算机进行资源管理和调度的独立单位。特征有五大特征结构性:进程由三部分组成,程序+数据+PCB(进程控制块)动态性:进程可以看作是......