《存储IO路径》专题:不同IO调度器的差异

在计算机世界中,有一个神秘的王国,叫做IO王国。这个王国里有四种奇怪的生物,它们分别是Noop调度器、Anticipatory调度器、Deadline调度器和CFQ调度器。IO调度器负责管理计算机中的IO请求,确保它们有序地通过。就像一个交警会根据车流量来指挥交通一样,IO调度器也会根据IO请求的到达顺序来决定处理它们的顺序。

Noop调度器是IO王国的小学生,它非常乖巧,总是按照规矩排队。它的行为非常简单,只会按照IO请求的到达顺序进行处理。有一天,一个叫SSD的小朋友来到IO王国,它想要成为Noop调度器的好朋友。但是,Noop调度器却觉得SSD小朋友不够聪明,因为SSD小朋友不需要排队,可以直接进入王国。于是,Noop调度器就拒绝了SSD小朋友的请求。但是,SSD小朋友并不气馁,它决定要找到一个更聪明的调度器来和它做朋友。

Anticipatory调度器是IO王国的小聪明,它能够预测未来的IO请求。有一天,一个叫Web服务器的小朋友来到IO王国,它想要成为Anticipatory调度器的好朋友。但是,Anticipatory调度器却觉得Web服务器小朋友的请求太简单了,因为它只能处理GET和POST请求。于是,Anticipatory调度器就拒绝了Web服务器小朋友的请求。但是,Web服务器小朋友并不气馁,它决定要找到一个能够理解它请求的调度器来和它做朋友。

Deadline调度器是IO王国的小严格,它要求每个IO作业都要按时完成任务。有一天,一个叫文件服务器的小朋友来到IO王国,它想要成为Deadline调度器的好朋友。但是,Deadline调度器却觉得文件服务器小朋友的请求太慢了,因为它需要很长时间才能完成任务。于是,Deadline调度器就拒绝了文件服务器小朋友的请求。但是,文件服务器小朋友并不气馁,它决定要找到一个能够理解它请求的调度器来和它做朋友。

CFQ调度器是IO王国的小公平,它是一个公平的裁判,确保每个进程都能得到平等的待遇。有一天,一个叫桌面多任务的小朋友来到IO王国,它想要成为CFQ调度器的好朋友。但是,CFQ调度器却觉得桌面多任务小朋友的请求太复杂了,因为它需要同时处理多个任务。于是,CFQ调度器就拒绝了桌面多任务小朋友的请求。但是,桌面多任务小朋友并不气馁,它决定要找到一个能够理解它请求的调度器来和它做朋友。

这四个调度器都有自己的特点,各有优缺点。在选择合适的调度器时,我们要根据具体情况来考虑,是乖巧的SSD、聪明的Web服务器、严格的文件服务器还是公平的桌面多任务系统。不管怎样,这些调度器都在用自己的方式维护着计算机的IO秩序,确保IO请求有序地处理。

接下来,结合代码来对比几种调度器的差异

  1. Noop IO调度器

Noop调度器是一个简单的调度器,它按照请求到达的顺序服务I/O请求。它的实现比较简单,适用于一些简单的存储设备。以下是Noop调度器的代码实现:

struct request_queue {  
    struct request *requests;  
    int head;  
    int tail;  
};  
  
void enqueue_request(struct request_queue *queue, struct request *req) {  
    queue->requests[queue->tail] = *req;  
    queue->tail = (queue->tail + 1) % MAX_REQUESTS;  
}  
  
struct request *dequeue_request(struct request_queue *queue) {  
    return &queue->requests[queue->head];  
}  
  
void noop_schedule(struct request_queue *queue) {  
    struct request *req;  
    while ((req = dequeue_request(queue)) != NULL) {  
        // 处理请求  
    }  
}
  1. Anticipatory IO调度器

Anticipatory调度器是一个基于预计执行时间的调度器,它根据每个I/O作业的预计执行时间来决定服务顺序。它认为一些I/O作业可能会引起其他相关作业的访问,因此它会在预计执行时间更短的作业之前先服务一些其他相关作业。以下是Anticipatory调度器的代码实现:

struct job {  
    int sector;  
    int arrival_time;  
    int service_time;  
};  
  
struct job_queue {  
    struct job *jobs;  
    int head;  
    int tail;  
};  
  
void enqueue_job(struct job_queue *queue, struct job *job) {  
    queue->jobs[queue->tail] = *job;  
    queue->tail = (queue->tail + 1) % MAX_JOBS;  
}  
  
struct job *dequeue_job(struct job_queue *queue) {  
    return &queue->jobs[queue->head];  
}  
  
void anticipatory_schedule(struct job_queue *queue) {  
    struct job *job;  
    while ((job = dequeue_job(queue)) != NULL) {  
        // 处理请求  
    }  
}
  1. Deadline IO调度器

Deadline调度器是一个具有超时时间的调度器,它为每个I/O作业设定一个超时时间。如果某个I/O作业等待时间超过了设定的超时时间,那么它会被优先服务。以下是Deadline调度器的代码实现:

struct request {  
    int sector;  
    int queue_num;  
    int arrival_time;  
};  
  
struct request_queue {  
    struct request *requests;  
    int head;  
    int tail;  
};  
  
void enqueue_request(struct request_queue *queue, struct request *req) {  
    queue->requests[queue->tail] = *req;  
    queue->tail = (queue->tail + 1) % MAX_REQUESTS;  
}  
  
