20211314王艺达学习笔记10

发布时间 2023-11-16 16:45:58作者: 20211314王艺达

第12章 块设备I/O和缓冲区管理

一、块设备I/O缓冲区

I/O缓冲的基本原理非常简单。文件系统使用一系列I/O缓冲区作为块设备的缓存内存。当进程试图读取(dev,blk)标识的磁盘块时,它首先在缓冲区缓存中搜索分配给磁盘块的缓冲区。如果该缓冲区存在并且包含有效数据,那么它只需从缓冲区中读取数据,而无须再次从磁盘中读取数据块。如果该缓冲区不存在,它会为磁盘块分配一个缓冲区,将数据从磁盘读入缓冲区,然后从缓冲区读取数据。当某个块被读入时,该缓冲区将被保存在缓冲区缓存中。以供任意进程对同一个块的下一次读/写请求使用。同样,当进程写入磁盘块时,它首先会获取一个分配给该块的缓冲区。然后,它将数据写人缓冲区,将缓冲区标记为脏,以延迟写入,并将其释放到缓冲区缓存中。由于脏缓冲区包含有效的数据,因此可以使用它来满足对同一块的后续读/写请求,而不会引起实际磁盘1O。脏缓冲区只有在被重新分配到不同的块时才会写人磁盘。

在讨论缓冲区管理算法之前.我们先来介绍以下术语。在 read_flle/write_file 中,我们 假设它们从内存中的一个专用缓冲区进行读/写。对于 I/O 缓冲,将从缓冲区缓存中动态分配缓冲区。假设 BUFFER 是缓冲区(见下文定义)的结构类型,而且 getblk(dev, blk) 从缓冲区缓存中分配一个指定给(dev, blk)的缓冲区。定义一个 bread(dev, blk) 函数,它会返回一个包含有效数据的缓冲区(指针)。
从缓冲区读取数据后,进程通过 brelse(bp)将缓冲区释放回缓存。同理,定义一个 write_block(dev,blk,data)函数,如:

BUFFER *break(dev,blk)
{
    BUFFER *bp=getblk(dev,blk);
    if(bp data valid)
	    return bp;
    bp->opcode=READ;
    start_io(bp);
    wait for I/O completion;
    return bp;
}

其中 bwrite(bp)表示同步写入,dwrite(bp)表示延迟写入,如下文所示。

bwrite(BUFFER *bp){
    bp->opcode=WRITE;
    start_io(bp);
    wait for I/O completion;
    brelse(bp);	// release bp
}

dwrite(BUFFER *bp){
    mark bp dirty for delay_write;
    brelse(bp);	// release bp
}

同步写入操作等待写操作完成。它用于顺序块或可移动块设备,如USB驱动器。对于随机访问设备,例如硬盘,所有的写操作都是延迟写操作。在延迟写操作中,dwrite(bp) 将缓冲区标记为脏,并将其释放到缓冲区缓存中。由于脏缓冲区包含有效数据,因此可用来满足同一个块的后续读/写请求。这不仅减少了物理磁盘 I/O 的数量,而且提高了缓冲区缓存的效果。脏缓冲区只有在被重新分配到不同的磁盘块时才会被写入磁盘,此时缓冲区将被以下代码写入:

awrite(BUFFER *bp)
{
    bp->opcode = ASYNC;	//for ASYNC write
    start_io(bp);
}

awrite()会调用 start_io() 在缓冲区开始 I/O 操作,但是不会等待操作完成。当异步 (ASYNC) 写操作完成后,磁盘中断处理程序将释放缓冲区。

物理块设备I/O :每个设备都有一个 I/O 队列,其中包含等待 I/O 操作的缓冲区。缓冲区上的start_io() 操作如下:

start_io(BUFFER *bp)
{
    enter bp into device I/O queue; 
    if (bp is first buffer in I/O queue)
	    issue I/O command for bp to device;
}

当 I/O 操作完成后,设备中断处理程序会完成当前缓冲区上的 I/O 操作,并启动 I/O 队列中的下一个缓冲区的 I/O (如果队列不为空)。设备中断处理程序的算法如下:

IntermiptHandler ()
{
    bp = dequeue(device I/O queue); // bp = remove head of I/O queue 
    (bp->opcode == ASYNC)? brelse(bp) : unblock process on bp;
    if (1 empty(device I/O queue))
	    issue I/O command for first bp in I/O queue;
}

当 I/O 操作完成后,设备中断处理程序会完成当前缓冲区上的 I/O 操作,并启动 I/O 队列中的下一个缓冲区的 I/O (如果队列不为空)。设备中断处理程序的算法如下:

IntermiptHandler ()
{
    bp = dequeue(device I/O queue); // bp = remove head of I/O queue 
    (bp->opcode == ASYNC)? brelse(bp) : unblock process on bp;
    if (!empty(device I/O queue))
	    issue I/O command for first bp in I/O queue;

二、Unix I/O缓冲区管理算法

Unix 缓冲区管理子系统由以下几部分组成。

  • (1)I/O 缓冲区:内核中的一系列NBUF缓冲区用作缓冲区缓存。每个缓冲区用一个结构体表示。
      缓冲区结构体由两部分组成:用于缓冲区管理的缓冲头部分和用于数据块的数据部分。 为了保护内核内存,状态字段可以定义为一个位向量,其中每个位表示一个唯一的状态条 件。为了便于讨论,这里将它们定义为int。

  • (2)设备表:每个块设备用一个设备表结构表示。
      每个设备表都有一个 dev_list,包含当前分配给该设备的 I/O 缓冲区,还有一个io_queue,包含设备上等待 I/O 操作的缓冲区。I/O 队列的组织方式应确保最佳 I/O 操作。例如, 它可以实现各种磁盘调度算法,如电梯算法或线性扫描算法等。为了简单起见,Unix 使用 FIFO I/O 队列。

  • (3)缓冲区初始化:当系统启动时,所有I/O缓冲区都在空闲列表中,所有设备列表和 I/O队列均为空。

  • (4)缓冲区列表:当缓冲区分配给(dev, blk)时,它会被插入设备表的dev_list中。如果缓冲区当前正在使用,则会将其标记为 BUSY(繁忙)并从空闲列表中删除。

  • (5)Unix getblk/brelse 算法

下面是关于Unix算法的一些具体说明。

    1. 数据一致性:为了确保数据一致性,getblk一定不能给同一个(dev, blk)分配多个缓冲区这可以通过让进程从休眠状态唤醒后再次执行“重试循环”来实现。读者可以验证分配的每个缓冲区都是唯一的一其次,脏缓冲区在重新分配之前被写出来,这保证了数据的一致性。
    1. 缓存效果:缓存效果可通过以下方法实现 释放的緩冲区保留在设备列表中,以便可能重用,标记为延迟写入的緩冲区不会立即产生I/O,并且可以重用。缓冲区会被释放到空闲列表的末尾,但分配是从空闲列表的前面开始的,这是基于LRU (最近最少使用)原则,它有助于延长所分配缓冲区的使用期,从而提高它们的缓存效果。
    1. 临界区:设备中断处理程序可操作缓冲区列表,例如从设备表的I/O队列中删除 bp,更改其状态并调用brelse(bp)。所以,在getblk和brelse中,设备中断在这些临界区中会被屏蔽。这些都是隐含的,但没有在算法中表现出来

Unix算法的缺点

虽然Unix算法非常简单和简洁.但它也有以下缺点。

  • (1) 效率低下:该算法依赖于重试循环,例如,释放缓冲区可能会唤醒两组进程:需要 释放的缓冲区的进程,以及只需要空闲缓冲区的进程。由于只有一个进程可以获取释放的缓 冲区,所以,其他所有被唤醒的进程必须重新进入休眠状态。从休眠状态唤醒后,每个被唤醒的进程必须从头开始重新执行算法,因为所需的缓冲区可能已经存在。这会导致过多的进程切换。
  • (2) 缓存效果不可预知:在Unix算法中,每个释放的缓冲区都可被获取。如果缓冲区由需要空闲缓冲区的进程获取,那么将会重新分配缓冲区,即使有些进程仍然需要当前的缓冲区。
  • (3) 可能会出现饥饿:Unix算法基于“自由经济”原则,即每个进程都有尝试的机会,但不能保证成功,因此,可能会出现进程饥饿。
  • (4) 该算法使用只适用于单处理器系统的休眠/唤醒操作。

三、新的 I/O 缓冲区管理算法

在本节中,我们将展示一种用于 I/O 缓冲区管理的新算法。我们将在信号址上使用 p/v 来实现进程同步,而不是使用休眠/唤醒,与休眠/唤醒相比,信号量的主要优点是:
  (1) 计数信号量可用来表示可用资源的数量,例如:空闲缓冲区的数量。
  (2) 当多个进程等待一个资源时,信号量上的 V 操作只会释放一个等待进程,该进程 不必重试,因为它保证拥有资源。
  这些信号量属性可用于设计更有效的缓冲区管理算法。

使用信号量的缓冲区管理算法
  假设有一个单处理器内核(一次运行一个进程)。使用计数信号量上的 P/V 来设计满足 以下要求的新的缓冲区管理算法:
  (1) 保证数据一致性。
  (2) 良好的缓存效果。
  (3) 高效率:没有重试循环,没有不必要的进程“唤醒”。
  (4) 无死锁和饥饿。
  注意,仅通过信号量上的 P/V 来替换 Unix 算法中的休眠/唤醒并不可取,因为这样会 保留所有的重试循环。我们必须重新设计算法来满足所有上述要求,并证明新算法的确优于 Unix算法。首先,我们定义以下信号量。

BUFFER buf[NBUF]; 		// NBUF I/O buffers
SEMAPHORE free = NBUF; 	// counting semaphore for FREE buffers
SEMAPHORE buf[i].sem = 1;	// each buffer has a lock sem=l;

为了简化符号,我们将用缓冲区本身来表示每个缓冲区的信号量。与 Unix 算法一样,最开始,所有缓冲区都在空闲列表中,所有设备列表和 I/O 队列均为空。

四、P/V算法

  • 算法如下:
BUFFER *getblk(dev,blk)
{
    while(1){
  (1).      p(free);                              //首先获取一个空闲缓冲区
  (2).      if (bp in dev_list){                  //若该缓冲区在设备表的dev_list中
  (3).          if (bp not BUSY){                 //且处于空闲状态
                    remove from freelist;         //将其从空闲列表中删除
                    P(bp);                        //lock bp not wait
                    return bp;
                 }
            //若缓冲区存在缓存内且繁忙
                V(free);                          //放弃空闲缓冲区
  (4).          P(bp);                            //在缓冲队列中等待
                return bp;
           }
            //缓冲区不在缓存中,为磁盘创建一个缓冲区
  (5).     bp = first buffer taken out of freelist;
           P(bp);                             //lock bp no wait
  (6).     if (bp dirty){                     //若为脏缓冲区
              awrite(bp);                     //缓冲区写入磁盘
              continue;
           }
  (7).     reassign bp to (dev,blk);          //重新分配
           return bp;
      }
}
brelse (BUFFER *bp)
{
    (8).if (bp queue has waiter) {V(bp); return; }
    (9).if (bp dirty && freee queue has waiter){ awrite(bp); return;}
    (10).enter bp into (tail of) freelist; V(bp); V(free);
}
  • 证明PV算法的正确性:
  • (1)缓冲区唯一性:在 getblk()中,如果有空闲缓冲区,则进程不会在(1)处等待,而是会搜索 dev list。如果所需的缓冲区已经存在,则进程不会重新创建同一个缓冲区。如果所需的缓冲区不存在。则进程会使用个空闲缓冲区来创建所需的缓冲区。而这个空闲缓冲区保证是存在的。如果没有空闲缓冲区,则需要同一个缓冲区的几个进程可能在(1)处阻塞。
  • (2)无重试循环:进程重新执行while(1)循环的唯一位置是在(6)处,但这不是重试,因为进程正在不断地执行。
  • (3)无不必要唤醒:在 getblk(中,进程可以在(1)处等待空闲缓冲区也可以在(4)处等待所需的缓冲区。在任意一种情况下,在有缓冲区之前,都不会唤醒进程重新运行。。
  • (4)缓存效果:在 Unix算法中,每个释放的缓冲区都可被获取。而在新的算法中,始终保留含等待程序的缓冲区以供重用。只有缓冲区不含等待程序时,才会被释放为空闲。这样可以提高缓冲区的缓存效果。
  • (5)无死锁和饥饿:在 getblk()中,信号量锁定顺序始终是单向的,即 P(free),然后是P(bp),但决不会反过来,因此不会发生死锁。

实践内容

生产者——消费者

  • 代码如下:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define N 100
#define true 1
#define producerNum  10
#define consumerNum  5
#define sleepTime 1000

typedef int semaphore;
typedef int item;
item buffer[N] = {0};
int in = 0;
int out = 0;
int proCount = 0;
semaphore mutex = 1, empty = N, full = 0, proCmutex = 1;

void * producer(void * a){
    while(true){
        while(proCmutex <= 0);
        proCmutex--;
        proCount++;
        printf("produce a product: ID %d, buffer location:%d\n",proCount,in);
        proCmutex++;

        while(empty <= 0){
            printf("buffer is full\n");
        }
        empty--;

        while(mutex <= 0);
        mutex--;

        buffer[in] = proCount;
        in = (in + 1) % N;

        mutex++;
        full++;
        sleep(sleepTime);
    }
}

void * consumer(void *b){
    while(true){
        while(full <= 0){
            printf("buffer is empty\n");
        }
        full--;

        while(mutex <= 0);
        mutex--;

        int nextc = buffer[out];
        buffer[out] = 0;//消费完将缓冲区设置为0

        out = (out + 1) % N;

        mutex++;
        empty++;

        printf("produce a product: ID %d, buffer location:%d\n", nextc,out);
        sleep(sleepTime);
    }
}

int main()
{
    pthread_t threadPool[producerNum+consumerNum];
    int i;
    for(i = 0; i < producerNum; i++){
        pthread_t temp;
        if(pthread_create(&temp, NULL, producer, NULL) == -1){
            printf("ERROR, fail to create producer%d\n", i);
            exit(1);
        }
        threadPool[i] = temp;
    }//创建生产者进程放入线程池


    for(i = 0; i < consumerNum; i++){
        pthread_t temp;
        if(pthread_create(&temp, NULL, consumer, NULL) == -1){
            printf("ERROR, fail to create consumer%d\n", i);
            exit(1);
        }
        threadPool[i+producerNum] = temp;
    }//创建消费者进程放入线程池


    void * result;
    for(i = 0; i < producerNum+consumerNum; i++){
        if(pthread_join(threadPool[i], &result) == -1){
            printf("fail to recollect\n");
            exit(1);
        }
    }//运行线程池
    return 0;
}
  • 运行结果:

学习自测