学习笔记10
第12章 块设备I/O和缓冲区管理
本章讨论了块设备I/O和缓冲区管理;解释了块设备I/O的原理和I/O缓冲的优点;论述了Unix的缓冲区管理算法;利用信号量设计了新的缓冲区管理算法以提高I/O缓冲区的缓存效率和性能。
块设备I/O缓冲区:
大多数文件系统使用I/O缓冲来减少进出存储设备的物理I/O数量
I/O缓冲的基本原理:
文件系统使用一系列I/O缓冲区作为块设备的缓存内存。当进程试图读取(dev,blk)标识的磁盘块时,它首先在缓冲区缓存中搜索分配给磁盘块的缓冲区。如果该缓冲区存在并且包含有效数据,那么它只需从缓冲区中读取数据,而无须再次从磁盘中读取数据块。如果该缓冲区不存在,它会为磁盘块分配一个缓冲区,将数据从磁盘读入缓冲区,然后从缓冲区读取数据。当某个块被读入时,该缓冲区将被保存在缓冲区缓存中,以供任意进程对同一个块的下一次读/ 写请求使用。同样,当进程写入磁盘块时,它首先会获取一个分配给该块的缓冲区。然后,它将数据写入缓冲区,将缓冲区标记为脏,以延迟写入,并将其释放到缓冲区缓存中。由于脏缓冲区包含有效的数据,因此可以使用它来满足对同一块的后续读/ 写请求,而不会引起实际磁盘 I/0。脏缓冲区只有在被重新分配到不同的块时才会写入磁盘。
Unix I/O 缓冲区管理算法
Unix I/O 缓冲区管理子系统由以下几部分组成:
1.I/O缓冲区:内核中的一系列NBUF缓冲区用作缓冲区缓存。每个缓冲区用一个结构体表示。
typdef struct buf {
struct buf *next_ free; // freelist pointer
struct buf *next_dev; //I dev_list pointer
int dev, blk; // assigned disk block;
int opcode; //READ WRITE
int dirty; // buffer data modified
int async; // ASYNC write flag
int valid; // buffer data valid
int busy; //buffer is in use
int wanted; // some process needs this buffer
struct semaphore 1ock=1: // buffer locking semaphore; value=1
struct semaphore iodone=0; // for process to wait for I/0 completion;
char buf [BLKSIZE]; } BUFFER; // block data area
BEPER buf [NBUV], *freelist: // NBUF buffers and free buffer list
2.设备表:每个设备用一个设备表结构表示
struct devtab {
u16 dev; // major device number
BUFFER *dev_list; // device buffer list
BUFFER *io_queue; // device I/0 queue
} devtab [NDEV];
3.缓冲区初始化:当系统启动时,所有I/O缓冲区都在空闲列表中,所有设备列表和I/O队列均为空。
4.缓冲区列表:当缓冲区分配给(dev,blk)时,它会被插入设备表中的dev_list中、
5.Unix getblk/brelse算法
关于Unix算法的一些具体说明:
(1)数据一致性:为了确保数据一致性,getblk一定不能给同一个(dev, blk)分配多个缓冲区。这可以通过让进程从休眠状态唤醒后再次执行 “重试循环”来实现。读者可以验证分配的每个缓冲区都是唯一的。其次,脏缓冲区在重新分配之前被写出来,这保证了数据的一致性。
(2)缓存效果:缓存效果可通过以下方法实现。释放的缓冲区保留在设备列表中,以便可能重用。标记为延迟写人的缓冲区不会立即产生 1/0,并且可以重用。缓冲区会被释放到空闲列表的末尾,但分配是从空闲列表的前面开始的。这是基于 LRU(最近最少使用) 原则,它有助于延长所分配缓冲区的使用期,从而提高它们的级存效果。
(3)临界区:设备中断处理程序可操作缓冲区列表,例如从设备表的 I/0队列中删除切,更改其状态并调用 brelse(bp)。所以,在getblk和brelse中,设备中断在这些临果区中会被屏蔽。这些都是隐含的,但没有在算法中表现出來。
Unix算法的缺点:
(1)效率低下:该算法 依赖于重试循环。例如,释放缓冲区可能会唤醒两组进程:需要轻放的缓冲区的进程,以及只需要空闲缓冲区的进程。由于只有一个进程可以获取释放的绥冲区,所以,其他所有被唤醒的进程必须重新进人休眠状态。从休眠状态唤醒后,每个被唤醒的进程必领从头开始重新执行算法,因为所需的绥冲区可能已经存在。这会导致过多的进程切换。
(2)缓存效果不可预知:在Unix 算法中,每个释放的缓冲区都可被获取。如果缓冲区由需要空闲缓冲区的进程获取,那么将会重新分配缓冲区,即使有些进程仍然需要当前的缓冲区。
(3)可能会出现饥饿:Unix 算法基于“自由经济” 原则,即每个进程都有尝试的机会,目不能保证成功。因此,可能会出现进程饥饿。
(4)该算法使用只适用于单处理器系统的休眠/ 唤醒操作。
苏格拉底挑战
课堂笔记
万能函数:
void * func(void *parameter)
typedef void * (*uf)(void *para)
C语言可以通过不同的函数签名来实现求两个整数和的操作。以下是十种不同的函数签名示例:
1. 使用int返回类型和两个int参数:
int add(int a, int b);
2. 使用void返回类型和两个int参数,通过指针修改其中一个参数的值:
void add(int a, int b, int *result);
3. 使用long返回类型和两个int参数:
long add(int a, int b);
4. 使用double返回类型和两个int参数:
double add(int a, int b);
5. 使用宏定义来计算两个整数的和:
#define ADD(a, b) ((a) + (b))
6. 使用函数指针作为参数,以及两个int参数:
int operate(int a, int b, int (*operation)(int, int));
7. 使用变长参数列表(可变参数)的函数签名:
int add(int num, ...);
8. 使用内联函数的方式实现加法操作:
inline int add(int a, int b);
9. 使用模板函数(C++)的方式实现加法操作:
template <typename T>
T add(T a, T b);
10. 使用函数重载的方式实现加法操作,其中参数类型可以不同:
int add(int a, int b);
double add(double a, double b);
你可以根据你的需求和具体情况选择其中的一种函数签名来实现求两个整数和的功能。不同的签名适用于不同的编程场景和要求。