struct request *dequeue_request(struct request_queue *queue) {  
    return &queue->requests[queue->head];  
}  
  
void deadline_schedule(struct request_queue *queue) {  
    struct request *req;  
    while ((req = dequeue_request(queue)) != NULL) {  
        // 处理请求  
    }  
}

4.CFQ调度器

CFQ调度器是一种通用型的I/O调度算法,旨在为所有进程提供公平的I/O带宽。它是Linux内核中使用最广泛的调度器之一,并且在许多操作系统中都有实现。

CFQ调度器通过为每个进程创建一个I/O队列来实现公平的I/O分配。每个进程的队列都是一个先进先出(FIFO)的队列,其中包含了该进程的I/O请求。当一个进程的I/O请求到达时,它会被添加到该进程的队列中,等待服务。

在CFQ调度器中,每个进程的I/O带宽是按照其进程优先级和等待时间来分配的。调度器会根据进程的优先级和等待时间来决定服务哪个进程的I/O请求。如果两个进程的优先级和等待时间相同,那么调度器会按照先来先服务的原则进行处理。

下面是一个简单的CFQ调度器的代码实现,供参考:

struct cfq_io_data {  
    struct io_queue *queue;  
    struct io_priority_data prio;  
    unsigned long last_end_time;  
    s64 slice;  
    enum class_type type;  
    void *problem_data;  
};  
  
static void cfq_dequeue_request(struct cfq_io_data *io_data)  
{  
    struct io_request *rq = dequeue_next(&io_data->queue->queue);  
    if (!rq)  
        return;  
    cfq_update_io_dispatch(cfqd, cfq_prio_to_weight(&io_data->prio));  
}  
  
static void cfq_dispatch_request(struct cfq_io_data *io_data)  
{  
    struct cfq_data *cfqd = &io_data->queue->cfqd;  
    struct io_request *rq = dequeue_next(&io_data->queue->queue);  
    if (!rq)  
        return;  
    cfq_change_class_weight(&io_data->prio, cfq_classify(rq));  
    cfq_update_io_dispatch(cfqd, cfq_prio_to_weight(&io_data->prio));  
}

上述代码中,cfq_dequeue_request()函数从当前进程的队列中取出下一个I/O请求,而cfq_dispatch_request()函数则根据当前进程的队列中的I/O请求来更新I/O调度器的权重。这些权重将用于决定服务哪个进程的I/O请求。

CFQ调度器通过公平地分配I/O带宽,使得所有进程都能够获得公平的I/O服务。这种公平性对于多任务操作系统来说非常重要,可以避免某些进程因为得不到足够的I/O资源而出现性能问题。同时,CFQ调度器的实现也比较简单和高效,因此在许多系统中都得到了广泛的应用。

在Linux内核中,修改IO调度的算法也比较简单,简单例子参考:

1.查看当前调度算法是NOOP
$ cat /sys/block/nvme0n1/queue/scheduler
[noop] deadline cfq

2.将NOOP算法调整为CFQ
$ echo 'cfq'>/sys/block/nvme0n1/queue/scheduler

3.查看修改后的算法为CFQ
$ cat /sys/block/nvme0n1/queue/scheduler
noop deadline [cfq]

不同的调度器,在不同类型的存储设备上的性能和使用场景都有一定的差异:

  1. Noop调度器:
    • 性能表现:Noop调度器是一个简单的调度程序,它本质上是一个链表实现的FIFO队列,对请求进行简单的合并处理。由于它的实现简单,因此在各种类型的存储设备上性能表现相对较差。
    • 使用场景:Noop调度器适用于固态硬盘(SSD)等存储设备,因为固态硬盘的IO调度算法越简单,效率就越高。
  1. Anticipatory调度器:
    • 性能表现:Anticipatory调度器通过预测未来的IO请求,将硬盘的I/O请求预先发送到硬盘中,以减少磁头的移动和寻道时间,提高整体性能。它在处理有一定数据量但IO压力不是非常大的场景中表现最为出色。
    • 使用场景:Anticipatory调度器适用于机械硬盘(HDD)和SATA SSD等存储设备,因为在这些存储设备上,提高效率需要考虑寻道时间和磁头移动等因素。
  1. Deadline调度器:
    • 性能表现:Deadline调度器以减少磁头的移动和寻道时间为目标,尽可能地满足I/O请求的截止期限。它适合用在有大量I/O请求并且IO压力比较重的业务,比如数据库系统。
    • 使用场景:Deadline调度器适用于机械硬盘(HDD)和SATA SSD等存储设备,因为在这些存储设备上,提高效率需要考虑寻道时间和磁头移动等因素。对于业务比较单一并且IO压力比较重的业务,Deadline调度器是最佳的选择。
  1. CFQ调度器:
    • 性能表现:CFQ调度器是一种通用的调度算法,它以进程为出发点考虑,保证所有进程都能公平地分配到I/O资源。它适合用于多任务、多媒体等需要公平分配I/O资源的场景中。
    • 使用场景:CFQ调度器适用于机械硬盘(HDD)、SATA SSD和NVMe SSD等存储设备。然而,由于CFQ调度器的复杂度较高,因此在固态硬盘这种场景下,其效率可能会比Noop和Anticipatory调度器低。

综上所述,不同类型的调度器在不同的存储设备上表现会有所差异。在选择合适的调度器时,需要根据具体的存储设备类型和使用场景来考虑。

猜你喜欢

转载自blog.csdn.net/zhuzongpeng/article/details/132724689
Io
IO: