Linux内核——块I/O层及I/O调度

一、块I/O基本概念

字符设备:按照字符流 的方式被有序访问的设备。如串口、键盘等。

块设备:系统中不能随机(不需要按顺序)访问固定大小的数据片 (chunk 块)的设备。

    如:硬盘、软盘、CD-ROM驱动器、闪存等。都是通过以安装文件系统的方式使用。

块设备的组成:

扇区:是块设备 中最小的可寻址单元(常见大小512字节);是块设备的基本寻址和操作单元。

块:是文件系统 最小逻辑可寻址单元,文件系统的抽象,只能通过块访问文件系统。通常包含多个扇区。

当一个块被调入内存时(读入后或等待写出时),它要存储在一个缓冲区中;每个缓冲区与一个块对应,缓冲区相当于是磁盘块在内存中的表示;块大小不超过一个页面,一个页可以容纳一个或多个内存中的块。

缓冲区:是内核操作块设备的逻辑单元 ,每个缓冲区需要一个描述符 来表示块的控相关制信息。

数据结构:缓冲区头buffer_head,虽然不用于I/O操作,但是它负责描述磁盘块到页面的映射;

内核操作I/O块基本容器是:bio。操作内核中所有的缓冲区对应的I/O块。

请求队列:块设备将他们挂起的块I/O请求保存在请求队列中。

二、I/O调度机制

简单的以内核产生I/O请求的次序直接将请求发向块设备,造成性能将难以接受。因为磁盘寻址是整个计算机中最慢 的操作之一,每一次寻址定位硬盘磁头到特定块上某个位置需要花费不少时间;要提高I/O操作性能,尽量缩短磁盘寻址时间。

在提交请求到块设备前,内核 需要对请求进行处理:先执行合并与排序的预操作 ——I/O调度机制子系统,负责I/O请求的提交。

I/O调度程序管理块设备的请求队列,决定队列中的请求排列顺序,何时派发请求到设备。以减少磁盘寻址时间,提高全局吞吐量。

其实现的方法是合并与排序:

合并:将两个或多个请求结合成一个 新的请求,比如访问磁盘扇区相邻 时,合并为一个对单个和多个相邻磁盘扇区操作的新请求。合并后仅需要一次请求一条寻址命令。

排序:没有相邻操作扇区请求时,但可能是比较接近的 ;将整个请求队列按扇区增长方向有序排列 ,操作时保持磁头以直线一个方向移动,缩短请求磁盘寻址时间。

三、调度程序实现

1、Linus 电梯

在Linux2.4或更早的版本的调度程序,那时只有这一种I/O调度算法。

当一个请求加入到队列时:

  如果队列已存在一个对相邻 磁盘扇区操作的请求,将新请求和这个已存在的请求合并 成一个请求。

  如果队列中存在一个驻留时间 过长的请求,将新请求插入到队列尾部 ,防止请求发生饥饿。

  如果队列中以扇区方向为序存在合适插入位置 ,将新请求插入到该位置,与被访问磁盘物理位置为序排列。

  如果队列不存在合适 位置插入,将请求插入到队列尾部

2、最终期限I/O调度程序

Linus电梯 调度程序存在使请求发生饥饿的情况:

  对某个磁盘区域繁重操作 ,使得磁盘其他位置上的操作请求得不到运行;

  同一位置顺序上的请求流可以造成较远位置 请求得不到运行;

  写操作和提交应用程序是异步执行,读操作和提交应用程序是同步执行会阻塞,读操作响应时间影响性能 。要在提高全局吞吐量和使请求得到公平处理之间进行平衡。

最终期限I/O调度程序中:每个请求都有一个 超时时间,读请求默认500毫秒,写请求5秒。

提交请求时:

  一个请求递交给排序队列 ,按照合并和排序插入队列;

  将读请求按次序插入到读FIFO队列中;

  将写请求按次序插入到写FIFO队列中;

派发请求时:

通常从排序队列中取 队首请求加入到派发队列中;

  如果写FIFO队列首或读FIFO队列首请求超时,调度程序从FIFO队列中提取队首请求加入到派发队列中

如下图所示:

此方式能尽量保证:

  请求超时前得到执行,防止请求发生饥饿;

  读请求超时时间比写请求短很多,保证写请求不会因为堵塞读请求而使读请求发生饥饿。

3、预测I/O调度程序

最终期限调度程序降低请求发生饥饿的概率,同时降低了系统吞吐量。预测I/O调度程序的目标就是在保持****良好读响应同时提供良好的全局吞吐量

预测I/O调度程序与最终期限调度程序不同之处:

请求提交后 并不直接放回处理其他请求,而是会空闲片刻 (6毫秒),使应用程序有提交其他请求的机会——任何对相邻 磁盘位置的操作请求 都会立刻得到处理

等待结束后,预测I/O调度程序重新返回原来的位置,继续执行以前的剩下请求。

预测I/O调度程序所能带来的优势取决于能否正确预测应用程序和文件系统的行为,需要启发和统计工作,预测准确能够减少寻址开销,提高系统响应,提高吞吐量

4、完全公正的排队I/O调度

完全公正的排队(Complete Fair Queuing, CFQ )I/O调度是为专有工作负荷设计的 ,它和之前提到的I/O调度有根本的不同。

CFQ I/O调度算法中,每个进程都有自己的I/O队列

CFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取 一定的请求数(默认4个 ),然后进行下一轮调度。

CFQ I/O调度在进程级 提供了公平

CFQ试图均匀地分布对I/O带宽的访问,避免进程被饿死并实现较低的延迟,是deadline和as调度器的折中.

CFQ对于多媒体应用(video,audio)和桌面系统是最好的选择。

在最新的内核版本和发行版中,都选择CFQ做为默认的I/O调度器,对于通用的服务器也是最好的选择。

5、空操作的I/O调度

空操作(Noop)I/O调度几乎不做什么事情,这也是它这样命名的原因。

空操作I/O调度只做一件事情 ,当有新的请求到来时,把它与任一相邻的请求 合并。

空操作I/O调度主要用于闪存卡之类 的块设备,这类设备没有磁头,没有寻址的负担

最后,作为默认方式,块设备 使用完全公平的I/O调度程序。在启动时,可以通过命令行选项elevator=xxx覆盖默认设置。

elevator选项参数如下:

参数

I/O调度程序

as

预测

cfq

完全公正排队

deadline

最终期限

noop

空操作

如果启动预测I/O调度,启动的命令行参数中加上 elevator=as 。

参考资料:

《Linux内核设计与实现》原书第三版

https://www.cnblogs.com/tdyizhen131

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这里是一个简单的内核和应用层内存映射 I/O 的完整源码,包含了内核和用户程序部分: ##### 内核部分 ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/mm.h> #define DEVICE_NAME "my_mmap" MODULE_LICENSE("GPL"); static int major; static char* buffer; static int mmap_fault(struct vm_area_struct* vma, struct vm_fault* vmf) { unsigned long offset = vmf->pgoff << PAGE_SHIFT; struct page* page = virt_to_page(buffer + offset); get_page(page); vmf->page = page; return 0; } static const struct vm_operations_struct mmap_vm_ops = { .fault = mmap_fault }; static int mmap_mmap(struct file* filp, struct vm_area_struct* vma) { unsigned long size = vma->vm_end - vma->vm_start; unsigned long pfn = virt_to_phys(buffer) >> PAGE_SHIFT; int ret = io_remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot); if (ret) { printk(KERN_ERR "io_remap_pfn_range failed\n"); return ret; } vma->vm_ops = &mmap_vm_ops; mmap_fault(vma, NULL); return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .mmap = mmap_mmap }; static int __init mmap_init(void) { major = register_chrdev(0, DEVICE_NAME, &fops); if (major < 0) { printk(KERN_ERR "Failed to register_chrdev\n"); return major; } buffer = kmalloc(PAGE_SIZE, GFP_KERNEL); if (!buffer) { printk(KERN_ERR "Failed to allocate buffer\n"); unregister_chrdev(major, DEVICE_NAME); return -ENOMEM; } memset(buffer, 'A', PAGE_SIZE); return 0; } static void __exit mmap_exit(void) { kfree(buffer); unregister_chrdev(major, DEVICE_NAME); } module_init(mmap_init); module_exit(mmap_exit); ``` 这份代码实现了一个简单的内核,它注册了一个名为 "my_mmap" 的字符设备,提供了一个 mmap 方法用于内存映射操作。mmap 方法的实现过程中,使用了 io_remap_pfn_range 函数将内核缓冲区的物理地址映射到用户进程的虚拟地址空间中。具体实现细节可以参考代码注释。 ##### 用户程序部分 ```c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #define DEVICE_NAME "/dev/my_mmap" int main(int argc, char** argv) { int fd; char* buffer; size_t offset, length; if (argc < 3) { printf("Usage: %s <offset> <length>\n", argv[0]); return EXIT_FAILURE; } offset = atol(argv[1]); length = atol(argv[2]); fd = open(DEVICE_NAME, O_RDONLY); if (fd == -1) { perror("open"); return EXIT_FAILURE; } buffer = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset); if (buffer == MAP_FAILED) { perror("mmap"); close(fd); return EXIT_FAILURE; } for (size_t i = 0; i < length; ++i) { putchar(buffer[i]); } if (munmap(buffer, length) == -1) { perror("munmap"); close(fd); return EXIT_FAILURE; } close(fd); return EXIT_SUCCESS; } ``` 这份代码实现了一个用户程序,它打开了内核注册的字符设备 "/dev/my_mmap",使用 mmap 函数将设备文件的一部分映射到进程的虚拟地址空间中,并输出映射区域的内容。具体实现细节可以参考代码注释。 需要注意的是,用户程序和内核需要分别编译并加载到内核和用户空间中。可以使用 Makefile 管理编译过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值