第十一周Linux教材第十二章学习笔记——块设备I/O和缓冲区管理

发布时间 2023-11-19 11:30:43作者: 20211115fyq

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

本章讨论了块设备1/O和缓冲区管理;解释了块设备1/O的原理和T/O缓冲的优点;论述了Unix的缓冲区管理算法,并指出了其不足之处;还利用信号量设计了新的缓冲区管理算法,以提高1/O缓冲区的缓存效率和性能;表明了简单的PV算法易于实现,缓存效果好,不存在死锁和饥饿问题;还提出了一个比较Unix缓冲区管理算法和PV算法性能的编程方案。编程项目还可以帮助读者更好地理解文件系统中的I/O操作。

12.1 块设备I/O和缓冲区

  • 块设备 I/O:

  1. 概念: 块设备是一种按块(通常为固定大小的块)进行数据传输的设备,典型的例子是硬盘驱动器。块设备 I/O 涉及到对存储介质上的数据块的读取和写入。

  2. 块大小: 数据在块设备上以块为单位进行读写,块的大小通常是固定的,例如,一个典型的块大小可以是 512 字节或 4KB。

  3. 读取操作: 当应用程序请求从块设备读取数据时,操作系统会发起相应的块设备 I/O 操作,将数据块读取到内存中的缓冲区。

  4. 写入操作: 类似地,当应用程序希望向块设备写入数据时,操作系统将数据从内存缓冲区写入到块设备上的相应块。

  5. 缓存: 操作系统通常会使用缓存来存储最近读取或写入的块,以提高性能。这就是为什么在某些情况下,读取相同的块可能不会导致实际的物理 I/O 操作。

  • 缓冲区管理:

  1. 概念: 缓冲区是一个临时存储数据的区域,通常是为了减少对主存或磁盘等慢速设备的访问次数而引入的。缓冲区管理涉及到如何有效地使用和管理这些缓冲区。

  2. 读取缓冲: 当数据从块设备读取到内存时,通常会使用读取缓冲,将数据暂存于内存中,以便应用程序可以更快地访问。

  3. 写入缓冲: 类似地,当应用程序向块设备写入数据时,数据可能会首先被写入写入缓冲,然后由操作系统决定何时将缓冲中的数据写入实际的块设备。

  4. 缓存策略: 操作系统需要决定何时将数据从缓冲区写入块设备,以及何时从块设备读取新的数据到缓冲区。缓存策略的选择可以影响系统的性能。

  5. 脏页: 缓冲区中的数据可能与块设备上的数据不同步。当缓冲区中的数据被修改但尚未写回块设备时,该缓冲区被称为“脏页”。

12.2 Unix I/O缓冲区管理算法

  1. Unix系统中使用了一些经典的缓冲区管理算法,其中包括:

    • LRU(Least Recently Used): 最近最少使用算法,用于决定哪些缓冲区中的数据应该被替换。最久未被使用的数据会被最先替换。

    • FIFO(First-In-First-Out): 先进先出算法,按照数据进入缓冲区的顺序来选择替换哪些数据。

    • CLOCK算法: 一种近似LRU算法,通过维护一个时钟指针来模拟LRU的选择过程。

  2. 缓冲区刷新(Flush): 当应用程序对文件进行写操作时,数据通常首先被写入内核缓冲区而不是直接写入磁盘。缓冲区刷新是指将内核缓冲区中的数据写入到磁盘。这可以通过同步刷新或异步刷新来完成。

    • 同步刷新: 在写入系统调用完成之前,内核会等待数据被写入磁盘。这确保了数据的持久性,但可能导致性能下降。

    • 异步刷新: 内核会在后台将数据写入磁盘,而不阻塞应用程序。这提高了性能,但在发生系统崩溃时可能导致数据丢失。

  3. 预读和预写: 为了提高性能,Unix系统通常会使用预读和预写策略。预读是指在应用程序请求数据之前,系统会预先读取一些附近的数据到缓冲区。预写是指系统会提前将一些数据写入磁盘,而不仅仅在应用程序请求写入时才进行。

