第12章 块设备I/O和缓冲区管理
知识总结
- 解释块设备I/O的原理和I/O缓冲的优点
- 介绍Unix的缓冲区管理算法
- 利用信号量设计新的缓冲区管理算法,以提高I/O缓冲区的缓存效率和性能
- 介绍简单的PV算法及其特点
基本概念
读写普通文件的算法依赖于两个关键操作,即get_block和put_block,这两个操作将磁盘块读写到内存缓冲区中。由于与内存访问相比,磁盘I/O速度较慢,所以不希望在每次执行读写文件操作时都执行磁盘I/O。因此、大多数文件系统使用I/O缓冲来减少进出存储设备的物理I/O数量。
合理设计的I/O缓冲方案可显著提高文件I/O效率并增加系统吞吐量。
I/O缓冲的基本原理非常简单。文件系统使用系列I/O缓冲区作为块设备的缓存内存。当进程试图读取(dev,blk)标识的磁盘块时。它首先在缓冲区缓存中搜索分配给磁盘块的缓冲区。
如果该缓冲区存在并且包含有效数据、那么它只需从缓冲区中读取数据、而无须再次从磁盘中读取数据块。如果该缓冲区不存在,它会为磁盘块分配一个缓冲区,将数据从磁盘读人缓冲区,然后从缓冲区读取数据。当某个块被读入时,该缓冲区将被保存在缓冲区缓存中。以供任意进程对同一个块的下一次读/写请求使用。同样。当进程写入磁盘块时。它首先会获取一个分配给该块的缓冲区。然后,它将数据写入缓冲区,将缓冲区标记为脏、以延迟写入,并将其释放到缓冲区缓存中。由于脏缓冲区包含有效的数据,因此可以使用它来满足对同一块的后续读/写请求,而不会引起实际磁盘I/O。脏缓冲区只有在被重新分配到不同的块时才会写入磁盘。
在read file/write file中,我们假设它们从内存中的一个专用缓冲区进行读/写。
对于I/O缓冲,将从缓冲区缓存中动态分配缓冲区。假设BUFFER是缓冲区的结构类型,而且getblk(dev,blk)从缓冲区缓存中分配一个指定给(dev,blk)的缓冲区。定义一个bread(dev,blk)函数,它会返回一个包含有效数据的缓冲区(指针)。
从缓冲区读取数据后,进程通过brelse(bp)将缓冲区释放会缓冲区缓存。同理,定义一个write_block(dev, blk, data)函数。
同步写入操作等待写操作完成,用于顺序块或可移动块设备。
当I/O操作完成后,设备中断处理程序会完成当前缓冲去上的I/O操作,并启动I/O队列中下一个缓冲区的I/O。
Unix I/O缓冲区管理算法
实践过程
-
信号同步
生产者消费者问题
“生产者—消费者”问题 (producer/consumer problem) 是最著名的进程同步问题。
该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。
与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。它是许多相互合作进程的抽象,如输入进程与计算进程;计算进程与打印进程等。
要解决该问题,就必须让生产者在缓冲区满时休眠,等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
设置两个资源信号量及一个互斥信号量。
资源信号量 empty:说明空缓冲区的数目,其初值为有界缓冲池的大小n。
资源信号量 full:说明满缓冲区的数目(即产品数目),其初值为 0。full+ empty= n。
互斥信号量 s: 说明该有界缓冲池是一个临界资源,必须互斥使用,其初值为 1。//生产者消费者问题 #include<stdio.h> #include<unistd.h> #include<semaphore.h> #include<pthread.h> sem_t empty,full,s; int buffer[10]={-1}; int fill=0; int use=0; void put(int value){ buffer[fill]=value; fill=(fill+1)%11; } int get(){ int tmp=buffer[use]; use=(use+1)%11; return tmp; } void *producer(void *arg){ printf("producer\n"); int i=0; for(i=0;i<=20;i++){ //sleep(3); sem_wait(&empty); sem_wait(&s); put(i); sem_post(&s); sem_post(&full); printf("producer put:%d\n",i); } //pthread_exit(0); } void *consumer(void *arg){ printf("consumer\n"); int i=0; for(i=0;i<=20;i++){ //sleep(3); sem_wait(&full); sem_wait(&s); int tmp=get(); sem_post(&s); sem_post(&empty); printf("consumer get:%d\n",tmp); } //pthread_exit(0); } int main(int argv,char * args[]){ sem_init(&empty,0,10); sem_init(&full,0,0); sem_init(&s,0,1); pthread_t pro,con; pthread_create(&pro,NULL,producer,NULL); pthread_create(&con,NULL,consumer,NULL); pthread_join(con,NULL); pthread_join(pro,NULL); }
-
吃水果问题
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
//定义信号量
//盘子空信号量
sem_t empty;//初始值为1,表示当前盘子为空
//苹果信号量
sem_t apple;//初始值为0,表示当前盘子没有苹果
//橘子信号量
sem_t orange;//初始值为0,表示当前盘子没有橘子
//函数声明
void* father(void* arg);//父亲线程执行函数
void* mother(void* arg);//母亲线程执行函数
void* son(void* arg);//儿子线程执行函数
void* daughter(void* arg);//女儿线程执行函数
void* father(void* arg) {
while(1) {
sem_wait(&empty);
// 放入一个苹果
printf("father --> apple\n");
sem_post(&apple);
sleep(rand() % 10); // 随机休眠一段时间
}
}
void* mother(void* arg)
{
while(1)
{
sem_wait(&empty);
//放入一个橘子
printf("mother --> orange\n");
sem_post(&orange);
sleep(rand() % 10); // 随机休眠一段时间
}
}
void* son(void* arg)
{
while(1)
{ //等盘子不为空才开始执行下面的,盘子不为空说明有橘子或者苹果
sem_wait(&apple);//当苹果为1时,做减法,0则不做
printf("son --> apple\n");
//把盘子信号量释放,表示盘子为空,信号量加1
sem_post(&empty);
sleep(rand() % 10); // 随机休眠一段时间
}
}
void* daughter(void* arg)
{
while(1)
{ //等盘子不为空才开始执行下面的,盘子不为空说明有橘子或者苹果
sem_wait(&orange);//当橘子为1时,做减法,0则不做
printf("daughter --> orange\n");
//把盘子信号量释放,表示盘子为空,信号量加1
sem_post(&empty);
sleep(rand() % 10); // 随机休眠一段时间
}
}
int main()
{ //定义父亲,母亲,儿子,女儿线程
pthread_t fatherThread,motherThread,sonThread,daughterThread;
//初始化信号量,empty 1,orange 0,apple 0;
sem_init(&empty,0,1);
sem_init(&orange,0,0);
sem_init(&apple,0,0);
//创建线程
pthread_create(&fatherThread, NULL, father, NULL);
pthread_create(&motherThread, NULL, mother, NULL);
pthread_create(&sonThread, NULL, son, NULL);
pthread_create(&daughterThread, NULL, daughter, NULL);
pthread_join(fatherThread, NULL);
pthread_join(motherThread, NULL);
pthread_join(sonThread, NULL);
pthread_join(daughterThread, NULL);
}
- setbuf()和setvbuf()函数的实际意义在于:用户打开一个文件后,可以建立自己的文件缓冲区,而不必使用fopen()函数打开文件时设定的默认缓冲区。这样就可以让用户自己来控制缓冲区,包括改变缓冲区大小、定时刷新缓冲区、改变缓冲区类型、删除流中默认的缓冲区、为不带缓冲区的流开辟缓冲区等。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
char buff[1024];
memset( buff, '\0', sizeof( buff ));
fprintf(stdout, "启用全缓冲\n");
setvbuf(stdout, buff, _IOFBF, 1024);
fprintf(stdout, "这里是 runoob.com\n");
fprintf(stdout, "该输出将保存到 buff\n");
fflush( stdout );
fprintf(stdout, "这将在编程时出现\n");
fprintf(stdout, "最后休眠五秒钟\n");
sleep(5);
return(0);
}