Hamo's

$ cat /boot/vmlinuz /dev/{log, random, zero, null}

Docker源码分析3-daemon启动(API)

| Comments

在该系列的 上一篇文章中, 我们介绍了Docker daemon启动流程的前半部分,后半部分的代码会完成daemon 启动中最重要的两个步骤:

  • Daemon结构的初始化
  • API的初始化

在这篇文章中,我们将首先分析API初始化相关的代码,其利用Golang标准库 “net/http”生成一个RESTful的HTTP接口与client通信。

Docker源码分析1-简介

| Comments

Docker是一款由docker, Inc发起的开源Linux容器引擎。由Golang编写完成。它 基于Linux Container技术。Linux Container技术,是一种操作系统层次的虚拟 化技术,提供了系统隔离,资源限制等功能。与Linux下传统的KVM虚拟机相比, container技术更轻量,所有容器运行在Host的内核上,共享Host的硬件资源。

由于Docker依赖于Linux Container技术,所以现在只能运行在Linux系统上。为 了解决这个问题, boot2docker项目应运而生。 该项目为Windows和Mac OS的用户提供了一个包含Docker的最小化的Virtualbox 虚拟机镜像,借助Virtualbox虚拟机让Docker运行在这两种操作系统上。

本系列的Docker源码分析将基于Docker v1.2.0版本进行。读者可以从Docker项 目在github的页面获取到该项目的源 代码。

下一篇,我们将首先对Docker daemon的启动代码进行分析。

Linux内核中的IO调度器简介

| Comments

从2.6系列开始,Linux内核引入了全新的IO调度子系统。与2.4系列内核只有一个惟一的,通用的I/O调度器相比,最新的Linux内核提供了CFQ(默认), deadline和noop三种IO调度器。(anticipatory调度器从2.6.33开始被移除,commit 492af6350a5ccf087e4964104a276ed358811458,被CFQ所取代)。在这篇文章里,我会首先介绍三种IO调度器各自的特点和应用场景,之后会介绍Linux内核提供的为每一个块设备指定IO调度器和调整IO调度器参数的接口。Ok, let’s go!

CFQ(Complete Fair Queuing)

CFQ实现了一种QoS的IO调度算法。该算法为每一个进程分配一个时间窗口,在该时间窗口内,允许进程发射IO请求。通过时间窗口在不同进程间的移动,保证了对于所有进程而言都有公平的发射IO请求的权利。同时,与CFS(Complete Fair Schedule)相同,该算法也实现了进程的优先级控制,可以保证高优先级进程可以获得更长的时间窗口。主要代码位于 block/cfq-iosched.c CFQ调度算法适用于系统中存在多任务I/O请求的情况,通过在多进程中轮换,保证了系统I/O请求整体的低延迟。但是,对于只有少数进程存在大量密集的I/O请求的情况,则会出现明显的I/O性能下降。

CFQ调度器主要提供如下三个优化参数:

  1. slice_idle

如果一个进程在自己的时间窗口里,经过slice_idle时间都没有发射I/O请求,则调度选择下一个程序。通过该机制,可以有效利用I/O请求的局部性原理,提高系统的I/O吞吐量。

  1. quantum

该参数控制在一个时间窗口内可以发射的I/O请求的最大数目。

  1. low_latency

对于I/O请求延时非常重要的任务,将该参数设置为1可以降低I/O请求的延时。

NOOP

顾名思义,NOOP调度器并不完成任何复杂的工作,只是将上层发来的I/O请求直接发送至下层,实现了一个FIFO的I/O请求队列。主要代码位于 block/noop-iosched.c 其应用环境主要有以下两种:一是物理设备包含自己的I/O调度程序,比如SCSI的TCQ;二是寻道时间可以忽略不计的设备,比如SSD等。

DEADLINE

DEADLINE调度算法主要针对I/O请求的延时而设计,每个I/O请求都被附加一个最后执行期限。该算法维护两类队列,一是按照扇区排序的读写请求队列;二是按照过期时间排序的读写请求队列。如果当前没有I/O请求过期,则会按照扇区顺序执行I/O请求;如果发现过期的I/O请求,则会处理按照过期时间排序的队列,直到所有过期请求都被发射为止。在处理请求时,该算法会优先考虑读请求。主要代码位于 block/deadline-iosched.c 当系统中存在的I/O请求进程比较少时,与CFQ算法相比,DEADLINE算法可以提供较高的I/O吞吐率,特别是对于使用了自带I/O请求队列的设备,如SCSI的TCQ的时候。

DEADLINE调度算法提供如下三个参数:

  1. writes_starved

该参数控制当读写队列均不为空时,发射多少个读请求后,允许发射写请求。

  1. read_expire

参数控制读请求的过期时间,单位毫秒。

  1. write_expire

参数控制写请求的过期时间,单位毫秒。

为块I/O设备设置I/O调度算法的方法

Linux内核允许用户为每个单独的块I/O设备设置不同的I/O调度算法。这样,根据块设备的不同以及读写该设备的应用不同,可以最大限度的提升系统的I/O吞吐率。设置接口通过sysfs导出。如果当前系统中没有挂载sysfs的话,使用如下命令可以将sysfs挂载至/sys目录下。

1
root@trinity:~# mount -t sysfs sysfs /sys

我们以sda为例,介绍修改设备调度算法的方式。

1
2
3
4
5
root@trinity:~# cat /sys/block/sda/queue/scheduler
noop deadline [cfq] 
root@trinity:~# echo "deadline" > /sys/block/sda/queue/scheduler
root@trinity:~# cat /sys/block/sda/queue/scheduler
noop [deadline] cfq 

当然,这里的修改是临时的。系统重启后设备的调度算法会重置为默认的CFQ。如果希望保证每次系统启动都使用自定的调度算法,可以在 /etc/fstab 文件中为对应的设备传递如下参数: elevator=deadline 便会将对应设备的调度算法设置为deadline,其他算法类似。

ARM体系架构下的同步操作(二)

| Comments

上一篇文章中,我们介绍了ARM体系架构下,为了实现对内存地址同步访问而引入的 LDREX STREX 两条指令。在这篇文章里,首先会以Linux Kernel中ARM架构的原子相加操作为例,介绍这两条指令的使用方法;之后,会介绍GCC提供的一些内置函数,这些同步函数使用这两条指令完成同步操作。

Linux Kernel中的atomic_add函数

如下是Linux Kernel中使用的atomic_add函数的定义,它实现原子的给v指向的atomic_t增加i的功能。

atomic_add
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline void atomic_add(int i, atomic_t *v)
{
        unsigned long tmp;
        int result;

        __asm__ __volatile__("@ atomic_add\n"
"1:     ldrex   %0, [%3]\n"
"       add     %0, %0, %4\n"
"       strex   %1, %0, [%3]\n"
"       teq     %1, #0\n"
"       bne     1b"
        : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
        : "r" (&v->counter), "Ir" (i)
        : "cc");
}

在第7行,使用LDREX指令将v->counter所指向的内存地址的值装入寄存器中,并初始化exclusive monitor。 在第8行,将该寄存器中的值与i相加。 在第9,10,11行,使用STREX指令尝试将修改后的值存入原来的地址,如果STREX写入%1寄存器的值为0,则认为原子更新成功,函数返回;如果%1寄存器的值不为0,则认为exclusive monitor拒绝了本次对内存地址的访问,则跳转回第7行重新进行以上所述的过程,直到成功将修改后的值写入内存为止。该过程可能多次反复进行,但可以保证,在最后一次的读-修改-写回的过程中,没有其他代码访问该内存地址。

GCC内置的原子操作函数

看了上面的GCC内联汇编,是不是有点晕?在用户态下,GCC为我们提供了一系列内置函数,这些函数可以让我们既享受原子操作的好处,又免于编写复杂的内联汇编指令。这一系列的函数均以__sync开头,分为如下几类:

1
2
3
4
5
6
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)

这一系列函数完成对ptr所指向的内存地址的对应操作,并返回操作之前的值。

1
2
3
4
5
6
type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)

这一系列函数完成对ptr所指向的内存地址的对应操作,并返回操作之后的值。

1
2
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)

这两个函数完成对变量的原子比较和交换。即如果ptr所指向的内存地址存放的值与oldval相同的话,则将其用newval的值替换。 返回bool类型的函数返回比较的结果,相同为true,不同为false;返回type的函数返回的是ptr指向地址交换前存放的值。

ARM体系架构下的同步操作(一)

| Comments

处理器在访问共享资源时,必须对临界区进行同步,即保证同一时间内,只有一个对临界区的访问者。当共享资源为一内存地址时,原子操作是对该类型共享资源同步访问的最佳方式。随着应用的日益复杂和SMP的广泛使用,处理器都开始提供硬件同步原语以支持原子地更新内存地址。

CISC处理器比如IA32,可以提供单独的多种原子指令完成复杂的原子操作,由处理器保证读-修改-写回过程的原子性。而RISC则不同,由于除Load和Store的所有操作都必须在寄存器中完成,如何保证从装载内存地址到寄存器,到修改寄存器中的值,再到将寄存器中的值写回内存中可以原子性的完成,便成为了处理器设计的关键。

从ARMv6架构开始,ARM处理器提供了Exclusive accesses同步原语,包含两条指令: LDREX STREX LDREX和STREX指令,将对一个内存地址的原子操作拆分成两个步骤,同处理器内置的记录exclusive accesses的exclusive monitors一起,完成对内存的原子操作。

LDREX

LDREX与LDR指令类似,完成将内存中的数据加载进寄存器的操作。与LDR指令不同的是,该指令也会同时初始化exclusive monitor来记录对该地址的同步访问。例如

1
LDREX R1, [R0]

会将R0寄存器中内存地址的数据,加载进R1中并更新exclusive monitor。

STREX

该指令的格式为:

1
STREX Rd, Rm, [Rn]

STREX会根据exclusive monitor的指示决定是否将寄存器中的值写回内存中。如果exclusive monitor许可这次写入,则STREX会将寄存器Rm的值写回Rn所存储的内存地址中,并将Rd寄存器设置为0表示操作成功。如果exclusive monitor禁止这次写入,则STREX指令会将Rd寄存器的值设置为1表示操作失败并放弃这次写入。应用程序可以根据Rd中的值来判断写回是否成功。

在下篇文章中,我会以Linux Kernel中如何编写原子操作代码具体介绍LDREX和STREX的使用方法,并介绍gcc提供的ARM架构下的关于这两条指令的C语言扩展。