算法缺点:

  1. 效率低下;同时唤醒多组进程,但最后只有一组进程可以获取释放的缓冲区,其他所有被唤醒的进程必须重新进入休眠状态。
  2. 缓存效果不可预知;每个释放的缓冲区都有可能被获取。如果缓冲区由需要空闲缓冲区的进程获取,那么将会重新分配缓冲区,即使有些进程仍然需要当前的缓冲区。
  3. 可能会出现进程饥饿;
  4. 该算法使用只适用于单处理系统的休眠/唤醒操作。

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

使用信号量的缓冲区管理算法:

使用信号量的缓冲区管理算法通常涉及到解决多线程或多进程之间的竞态条件和同步问题。信号量是一种用于线程或进程之间同步的机制,可以用于确保在共享资源上的互斥访问。

下面是一个简单的使用信号量的缓冲区管理算法的示例,其中涉及一个生产者-消费者场景:

信号量定义:

  1. 一个用于表示可用缓冲区数量的信号量(empty)。
  2. 一个用于表示已用缓冲区数量的信号量(full)。
  3. 一个用于互斥访问共享资源的信号量(mutex)。
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <pthread.h>
 4 #include <semaphore.h>
 5 
 6 #define BUFFER_SIZE 5
 7 
 8 int buffer[BUFFER_SIZE];
 9 sem_t empty, full, mutex;
10 int in = 0, out = 0;
11 
12 void produce(int item) {
13     // Produce an item
14     buffer[in] = item;
15     in = (in + 1) % BUFFER_SIZE;
16 }
17 
18 int consume() {
19     // Consume an item
20     int item = buffer[out];
21     out = (out + 1) % BUFFER_SIZE;
22     return item;
23 }
24 
25 void* producer(void* arg) {
26     int item;
27     while (1) {
28         // Produce an item
29         item = rand() % 100;
30         
31         // Wait for an empty buffer slot
32         sem_wait(&empty);
33         
34         // Wait for mutex to access the buffer
35         sem_wait(&mutex);
36         
37         // Produce the item into the buffer
38         produce(item);
39         
40         // Release mutex
41         sem_post(&mutex);
42         
43         // Signal that a buffer slot is full
44         sem_post(&full);
45     }
46     return NULL;
47 }
48 
49 void* consumer(void* arg) {
50     int item;
51     while (1) {
52         // Wait for a full buffer slot
53         sem_wait(&full);
54         
55         // Wait for mutex to access the buffer
56         sem_wait(&mutex);
57         
58         // Consume an item from the buffer
59         item = consume();
60         
61         // Release mutex
62         sem_post(&mutex);
63         
64         // Signal that a buffer slot is empty
65         sem_post(&empty);
66         
67         // Process the consumed item
68         printf("Consumed: %d\n", item);
69     }
70     return NULL;
71 }
72 
73 int main() {
74     // Initialize semaphores
75     sem_init(&empty, 0, BUFFER_SIZE); // Number of empty buffer slots
76     sem_init(&full, 0, 0); // Number of full buffer slots
77     sem_init(&mutex, 0, 1); // Mutex for buffer access
78 
79     // Create producer and consumer threads
80     pthread_t producer_thread, consumer_thread;
81     pthread_create(&producer_thread, NULL, producer, NULL);
82     pthread_create(&consumer_thread, NULL, consumer, NULL);
83 
84     // Wait for threads to finish (this example runs indefinitely)
85     pthread_join(producer_thread, NULL);
86     pthread_join(consumer_thread, NULL);
87 
88     // Destroy semaphores
89     sem_destroy(&empty);
90     sem_destroy(&full);
91     sem_destroy(&mutex);
92 
93     return 0;
94 }

这是一个简单的生产者-消费者模型,使用信号量确保在缓冲区上的互斥访问,并通过信号量控制缓冲区的状态。 empty 信号量跟踪可用的空缓冲区槽,full 信号量跟踪已用的缓冲区槽,而 mutex 信号量用于互斥访问共享资源(缓冲区)。

12.4 PV算法

PV算法(也称为P和V算法)是用于管理信号量的一组基本操作。这些操作包括:

  1. P(Produce)操作: 也称为wait操作,用于申请资源。如果资源可用,则减少信号量的值;否则,阻塞直到资源可用。

  2. V(Vaporate)操作: 也称为signal操作,用于释放资源。增加信号量的值,表示资源可用。

这两个操作是原子的,即它们在执行过程中不能被中断。PV算法通常与信号量一起使用,用于确保对共享资源的互斥访问。信号量是一个整数,用于表示可用的资源数量。当信号量的值为正时,表示有可用资源;当值为零时,表示资源已经被占用;当值为负时,表示有等待资源的进程或线程。

下面是PV算法的一般形式:

P操作(wait):

P(S):
  while S <= 0 do
    ; // 等待资源可用
  S = S - 1; // 申请资源

V操作(signal):

V(S):
  S = S + 1; // 释放资源

这里,S 是信号量。P(S) 操作尝试获取资源,如果资源不可用,则进入等待状态。V(S) 操作释放资源,增加信号量的值,从而允许其他等待资源的进程或线程继续执行。

PV算法是用于实现进程同步和互斥的基础机制,常用于多进程或多线程的操作系统中。这些算法有助于防止多个进程或线程同时访问共享资源,确保数据的一致性和正确性。

12.5 编程实践

PV操作代码

 1 #include <stdio.h>
 2 #include <pthread.h>
 3 
 4 int S = 1;  // 信号量
 5 
 6 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 7 pthread_cond_t condition = PTHREAD_COND_INITIALIZER;
 8 
 9 // P(wait)操作
10 void P() {
11     pthread_mutex_lock(&mutex);
12 
13     while (S <= 0) {
14         // 等待资源可用
15         pthread_cond_wait(&condition, &mutex);
16     }
17 
18     // 申请资源
19     S--;
20 
21     pthread_mutex_unlock(&mutex);
22 }
23 
24 // V(signal)操作
25 void V() {
26     pthread_mutex_lock(&mutex);
27 
28     // 释放资源
29     S++;
30 
31     // 唤醒等待资源的线程
32     pthread_cond_signal(&condition);
33 
34     pthread_mutex_unlock(&mutex);
35 }
36 
37 void* producer(void* arg) {
38     while (1) {
39         P();  // 申请资源
40 
41         // 生产者的操作
42 
43         V();  // 释放资源
44     }
45     return NULL;
46 }
47 
48 void* consumer(void* arg) {
49     while (1) {
50         P();  // 申请资源
51 
52         // 消费者的操作
53 
54         V();  // 释放资源
55     }
56     return NULL;
57 }
58 
59 int main() {
60     pthread_t producer_thread, consumer_thread;
61 
62     // 创建生产者和消费者线程
63     pthread_create(&producer_thread, NULL, producer, NULL);
64     pthread_create(&consumer_thread, NULL, consumer, NULL);
65 
66     // 主线程等待线程结束
67     pthread_join(producer_thread, NULL);
68     pthread_join(consumer_thread, NULL);
69 
70     return 0;
71 }

运行结果

这段代码是一个简单的生产者-消费者模型,其中包含了P和V操作。由于代码中的生产者和消费者是无限循环的,所以程序将永远运行下去。

在这个简化的例子中,P操作和V操作的具体实现只是简单地增加或减少信号量S的值,并使用互斥锁和条件变量来确保对S的操作是原子的。

运行结果会是无限循环地执行生产者和消费者的操作。由于互斥锁的存在,每次只有一个线程能够执行P和V操作,从而保证对S的操作不会出现竞态条件。

请注意,实际的生产者-消费者模型可能涉及更多的细节和同步机制,例如缓冲区,以确保正确的生产和消费过程。这只是一个基本的示例,实际应用中可能需要更多的考虑和改进。

向ChatGpt请求苏格拉底式询问