浏览器和 Node.js 的事件循环机制

Libuv

Libuv 是 Node.js 关键的组成部分. 它包含了定时器, 非阻塞的网络 I/O, 异步文件系统访问, 子进程等功能. 它封装了 Libev, Libeio 以及 IOCP, 保证了跨平台的通用性. Libuv 的组成大致如下:

libuv_architecture

Node.js 事件循环

事件循环(event loop)是 Node.js 实现非阻塞 I/O 的关键. 尽管 JavaScript 的执行是单线程的, 但事件循环将操作尽可能的交由系统内核去执行. 而由于现代系统内核都是多线程的, 因此可以在后台同时处理多个操作. 当操作中的一个任务完成时, 系统内核告诉 Node.js 将回调添加到轮询队列中以等待执行.

Node.js 的事件循环可以用下图表示

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

上面的每一阶段都有一个先进先出的回调队列等待执行. 当事件循环进入该阶段时, 它将执行该阶段应该做的操作, 然后执行该阶段的队列中的回调, 直到队列耗尽或最大数量的回调执行. 之后事件循环将进入下一阶段.

长时间的回调的执行可能使得 poll 阶段的运行时间比 timers 的阀值更长.

  • timers

    this phase executes callbacks scheduled by setTimeout() and setInterval().

    该阶段会执行到期的定时器如 setTimeout 和 setInterval 的回调.

  • I/O callbacks

    executes almost all callbacks with the exception of close callbacks, the ones scheduled by timers, and setImmediate().

    该阶段执行某些系统操作的回调比如 TCP 错误. 当 TCP socket 连接出现 CONNREFUSED 错误时, *nix 系统会把它加入到 I/O callbacks 等待处理.

  • idle, prepare

    only used internally.

  • poll

    retrieve new I/O events; node will block here when appropriate.

    poll 阶段有两个重要任务:

    1. 执行到达阀值的计时器的回调
    2. 处理轮询队列(poll 队列)中的事件

    该阶段会首先检查计时器, 如果没有计时器需要被调度, 则

    • 如果轮询队列不为空, 事件循环将循环遍历轮询队列, 并同步的执行回调直到队列为空或者达到系统限制.
    • 如果轮询队列为空
    • 如果 immediateList 队列不为空, 则进入 check 阶段来执行 setImmediate 的任务
    • 如果 immediateList 队列为空, 则等待任务被加入到轮询队列然后执行, 一旦检测到则立即执行其回调

    当轮询队列为空时, 事件循环会去检查那些已经到达阀值的定时器, 如果有定时器已经准备好, 则事件循环将重新进入 timer 阶段去执行定时器回调.

  • check

    setImmediate() callbacks are invoked here.

    该阶段允许用户在 poll 阶段后立即执行一段回调. 如果 poll 阶段变为空闲并且脚本已经通过 setImmediate 排队, 那么事件循环会继续进入 check 阶段而不是继续等待

    setImmediate 实际上是一个在事件循环中的一个单独阶段运行的特殊定时器, 它使用了 libuv 的 API 来调度回调在轮询阶段完成后执行.

    一般的, 事件循环将进入 poll 阶段进行等待, 等待传入连接, 请求等等. 除非有任务通过 setImmediate 调度并且当前 poll 阶段为空闲, 事件循环将结束 poll 阶段进入 check 阶段.

  • close callbacks

    e.g. socket.on('close', ...).

    如果一个 socket 被突然关闭, 例如通过 socket.destroy(), 那么 close 事件将会在该阶段被触发. 否则它将通过 process.nextTick() 发出.

因此, 理解每个阶段的任务之后, 我们来理解 setTimeout, setImmediate 的执行时机就不难了.

  • setTimeout/setInterval 会在 timers 阶段执行, poll 阶段为空闲时且 immediateList 队列为空时. 它会检测 timer, 如果有准备好的 timer, 则切入 timers 阶段去执行. setTimeout 是被设计在超过阀值时后要运行的脚本.
  • setImmediate 只在 check 阶段执行, 通常是 poll 阶段为空闲时且 immediateList 不为空时切入 check 阶段执行. setImmediate 被设计用来在 poll 阶段执行完成后执行一段用户代码.

如果我们在非 I/O 回调中执行以下脚本, 那么这两个定时器(setImmediate 是是一个在事件循环中的一个单独阶段运行的特殊定时器)的执行顺序是不确定的. 因为它受到进程性能的限制.

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

但在 I/O 回调中他们的顺序就是确定的, setImmediate 总会先于 setTimeout 先执行. 因为 poll 阶段同步的执行 poll 队列的回调, 此时 setTimeout 和 setImmediate 各自注册了定时器, 之后当轮询队列为空时, 检查 immediateList 队列从而进入 check 阶段执行 immediateTask, 而后回到 poll 阶段之后才检测 timer 并进入 timers 阶段.

process.nextTick 是比较特殊的, 因为它并不是事件循环的一部分. 事实上, nextTickQueue 会在当前操作处理后执行, 无论它当前处在事件循环的哪一个阶段. process.nextTick 的问题在于他可能会导致其他阶段的任务被饿死, 并阻止事件循环进入到 poll 阶段. 既然如此, 为什么要设计这样一个 API 呢?

- 阅读剩余部分 -

Node.js 垃圾回收机制

在计算机科学中, 垃圾回收是一种自动的内存管理机制. 当一个电脑上的动态内容不再需要时, 就应该予以释放, 以让出内存, 这种内存资源管理, 成为垃圾回收. 垃圾回收期可以让程序员减轻许多负担, 也减少程序员犯错的机会. 垃圾回收最早用于 LISP 语言, 由 John McCarthy 提出.

常用的两种内存回收机制

引用计数法

引用计数法是最简单的垃圾回收算法. 此算法把 "对象是否不再需要" 简化定义为 "对象有没有被其他对象引用". 如果没有引用指向对象, 对象就被垃圾回收机制回收.

引用计数法的限制在遇到循环引用的情况, 如下:

function f () {
  const o = {}
  const o2 = {}
  o.a = o2  // o 引用 o2
  o2.a = o  // o2 引用 o
}
f()

在该例子中, 由于 oo2 互相引用, 导致他们两者都没办法被回收.

IE 6, 7 使用引用计数法对 DOM 对象进行垃圾回收, 所以容易导致因循环引用产生的内存泄漏问题.

标记清除法 (mark-sweep)

标记清除法是最早开发出的 GC 算法. 标记清除法把 "对象是否不再需要" 定义为 "对象是否可以获得". 它定期的从根 (在 JavaScript 中, 根就是全局对象) 开始将可能被引用的对象用递归的方式进行标记, 然后将没有标记到的对象作为垃圾回收.

标记清除法的限制在于无法从根对象查询到的对象都将被清除, 但是这对于大多数人来说并不是问题.

从 2012 年开始, 所有现代浏览器都使用了标记清除垃圾回收算法. 所有对 JavaScript 垃圾回收算法的改进都是基于标记清除法的改进.

V8 的垃圾回收机制

接下来我们主要讨论下 Node.js, 或者说是 V8 的垃圾回收表现.

在 V8 中, 栈用于存储原始类型和对象的引用, 堆用于存储引用类型如对象, 字符串或闭包. 引用类型在没有引用之后, 会通过 V8 的 GC 自动回收. 值类型如果是处于闭包中, 要等闭包没有引用才会被 GC 回收, 非闭包的情况下等待 V8 的新生代切换的时候回收.

V8 堆的分配

V8 采用了一个分代垃圾回收器, 并将堆又分为了几个不同的区域:

  • 新生区: 存活时间较短的对象, 新分配对象. 新生区大小一般在 1-8 MB. 新生区的垃圾回收非常频繁也非常快.
  • 老生指针区: 包含大多数可能存在指向其他对象的指针的对象. 大多数在新生区存活一段时间之后的对象会被挪到这里.
  • 老生数据区: 存放只包含原始数据的对象(这些对象没有指向其他对象的指针). 字符串, 封箱的数字以及未封箱的双精度数字数组, 在新生区经历一次 Scavenge 后会被移动到这里.
  • 大对象区: 存放体积超过 1MB 大小的对象. 每个对象都有自己 mmap 产生的内存, 垃圾回收器从不移动大对象.
  • Code 区: 代码对象, 也就是包含 JIT 之后指令的对象, 会被分配到这里.
  • Misc 区: 因为存放的是相同大小的元素, 所以内存结构很简单.

leaks-post-021.jpeg

V8 的内存回收算法

V8 分别对新生区和老生区采用不同的垃圾回收算法来提升垃圾回收的效率.

  • Scavenge Collection

    新生代使用半空间(semi-space) 的分配策略, 其中新对象最初分配在新生代的活跃半空间内也叫为 From 区(某些特定类型的对象, 如可执行的代码对象是分配在老生区的).

    在 Scavenge 的具体实现上, 主要采用了 Cheney 算法. 关于 V8 的具体实现, 有两种说法 (不确定哪一种):

    1. 当开始垃圾回收的时候, 会检查 From 区的存活对象, 这些存活对象会被复制到 To 区中, 而非存活对象占用的空间将会被释放. 完成复制后, From 区和 To 区空间的角色发生兑换. 在一定条件下, 存活周期较长的对象会晋升到老生代中. (来源: 《深入浅出 Node.js》)

    2. 一旦 To 区已满, 一个 Scavenge 操作将交换 From 区和 To 区, 然后将 From 区中活跃的对象复制到 To 区或晋升到老生区中. (来源: https://alinode.aliyun.com/blog/14, https://phptouch.com/2016/06/07/does-my-nodejs-application-leak-memory-4/)

      如下图: 蓝色代表存活对象, 灰色代表非存活对象.

      leaks-post-025.jpeg

    新生代对象的 Scavenge 操作的持续时间取决于新生代中存活对象的数量. 在大部分新生代对象存活时间不长的情况下, 一个 Scavenge 操作非常快(< 1ms). 然而, 如果大多数对象都需要被 Scavenge 的时候, Scavenge 操作的持续时间显然会更长.

    Scavenge 操作对于快速回收, 紧缩小片内存效果很好, 但对于大片内存则消耗过大. 因为 Scavenge 操作需要出区和入区两个区域. 老生代所保存的对象大多数是生存周期很长的甚至是常驻内存的对象, 而且老生代占用的内存较多. 因此该算法明显不适合老生代.

    另外, 对于如何判断一个对象是否是存活的, V8 实际上在写缓冲区有一个列表记录所有老生区对象指向新生区的情况. 新对象诞生的时候并不会有指向它的指针, 而当老生区中的对象出现指向新生区对象的指针的时候, 我们便记录下来这样的跨区指向. 这种操作也叫写屏障.

  • Mark-Sweep Collection & Mark-Compact

    在 V8 老生代的垃圾回收采用的是标记清除(Mark-Sweep) 和 标记紧缩(Mark-Compact) 结合的策略. 当老生代的活动对象增长超过一个预设的限制的时候, 将对堆栈执行一个大回收. 老生代垃圾回收使用了 Mark-Sweep 策略, 并采用了几种优化方法来改善延迟和内存消耗. V8 采用了一种增量标记的方式标记活跃对象, 将完整的标记拆分成很多小的步骤, 每做一步旧停下来让 JavaScript 的应用线程执行一会.

    标记完成后, 所有对象都已经被标记, 即不是活跃对象就是死亡对象. 清除时, 垃圾回收期会扫描连续存放的死对象把其变为空闲空间. 这个任务是由专门的清扫线程同步执行. 最后, 为减少老生代对象产生的内存碎片, 还要执行内存紧缩. 内存紧缩可能是非常耗时的, 并且仅当内存碎片成为问题的时候才进行.

    在 Chrome 中, 带内存紧缩的完整垃圾回收只有在 Chrome 空闲足够长的时间才被执行.

- 阅读剩余部分 -

基于 redsocks 实现全局 TCP 流量代理

在 Arch Linux 上使用 Shadowsocks, redsocks, iptables, pcap-dnsproxy 搭建一个舒适的科学上网环境~

对于我来说, 系统安装好之后的第一件事就是翻墙. 我们使用 shadowsocks + redsocks + iptables + pcap-dnsproxy 来实现全局自动代理.

yaourt -S shadowsocks redsocks-git pcap-dnsproxy-git

以上命令同时安装了三个软件. iptables 已经自带不需要安装.

接着到 /etc/shadowsocks 文件夹下创建一个新的配置文件, 配置你的 shadowsocks. 然后

sudo systemctl start shadowsocks@配置文件名
sudo systemctl enable shadowsocks@配置文件名

然后我们配置 redsocks. redsocks 的作用是把全部的 TCP 流量转发到 shadowsocks 上面去, iptables 的话则是拦截这一层, 我们可以在 iptables 配置说如果访问的是中国 ip 则不流向 shadowsocks. 我对 redsocks 了解并不多, 这里只说下我是如何配置的.

redsocks 的配置文件在 /etc/redsocks.conf , 编辑它, 把 redudp 和 dnstc 全部注释掉. redsocks 配置除去注释其实只有这五行:

redsocks {
  local_ip = 127.0.0.1;
  local_port = 31338; # redsocks 运行端口
  ip = 127.0.0.1;
  port = 1080;  # shadowsocks 端口
  type = socks5;
}

根据你的情况需要修改的修改.

接着修改 /etc/iptables/redsocks.rules 文件, 然后把你的 shadowsocks 服务器 IP 地址以及中国 IP 地址加进来, 格式如下:

-A REDSOCKS -d your_ip -j RETURN

国内 IP 列表可以从 apnic 获取得到, 你也可以看我整理的: china.rules

然后, 运行以下命令开启

sudo touch /etc/iptables/iptables.rules # 如果没有这个文件就创建它
sudo systemctl start  redsocks.service iptables.service
sudo systemctl enable  redsocks.service iptables.service
sudo iptables-restore < /etc/iptables/redsocks.rules # 导入规则

然后配置 pcap-dnsproxy. 以上虽然把发往国外的 TCP 流量发到自己的 shadowsocks 服务器上面处理, 这是一种突破 GFW 的方式, 但是这不能避免 DNS 污染的问题, 所以接下来我们配置的 pcap-dnsproxy 来解决.

Arch Linux 安装 pcap-dnsproxy 之后其实我们并不需要做什么, 直接启动就可以用了. 但是要知道的是 pcap-dnsproxy 提供了非常丰富的配置, 你可以自定义, 比如默认 pcap-dnsproxy 是没有局域网解析的, 可以编辑 /etc/pcap-dnsproxy/Config.conf 把 Listen['Operation Mode'] 改为 Server. 如果你需要自定义 Host 的话就修改 /etc/pacp-dnsproxy/Hosts.conf 文件.

直接启动 pcap-dnsproxy

sudo systemctl start pcap-dnsproxy
sudo systemctl enable pcap-dnsproxy

然后编辑 /etc/resolv.conf 文件, 确保首行的 nameserver 为 127.0.0.1. 然后运行

yaourt -S dnsutils # 为了使用 dig 命令
dig www.google.com

如果结果是类似以下形式, 表示 pcap-dnsproxy 已经正常运行.

2017-05-13 15-21-44 的屏幕截图.png

只要看下域名是否被大小写混淆过即可. 如果没有的话可以运行 systemctl status pcap-dnsproxy 或者 journalctl -xe 查看错误日志.

如果还是不行, 可以把 /etc/iptables/redsocks.rules 下所有内容复制粘贴到 /etc/iptables/iptables.rules 下. 然后重启下 iptables.

如果你打开浏览器并且已经可以成功的访问 Google 或者 Y2B, 再访问 ip.cn 这个网站查看当前显示的 IP 是否为你国内的 IP. 如果不是那你的中国 IP 过滤设置还有问题.

- 阅读剩余部分 -

Arch Linux 安装和配置小记

本文章涉及 Arch Linux 的安装, 常用软件和配置. 适用于在 UEFI 上面进行引导安装.

安装

  • 联网

    第一件事当然是联网啦, 如果是无线网络的话使用 wifi-menu.

  • 分区和挂载

    新手安装 Linux 可能会在分区这里感到很疑惑. 其实很简单, 通常我们只考虑为 Linux 分三个或四个区, 三个区的情况分别为 boot 分区, 根分区, home 分区以及交换分区.

    boot 分区是存放启动和引导的; home 分区是存放用户个人文件夹的, 它不是必选的, 但是如果有专门的 home 分区的话以后迁移会很方便; 交换分区主要用途是当虚拟内存用, 如果想要有休眠功能必须要有交换分区(或交换文件), 同样也是可选的; 根分区是必须有的, 以上所有分区都挂载到根分区中.

    Linux 的文件系统格式流行的有两种, 一种是 Ext4, 另一种是 Btrfs. Ext4 是用得最多的. Btrfs 相比 Ext4 支持写时复制以及快照, 子卷等.

    你可以运行 lsblk 命令查看当前的硬盘, 看仔细了别选错了. 假设你要安装的是 /dev/sda 上.

    • 方案一: 使用 Ext4 文件系统

    以下创建三个分区, boot 分区, swap 分区以及 / 分区. 如果需要 home 分区把 / 分区拆成两部分即可.

    parted /dev/sda
    mklabel gpt
    mkpart ESP fat32 1M 513M # Boot 分区
    set 1 boot on
    mkpart primary ext4 513M 8.5G # 交换分区
    mkpart primary ext4 8.5G 100% # 根分区
    quit

    接着运行下格式化以及设置交换分区操作.

    mkfs.fat -F32 /dev/sda1
    mkswap /dev/sda2
    swapon /dev/sda2
    mkfs.ext4 /dev/sda3

    分区就完成了. 接下来我们要进行挂载.

    mount /dev/sda3 /mnt
    mkdir /mnt/{boot,home}
    mount /dev/sda1 /mnt/boot

    这样就完成挂载了.

    • 方案二: 使用 Btrfs 文件系统

    Btrfs 不支持交换分区, 所以就不用设置了, 以下创建两个分区, boot 分区和 / 分区. Btrfs 对 SSD 提供了优化, 如果你没有用 SSD, 那么把后面的挂载参数中的 ssddiscard 都去掉.

    parted /dev/sda
    mklabel gpt
    mkpart ESP fat32 1M 513M # Boot 分区
    set 1 boot on
    mkpart primary btrfs 513M 100% # 根分区
    quit

    接着我们要在根分区创建两个子卷, 具体如下:

    mount -o defaults,ssd,discard,noatime,compress=lzo,space_cache,autodefrag /dev/sda2 /mnt
    cd /mnt
    btrfs subvolume create arch # arch 卷作为 / 分区
    btrfs subvolume create home # home 卷作为 home 分区
    umount /mnt

    接着挂载刚才创建的子卷

    mount -o subvol=arch,defaults,ssd,discard,noatime,compress=lzo,space_cache,autodefrag /dev/sda2 /mnt
    mkdir /mnt/{boot,home}
    mount -o subvol=home,defaults,ssd,discard,noatime,compress=lzo,space_cache,autodefrag /dev/sda2 /mnt/home
    mount /dev/sda1 /mnt/boot

- 阅读剩余部分 -

从 Unix 哲学联系到 Web 开发

这是 5 月 12 号在团队分享的一个话题, 目的是想介绍以下《UNIX 编程艺术》这本书中提到的十七条原则, 但感觉可能过于枯燥, 所以也加入了一些我们在 Web 开发中可以去贯彻的一些做法.

Unix 哲学起源于 Ken Thompson 早期关于如何设计一个服务接口简洁, 小巧精干的操作系统的思考, 随着 Unix 文化在学习如何尽可能发掘 Thompson 设计思想的过程中不断成长, 同时一路上还从其他许多地方博采众长.

Unix 哲学是自下而上的, 而不是自上而下的. Unix 哲学注重实效, 立足于丰富的经验. Unix 哲学中更多的内容不是先哲们口头表述出来的, 而是由他们所作的一切和 Unix 本身所作出的榜样体现出来的. 从整体上来说, 可以概括为一下几点.

  1. 模块原则 (使用简洁的接口拼合简单的部件)

    Brian Kernighan 是《C程序设计语言》的作者, 创作了许多Unix上的程式, 包括在Version 7 Unix上的 ditroff 与 cron. 他曾经说过:"计算机编程的本质就是控制复杂度".

    如何应用: 后端开发良好的目录结构, MVC 架构.

  2. 清晰原则 (清晰胜于机巧)

    在写程序时, 要想到你不是写给执行代码的计算机看的, 而是给人--将来阅读维护源码的人, 包括你自己--看的.

    在 Unix 传统中, 这个建议不仅意味着代码注释. 良好的 Unix 实践同样信奉再选择算法和实现时就应该考虑到将来的可扩展性.

    如何应用: 详细的注释, 考虑扩展性写入配置文件或者常量而不是写死.

  3. 组合原则 (设计时考虑拼接组合)

    如果程序彼此之间不能有效通信, 那么软件就难免会陷入复杂度的泥淖.

    在输入输出方面, Unix 传统极力提倡采用简单, 文本化, 面向流, 设备无关的格式. 要想让程序具有组合性, 就要使程序彼此独立.

    如何应用: JSON

  4. 分离原则 (策略同机制分离, 接口同引擎分离)

    X 系统的设计者再设计中的基本抉择是实行 "机制, 而不是策略" 这种做法, 使 X 成为一个通用图形引擎, 而将用户界面风格留给工具包或者系统的其它层次来决定.

    当编制一个框架时, 框架是机制, 应尽可能少地包含策略. 尽可能将行为分解到使用框架的模块中去.

    如何应用: 前后端分离, 插件机制

  5. 简洁原则 (设计要简洁, 复杂度能低则低)

    来自多方面的压力常常会让程序变得复杂, 其中一种压力就是来自技术上的虚荣心理. 程序员常常以能玩转复杂东西和耍弄抽象概念的能力为傲, 他们常常会与同行们比较, 看看谁能捣鼓出最错综复杂的美妙食物.

    要避免这些陷阱, 唯一的方法就是鼓励另一种软件文化, 以简洁为美, 人人对庞大复杂的东西群起而攻之.

    如何应用: 系统解耦分离为多个子系统, 例如调度系统

  6. 吝啬原则 (除非确无它法, 不要编写庞大的程序)

    首先寻找小巧程序的解决方案, 如果单个小程序无法完成这项工作, 尝试在现有框架结构内构造一个协作小程序工具包来解决问题. 如果两者都失败了, 才可以自由地构建一个巨型程序或一个新框架.

    如何应用: 尽可能使用现有框架或类库, 避免重复造轮子

  7. 透明性原则 (设计要可见, 以便审查和调试)

    软件系统的透明性是指你一眼就能够看出软件是在做什么以及怎么做的. 显见性指程序带有监视和显示内部状态的功能. 处于充分考虑透明性和显见性的目的, 还应该提倡接口简洁, 以方便其它程序对其进行操作.

    透明性和可显性, 主要是设计的特性而不是代码的特性. 这些特性更多与代码中不易规定的特性有关. 如程序调用层次中最大的静态深度? 是否存在太多的特征标志和模式位? 是否容易找到给定函数的代码部分? 代码中有多少个模糊变量等等.

    如何应用: ESLint 规范代码以及约束一些书写方式

  8. 健壮原则 (健壮源于透明与简洁)

    软件的健壮性指软件不仅能在正常的情况下运行良好, 而且在超出设计者设想的意外条件下也能够运行良好.

    要做到程序健壮, 关键的是透明化和简洁化. 人们不需要绞尽脑汁就能够推断出所有可能的情况, 那么这个程序就是简洁的, 越简洁, 越透明, 也就越健壮.

    如何应用: 使用 Joi 校验参数, 避免 property on undefined, 使用安全方法例如 Reflect.defineProperty 替代 Object.defineProperty 等

- 阅读剩余部分 -

Linux I/O 调度器

Linux I/O 调度器

Linux I/O 调度器是控制内核提交读写请求给磁盘的方式. Linux 支持三种 I/O 电梯调度算法, 分别为 CFQ, Noop, Deadline 三种. 简单介绍下这三种.

CFQ (Completely Faire Scheduler)

完全公平调度器 (CFQ) 自 kernel2.6.18 后成为了 Linux 默认的 I/O 调度器, 它也是大多数 Linux 发行版的默认调度器. CFQ 调度器的目的是为所有请求 I/O 操作的进程提供一个公平的磁盘 I/O 带宽分配.

CFQ 调度器为每个进程单独创建一个同步 I/O 队列来管理其同步的请求然后为每个队列分配访问磁盘的时间片. 时间片长度和队列允许提交的请求数取决于其进程的 I/O 优先级. 以此保证每个进程的 I/O 资源占用是公平的.

CFQ 算法的特点是按照 I/O 请求的地址进行排序, 而不是按照先来后到的顺序进行响应. 这样减少了不必要的磁盘寻道, 提高了吞吐量. 但也意味着, 先来的 I/O 请求可能不能尽快得到响应, 可能出现饿死现象.

Deadline

Deadline 调度器主要目的是保证请求的启动服务时间. 它在所有的 I/O 请求上加上一个期限来防止请求饿死. 除了本身提供的 I/O 排队队列之外, Deadline 额外分为为读 I/O 和写 I/O 提供了 FIFO 队列. 排序队列按照按照扇区数排序, FIFO 队列按照截止时间排序. FIFO 队列优先级高于排队队列, 读队列优先级高于写队列. 排序队列使用红黑树结构, 而 FIFO 使用链表结构. 读 FIFO 队列的最大等待时间为 500ms, 写FIFO队列的最大等待时间为 5s.

Deadline 算法会在写队列没有发生饥饿的情况下处理读队列, 在进入读队列处理时, 会首先检查 FIFO 读队列查看是否有过期请求, 如果有就优先处理它. 如果前面写队列出现饥饿, 意即 FIFO 写队列有过期请求, 它就会进入写队列处理然后处理 FIFO 中过期的写请求.

Deadline 本质还是基于 CFQ 的, 只不过加了两个 FIFO 队列来避免饿死问题.

Deadline 调度器更适合用于多线程处理环境和数据库环境. 避免了 CFQ 调度器的饿死问题.

Noop

Noop 调度器非常简单, 它提供了一个最简单的 FIFO 队列并对请求进行简单的合并处理. 当确定不需要根据扇区号来对请求排序时, 这个调度器是非常高效的. 比较适合在 SSD 上面应用.

总的来看, Noop 更适合固态硬盘. Deadline 适合需要优先保证读操作且避免饿死的情况如数据库场景. CFQ 比较复杂, 容易出现饿死问题, 但保证了尽可能的公平. 推荐固态硬盘下可以考虑把默认的 CFQ 调度器改为 Noop 调度器或者 Deadline 调度器.

更换 Linux I/O 调度器

可以通过如下命令查看你当前的 I/O 调度器:

cat /sys/block/sda/queue/scheduler

如果想修改的话, 可以先进入 su 然后执行:

echo noop > /sys/block/sda/queue/scheduler

可以把 noop 换位 cfq 或者 deadline.

这个更改会立即生效, 但重启会失效. 如果想设为永久, 在 Arch Linux 上面可以如下操作:

% cat /etc/udev/rules.d/60-scheduler.rules
# Set deadline scheduler for non-rotational disks
ACTION=="add|change", KERNEL=="sd[a-z]", TEST!="queue/rotational", ATTR{queue/scheduler}="deadline"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="deadline"

/etc/udev/rules.d/60-scheduler.rules (没有的话新建) 加上上面两行, 然后再运行该命令:

mkinitcpio -p linux

这样就可以了~

- 阅读剩余部分 -

This page loaded in 0.001410 seconds