第十二章学习笔记
一、梗概
- 本章讨论了块设备 I/O 和缓冲区管理;解释了块设备 I/O 的原理和 I/O 缓冲的优点;论述了 Unix 的缓冲区管理算法,并指出了其不足之处;还利用信号量设计了新的缓冲区管理算法,以提高 1/O 缓冲区的缓存效率和性能:表明了简单的 PV 算法易于实现,缓存效果好,不存在死锁和饥饿问题;还提出了一个比较 Unix 缓冲区管理算法和 PV算法性能的编程方案。
二、知识点归纳
1、块设备I/O
块设备基本概念
块设备将信息存储在固定大小的块中,每个块都有自己的地址。对操作系统来说,块设备是以字符设备的外观展现的,虽然对这种字符设备可以按照字节为单位进行访问,但是实际上到块设备上却是以块为单位(最小512byte,既一个扇区)。这之间的转换是由操作系统来完成的。
- 下面介绍块设备的基本概念:
扇区:磁盘盘片上的扇形区域,逻辑化数据,方便管理磁盘空间,是硬件设备数据传输的基本单位,一般为512byte。
块:块是VFS(虚拟文件系统)和文件系统数据传输的基本单位,必须是扇区的整数倍,格式化文件系统时,可以指定块大小。
- 块设备I/O栈的基本概念。
bio:bio是通用块层I/O请求的数据结构,表示上层提交的I/O求情。一个bio包含多个page(既page cache 内核缓冲页 在内存上),这些page对应磁盘上的一段连续的空间。由于文件在磁盘上并不连续存放,文件I/O提交到块这杯之前,极有可能被拆分成多个bio结构。
request:表示块设备驱动层I/O请求,经由I/O调度层转换后(既电梯算法合并)的I/O请求,将会被发送到块设备驱动层进行处理。
request_queue: 维护块设备驱动层I/O请求的队列,所有的request都插入到该队列,每个磁盘设备都只有一个queue(多个分区也只有一个)。
这三个结构的关系如下图所示,一个request_queue中包含多个request,每个request可能包含多个bio,请求的合并就是根据各种算法(1.noop 2.deadline 3.CFQ)将多个bio加入到同一个request中。
物理块设备1/O:每个设备都有一个I/O 队列,其中包含等待 I/O 操作的缓冲区。缓冲区上的 start io0 操作如下:
start_io(BUFFER *bp)
{
enter bp into device I/O queuei
if(bp is first buffer in I/o queue)
issue I/o command for bp to devicei
}
当 I/O 操作完成后,设备中断处理程序会完成当前缓冲区上的 I/O 操作,并启动I/O队列中的下一个缓冲区的I/O(如果队列不为空)。设备中断处理程序的算法如下:
InterruptHandler()
{
bp = dequeue(device I/0 queue):// bp e remove head of I/o queue(bp->opcode ASYNC)? brelse(bp)+ unblock process on bpi if(!empty(device I/o queue))
issue I/o command for first bp in I/o queuei
}
2、Unix I/O缓冲区管理算法
(1)I/O 缓冲区:内核中的一系列 NBUF 缓冲区用作缓冲区缓存。每个缓冲区用一个结构体表示。
typdef struct buf{
struct buf *next_freei // freelist pointer
struct buf inext devi // dev list pointer
int dev,blki // assigmed disk block;
int opcode; //READ|WRITE
int airty: // buffer data modified
int asynci // ASYNC write flag
int valid; // buffer data valid
int buayi // buffer is in use
int wanted; // some process needs this buffer
struct gemaphore 1ock=1; // buffer 1ocking semaphore; value=1
struct semaphore iodone=0:// for process to wait Eor I/O completioni
char buf[BLKSIZE]1 // block data area
}BUFFER:
BUFFER buf[NBUFl,*freelist; // NBUF buffers and free buffer list
缓冲区结构体由两部分组成:用于缓冲区管理的缓冲头部分和用于数据块的数据部分。为了保护内核内存,状态字段可以定义为一个位向量,其中每个位表示一个唯一的状态条件。为了便于讨论,这里将它们定义为inte
(2)设备表:每个块设备用一个设备表结构表示。
struct devtab{
u16 devi // major device number
BUFFER *dev 1ist; /l device buffer 1ist
BUFFER +io_queuel // device I/O queue
}devtab[NDEV];
每个设备表都有一个dev list,包含当前分配给该设备的I/O缓冲区,还有一个io qucue,包含设备上等待 I/O操作的缓冲区。I/O 队列的组织方式应确保最佳 I/O 操作。例如,它可以实现各种磁盘调度算法,如电梯算法或线性扫描算法等。为了简单起见,Unix 使用 FIFO I/O 队列。
(3)缓冲区初始化:当系统启动时,所有 I/O缓冲区都在空闲列表中,所有设备列表和 I/O 队列均为空。
(4)缓冲区列表:当缓冲区分配给(dev,blk)时,它会被插入设备表的dev_list 中。如果缓冲区当前正在使用,则会将其标记为 BUSY(繁忙)并从空闲列表中删除。繁忙缓冲区也可能会在设备表的I/O 队列中。由于一个缓冲区不能同时处于空闲状态和繁忙状态,所以可通过使用相同的 next free 指针来维护设备 1/O 队列。当缓冲区不再繁忙时,它会被释放回空闲列表,但仍保留在dev list中,以便可能重用。只有在重新分配时,缓冲区才可能从一个dev list更改到另一个dev list中。如前文所述,读/写磁盘块可以表示为 bread bwrite 和 dwrite,它们都要依赖于 getblk 和 brelse。因此,getblk 和 brelse 构成了 Unix 缓冲区管理方案的核心。getblk 和 brelse 算法如下。
(5)Unix getblk/brelse 算法((Lion 1996;(Bach 1990)第3章)。
Unix算法的缺点
1)效率低下
2)缓存效果不可预知
3)可能会出现饥饿
4)该算法只适用于单处理系统的休眠/唤醒操作
3、新的I/O缓冲区管理算法
假设有一个单处理器内核(一次运行一个进程)。使用计数信号量上的 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=1;
为了简化符号,我们将用缓冲区本身来表示每个缓冲区的信号量。与Unix 算法一样,最开始,所有缓冲区都在空闲列表中,所有设备列表和 1/0 队列均为空。下面展示一个使用信号量的简单缓冲区管理算法。pv算法,参考哲学家进餐问题。
三、实践内容
模拟实现信号量实现进程间通信
参考代码:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <errno.h>
#define total 20
sem_t remain, apple, pear, mutex;
static unsigned int vremain = 20, vapple = 0, vpear = 0;
void *father(void *);
void *mather(void *);
void *son(void *);
void *daughter(void *);
void print_sem();
int main()
{
pthread_t fa, ma, so, da;
sem_init(&remain, 0, total);//总数初始化为20
sem_init(&apple, 0, 0);//盆子中苹果数, 开始为0
sem_init(&pear, 0, 0);//盆子中梨子数, 开始为0
sem_init(&mutex, 0, 1);//互斥锁, 初始为1
pthread_create(&fa, NULL, &father, NULL);
pthread_create(&ma, NULL, &mather, NULL);
pthread_create(&so, NULL, &son, NULL);
pthread_create(&da, NULL, &daughter, NULL);
for(;;);
}
void *father(void *arg)
{
while(1)
{
sem_wait(&remain);
sem_wait(&mutex);
printf("父亲: 放苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain--, vapple++);
printf("父亲: 放苹果之后, 剩余空间=%u, 苹果数=%u\n", vremain, vapple);
sem_post(&mutex);
sem_post(&apple);
sleep(1);
}
}
void *mather(void *arg)
{
while(1)
{
sem_wait(&remain);
sem_wait(&mutex);
printf("母亲: 放梨子之前, 剩余空间=%u, 梨子数=%u\n", vremain--, vpear++);
printf("母亲: 放梨子之后, 剩余空间=%u, 梨子数=%u\n", vremain, vpear);
sem_post(&mutex);
sem_post(&pear);
sleep(2);
}
}
void *son(void *arg)
{
while(1)
{
sem_wait(&pear);
sem_wait(&mutex);
printf("儿子: 吃梨子之前, 剩余空间=%u, 梨子数=%u\n", vremain++, vpear--);
printf("儿子: 吃梨子之后, 剩余空间=%u, 梨子数=%u\n", vremain, vpear);
sem_post(&mutex);
sem_post(&remain);
sleep(3);
}
}
void *daughter(void *arg)
{
while(1)
{
sem_wait(&apple);
sem_wait(&mutex);
printf("女儿: 吃苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain++, vapple--);
printf("女儿: 吃苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain, vapple);
sem_post(&mutex);
sem_post(&remain);
sleep(3);
}
}
void print_sem()
{
int val1, val2, val3;
sem_getvalue(&remain, &val1);
sem_getvalue(&apple, &val2);
sem_getvalue(&pear, &val3);
printf("Semaphore: remain:%d, apple:%d, pear:%d\n", val1, val2, val3);
}
运行结果:
四、问题与解决
1、为什么要有输入输出缓冲区?
答:
有输入输出缓冲区用以暂时存放读写期间的文件数据而在内存区预留的一定空间。即利用主存的存储空间来暂存从磁盘中输入输出的信息。目的是缓和CPU 与 I/O 设备间速度不匹配的矛盾。减少对 CPU 的中断频率,放宽对 CPU 中断响应时间的限制。提高 CPU和 I/O 设备之间的并行性。
2、日常较为常见的计算机缓冲区有哪几种类型?
答:
日常较为常见的缓冲区,根据缓冲的应用层次不同,分别可以分为以下几种类型:主板与CPU的缓存,这两者是基于计算机硬件层次的缓冲区,能够有效地提高计算机的数据处理能力;操作系统与网络协议层的缓冲区,这则是在系统软件层的分类,为了提高访问速度,网站门户常常会基于缓冲原理使用一些组件,以实现信息的快速交互;在应用程序这一次层,缓冲区又可分为应用程序、数据库系统的缓冲区等等,一般来说,开发较为完善的大型软件会自己配备内存管理程序,在运行软件运行时自动进行对缓冲区的管理。