Docker 介绍: 相关技术(LXC)

http://www.ttsgs.com/tag/linux-kernel/

Abstract

本文在现有文档的基础上总结了以下几点内容

  1. docker的介绍,包括由来、适用场景等

  2. docker背后的一系列技术 – namespace, cgroup, lxc, aufs等

  3. docker在利用LXC的同时提供了哪些创新

  4. 笔者对docker这种container, PaaS的一些理解

  5. docker存在的问题和现有的解决思路

Docker 简介

Docker is an open-source engine that automates the deployment of any application as a lightweight, portable, self-sufficient container that will run virtually anywhere.

Docker 是 PaaS 提供商 dotCloud 开源的一个基于 LXC 的高级容器引擎,
源代码托管在 Github 上, 基于go语言并遵从Apache2.0协议开源。
Docker近期非常火热,无论是从 github 上的代码活跃度,还是Redhat在RHEL6.5中集成对Docker的支持, 就连 Google 家的 Compute Engine 也支持 docker 在其之上运行, 最近百度也用 Docker 作为其PaaS的基础(不知道规模多大)。

一款开源软件能否在商业上成功,很大程度上依赖三件事 – 成功的 user case, 活跃的社区和一个好故事。 dotCloud 自家的 PaaS 产品建立在docker之上,长期维护
且有大量的用户,社区也十分活跃,接下来我们看看docker的故事。

  • 环境管理复杂 – 从各种OS到各种中间件到各种app, 一款产品能够成功作为开发者需要关心的东西太多,且难于管理,这个问题几乎在所有现代IT相关行业都需要面对

  • 云计算时代的到来 – AWS的成功, 引导开发者将应用转移到 cloud 上, 解决了硬件管理的问题,然而中间件相关的问题依然存在 (所以openstack HEAT和 AWS cloudformation 都着力解决这个问题)。开发者思路变化提供了可能性。

  • 虚拟化手段的变化 – cloud 时代采用标配硬件来降低成本,采用虚拟化手段来满足用户按需使用的需求以及保证可用性和隔离性。然而无论是KVM还是Xen在 docker 看来,
    都在浪费资源,因为用户需要的是高效运行环境而非OS, GuestOS既浪费资源又难于管理, 更加轻量级的LXC更加灵活和快速

  • LXC的移动性 – LXC在 linux 2.6 的 kernel 里就已经存在了,但是其设计之初并非为云计算考虑的,缺少标准化的描述手段和容器的可迁移性,决定其构建出的环境难于
    迁移和标准化管理(相对于KVM之类image和snapshot的概念)。docker 就在这个问题上做出实质性的革新。这正式笔者第一次听说docker时觉得最独特的地方。

Docker 介绍: 相关技术(LXC)_第1张图片

面对上述几个问题,docker设想是交付运行环境如同海运,OS如同一个货轮,每一个在OS基础上的软件都如同一个集装箱,用户可以通过标准化手段自由组装运行环境,
同时集装箱的内容可以由用户自定义,也可以由专业人员制造。这样,交付一个软件,就是一系列标准化组件的集合的交付,如同乐高积木,用户只需要选择合适的积木组合,
并且在最顶端署上自己的名字(最后个标准化组件是用户的app)。这也就是基于docker的PaaS产品的原型。

What Docker Can Do

在docker的网站上提到了docker的典型场景:

  • Automating the packaging and deployment of applications

  • Creation of lightweight, private PAAS environments

  • Automated testing and continuous integration/deployment

  • Deploying and scaling web apps, databases and backend services

由于其基于LXC的轻量级虚拟化的特点,docker相比KVM之类最明显的特点就是启动快,资源占用小。因此对于构建隔离的标准化的运行环境,轻量级的PaaS(如dokku), 构建自动化测试和持续集成环境,以及一切可以横向扩展的应用(尤其是需要快速启停来应对峰谷的web应用)。

  1. 构建标准化的运行环境,现有的方案大多是在一个base OS上运行一套puppet/chef,或者一个image文件,其缺点是前者需要base OS许多前提条件,后者几乎不可以修改(因为copy on write 的文件格式在运行时rootfs是read only的)。并且后者文件体积大,环境管理和版本控制本身也是一个问题。

  2. PaaS环境是不言而喻的,其设计之初和dotcloud的案例都是将其作为PaaS产品的环境基础

  3. 因为其标准化构建方法(buildfile)和良好的REST API,自动测试和持续集成/部署能够很好的集成进来

  4. 因为LXC轻量级的特点,其启动快,而且docker能够只加载每个container变化的部分,这样资源占用小,能够在单机环境下与KVM之类的虚拟化方案相比能够更加快速和占用更少资源

What Docker Can NOT Do

Docker并不是全能的,设计之初也不是KVM之类虚拟化手段的替代品,个人简单总结了几点

  1. Docker是基于Linux 64bit的,无法在windows/unix或32bit的linux环境下使用(虽然64-bit现在很普及了)
  2. LXC是基于cgroup等linux kernel功能的,因此container的guest系统只能是linux base的
  3. 隔离性相比KVM之类的虚拟化方案还是有些欠缺,所有container公用一部分的运行库
  4. 网络管理相对简单,主要是基于namespace隔离
  5. cgroup的cpu和cpuset提供的cpu功能相比KVM的等虚拟化方案相比难以度量(所以dotcloud主要是安内存收费)
  6. docker对disk的管理比较有限
  7. container随着用户进程的停止而销毁,container中的log等用户数据不便收集

针对1-2,有windows base应用的需求的基本可以pass了; 3-5主要是看用户的需求,到底是需要一个container还是一个VM, 同时也决定了docker作为 IaaS 不太可行。
针对6,7虽然是docker本身不支持的功能,但是可以通过其他手段解决(disk quota, mount --bind)。总之,选用container还是vm, 就是在隔离性和资源复用性上做tradeoff

另外即便docker 0.7能够支持非AUFS的文件系统,但是由于其功能还不稳定,商业应用或许会存在问题,而AUFS的稳定版需要kernel 3.8, 所以如果想复制dotcloud的
成功案例,可能需要考虑升级kernel或者换用ubuntu的server版本(后者提供deb更新)。我想这也是为什么开源界更倾向于支持ubuntu的原因(kernel版本)

Docker Usage

由于篇幅所限,这里就不再展开翻译,可参见链接 – http://docs.docker.io/en/latest/use/

Docker Build File

由于篇幅所限,这里就不再展开翻译,可参见链接 – http://docs.docker.io/en/latest/use/builder/


Docker’s Trick

What Docker Needs

Docker核心解决的问题是利用LXC来实现类似VM的功能,从而利用更加节省的硬件资源提供给用户更多的计算资源。同VM的方式不同, LXC 其并不是一套硬件虚拟化方法 – 无法归属到全虚拟化、部分虚拟化和半虚拟化中的任意一个,而是一个操作系统级虚拟化方法, 理解起来可能并不像VM那样直观。所以我们从虚拟化要docker要解决的问题出发,看看他是怎么满足用户虚拟化需求的。

用户需要考虑虚拟化方法,尤其是硬件虚拟化方法,需要借助其解决的主要是以下4个问题:

  • 隔离性 – 每个用户实例之间相互隔离, 互不影响。 硬件虚拟化方法给出的方法是VM, LXC给出的方法是container,更细一点是kernel namespace
  • 可配额/可度量 – 每个用户实例可以按需提供其计算资源,所使用的资源可以被计量。硬件虚拟化方法因为虚拟了CPU, memory可以方便实现, LXC则主要是利用cgroups来控制资源
  • 移动性 – 用户的实例可以很方便地复制、移动和重建。硬件虚拟化方法提供snapshot和image来实现,docker(主要)利用AUFS实现
  • 安全性 – 这个话题比较大,这里强调是host主机的角度尽量保护container。硬件虚拟化的方法因为虚拟化的水平比较高,用户进程都是在KVM等虚拟机容器中翻译运行的, 然而对于LXC, 用户的进程是lxc-start进程的子进程, 只是在Kernel的namespace中隔离的, 因此需要一些kernel的patch来保证用户的运行环境不会受到来自host主机的恶意入侵, dotcloud(主要是)利用kernel grsec patch解决的.

Linux Namespace (ns)

LXC所实现的隔离性主要是来自kernel的namespace, 其中pidnetipcmntuts 等namespace将container的进程, 网络, 消息, 文件系统和hostname 隔离开。

pid namespace

之前提到用户的进程是lxc-start进程的子进程, 不同用户的进程就是通过pidnamespace隔离开的,且不同 namespace 中可以有相同PID。具有以下特征:

  1. 每个namespace中的pid是有自己的pid=1的进程(类似/sbin/init进程)
  2. 每个namespace中的进程只能影响自己的同一个namespace或子namespace中的进程
  3. 因为/proc包含正在运行的进程,因此在container中的pseudo-filesystem的/proc目录只能看到自己namespace中的进程
  4. 因为namespace允许嵌套,父namespace可以影响子namespace的进程,所以子namespace的进程可以在父namespace中看到,但是具有不同的pid

正是因为以上的特征,所有的LXC进程在docker中的父进程为docker进程,每个lxc进程具有不同的namespace。同时由于允许嵌套,因此可以很方便的实现 LXC in LXC

net namespace

有了 pid namespace, 每个namespace中的pid能够相互隔离,但是网络端口还是共享host的端口。网络隔离是通过netnamespace实现的,
每个net namespace有独立的 network devices, IP addresses, IP routing tables, /proc/net 目录。这样每个container的网络就能隔离开来。
LXC在此基础上有5种网络类型,docker默认采用veth的方式将container中的虚拟网卡同host上的一个docker bridge连接在一起。

ipc namespace

container中进程交互还是采用linux常见的进程间交互方法(interprocess communication – IPC), 包括常见的信号量、消息队列和共享内存。然而同VM不同,container
的进程间交互实际上还是host上具有相同pid namespace中的进程间交互,因此需要在IPC资源申请时加入namespace信息 – 每个IPC资源有一个唯一的 32bit ID。

mnt namespace

类似chroot,将一个进程放到一个特定的目录执行。mnt namespace允许不同namespace的进程看到的文件结构不同,这样每个 namespace 中的进程所看到的文件目录就被隔离开了。同chroot不同,每个namespace中的container在/proc/mounts的信息只包含所在namespace的mount point。

uts namespace

UTS(“UNIX Time-sharing System”) namespace允许每个container拥有独立的hostname和domain name,
使其在网络上可以被视作一个独立的节点而非Host上的一个进程。

user namespace

每个container可以有不同的 user 和 group id, 也就是说可以以container内部的用户在container内部执行程序而非Host上的用户。

有了以上6种namespace从进程、网络、IPC、文件系统、UTS和用户角度的隔离,一个container就可以对外展现出一个独立计算机的能力,并且不同container从OS层面实现了隔离。
然而不同namespace之间资源还是相互竞争的,仍然需要类似ulimit来管理每个container所能使用的资源 – LXC 采用的是cgroup

参考文献

[1]http://blog.dotcloud.com/under-the-hood-linux-kernels-on-dotcloud-part

[2]http://lwn.net/Articles/531114/

Control Groups (cgroups)

cgroups 实现了对资源的配额和度量。 cgroups 的使用非常简单,提供类似文件的接口,在 /cgroup目录下新建一个文件夹即可新建一个group,在此文件夹中新建task
文件,并将pid写入该文件,即可实现对该进程的资源控制。具体的资源配置选项可以在该文件夹中新建子 subsystem ,{子系统前缀}.{资源项} 是典型的配置方法,
memory.usage_in_bytes 就定义了该group 在subsystem memory中的一个内存限制选项。
另外,cgroups中的 subsystem可以随意组合,一个subsystem可以在不同的group中,也可以一个group包含多个subsystem – 也就是说一个 subsystem

关于术语定义

A *cgroup* associates a set of tasks with a set of parameters for one
or more subsystems.

A *subsystem* is a module that makes use of the task grouping
facilities provided by cgroups to treat groups of tasks in
particular ways. A subsystem is typically a "resource controller" that
schedules a resource or applies per-cgroup limits, but it may be
anything that wants to act on a group of processes, e.g. a
virtualization subsystem.

我们主要关心cgroups可以限制哪些资源,即有哪些subsystem是我们关心。

cpu : 在cgroup中,并不能像硬件虚拟化方案一样能够定义CPU能力,但是能够定义CPU轮转的优先级,因此具有较高CPU优先级的进程会更可能得到CPU运算。
通过将参数写入cpu.shares,即可定义改cgroup的CPU优先级 – 这里是一个相对权重,而非绝对值。当然在cpu这个subsystem中还有其他可配置项,手册中有详细说明。

cpusets : cpusets 定义了有几个CPU可以被这个group使用,或者哪几个CPU可以供这个group使用。在某些场景下,单CPU绑定可以防止多核间缓存切换,从而提高效率

memory : 内存相关的限制

blkio : block IO相关的统计和限制,byte/operation统计和限制(IOPS等),读写速度限制等,但是这里主要统计的都是同步IO

net_cls, cpuacct , devices , freezer 等其他可管理项。

参考文献

http://blog.dotcloud.com/kernel-secrets-from-the-paas-garage-part-24-c

http://en.wikipedia.org/wiki/Cgroups

https://www.kernel.org/doc/Documentation/cgroups/cgroups.txt

LinuX Containers(LXC)

借助于namespace的隔离机制和cgroup限额功能,LXC提供了一套统一的API和工具来建立和管理container, LXC利用了如下 kernel 的features:

  • Kernel namespaces (ipc, uts, mount, pid, network and user)
  • Apparmor and SELinux profiles
  • Seccomp policies
  • Chroots (using pivot_root)
  • Kernel capabilities
  • Control groups (cgroups)

LXC 向用户屏蔽了以上 kernel 接口的细节, 提供了如下的组件大大简化了用户的开发和使用工作:

  • The liblxc library
  • Several language bindings (python3, lua and Go)
  • A set of standard tools to control the containers
  • Container templates

LXC 旨在提供一个共享kernel的 OS 级虚拟化方法,在执行时不用重复加载Kernel, 且container的kernel与host共享,因此可以大大加快container的
启动过程,并显著减少内存消耗。在实际测试中,基于LXC的虚拟化方法的IO和CPU性能几乎接近 baremetal 的性能(论据参见文献[3]), 大多数数据有相比
Xen具有优势。当然对于KVM这种也是通过Kernel进行隔离的方式, 性能优势或许不是那么明显, 主要还是内存消耗和启动时间上的差异。在参考文献[4]中提到了利用iozone进行
Disk IO吞吐量测试KVM反而比LXC要快,而且笔者在device mapping driver下重现同样case的实验中也确实能得到如此结论。参考文献[5]从网络虚拟化中虚拟路由的场景(个人理解是网络IO和CPU角度)比较了KVM和LXC, 得到结论是KVM在性能和隔离性的平衡上比LXC更优秀 – KVM在吞吐量上略差于LXC, 但CPU的隔离可管理项比LXC更明确。

关于CPU, DiskIO, network IO 和 memory 在KVM和LXC中的比较还是需要更多的实验才能得出可信服的结论。

参考文献

[1]http://linuxcontainers.org/

[2]http://en.wikipedia.org/wiki/LXC

[3]http://marceloneves.org/papers/pdp2013-containers.pdf (性能测试)

[4]http://www.spinics.net/lists/linux-containers/msg25750.html (与KVM IO比较)

[5]http://article.sciencepublishinggroup.com/pdf/10.11648.j.ajnc.20130204.11.pdf

AUFS

Docker对container的使用基本是建立唉LXC基础之上的,然而LXC存在的问题是难以移动 – 难以通过标准化的模板制作、重建、复制和移动 container。
在以VM为基础的虚拟化手段中,有image和snapshot可以用于VM的复制、重建以及移动的功能。想要通过container来实现快速的大规模部署和更新, 这些功能不可或缺。
Docker正是利用AUFS来实现对container的快速更新 – 在docker0.7中引入了storage driver, 支持AUFS, VFS, device mapper, 也为BTRFS以及ZFS引入提供了可能。 但除了AUFS都未经过dotcloud的线上使用,因此我们还是从AUFS的角度介绍。

AUFS (AnotherUnionFS) 是一种 Union FS, 简单来说就是支持将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)的文件系统, 更进一步地, AUFS支持为每一个成员目录(AKA branch)设定’readonly’, ‘readwrite’ 和 ‘whiteout-able’ 权限, 同时AUFS里有一个类似
分层的概念, 对 readonly 权限的branch可以逻辑上进行修改(增量地, 不影响readonly部分的)。通常 Union FS有两个用途, 一方面可以实现不借助 LVM, RAID 将多个disk和挂在到一个目录下, 另一个更常用的就是将一个readonly的branch和一个writeable的branch联合在一起,Live CD正是基于此可以允许在 OS image 不变的基础上允许用户在其上进行一些写操作。Docker在AUFS上构建的container image也正是如此,接下来我们从启动container中的linux为例介绍docker在AUFS特性的运用。

典型的Linux启动到运行需要两个FS – bootfs + rootfs (从功能角度而非文件系统角度)

Docker 介绍: 相关技术(LXC)_第2张图片

bootfs (boot file system) 主要包含 bootloader 和 kernel, bootloader主要是引导加载kernel, 当boot成功后 kernel 被加载到内存中后 bootfs就被umount了.
rootfs (root file system) 包含的就是典型 Linux 系统中的 /dev/proc/bin/etc 等标准目录和文件。

由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs 如下图:

Docker 介绍: 相关技术(LXC)_第3张图片

典型的Linux在启动后,首先将 rootfs 置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite” 供用户使用。在docker中,起初也是将 rootfs 以readonly方式加载并检查,然而接下来利用 union mount 的将一个 readwrite 文件系统挂载在 readonly 的rootfs之上,并且允许再次将下层的 file system设定为readonly 并且向上叠加, 这样一组readonly和一个writeable的结构构成一个container的运行目录, 每一个被称作一个Layer。如下图:

Docker 介绍: 相关技术(LXC)_第4张图片

得益于AUFS的特性, 每一个对readonly层文件/目录的修改都只会存在于上层的writeable层中。这样由于不存在竞争, 多个container可以共享readonly的layer。
所以docker将readonly的层称作 “image” – 对于container而言整个rootfs都是read-write的,但事实上所有的修改都写入最上层的writeable层中,
image不保存用户状态,可以用于模板、重建和复制。

Docker 介绍: 相关技术(LXC)_第5张图片Docker 介绍: 相关技术(LXC)_第6张图片

上层的image依赖下层的image,因此docker中把下层的image称作父image,没有父image的image称作base image

Docker 介绍: 相关技术(LXC)_第7张图片

因此想要从一个image启动一个container,docker会先加载其父image直到base image,用户的进程运行在writeable的layer中。所有parent image中的数据信息以及
ID、网络和lxc管理的资源限制等具体container的配置,构成一个docker概念上的container。如下图:

Docker 介绍: 相关技术(LXC)_第8张图片

由此可见,采用AUFS作为docker的container的文件系统,能够提供如下好处:

  1. 节省存储空间 – 多个container可以共享base image存储

  2. 快速部署 – 如果要部署多个container,base image可以避免多次拷贝

  3. 内存更省 – 因为多个container共享base image, 以及OS的disk缓存机制,多个container中的进程命中缓存内容的几率大大增加

  4. 升级更方便 – 相比于 copy-on-write 类型的FS,base-image也是可以挂载为可writeable的,可以通过更新base image而一次性更新其之上的container

  5. 允许在不更改base-image的同时修改其目录中的文件 – 所有写操作都发生在最上层的writeable层中,这样可以大大增加base image能共享的文件内容。

以上5条 1-3 条可以通过 copy-on-write 的FS实现, 4可以利用其他的union mount方式实现, 5只有AUFS实现的很好。这也是为什么Docker一开始就建立在AUFS之上。

由于AUFS并不会进入linux主干 (According to Christoph Hellwig, linux rejects all union-type filesystems but UnionMount.),
同时要求kernel版本3.0以上(docker推荐3.8及以上),因此在RedHat工程师的帮助下在docker0.7版本中实现了driver机制, AUFS只是其中的一个driver,
在RHEL中采用的则是Device Mapper的方式实现的container文件系统,相关内容在下文会介绍。

参考文献

[1]https://groups.google.com/forum/#!topic/docker-dev/KcCT0bACksY

[2]http://blog.docker.io/2013/11/docker-0-7-docker-now-runs-on-any-linux-distribution/

[3]http://blog.dotcloud.com/kernel-secrets-from-the-paas-garage-part-34-a

[4]http://aufs.sourceforge.net/aufs.html

[5]http://aufs.sourceforge.net/

[6]http://en.wikipedia.org/wiki/Aufs

[7]http://docs.docker.io/en/latest/terms/filesystem/

[8]http://docs.docker.io/en/latest/terms/layer/

[9]http://docs.docker.io/en/latest/terms/image/

[10]http://docs.docker.io/en/latest/terms/container/

GRSEC

grsec是linux kernel安全相关的patch, 用于保护host防止非法入侵。由于其并不是docker的一部分,我们只进行简单的介绍。
grsec可以主要从4个方面保护进程不被非法入侵:

  • 随机地址空间 – 进程的堆区地址是随机的
  • 用只读的memory management unit来管理进程流程, 堆区和栈区内存只包含数据结构/函数/返回地址和数据, 是non-executeable
  • 审计和Log可疑活动
  • 编译期的防护

安全永远是相对的,这些方法只是告诉我们可以从这些角度考虑container类型的安全问题可以关注的方面。

参考文献

[1] http://blog.dotcloud.com/kernel-secrets-from-the-paas-garage-part-44-g

[2] http://grsecurity.net/


What docker do more than LXC

看似docker主要的OS级虚拟化操作是借助LXC, AUFS只是锦上添花。那么肯定会有人好奇docker到底比LXC多了些什么。无意中发现 stackoverflow 上正好有人问这个问题,
回答者是Dotcloud的创始人,出于备忘目的原文摘录如下。

http://stackoverflow.com/questions/17989306/what-does-docker-add-to-just-plain-lxc

On top of this low-level foundation of kernel features, Docker offers a high-level tool with several powerful functionalities:

  • Portable deployment across machines. Docker defines a format for bundling an application and all its dependencies into a single object which can be transferred to any docker-enabled machine, and executed there with the guarantee that the execution environment exposed to the application will be the same. Lxc implements process sandboxing, which is an important pre-requisite for portable deployment, but that alone is not enough for portable deployment. If you sent me a copy of your application installed in a custom lxc configuration, it would almost certainly not run on my machine the way it does on yours, because it is tied to your machine’s specific configuration: networking, storage, logging, distro, etc. Docker defines an abstraction for these machine-specific settings, so that the exact same docker container can run – unchanged – on many different machines, with many different configurations.

  • Application-centric. Docker is optimized for the deployment of applications, as opposed to machines. This is reflected in its API, user interface, design philosophy and documentation. By contrast, the lxc helper scripts focus on containers as lightweight machines – basically servers that boot faster and need less ram. We think there’s more to containers than just that.

  • Automatic build. Docker includes a tool for developers to automatically assemble a container from their source code, with full control over application dependencies, build tools, packaging etc. They are free to use make, maven, chef, puppet, salt, debian packages, rpms, source tarballs, or any combination of the above, regardless of the configuration of the machines.

  • Versioning. Docker includes git-like capabilities for tracking successive versions of a container, inspecting the diff between versions, committing new versions, rolling back etc. The history also includes how a container was assembled and by whom, so you get full traceability from the production server all the way back to the upstream developer. Docker also implements incremental uploads and downloads, similar to “git pull”, so new versions of a container can be transferred by only sending diffs.

  • Component re-use. Any container can be used as an “base image” to create more specialized components. This can be done manually or as part of an automated build. For example you can prepare the ideal python environment, and use it as a base for 10 different applications. Your ideal postgresql setup can be re-used for all your future projects. And so on.

  • Sharing. Docker has access to a public registry (http://index.docker.io) where thousands of people have uploaded useful containers: anything from redis, couchdb, postgres to irc bouncers to rails app servers to hadoop to base images for various distros. The registry also includes an official “standard library” of useful containers maintained by the docker team. The registry itself is open-source, so anyone can deploy their own registry to store and transfer private containers, for internal server deployments for example.

  • Tool ecosystem. Docker defines an API for automating and customizing the creation and deployment of containers. There are a huge number of tools integrating with docker to extend its capabilities. PaaS-like deployment (Dokku, Deis, Flynn), multi-node orchestration (maestro, salt, mesos, openstack nova), management dashboards (docker-ui, openstack horizon, shipyard), configuration management (chef, puppet), continuous integration (jenkins, strider, travis), etc. Docker is rapidly establishing itself as the standard for container-based tooling.

What we can do with Docker

有了docker这么个强有力的工具,更多的玩家希望了解围绕docker能做什么

Sandbox

作为sandbox大概是container的最基本想法了 – 轻量级的隔离机制, 快速重建和销毁, 占用资源少。用docker在开发者的单机环境下模拟分布式软件部署和调试,可谓又快又好。
同时docker提供的版本控制和image机制以及远程image管理,可以构建类似git的分布式开发环境。可以看到用于构建多平台image的packer以及同一作者的vagrant已经在这方面有所尝试了,笔者会后续的blog中介绍这两款来自同一geek的精致小巧的工具。

PaaS

dotcloud、heroku以及cloudfoundry都试图通过container来隔离提供给用户的runtime和service,只不过dotcloud采用docker, heroku采用LXC, cloudfoundry采用
自己开发的基于cgroup的warden。基于轻量级的隔离机制提供给用户PaaS服务是比较常见的做法 – PaaS 提供给用户的并不是OS而是runtime+service, 因此OS级别的隔离机制
向用户屏蔽的细节已经足够。而docker的很多分析文章提到『能够运行任何应用的“PaaS”云』只是从image的角度说明docker可以从通过构建image实现用户app的打包以及标准服务service image的复用, 而非常见的buildpack的方式。

由于对Cloud Foundry和docker的了解, 接下来谈谈笔者对PaaS的认识。PaaS号称的platform一直以来都被当做一组多语言的runtime和一组常用的middleware,提供这两样东西
即可被认为是一个满足需求的PaaS。然而PaaS对能部署在其上的应用要求很高:

  • 运行环境要简单 – buildpack虽然用于解决类似问题,但仍然不是很理想
  • 要尽可能的使用service – 常用的mysql, apache倒能理解,但是类似log之类的如果也要用service就让用户接入PaaS平台, 让用户难以维护
  • 要尽可能的使用”平台” – 单机环境构建出目标PaaS上运行的实际环境比较困难,开发测试工作都离不开”平台”
  • 缺少可定制性 – 可选的中间件有限,难于调优和debug。

综上所述部署在PaaS上的应用几乎不具有从老平台迁移到之上的可能,新应用也难以进入参数调优这种深入的工作。个人理解还是适合快速原型的展现,和短期应用的尝试。

然而docker确实从另一个角度(类似IaaS+orchestration tools)实现了用户运行环境的控制和管理,然而又基于轻量级的LXC机制,确实是一个了不起的尝试。
笔者也认为IaaS + 灵活的orchestration tools(深入到app层面的管理 如bosh)是交付用户环境最好的方式。


Open Solution

前文也提到docker存在disk/network不便限额和在较低版本kernel中(如RHEL的2.6.32)AUFS不支持的问题。本节尝试给出解答。

disk/network quota

虽然cgroup提供IOPS之类的限制机制,但是从限制用户能使用的磁盘大小和网络带宽上还是非常有限的。

Disk/network的quota现在有两种思路:

  • 通过docker run -v命令将外部存储mount到container的目录下,quota从Host方向限制,在device mapper driver中更采用实际的device因此更好控制。
    参考[1]

  • 通过使用disk quota来限制AUFS的可操作文件大小。类似cloud foundry warden的方法, 维护一个UID池,每次创建container都从中取一个user name,
    在container里和Host上用这个username创建用户,在Host上用setquota限制该username的UID的disk. 网络上由于docker采用veth的方式,可以采用tc来控制host上的veth的设备。参考[2]

参考文献:

[1]https://github.com/dotcloud/docker/issues/111

[2]https://github.com/dotcloud/docker/issues/471

RHEL 6.5

这里简单介绍下device mapper driver的思路,参考文献[2]中的讨论非常有价值。
docker的dirver要利用snapshot机制,起初的fs是一个空的ext4的目录,然后写入每个layer。每次创建image其实就是对其父image/base image进行snapshot,
然后在此snapshot上的操作都会被记录在fs的metadata中和AUFS layer(没读代码不是很理解?),docker commit将 diff信息在parent image上执行一遍.
这样创建出来的image就可以同当前container的运行环境分离开独立保存了。

这里仅仅查看材料理解不是很透彻,还是需要深入代码去了解详情。贴出 mail list 的片段,如果有理解的请不吝赐教。

The way it works is that we set up a device-mapper thin provisioning pool with a single base device containing an empty ext4 filesystem. Then each time we create an image we take a snapshot of the parent image (or the base image) and manually apply the AUFS layer to this. Similarly we create snapshots of images when we create containers and mount these as the container filesystem.

"docker diff" is implemented by just scanning the container filesystem and the parent image filesystem, looking at the metadata for changes. Theoretically this can be fooled if you do in-place editing of a file (not changing the size) and reset the mtime/ctime, but in practice I think this will be good enough. 

"docker commit" uses the above diff command to get a list of changed files which are used to construct a tarball with files and AUFS whiteouts (for deletes). This means you can commit containers to images, run new containers based on the image, etc. You should be able to push them to the index too (although I've not tested this yet).

Docker looks for a "docker-pool" device-mapper device (i.e. /dev/mapper/docker-pool) when it starts up, but if none exists it automatically creates two sparse files (100GB for the data and 2GB for the metadata) and loopback mount these and sets these up as the block devices for docker-pool, with a 10GB ext4 fs as the base image. 

This means that there is no need for manual setup of block devices, and that generally there should be no need to pre-allocate large amounts of space (the sparse files are small, and we things up so that discards are passed through all the way back to the sparse loopbacks, so deletes in a container should fully reclaim space.

目前已知存在的问题是删除的image的 block 文件没有被删除,见https://github.com/dotcloud/docker/issues/3182,
笔者发现此问题前4个小时作者给出了原因,看起来是kernel的issue,在讨论中包含work around的方法。

参考文献:

[1]http://blog.docker.io/2013/11/docker-0-7-docker-now-runs-on-any-linux-distribution/

[2]https://groups.google.com/forum/#!topic/docker-dev/KcCT0bACksY


Summary

本文总结了以下几点内容

  1. docker的介绍,包括由来、适用场景等

  2. docker背后的一系列技术 – namespace, cgroup, lxc, aufs等

  3. docker在利用LXC的同时提供了哪些创新

  4. 笔者对docker这种container, PaaS的一些理解

  5. docker存在的问题和现有的解决思路

希望能对想要了解docker的朋友有所帮助,更细致的了解还是得深入代码, 了解个中原委。

docker@github – https://github.com/dotcloud/docker

docker_maillist – https://groups.google.com/forum/#!forum/docker-dev

This entry was posted in  默认分类 and tagged  cloud foundry,  cloudfoundry,  go语言,  Linux Kernel, stackoverflow,  web应用,  开源软件,  操作系统,  数据结构,  版本控制,  自动化测试 on  2013/12/21.

开始学Go

开始学Go

八 14

bigwhite技术志 Blog, Blogger, C, Cpp, Dr.Dobb’s, Go, Google, Java, Linux, Opensource,Programmer, Ubuntu, Unix, 博客, 开源, 程序员, 翻译 No Comments

本文翻译自Dr.Dobb’s的”Getting Going with Go“。

本文是有关Google新的系统原生语言的五周教程的第一部分,这里将先向大家展示如何建立Go语言开发环境以及构建程序,然后带领大家浏览 一些代码范例来着重了解一下这门语言的一些有趣的特性。

这个教程系列将连续刊登五周。在今天这一部分中,Go语言专家Mark Summerfield将讲解如何建立Go语言开发环境,提供两个Go语言范例并给予深度解析。这些样例程序会向大家局部地展示了Go语言的一些关键特性 以及包。接下来几周将展示其余的关键特性,并特别为C、C++和Java程序员们深入研究那些Go语言独有的特性。

正如本周主编文章中所解释的那样,Go语言拥有许多独一无二的特性,因此它也许可以被称为二十一世纪的C语言。而且考虑到Ken Thompson也是该语言的设计者之一,这两种语言的确是有共同的祖先。

开始

Go是编译型语言,而不是解释型的。Go的编译速度非常快– 甚至远远快过其他同类语言- 知名的如C和C++。

标准Go语言编译器被称为gc,与其相关的工具链包括用于编译的5g、6g和8g;用于链接的5l、6l和8l以及用于查看Go语言文档的 godoc(在Windows平台上这些程序为5g.exe、6g.exe等等诸如此类)。这些奇怪的名字遵循了Plan 9操作系统编译器的命名方式,即用数字表示处理器体系(“5″代表ARM,”6″代表AMD64-包括Intel 64位处理器- “8″代表Intel 386)。幸运的是,我们无需对此产生忧虑,Go语言提供了一个更高级别的Go语言构建工具,这个工具可以为我们处理编译和链接任务。

本文中的所有代码使用的都是Go 1版本语法,并在Linux、Mac OS X以及Windows上用gc测试通过了。Go语言的开发者计划让随后所有Go 1.x版本支持Go 1向后兼容,因此这里的代码和例子将适用于所有1.x系列版本。

要下载和安装Go,请访问golang.org/doc/install.html,那里提供了下载链接与安装指令。Go 1为FreeBSD 7+、Linux 2.6+、Mac OS X (Snow Leopard和Lion)以及Windows 2000+提供源码包以及二进制形式安装包,可以支持所有Intel 32位和AMD 64位处理器体系。Go还支持ARM处理器版本的Linux,为Ubuntu Linux发布版提供预建go包。当你读到这里时,也许已经有其他Linux发布版的Go安装包了。

使用gc编译器的程序使用了一种特定的调用惯例(call convention)。这意味着使用gc编译的程序只可以与使用同样调用惯例编译的外部库进行链接 – 除非使用某适合的工具消除这些差异。使用cgo工具Go可以支持在Go程序中使用外部C代码,并且至少在Linux和BSD系统上,通过SWIG工具我们 可以将C和C++代码用于Go程序中。

除了gc,还有一种编译器称为gccgo。它是Gcc的一个Go特定前端,Gcc 4.6及以后版本才能支持。像gc一样,gccgo也许内建在一些Linux发行版中。构建和安装gccgo的指令在Go主站点上可以找到。

Go文档

Go的官方站点上维护着一份最新的Go文档。”Packages”链接提供了有关所有Go标准库包的访问方式- 以及它们的源码,这些源码在文档还很稀缺时十分有用。通过”Commands”链接你可以找到与Go一起发布的相关程序的文档(诸如编译器,构建工具 等)。通过”Specification”链接,你可以找到一份非正式,但很全面的Go语言规范。通过”Effective Go”链接,你可以找到一份介绍Go最佳实践的文档。

该站点还提供了一个沙箱特性,用于在线编写、编译以及运行Go小程序(稍有限制)。这个特性十分有用,便于初学者试验一些古怪的语法。Go站点上 的搜索框只能用于在Go文档中搜索;如果要对Go的资源进行全面搜索,请访问http://go-lang.cat-v.org/go- search。

Go的文档也可以在本地浏览,例如在Web浏览器中。如果要这样做,可运行Go的godoc工具,并通过传入命令行参数告知它以一个web服务器 的方式运行。下面是在一个Unix或Windows控制台中进行这个操作的方法,假设PATH环境变量中已经设置了godoc:

$ godoc -http=:8000

这个例子中的端口号可以是任意的- 如果它与一个已存在的服务器端口冲突,可以使用其他任一个端口号。

要想查看文档,打开一个浏览器,输入地址http://localhost:8000。一个类似golang.org首页的页面将会呈现在你的面 前。”Package”链接将指向Go标准库以及安装在GOROOT环境变量下的第三方包的文档。如果你定义了GOPATH环境变量(比如,为本 地程序和包),一个链接将会出现在”Packages”链接旁,通过这个链接你可以访问其他相关文档。(本文后续会讨论GOROOT和 GOPATH环境变量)

编辑,编译和运行

Go程序用UTF-8编码的普通的Unicode文本编写。绝大多数现代文本编辑器都可以自动处理这些代码,并且一些最流行的编辑器可以支持Go 源码的语法色彩高亮以及自动缩进。如果你的编辑器不支持Go,可以尝试在Go搜索引擎中输入你的编辑器的名字,查看一下是否有合适的插件。作为编 辑惯例,Go所有的关键字和操作符都使用ASCII字母;然而,Go的标识符可以以任意Unicode字母作为起始,后面可以跟着任意 Unicode字母或数字,因此Go开发者可以自由使用他们的母语。

为了掌握如何编辑、编译以及运行一个Go程序,我开始会用经典的”Hello World”程序作为例子- 然而我编写这个程序比寻常的稍复杂些。

如果你已经用二进制包或通过源码安装了Go,并且是以root或管理员权限安装的,你应该至少设置一个环境变量:GOROOT,该变量指示Go的 安装路径信息,你的PATH变量现在应该包含$GOROOT/bin或%GOROOT%\bin。为了检查Go安装地是否正确,可在控制台下输入 下面命令:

$ go version

如果你得到”command not found”或”‘go’ is not recognized…”的错误信息,那就意味着在PATH变量配置的路径下没有Go。

编译与链接

构建一个Go程序需要两步:编译和链接。(由于我们假设使用gc编译器,使用gccgo编译器的读者需要遵循golang.org/doc /gccgo_install.html中描述的编译和链接过程,同样,使用其他编译器的读者需要根据其编译器的指令进行编译和链接)。编译和链 接过程都由工具go处理,它不仅可以构建本地程序和包,还能够获取、构建以及安装第三方程序和包。

要想go能够编译本地程序和包,有三个要求。第一,Go的bin目录($GOROOT/bin或%GOROOT%\bin)必须在PATH环境变 量下。第二,必须存在一个目录,该目录下包含一个src目录,本地程序和包的源码就驻留在src目录下。例如,例子代码会解包到goeg/src /hello、goeg/src/bigdigits下等。最后,包含src的那个目录必须在GOPATH环境变量中设置。例如,要使用go工具 构建hello这个例子,我们必须这么做:

$ export GOPATH=$HOME/goeg
$ cd $GOPATH/src/hello
$ go build

两个例子中,我们都假设PATH环境变量中包含了$GOROOT/bin或%GOROOT%\bin。一旦go编译程序完毕,我们就可以运行这个 程序了。默认情况下,go会用可执行文件所在目录的名字来命名该文件(例如,在类Unix系统上是hello,而在Windows上为 hello.exe)。一旦构建完毕,我们就可以按常规方式运行它。

$ ./hello
Hello World!

注意,我们不需要编译或显式地链接其他包(即便后续我们将看到,hello.go使用了三个标准库的包)。这也是Go程序编译如此之快的一个原 因。

如果我们有多个Go程序,若他们的可执行文件能够放在同一个目录下将会非常方便,后续只需将这个目录加入到PATH环境变量中。幸运地是go支持 这样的情况:

$ export  GOPATH=$HOME/goeg
$ cd  $GOPATH/src/hello
$ go install

go install命令除了做了go build所做的事情之外,还将可执行文件放在标准位置($GOPATH/bin或%GOPATH%\bin)。这意味着将一个单一路径($GOPATH /bin或%GOPATH>%\bin)加入到PATH环境变量中,我们安装的所有Go程序就可以方便地被加入到PATH中。

除了这里给出的例子外,我们很可能想要在我们自己的目录下开发我们自己的Go程序和包。通过为GOPATH环境变量设置两个(或更多)冒号分隔的 路径(在Windows上用分号分隔)我们可以很容易解决这个问题。

虽然Go使用go工具作为标准构建工具,但你仍然可以使用make或其他现代构建工具,或使用其他Go专用的构建工具,或一些流行IDE的插件。

和谁打招呼(Hello)?

既然我们已经看到了如何构建一个Hello程序,接下来我们来看看其源代码。下面是hello程序的完整源码(在文件 hello/hello.go中):

// hello.go
package main
import (
    “fmt”
    “os”
    “strings”
)
func main() {
    who := “World!”
    if len(os.Args) > 1 { /* os.Args[0] is “hello” or “hello.exe” */
        who = strings.Join(os.Args[1:], ” “)
    }
    fmt.Println(“Hello”, who)
}

Go用C++风格的注释符号//作为单行注释,用/* … */作为多行注释符号。依照惯例,Go中多使用单行注释,多行注释常用于在开发中注释掉代码块。

每段Go代码都存在于一个包内,并且每个Go程序必须具有一个包含main()函数的main包,其中main函数会作为程序执行的入口点,即这 个函数首先执行。事实上,Go包也可以定义在main函数之前执行的init函数。值得注意的是包名和函数名之间不会存在冲突的情况。

Go的操作是以包为单位的,而不是文件。这意味着我们可以根据需要任意地将一个包拆分到多个文件中。如果多个文件具有相同的包声明,Go语言认为 这些文件都是同一个包的组成部分,与所有内容在单一文件中无异。当然,我们也可以将应用的功能分解到许多本地包中,这样可以保持代码整洁地模块 化。

import语句从标准库导入三个包。fmt包提供格式化文本以及读取格式化文本的函数;os包提供平台无关的操作系统变量以及函 数;strings包提供操作字符串的函数。

Go的基本类型支持普通操作符(例如,+可用于数值加法以及字符串连接),Go的标准库通过提供操作基本类型的函数包补充功能,例如这里导入的 strings包。我们可以在基本类型的基础上创建我们自定义的类型并为它们定义相关方法- 自定义操作特性类型的函数。

读者也许已经注意到了Go源码中没有分号、import的包无需逗号分隔以及if条件语句不需要括号。在Go中,块(block),包括函数体以 及控制结构体(如for、if语句以及for循环),使用括号界定。缩进只是单纯用于提高代码的可读性。技术上而言,Go语句是用分号分隔的,但 这些分号由编译器插入,我们自己无需关心,除非我们要将多个语句放在同一行中时。没有分号、很少的逗号以及括号让Go程序看起来更简洁,需要的输 入也更少。

Go使用func关键字定义函数(function)和方法(method)。main包的main()函数总是具有相同的函数签名 – 没有参数、没有返回值。当main.main()结束时,程序将终止并返回0给操作系统。当然,我们可以在任意时刻返回并选择我们自己的返回值。

main()函数中的第一个语句(使用:=操作符)在Go的术语里被称为一个短变量声明。这个语句在同一时间声明并初始化一个变量。此外,我们无 需指定变量的类型,因为Go可根据初始值推导出变量的类型。因此在这个例子中,我们声明了一个string类型的变量who,感谢Go的强类型机 制,我们只需将字符串赋值给who即可。

os.Args变量是字符串的一个slice(片段)。Go使用array(数组)、slice和其他集合数据类型,但在这些例子中,知道下面这 些即可:使用内置的len()函数获取一个slice的长度以及通过[]下标操作符访问其中的元素。特别是,slice[n]返回slice的第 n个元素(从0起始),slice[:n]返回另外一个slice,这个新slice由原slice的第n个到最后一个之间的元素组成。在集合一 章,我们将看到有关这方面的一般性的Go语法。就这里的os.Args来说,这个slice总是应该至少在位置0处具有一个字符串(程序的名 字)。(所有Go的下标索引都是从0开始)

如果输入一个或更多命令行参数,if条件将被满足,我们将所有参数拼接成一个单一的字符串并赋值给who。在这里例子中,我们使用赋值操作符 (=),如果我们使用:=,我们将声明和初始化一个新的who变量,其影响范围将局限在if语句块中。strings.Join函数接受一个字符 串slice和一个分割符(可以为空,即”")作为参数,并返回一个由所有slice的字符串元素组成的,由分隔符分隔的单一字符串。这里我们用 空格分隔这些字符串。

最后,在最后一个语句中,我们输出Hello,一个空格,who变量中的字符串以及一个新行(newline)。fmt包中拥有许多不同的打印输 出变体,一些像fmt.Println()的将整齐地输出任何传入的值,其他的诸如fmt.Printf将使用占位符以提供更佳的格式控制。

另外一个例子 – 二维slices

下一个例子bigdigits程序从命令行(以一个字符串形式)读取一个数,并在控制台上使用”大号字体”输出这个数。早在二十世纪,在很多用户 共享一台高速行打印机的场合,按惯例将使用这种技术在每个用户的打印工作之前放置一个封面,该封面可以展示用于识别的细节,诸如用户名以及将被打 印的文件的名字。

我将分三段回顾这个程序的代码:首先是import部分,然后是静态数据,最后是处理过程。不过现在让我们来看看一个样例,了解一下这个程序是如 何工作的吧:

$ ./bigdigits  290175493

Docker 介绍: 相关技术(LXC)_第9张图片

每位数字都由一个字符串slice表示,所有数字一起由一个元素为字符串slice的slice表示。在查看数据之前,这里我们展示一下如何声明 和初始化一个一维的字符串和数字slice:

longWeekend := []string{“Friday”, “Saturday”, “Sunday”, “Monday”}
var lowPrimes = []int{2, 3, 5, 7, 11, 13, 17, 19}

slice用[]Type表示,如果我们要初始化它们,我们可以直接在后面跟上一个用大括号包裹、逗号分隔的对应类型的元素列表。我们本可以用同 样的变量声明语法来声明这两个变量,但在这里我们为展示两种语法的差异以及一个稍后即将说明的原因,lowPrimes slice使用了一个更长形式的声明。由于slice类型可以作为slice类型的元素类型,因此我们可以很容易地创建多维集合(slice的slice 等)。

bigdigits程序只需要导入四个包。

import (
    “fmt”
    “log”
    “os”
    “path/filepath”
)

fmt包提供了用于文本格式化和读取格式化文本的函数。log包提供了日志记录函数。os包提供了平台无关的操作系统变量以及函数,其中就包括持 有命令行参数值的[]string类型的os.Args变量。path包下面的filepath包提供跨平台操作文件名和路径的相关函数。注意对 于逻辑上存在包含关系的包(译注:如path/filepath)来说,我们在代码中使用时只指明其名字的最后部分(这里是filepath)。

对于bigdigits这个程序,我们需要一个二维的数据(一个字符串slice的slice)。下面就是创建它的方法,通过代表数字0的字符串 布局我们可以看出这个数字对应的字符串在输出时相应的行。3到8的数字对应的字符串这里省略了。

var bigDigits = [][]string{
   {“  000  “,
    ” 0   0 “,
    “0     0″,
    “0     0″,
    “0     0″,
     “0  0″,
     ” 000 “},
    {” 1 “, “11 “, ” 1 “, ” 1 “, ” 1 “, ” 1 “, “111″},
    {“222″,”2  2″,”  2″,”  2 “,”2  “,”2  “,”22222″},
    // … 3 to 8 …
    {” 9999″, “9  9″, “9  9″, ” 9999″, “  9″, “  9″, “  9″},

在函数或方法外面声明的变量不可以使用:=操作符,但我们可以使用长声明形式(使用关键字var)以及赋值操作符(=)来得到同样的效果,就如我 们这里为bigDigits程序中的变量所做的那样(前面对lowPrime变量的声明)。我们仍然无需指定bigDigits变量的类型,Go 可以从赋值中推断出其类型。

我们把计算工作留给了Go编译器,因此也没有必要指出slice的维数。Go的一个方便之处就是它对使用了括号的符合字面值的良好支持,这样我们 无需在一个地方声明一个数据变量,然后在另外一个地方用数据给它赋值了。

main()函数读取命令行,并使用这些数据产生输出,这个函数只有20行。

func main() {
    if len(os.Args) == 1 {
        fmt.Printf(“usage: %s \n”, filepath.Base(os.Args[0]))
        os.Exit(1)
    }
 
    stringOfDigits := os.Args[1]
    for row := range bigDigits[0] {
        line := “”
        for column := range stringOfDigits {
            digit := stringOfDigits[column] – ’0′
            if 0 <= digit && digit <= 9 {
                line += bigDigits[digit][row] + ” “
            } else {
                log.Fatal(“invalid whole number”)
            }
        }
        fmt.Println(line)
    }
}

程序一开始检查是否有任何命令行参数。如果没有,len(os.Args)的值将为1(回忆一下,os.Args[0]中存放的是程序名,因此这 个slice的长度至少是1)。如果这个if语句条件成立,我们将使用fmt.Printf输出一条适当程序用法信息,该Printf函数使用类 似C/C++中printf()或Python中%操作符的%占位符。

path/filepath包提供路径操作函数- 比如,filepath.Base()函数返回给定路径的基本名(basename,即文件名)。在输出这条信息后,程序使用os.Exit()函数结束 程序,并返回1给操作系统。在类Unix系统中,一个值为0的返回值用于表示成功,非0值标识用法错误或失败。

filepath.Base()函数的使用向我们说明了Go的一个美妙的特性:当一个包被导入时,无论它是顶层的包还是逻辑上内置于其他包中的包 (例如:path/filepath),我们总是可以只通过其名字的最后部分(即filepath)来引用它。我们还可以给包赋予本地名字以避免 名字冲突。

如果至少传入了一个命令行参数,第一个参数将被拷贝到stringOfDigits变量(string类型)中。要想将用户输入的数字转换成大数 字,我们必须迭代处理bigDigits slice的每一行,即每个数字的第一行(最上面的一行),接下来第二行,依次类推。我们假设所有bigDigits的slice都具有相同数量的行,这 样我们可以从第一个slice那里得到行数。Go的for循环对不同场景有不同的应对语法;在这个例子中,for…range循环返回 slice中每个元素的索引位置信息。

行和列的循环部分的代码可以这样来写:

for row := 0; row < len(bigDigits[0]); row++ {
    line := “”
    for column := 0; column < len(stringOfDigits); column++ {
        …

这是一个C、C++和Java程序员都熟悉的语法形式,在Go中它也是有效的。(与C、C++和Java不同在于,在Go中,++和–操作符只 能用作语句,而不能用作表达式。此外,它们只能被用作后缀操作符,而不能作为前缀操作符。这意味着求值顺序导致的相关问题在Go中不会发生- 谢天谢地,像f(i++)和a[i] = b[++i]这样的表达式在Go中是非法的。) 然而,for…range语法更加短小,也更加方便。

在每次行迭代时,代码会将行的line赋值为空字符串。接下来,我们做迭代处理从用户那里获取的stringOFDigits中的列(即,字 符)。Go的字符串使用UTF-8字符,因此本质上一个字符很可能用两个或更多字节表示。但这不是这里要讨论的话题,因为我们只关心数值0、 1、…、9,这些数值用UTF-8字符表示时只需一个字节,与用7比特ASCII字符表示所使用的字节值相同。

当我们索引字符串中的某个特定位置时,我们获取了那个位置的字节值。(在Go中byte类型是uint8类型的同义词。)因此我们获取到命令行字 符串特定列上的字节值,减去数字0的字节值后,得到它表示的数字。在UTF-8(以及7比特ASCII)中,字符’0′是码点(字符)十进制值 48,字符’1′是码点十进制值49,依次类推。这样举例,如果我们有字符’3′(码点1),我们可以通过做减法’3′ – ’0′(即51-48)的结果得到其整型值3。

Go使用单引号表示字符字面值,一个字符字面值是一个与Go任何整型类型都兼容的整数。Go的强类型意味着如果不进行显式转型,我们无法将一个 int32类型的数与一个int16类型的数相加,不过,Go中的数值常量和字面值自适应于其上下文,这样一来,这里的’0′被认为是一个字节。

如果这个数字(byte类型)在范围内,我们会将对应的字符串加到line变量中。(在if语句中,常量0和9被认为是byte类型,因为它们是 数值类型,但如果数值是一个不同的类型,比如说,int,它们将会被当作新类型对待。)虽然Go中的字符串是不可改变的,Go仍然支持+=附加操 作符以提供一个便于使用的语法。(它通过在后台替换掉原先的字符串。)Go同样支持+字符串连接操作符,该操作将返回一个由左右字符串操作数连接 而成的新字符串。

为了获取对应的字符串,我们根据这个数值访问bigDigits slice,然后访问其中我们需要的行(字符串)。

如果数值超出了范围(比如,由于stringOfDigits包含了一个非数值),我们调用log.Fatal()函数记录一条错误信息。如果没有显式指 定其他日志输出目标,这个函数会在os.Stderr中记录下日期、时间和错误信息。然后该函数调用os.Exit(1)结束程序。还有 一个名为log.Fatalf()的函数可以做同样的事情,但它接受%占位符。我们没有在第一个if语句中使用log.Fatal()函数,因为 我们想输出程序的使用方法信息,但不要log.Fatal()默认输出的日期和时间信息。

一旦给定行的所有数字的字符串都累加完毕,完整的一行就被输出。在这里,我们输出了7行,因为每个bigDigits slice中的数字由七个字符串表示。

最后一点是声明和定义的顺序无关紧要。因此在bigdigits/bigdigits.go文件中,我们可在main()函数前声明bigDigits变 量,也可在后面声明。在这个例子里,我们将main()函数放在前面,对于这篇文章中的例子,我们通常更倾向于自顶向下的排序。

这里的两个例子已经涵盖了大量特性,但它们所展示的资料与其他主流语言甚为相似,即便语法稍有不同。下周的文章将检视Go语言的其余特性,包含一些高级方面的特性。

© 2012, bigwhite. 版权所有.

Related posts:

  1. 使用Make的命令行变量
  2. 解决BuildBot构建结果mail无法发送的问题
  3. 也谈Linux Kernel Hacking – Kconfig与Kbuild
  4. 也谈共享库2
  5. C程序员驯服Common Lisp – 函数
This entry was posted in  默认分类 and tagged  arm处理器,  Linux Kernel,  Web服务器,  处理器,  控制台,  搜索引擎,  操作系统,  文本编辑,  格式化,  程序员,  编辑器 on  2013/12/21.

Docker 0.7 runs on all Linux distributions

So, Docker 0.7 is finally here! We hope you’ll like it. On top of countless bug fixes and small usability improvements, it introduces 7 major features since 0.6.0:

  • Feature 1: Standard Linux support
  • Feature 2: Storage drivers
  • Feature 3: Offline transfer
  • Feature 4: Links
  • Feature 5: Container naming
  • Feature 6: Advanced port redirects
  • Feature 7: Quality

You can see the full Changelog on the repository, and read below for details on each feature.

Thank you!

An incredible 130 people participated in this release! Including, but not limited to: Alex Larsson, Josh Poimboeuf, Lokesh Mandvekar, Vincent Batts, Adam Miller, Marek Goldmann and Matthew Miller from the Red Hat team, Fred Kautz, Tianon “andrew” Gravi, Paul Nasrat, Sven Dowideit, James Turnbull, Edmund Wagner, David Calavera, Travis Cline, Gurjeet Singh, Justin Force, Johan Euphrosine, Jerome Petazzoni, Ole Reifschneider, Andy Rothfusz, Will Rouesnel, Greg Thornton, Scott Bessler, Todd Lunter, Vladimir Rutsky, Nicolas Dudebout, Jim Alateras, Roger Peppe. To you and everyone else who has lend a helping hand: thank you!

Feature 1: Standard Linux support

Merged in 0.7.0. Special thanks to Alex Larsson and the Red Hat team, and to Junjiro R. Okajima for making aufs possible

This version introduces several major new features, but the most anticipated was definitely standard Linux support. As of today, Docker no longer requires a patched Linux kernel, thanks to a new storage driver contributed by Alex Larsson (see the next feature, “storage drivers”). This means that it will work out-of-the-box on all major distributions, including Fedora, RHEL, Ubuntu, Debian, Suse, Gentoo, Arch, etc. Look for your favorite distro in our installation docs!

 

Feature 2: Storage drivers

Merged in 0.7.0.

A key feature of Docker is the ability to create many copies of the same base filesystem almost instantly. Under the hood Docker makes heavy use of AUFS by Junjiro R. Okajima as a copy-on-write storage mechanism. AUFS is an amazing piece of software and at this point it’s safe to say that it has safely copied billions of containers over the last few years, a great many of them in critical production environments. Unfortunately, AUFS is not part of the standard linux kernel and it’s unclear when it will be merged. This has prevented docker from being available on all Linux systems. Docker 0.7 solves this problem by introducing a storage driver API, and shipping with several drivers. Currently 3 drivers are available: AUFS, VFS (which uses simple directories and copy) and DEVICEMAPPER, developed in collaboration with Alex Larsson and the talented team at Red Hat, which uses an advanced variation of LVM snapshots to implement copy-on-write. An experimental BTRFS driver is also being developed, with even more coming soon: ZFS, Gluster, Ceph, etc.

When the docker daemon is started it will automatically select a suitable driver depending on its capabilities. If your system supports AUFS, it will continue to use the AUFS driver and all your containers and images will automatically be available after you upgrade. Otherwise devicemapper will be used as the default driver. Drivers cannot share data on the same machine, but all drivers produce images which are compatible with each other and with all past versions of docker. This means every image on the index (including those produced byTrusted Builds) remains usable across all docker installations.

Feature 3: Offline transfer

Merged in 0.6.7. Special thanks to Frederic Kautz

With Offline Transfer, container images can now be exported to the local filesystem as a standalone bundle, and loaded into any other docker daemon. The resulting images will be fully preserved, including configuration, creation date, build history, etc. The exported bundles are regular directories, and can be transported by any file transfer mechanism, included ftp, physical media, proprietary installers, etc. This feature is particulary interesting for software vendors who need to ship their software as sealed appliances to their “enterprise” customers. Using offline transfer, they can use docker containers as the delivery mechanism for software updates, without losing control of the delivery mechanism or requiring that their customers relax their security policies.

As David Calavera from the Github enterprise team puts it: “Building on-premise products based on Docker containers is much easier now thanks to offline transfer. You can always make sure your containers arrive to places without registry access”

To use offline transfer, check out the new docker save and docker load commands.

 

Feature 4: Advanced port redirects

Merged in 0.6.5.

Note: this feature introduces 2 small breaking changes to improve security. See the end of this section for details.

The run -p flag has been extended to give you more control over port redirection. Instead of automatically redirecting on all host interfaces, you can specify which interfaces to redirect on. Note that this extends the existing syntax without breaking it.

For example:

  • -p 8080 will publish port 8080 of the container to all interfaces of the host with a dynamically allocated port
  • -p 8080:8080 will publish port 8080 of the container to all interfaces of the host with a static port of 8080
  • -p 127.0.0.1:80:80 # Publish port 80 of the container to localhost of the host with a static port to 80

You can also choose to not redirect on any host interface, effectively making that port unreachable from the outside. This is very useful in combination with links (see “Links” below), for example to expose an unprotected database port to an application container without publishing it on the public internet. You can do this without a Dockerfile thanks to the new -expose flag.

This release introduces two breaking changes to improve security:

First, we are changing the default behavior of docker run to not redirect ports on the host. This is better for security: ports are private by default, and you can explicitely publish them with the -p flag. If you currently rely on exposed ports being published on all host interfaces by default, that will no longer be true in 0.6.5. You can revert to the old behavior by simply adding the appropriate -p flags.

Second, we are deprecating the advanced “ : ” syntax of the EXPOSE build instruction. This special syntax allowed the Dockerfile to specify in advance that the exposed port should be published on a certain port on all host interfaces. We have found that this hurts separation of concerns between dev and ops, by restricting in advance the system administrator’s ability to configure redirects on the host. The regular “EXPOSE ” syntax is not affected.

For example:

  • Not deprecated: EXPOSE 80 will continue to expose tcp port 80 as usual.
  • Deprecated: EXPOSE 80:80 will trigger a warning, and be treated as identical to EXPOSE 80. The public port will simply be ignored.

We apologize for these breaking changes. We did our best to minimize the inconvenience, and we hope you’ll agree that the improvements are worth it!

Feature 5: Links

Merged in 0.6.5.

Links allow containers to discover and securely communicate with each other. Inter-container communication can now be disabled with the daemon flag -icc=false. With this flag set to false, Container A cannot access Container B unless explicitly allowed via a link. This is a huge win for securing your containers. When two containers are linked together Docker creates a parent child relationship between the containers. The parent container will be able to access information via environment variables of the child such as name, exposed ports, ip, and environment variables.

When linking two containers Docker will use the exposed ports of the container to create a secure tunnel for the parent to access. If a database container only exposes port 8080 then the linked container will only be allowed to access port 8080 and nothing else if inter-container communication is set to false.

Example

When running a WordPress container we need to be able to connect to a database such as MySQL or MariaDB. Using links we can easily swap out the backend database and not have to change our configuration files for the wordpress site.

In order to build a wordpress container that works with both databases the container should look for the alias, in our example `db`, when linking to the database. This will allow you to access the database information via consistent environment variables no matter what the name of the container is. Using the two database containers from the naming example we will create a link between them to our wordpress container.

To link just add the -link flag to docker run:

docker run -d -link mariadb:db user/wordpress or

docker run -d -link mysql:db user/wordpress

After creating the new container linked into the database container with the alias db you can inspect the environment of the wordpress container and view the ip and port of the database.

 

$DB_PORT_3306_TCP_PORT

$DB_PORT_3306_TCP_ADDR

 

The environment variables will be prefixed with the alias you specified on the -link flag.

For another example we encourage you to read the Building a redis container to link as a child of our web application example in the Docker documentation.

 

Feature 6: Container naming

Merged in 0.6.5

We are happy to announce that we can finally close issue #1! You can now give memorable names to your containers using the new -name flag for docker run. If no name is specified Docker will automatically generate a name. When you link one container to another you will have to provide the name and alias of the child that you want to link to via -link child_name:alias.

Examples

Enough talk, let’s see some examples! You can run two databases with corresponding names like so:

  • docker run -d -name mariadb user/mariadb
  • docker run -d -name mysql user/mysql

Every command that worked with a container_id can now be used with a name that you specified:

  • docker restart mariadb
  • docker kill mysql

 

Feature 7: Quality

Special thanks to Tianon Gravi, Andy Rothfusz and Sven Dowideit

Obviously, “quality” isn’t really a feature you can add. But it’s become important enough to us that we wanted to list it. Quality has been a big focus in 0.7, and that focus will only increase in future versions. It’s not easy keeping up with a project as incredibly active as Docker – especially when its success came so quickly. When the floodgates opened, much of the code was still immature, unfinished or scheduled to be refactored or rewritten. 6 months and 2,844 pull requests later, much of it is still there, drowned in a daily deluge of contributions and feature requests.

It took us a while to find our bearings and adapt to the new, crazy pace of Docker’s development. But we are finally figuring it out. Starting with 0.7, we are placing Quality in all its forms – user interface, test coverage, robustness, ease of deployment, documentation, consistency of APIs – at the center our development process. Of course, there is no silver bullet: Docker still has plenty of bugs and rough edges, and the progress will be slow and steady rather than the elusive “grand rewrite” we initially hoped for. But day after day, commit after commit, we will continue to make gradual improvements.

We are grateful that so many of you have understood the potential of Docker and shown so much patience for the quirks and bugs. “It’s OK”, you said. “Docker is so useful I don’t mind that it’s rough”. Soon we hope to hear you say “I use Docker because it’s useful AND reliable”.

Soon!

Thanks to all of you for your support.
Onwards to 0.8!

Solomon, Michael, Victor, Guillaume and all the Docker maintainers

Read more on

gigaom

This entry was posted in  默认分类 and tagged  application,  command,  Debian,  Fedora,  gigaom,  GitHub, interface,  jerome petazzoni,  Linux,  Linux Kernel,  Ubuntu on  2013/11/27.

Linux内核代码中的脏话统计

按脏话数/版本号统计

按脏话密度/版本号统计

上图显示的是对Linux内核里的c,h和S源代码里的脏话统计结果,我会每月更新一次这些数据,当有新版本发布时也会更新一次。我是受the linux kernel fuck count的启发,但遗憾的是它里面的数据已经过期了。

从图中可以很明显的看出,自从2.4版开始,脏话的数量有大量的增加。然而,总的代码量也增加了很多,所以,总的来看,平均每行的诅咒密度是减少的。

介绍一下统计方法:不论任何地方出现的脏话词汇都会计入总数——出现在另一个词内也算。本来可以做的更合理些,但结果发现FreeBSD的正则表达式引擎有严重的内存泄漏问题,我也就没有再改进了。一行里对一个脏词可能会统计出多次,因为有时候一个程序员会遇到非常非常懊恼的一天。

你可以在找到这个脚本,但它写的实在是太乱了,不推荐。

[英文原文: Linux kernel swear counts ]

分类:幽默讽刺,标签:linux, 脏话, 诅咒, 骂人

This entry was posted in  默认分类 and tagged  FreeBSD,  Linux Kernel,  linux内核,  内存泄漏,  发布,  数据,  标签, 正则表达式,  源代码,  程序员,  脚本 on  2013/09/22.

你可能不知道的Shell

(感谢404null投稿)

Shell也叫做命令行界面,它是*nix操作系统下用户和计算机的交互界面。Shell这个词是指操作系统中提供访问内核服务的程序。

这篇文章向大家介绍Shell一些非广为人知、但却实用有趣的知识,权当品尝shell主食后的甜点吧。

科普

先科普几个你可能不知道的事实:

  • Shell几乎是和Unix操作系统一起诞生,第一个Unix Shell是肯·汤普逊(Ken Thompson)以Multics上的Shell为模范在1971年改写而成,并命名Thompson sh。即便是后来流行的bash(shell的一种变体),它的年龄实际上比当前流行的所有的Linux kernel都大,可谓在Linux系统上是先有Shell再有Kernel。
  • 当前绝大部分*nix和MacOS操作系统里的默认的Shell都是bash,bash由Brian Fox在1987年创造,全称Bourne Again shell ( bash)。
  • 你或许听说除了bash之外,还有Bourne shell ( sh),Korn shell ( ksh),C shell (包括 csh and tcsh),但是你知道这个星球上一共存在着大约50多种不同的shell么?想了解他们,请参考 http://www.freebsd.org/ports/shells.html。
  • 每个月tiobe上都会给一个编程语言的排名,来显示各种语言的流行度。排名指数综合了全球范围内使用该语言的工程师人数、教学的课程数和第三方供应商数。截止至2012年11月份,tiobe公布的编程语言排行榜里,bash的指数是0.56%排名22位。如果算上它旗下的awk 0.21%和tcl 0.146%,大概就能排到14名。注意这里还不包括bash的同源的兄弟姐妹csh、ksh等,算上它们,shell家族有望接近前十。值得一提的是一直以来shell的排名就很稳定,不像某些“暴发户”语言,比如objective-c,这些语言的流行完全是因为当前Apple系的崛起,但这种热潮极有可能来得快去得更快。

全球最大的源代码仓库Github里,shell相关的项目数占到了8%,跻身前5和Java相当,可见在实战工程里,shell可谓宝刀不老。图片来源,参见这里

一些强大的命令

再分享一些可能你不知道的shell用法和脚本,简单&强大!

在阅读以下部分前,强烈建议读者打开一个shell实验,这些都不是shell教科书里的大路货哦:)

  • !$
    !$是一个特殊的环境变量,它代表了上一个命令的最后一个字符串。如:你可能会这样:
    $mkdir mydir
    $mv mydir yourdir

    $cd yourdir
    可以改成:
    $mkdir mydir
    $mv !$ yourdir
    $cd !$
  • sudo !!
    以root的身份执行上一条命令 。
    场景举例:比如Ubuntu里用apt-get安装软件包的时候是需要root身份的,我们经常会忘记在apt-get前加sudo。每次不得不加上sudo再重新键入这行命令,这时可以很方便的用sudo !!完事。
    (陈皓注:在shell下,有时候你会输入很长的命令,你可以使用!xxx来重复最近的一次命令,比如,你以前输入过,vi /where/the/file/is, 下次你可以使用 !vi 重得上次最近一次的vi命令。)
  • cd –
    回到上一次的目录 。
    场景举例:当前目录为/home/a,用cd ../b切换到/home/b。这时可以通过反复执行cd –命令在/home/a/home/b之间来回方便的切换。
    (陈皓注:cd ~ 是回到自己的Home目录,cd ~user,是进入某个用户的Home目录)
  • ‘ALT+.’ or ‘ .’
    热建alt+. 或 esc+. 可以把上次命令行的参数给重复出来。
  • ^old^new
    替换前一条命令里的部分字符串。
    场景:echo "wanderful",其实是想输出echo "wonderful"。只需要^a^o就行了,对很长的命令的错误拼写有很大的帮助。(陈皓注:也可以使用 !!:gs/old/new
  • du -s * | sort -n | tail
    列出当前目录里最大的10个文件。
  • :w !sudo tee %
    在vi中保存一个只有root可以写的文件
  • date -d@1234567890
    时间截转时间
  • > file.txt
    创建一个空文件,比touch短。
  • mtr coolshell.cn
    mtr命令比traceroute要好。
  • 在命令行前加空格,该命令不会进入history里。
  • echo “ls -l” | at midnight
    在某个时间运行某个命令。
  • curl -u user:pass -d status=”Tweeting from the shell” http://twitter.com/statuses/update.xml
    命令行的方式更新twitter。
  • curl -u username –silent “https://mail.google.com/mail/feed/atom” | perl -ne ‘print “\t” if //; print “$2\n” if /<(title|name)>(.*)<\/\1>/;’
    检查你的gmail未读邮件
  • ps aux | sort -nk +4 | tail
    列出头十个最耗内存的进程
  • man ascii
    显示ascii码表。
    场景:忘记ascii码表的时候还需要google么?尤其在天朝网络如此“顺畅”的情况下,就更麻烦在GWF多应用一次规则了,直接用本地的man ascii吧。
  • ctrl-x e
    快速启动你的默认编辑器(由变量$EDITOR设置)。
  • netstat –tlnp
    列出本机进程监听的端口号。(陈皓注:netstat -anop 可以显示侦听在这个端口号的进程)
  • tail -f /path/to/file.log | sed '/^Finished: SUCCESS$/ q'
    当file.log里出现Finished: SUCCESS时候就退出tail,这个命令用于实时监控并过滤log是否出现了某条记录。
  • ssh user@server bash < /path/to/local/script.sh
    在远程机器上运行一段脚本。这条命令最大的好处就是不用把脚本拷到远程机器上。
  • ssh user@host cat /path/to/remotefile | diff /path/to/localfile -
    比较一个远程文件和一个本地文件
  • net rpc shutdown -I ipAddressOfWindowsPC -U username%password
    远程关闭一台Windows的机器
  • screen -d -m -S some_name ping my_router
    后台运行一段不终止的程序,并可以随时查看它的状态。-d -m参数启动“分离”模式,-S指定了一个session的标识。可以通过-R命令来重新“挂载”一个标识的session。更多细节请参考screen用法 man screen
  • wget --random-wait -r -p -e robots=off -U mozilla http://www.example.com
    下载整个www.example.com网站。(注:别太过分,大部分网站都有防爬功能了:))
  • curl ifconfig.me
    当你的机器在内网的时候,可以通过这个命令查看外网的IP。
  • convert input.png -gravity NorthWest -background transparent -extent 720×200  output.png
    改一下图片的大小尺寸
  • lsof –i
    实时查看本机网络服务的活动状态。
  • vim scp://username@host//path/to/somefile
    vim一个远程文件
  • python -m SimpleHTTPServer
    一句话实现一个HTTP服务,把当前目录设为HTTP服务目录,可以通过http://localhost:8000访问 这也许是这个星球上最简单的HTTP服务器的实现了。
  • history | awk '{CMD[$2]++;count++;} END { for (a in CMD )print CMD[a] " " CMD[a]/count*100 "% " a }' | grep -v "./" | column -c3 -s " " -t | sort -nr | nl | head -n10
    (陈皓注:有点复杂了,history|awk ‘{print $2}’|awk ‘BEGIN {FS=”|”} {print $1}’|sort|uniq -c|sort -rn|head -10)
    这行脚本能输出你最常用的十条命令,由此甚至可以洞察你是一个什么类型的程序员。
  • tr -c “[:digit:]” ” ” < /dev/urandom | dd cbs=$COLUMNS conv=unblock | GREP_COLOR=”1;32″ grep –color “[^ ]“
    想看看Marix的屏幕效果吗?(不是很像,但也很Cool!)

看不懂行代码?没关系,系统的学习一下*nix shell脚本吧,力荐《Linux命令行与Shell脚本编程大全》。

最后还是那句Shell的至理名言:(陈皓注:下面的那个马克杯很不错啊,404null.com挺有意思的)

“Where there is a shell,there is a way!”


Unix Shell变色马克杯

参考文献:

  • Unix Shell Wiki
  • Github language ranking
  • An introduction of Unix Shell history
  • Tiobe Software
  • http://www.commandlinefu.com/

(转载本站文章请注明作者和出处 酷壳 – CoolShell.cn ,请勿用于任何商业用途)

——===  访问 酷壳404页面 寻找遗失儿童。 ===——
















This entry was posted in  默认分类 and tagged  http服务器,  Linux Kernel,  Objective-C,  shell,  工程师,  排行榜, 操作系统,  源代码,  程序员,  编程语言,  计算机 on  2013/09/14.

Linux内核 RPS/RFS功能详细测试分析

RPS和RFS

  • RPS 全称是 Receive Packet Steering, 这是Google工程师 Tom Herbert ([email protected] )提交的内核补丁, 在2.6.35进入Linux内核. 这个patch采用软件模拟的方式,实现了多队列网卡所提供的功能,分散了在多CPU系统上数据接收时的负载, 把软中断分到各个CPU处理,而不需要硬件支持,大大提高了网络性能。
  • RFS 全称是 Receive Flow Steering, 这也是Tom提交的内核补丁,它是用来配合RPS补丁使用的,是RPS补丁的扩展补丁,它把接收的数据包送达应用所在的CPU上,提高cache的命中率。
  • 这两个补丁往往都是一起设置,来达到最好的优化效果, 主要是针对单队列网卡多CPU环境(多队列多重中断的网卡也可以使用该补丁的功能,但多队列多重中断网卡有更好的选择:SMP IRQ affinity)

原理

RPS: RPS实现了数据流的hash归类,并把软中断的负载均衡分到各个cpu,实现了类似多队列网卡的功能。由于RPS只是单纯的把同一流的数据包分发给同一个CPU核来处理了,但是有可能出现这样的情况,即给该数据流分发的CPU核和执行处理该数据流的应用程序的CPU核不是同一个:数据包均衡到不同的cpu,这个时候如果应用程序所在的cpu和软中断处理的cpu不是同一个,此时对于cpu cache的影响会很大。那么RFS补丁就是用来确保应用程序处理的cpu跟软中断处理的cpu是同一个,这样就充分利用cpu的cache。

  • 应用RPS之前: 所有数据流被分到某个CPU, 多CPU没有被合理利用, 造成瓶颈

  • 应用RPS之后: 同一流的数据包被分到同个CPU核来处理,但可能出现cpu cache迁跃

  • 应用RPS+RFS之后: 同一流的数据包被分到应用所在的CPU核

必要条件

使用RPS和RFS功能,需要有大于等于2.6.35版本的Linux kernel.

如何判断内核版本?

$ uname -r
2.6.38-2-686-bigmem

对比测试

类别 测试客户端 测试服务端
型号 BladeCenter HS23p BladeCenter HS23p
CPU Xeon E5-2609 Xeon E5-2630
网卡 Broadcom NetXtreme II BCM5709S Gigabit Ethernet Emulex Corporation OneConnect 10Gb NIC
内核 3.2.0-2-amd64 3.2.0-2-amd64
内存 62GB 66GB
系统 Debian 6.0.4 Debian 6.0.5
超线程
CPU核 4 6
驱动 bnx2 be2net
  • 客户端: netperf
  • 服务端: netserver
  • RPS cpu bitmap测试分类: 0(不开启rps功能), one cpu per queue(每队列绑定到1个CPU核上), all cpus per queue(每队列绑定到所有cpu核上), 不同分类的设置值如下
  1. 0(不开启rps功能)
    /sys/class/net/eth0/queues/rx-0/rps_cpus 00000000
    /sys/class/net/eth0/queues/rx-1/rps_cpus 00000000
    /sys/class/net/eth0/queues/rx-2/rps_cpus 00000000
    /sys/class/net/eth0/queues/rx-3/rps_cpus 00000000
    /sys/class/net/eth0/queues/rx-4/rps_cpus 00000000
    /sys/class/net/eth0/queues/rx-5/rps_cpus 00000000
    /sys/class/net/eth0/queues/rx-6/rps_cpus 00000000
    /sys/class/net/eth0/queues/rx-7/rps_cpus 00000000
    
    /sys/class/net/eth0/queues/rx-0/rps_flow_cnt 0
    /sys/class/net/eth0/queues/rx-1/rps_flow_cnt 0
    /sys/class/net/eth0/queues/rx-2/rps_flow_cnt 0
    /sys/class/net/eth0/queues/rx-3/rps_flow_cnt 0
    /sys/class/net/eth0/queues/rx-4/rps_flow_cnt 0
    /sys/class/net/eth0/queues/rx-5/rps_flow_cnt 0
    /sys/class/net/eth0/queues/rx-6/rps_flow_cnt 0
    /sys/class/net/eth0/queues/rx-7/rps_flow_cnt 0
    
    /proc/sys/net/core/rps_sock_flow_entries 0
  2. one cpu per queue(每队列绑定到1个CPU核上)
    /sys/class/net/eth0/queues/rx-0/rps_cpus 00000001
    /sys/class/net/eth0/queues/rx-1/rps_cpus 00000002
    /sys/class/net/eth0/queues/rx-2/rps_cpus 00000004
    /sys/class/net/eth0/queues/rx-3/rps_cpus 00000008
    /sys/class/net/eth0/queues/rx-4/rps_cpus 00000010
    /sys/class/net/eth0/queues/rx-5/rps_cpus 00000020
    /sys/class/net/eth0/queues/rx-6/rps_cpus 00000040
    /sys/class/net/eth0/queues/rx-7/rps_cpus 00000080
    
    /sys/class/net/eth0/queues/rx-0/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-1/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-2/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-3/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-4/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-5/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-6/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-7/rps_flow_cnt 4096
    
    /proc/sys/net/core/rps_sock_flow_entries 32768
  3. all cpus per queue(每队列绑定到所有cpu核上)
    /sys/class/net/eth0/queues/rx-0/rps_cpus 000000ff
    /sys/class/net/eth0/queues/rx-1/rps_cpus 000000ff
    /sys/class/net/eth0/queues/rx-2/rps_cpus 000000ff
    /sys/class/net/eth0/queues/rx-3/rps_cpus 000000ff
    /sys/class/net/eth0/queues/rx-4/rps_cpus 000000ff
    /sys/class/net/eth0/queues/rx-5/rps_cpus 000000ff
    /sys/class/net/eth0/queues/rx-6/rps_cpus 000000ff
    /sys/class/net/eth0/queues/rx-7/rps_cpus 000000ff
    
    /sys/class/net/eth0/queues/rx-0/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-1/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-2/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-3/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-4/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-5/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-6/rps_flow_cnt 4096
    /sys/class/net/eth0/queues/rx-7/rps_flow_cnt 4096
    
    /proc/sys/net/core/rps_sock_flow_entries 32768

测试方法: 每种测试类型执行3次,中间睡眠10秒, 每种测试类型分别执行100、500、1500个实例, 每实例测试时间长度为60秒

  • TCP_RR 1 byte: 测试TCP 小数据包 request/response的性能
netperf -t TCP_RR -H $serverip -c -C -l 60
  • UDP_RR 1 byte: 测试UDP 小数据包 request/response的性能
    netperf -t UDP_RR -H $serverip -c -C -l 60
    
  • TCP_RR 256 byte: 测试TCP 大数据包 request/response的性能
    netperf -t TCP_RR -H $serverip -c -C -l 60 -- -r256,256
    
  • UDP_RR 256 byte: 测试UDP 大数据包 request/response的性能
    netperf -t UDP_RR -H $serverip -c -C -l 60 -- -r256,256
    

    TPS测试结果

    • TCP_RR 1 byte小包测试结果

    • TCP_RR 256 byte大包测试结果

    • UDP_RR 1 byte小包测试结果

    • UDP_RR 256 byte大包测试结果

    CPU负载变化

    在测试过程中,使用mpstat收集各个CPU核的负载变化

    1. 关闭RPS/RFS: 可以看出关闭RPS/RFS时,软中断的负载都在cpu0上,并没有有效的利用多CPU的特性,导致了性能瓶颈
      Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
      Average:     all    3.65    0.00   35.75    0.05    0.01   14.56    0.00    0.00   45.98
      Average:       0    0.00    0.00    0.00    0.00    0.00  100.00    0.00    0.00    0.00
      Average:       1    4.43    0.00   37.76    0.00    0.11   11.49    0.00    0.00   46.20
      Average:       2    5.01    0.00   45.80    0.00    0.00    0.00    0.00    0.00   49.19
      Average:       3    5.11    0.00   45.07    0.00    0.00    0.00    0.00    0.00   49.82
      Average:       4    3.52    0.00   40.38    0.14    0.00    0.00    0.00    0.00   55.96
      Average:       5    3.85    0.00   39.91    0.00    0.00    0.00    0.00    0.00   56.24
      Average:       6    3.62    0.00   40.48    0.14    0.00    0.00    0.00    0.00   55.76
      Average:       7    3.87    0.00   38.86    0.11    0.00    0.00    0.00    0.00   57.16
    2. 每队列关联到一个CPU TCP_RR: 可以看出软中断负载已经能分散到各个CPU核上,有效利用了多CPU的特性,大大提高了系统的网络性能
      Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
      Average:     all    5.58    0.00   59.84    0.01    0.00   22.71    0.00    0.00   11.86
      Average:       0    2.16    0.00   20.85    0.00    0.04   72.03    0.00    0.00    4.93
      Average:       1    4.68    0.00   46.27    0.00    0.00   42.73    0.00    0.00    6.32
      Average:       2    6.76    0.00   63.79    0.00    0.00   11.03    0.00    0.00   18.42
      Average:       3    6.61    0.00   65.71    0.00    0.00   11.51    0.00    0.00   16.17
      Average:       4    5.94    0.00   67.83    0.07    0.00   11.59    0.00    0.00   14.58
      Average:       5    5.99    0.00   69.42    0.04    0.00   12.54    0.00    0.00   12.01
      Average:       6    5.94    0.00   69.41    0.00    0.00   12.86    0.00    0.00   11.78
      Average:       7    6.13    0.00   69.61    0.00    0.00   14.48    0.00    0.00    9.77
    3. 每队列关联到一个CPU UDP_RR: CPU负载未能均衡的分布到各个CPU, 这是由于网卡hash计算在UDP包上的不足, 详细请见本文后记部分
      Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
      Average:     all    3.01    0.00   29.84    0.07    0.01   13.35    0.00    0.00   53.71
      Average:       0    0.00    0.00    0.08    0.00    0.00   90.01    0.00    0.00    9.91
      Average:       1    3.82    0.00   32.87    0.00    0.05   12.81    0.00    0.00   50.46
      Average:       2    4.84    0.00   37.53    0.00    0.00    0.14    0.00    0.00   57.49
      Average:       3    4.90    0.00   37.92    0.00    0.00    0.16    0.00    0.00   57.02
      Average:       4    2.57    0.00   32.72    0.20    0.00    0.09    0.00    0.00   64.42
      Average:       5    2.66    0.00   33.54    0.11    0.00    0.08    0.00    0.00   63.60
      Average:       6    2.75    0.00   32.81    0.09    0.00    0.06    0.00    0.00   64.30
      Average:       7    2.71    0.00   32.66    0.17    0.00    0.06    0.00    0.00   64.40
    4. 每队列关联到所有CPU: 可以看出软中断负载已经能分散到各个CPU核上,有效利用了多CPU的特性,大大提高了系统的网络性能
      Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
      Average:     all    5.39    0.00   59.97    0.00    0.00   22.57    0.00    0.00   12.06
      Average:       0    1.46    0.00   21.83    0.04    0.00   72.08    0.00    0.00    4.59
      Average:       1    4.45    0.00   46.40    0.00    0.04   43.39    0.00    0.00    5.72
      Average:       2    6.84    0.00   65.62    0.00    0.00   11.39    0.00    0.00   16.15
      Average:       3    6.71    0.00   67.13    0.00    0.00   12.07    0.00    0.00   14.09
      Average:       4    5.73    0.00   66.97    0.00    0.00   10.71    0.00    0.00   16.58
      Average:       5    5.74    0.00   68.57    0.00    0.00   13.02    0.00    0.00   12.67
      Average:       6    5.79    0.00   69.27    0.00    0.00   12.31    0.00    0.00   12.63
      Average:       7    5.96    0.00   68.98    0.00    0.00   12.00    0.00    0.00   13.06

    结果分析

    以下结果只是针对测试服务器特定硬件及系统的数据,在不同测试对象的RPS/RFS测试结果可能有不同的表现

    TCP性能:

    • 在没有打开RPS/RFS的情况下,随着进程数的增加,TCP tps性能并明显没有提升,在184~188k之间。
    • 打开RPS/RFS之后,随着RPS导致软中断被分配到所有CPU上和RFS增加的cache命中, 小数据包(1字节)及大数据包(256字节,相对小数据包而言, 而不是实际应用中的大数据包)的tps性能都有显著提升
    • 100个进程提升40%的性能(两种RPS/RFS设置的性能结果一致), cpu负载升高40%
    • 500个进程提升70%的性能(两种RPS/RFS设置的性能结果一致), cpu负载升高62%
    • 1500个进程提升75%的性能(两种RPS/RFS设置的性能结果一致), cpu负载升高77%

    UDP性能:

    • 在没有打开RPS/RFS的情况下,随着进程数的增加,UDP tps性能并明显没有提升,在226~235k之间。
    • 打开RPS/RFS之后,,随着RPS导致软中断被分配到所有CPU上和RFS增加的cache命中, 小数据包(1字节)及大数据包(256字节,相对小数据包而言, 而不是实际应用中的大数据包)的TPS性能, 在每队列关联到所有CPU的情况下有显著提升, 而每队列关联到一个CPU后反倒是导致了UDP tps性能下降1% (这是bnx2网卡不支持UDP port hash及此次测试的局限性造成的结果, 详细分析见: 后记)
    • 每队列关联到所有CPU的情况下, 在100个进程时小包提升40%的性能, cpu负载升高60%; 大包提升33%, cpu负载升高47%
    • 每队列关联到所有CPU的情况下, 在500个进程提小包提升62%的性能, cpu负载升高71%; 大包提升60%, cpu负载升高65%
    • 每队列关联到所有CPU的情况下, 在1500个进程提升65%的性能, cpu负载升高75%; 大包提升64%, cpu负载升高74%

    后记

    UDP在每队列绑定到一个CPU时性能下降,而绑定到所有CPU时,却有性能提升,这一问题涉及到几个因素,当这几个因素凑一起时,导致了这种奇特的表现。

    • 此次测试的局限性:本次测试是1对1的网络测试,产生的数据包的IP地址都是相同的
    • bnx2网卡在RSS hash上,不支持UDP Port,也就是说,网卡在对TCP数据流进行队列选择时的hash包含了ip和port, 而在UDP上的hash, 只有IP地址,导致了本次测试(上面的局限性影响)的UDP数据包的hash结果都是一样的,数据包被转送到同一条队列。
    • 单单上面两个因素,还无法表现出UDP在每队列绑定到一个CPU时性能下降,而绑定到所有CPU时,却有性能提升的现象。 因为RPS/RFS本身也有hash计算,也就是进入队列后的数据包,还需要经过RPS/RFS的hash计算(这里的hash支持udp port), 然后进行第二次数据包转送选择;如果每队列绑定到一个CPU, 系统直接跳过第二次hash计算,数据包直接分配到该队列关联的CPU处理,也就导致了在第一次hash计算后被错误转送到某一队列的UDP数据包,将直接送到cpu处理,导致了性能的下降; 而如果是每队列绑定到所有CPU, 那么进入队列后的数据包会在第二次hash时被重新分配,修正了第一次hash的错误选择。

    相关对比测试

    1. SMP IRQ affinity: http://www.igigo.net/archives/231

    参考资料

    • Software receive packet steering
    • Receive Packet Steering
    • Receive packet steering
    • Receive Flow Steering
    • linux kernel 2.6.35中RFS特性详解
    • Linux 2.6.35 新增特性 RPS RFS
    • kernel/Documentation/networking/scaling.txt

    分页:

  • This entry was posted in  默认分类 and tagged  Debian,  Linux Kernel,  linux内核,  内核,  大数据,  工程师,  应用程序,  服务器,  测试,  网络,  负载均衡 on  2013/09/09.

    一些节约时间的Linux小贴士

    As a programmer or system administrator, we have more chances of working on *nix platforms. It’s tough experience when first start use 8nix as we need to face a black screen without knowing what’s behind it. Now, if we can have some resources to rely on, then we will find the beautify of *nix. They are fast, efficient and most importantly sexy. Below are some great tips for helping Linux users get used to Linux.

    This list is a bit long. So be patient. To get more information on a command mentioned, first try “man “. The best way to learn something is to read the specification first then practice on it. This list is summarized by Joshua Levy,

    Basics

    • Learn basic Bash. Actually, read the whole bash man page; it’s pretty easy to follow and not that long. Alternate shells can be nice, but bash is powerful and always available (learning mainly zsh or tcsh restricts you in many situations).
    • Learn vim. There’s really no competition for random Linux editing (even if you use Emacs or Eclipse most of the time).
    • Know ssh, and the basics of passwordless authentication, via ssh-agent, ssh-add, etc.
    • Be familiar with bash job management: &, Ctrl-Z, Ctrl-C, jobs, fg, bg, kill, etc.
    • Basic file management: ls and ls -l (in particular, learn what every column in “ls -l” means), less, head, tail and tail -f, ln and ln -s (learn the differences and advantages of hard versus soft links), chown, chmod, du (for a quick summary of disk usage: du -sk *), df, mount.
    • Basic network management: ip or ifconfig, dig.
    • Know regular expressions well, and the various flags to grep/egrep. The -o, -A, and -B options are worth knowing.
    • Learn to use apt-get or yum (depending on distro) to find and install packages.

    Everyday use

    • In bash, use Ctrl-R to search through command history.
    • In bash, use Ctrl-W to kill the last word, and Ctrl-U to kill the line. See man readline for default keybindings in bash. There are a lot. For example Alt-. cycles through prevous arguments, and Alt-* expands a glob.
    • To go back to the previous working directory: cd -
    • Use xargs (or parallel). It’s very powerful. Note you can control how many items execute per line (-L) as well as parallelism (-P). If you’re not sure if it’ll do the right thing, use xargs echo first. Also, -I{} is handy. Examples:

    find . -name \*.py | xargs grep some_function

    cat hosts | xargs -I{} ssh root@{} hostname

    • pstree -p is a helpful display of the process tree.
    • Use pgrep and pkill to find or signal processes by name (-f is helpful).
    • Know the various signals you can send processes. For example, to suspend a process, use kill -STOP [pid].  For the full list, see man 7 signal
    • Use nohup or disown if you want a background process to keep running forever.
    • Check what processes are listening via netstat -lntp. See also lsof.
    • In bash scripts, use set -x for debugging output. Use set -e to abort on errors. Consider using set -o pipefail as well, to be strict about errors (though this topic is a bit subtle). For more involved scripts, also use trap.
    • In bash scripts, subshells (written with parentheses) are convenient ways to group commands. A common example is to temporarily move to a different working directory, e.g.

    # do something in current dir

    (cd /some/other/dir; other-command)

    # continue in original dir

    • In bash, note there are lots of kinds of variable expansion. Checking a variable exists: ${name:?error message}. For example, if a bash script requires a single argument, just write input_file=${1:?usage: $0 input_file}. Arithmetic expansion: i=$(( (i + 1) % 5 )). Sequences: {1..10}. Trimming of strings: ${var%suffix} and ${var#prefix}. For example if var=foo.pdf, then echo ${var%.pdf}.txt prints “foo.txt”.
    • The output of a command can be treated like a file via <(some command). For example, compare local /etc/hosts with a remote one: diff /etc/hosts <(ssh somehost cat /etc/hosts)
    • Know about “here documents” in bash, as in cat <
    • In bash, redirect both standard output and standard error via: some-command >logfile 2>&1. Often, to ensure a command does not leave an open file handle to standard input, tying it to the terminal you are in, it is also good practice to add “
    • Use man ascii for a good ASCII table, with hex and decimal values.
    • On remote ssh sessions, use screen or dtach to save your session, in case it is interrupted.
    • For web debugging, curl and curl -I are handy, and/or their wget equivalents.
    • To convert HTML to text: lynx -dump -stdin
    • If you must handle XML, xmlstarlet is good.
    • For Amazon S3, s3cmd is convenient (albeit immature, with occasional misfeatures).
    • In ssh, knowing how to port tunnel with -L or -D (and occasionally -R) is useful, e.g. to access web sites from a remote server.
    • It can be useful to make a few optimizations to your ssh configuration; for example, this .ssh/config contains settings to avoid dropped connections in certain network environments, not require confirmation connecting to new hosts, forward authentication, and use compression (which is helpful with scp over low-bandwidth connections):

    TCPKeepAlive=yes

    ServerAliveInterval=15

    ServerAliveCountMax=6

    StrictHostKeyChecking=no

    Compression=yes

    ForwardAgent=yes

    • If you are halfway through typing a command but change your mind, hit Alt-# to add a # at the beginning and enter it as a comment (or use Ctrl-A, #, enter). You can then return to it later via command history.

    Data processing

    • Know about sort and uniq (including uniq’s -u and -d options).
    • Know about cut, paste, and join to manipulate text files. Many people use cut but forget about join.
    • It is remarkably helpful sometimes that you can do set intersection, union, and difference of text files via sort/uniq. Suppose a and b are text files that are already uniqued. This is fast, and works on files of arbitrary size, up to many gigabytes. (Sort is not limited by memory, though you may need to use the -T option if /tmp is on a small root partition.)

    cat a b | sort | uniq > c   # c is a union b

    cat a b | sort | uniq -d > c   # c is a intersect b

    cat a b b | sort | uniq -u > c   # c is set difference a – b

    • Know that locale affects a lot of command line tools, including sorting order and performance. Most Linux installations will set LANG or other locale variables to a local setting like US English. This can make sort or other commands run many times slower. (Note that even if you use UTF-8 text, you can safely sort by ASCII order for many purposes.) To disable slow i18n routines and use traditional byte-based sort order, use export LC_ALL=C (in fact, consider putting this in your .bashrc).
    • Know basic awk and sed for simple data munging. For example, summing all numbers in the third column of a text file: awk ‘{ x += $3 } END { print x }’. This is probably 3X faster and 3X shorter than equivalent Python.
    • To replace all occurrences of a string in place, in files:

    perl -pi.bak -e ‘s/old-string/new-string/g’ my-files-*.txt

    • Use shuf to shuffle or select random lines from a file.
    • Know sort’s options. Know how keys work (-t and -k). In particular, watch out that you need to write -k1,1 to sort by only the first field; -k1 means sort according to the whole line.
    • Stable sort (sort -s) can be useful. For example, to sort first by field 2, then secondarily by field 1, you can use sort -k1,1 | sort -s -k2,2
    • If you ever need to write a tab literal in a command line in bash (e.g. for the -t argument to sort), press Ctrl-V or write $’\t’ (the latter is better as you can copy/paste it).
    • For binary files, use hd for simple hex dumps and bvi for binary editing.
    • Also for binary files, strings (plus grep, etc.) lets you find bits of text.
    • To convert text encodings, try iconv. Or uconv for more advanced use; it supports some advanced Unicode things. For example, this command lowercases and removes all accents (by expanding and dropping them):

    uconv -f utf-8 -t utf-8 -x ‘::Any-Lower; ::Any-NFD; [:Nonspacing Mark:] >; ::Any-NFC; ‘ < input.txt > output.txt

    • To split files into pieces, see split (to split by size) and csplit (to split by a pattern).

    System debugging

    • To know disk/cpu/network status, use iostat, netstat, top (or the better htop), and (especially) dstat. Good for getting a quick idea of what’s happening on a system.
    • To know memory status, run and understand the output of free and vmstat. In particular, be aware the “cached” value is memory held by the Linux kernel as file cache, so effectively counts toward the “free” value.
    • Java system debugging is a different kettle of fish, but a simple trick on Sun’s and some other JVMs is that you can run kill -3 and a full stack trace and heap summary (including generational garbage collection details, which can be highly informative) will be dumped to stderr/logs.
    • Use mtr as a better traceroute, to identify network issues.
    • For looking at why a disk is full, ncdu saves time over the usual commands like “du -sk *”.
    • To find which socket or process is using bandwidth, try iftop or nethogs.
    • The ab tool (comes with Apache) is helpful for quick-and-dirty checking of web server performance. For more complex load testing, try siege.
    • For more serious network debugging, wireshark or tshark.
    • Know strace and ltrace. These can be helpful if a program is failing, hanging, or crashing, and you don’t know why, or if you want to get a general idea of performance. Note the profiling option (-c), and the ability to attach to a running process (-p).
    • Know about ldd to check shared libraries etc.
    • Know how to connect to a running process with gdb and get its stack traces.
    • Use /proc. It’s amazingly helpful sometimes when debugging live problems. Examples: /proc/cpuinfo, /proc/xxx/cwd, /proc/xxx/exe, /proc/xxx/fd/, /proc/xxx/smaps.
    • When debugging why something went wrong in the past, sar can be very helpful. It shows historic statistics on CPU, memory, network, etc.
    • For deeper systems and performance analyses, look at stap (systemtap) and perf.
    • Use dmesg whenever something’s acting really funny (it could be hardware or driver issues).
    This entry was posted in  默认分类 and tagged  amazon s3,  Linux,  Linux Kernel,  parallel,  performance, Processing,  profiling,  random,  statistics,  web server,  wireshark on  2013/09/04.

    Linux内核协议栈对于timewait状态的处理

    最近在做操作系统升级时,发现升级后的系统处于TIME_WAIT状态的连接数明显增多(内核版本 2.6.18 -> 2.6.32)。

    原因

    2.6.18 与 2.6.32 的 diff 结果

    net/ipv4/inet_timewait_sock.c

    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
         @ @   - 178 , 15   + 212 , 14   @ @
           need_timer   =   0 ;
           if   ( inet_twdr_do_twkill_work ( twdr ,   twdr -> slot ) )   {
             twdr -> thread_slots   |=   ( 1   <<   twdr -> slot ) ;
         -     mb ( ) ;
             schedule_work ( & twdr -> twkill_work ) ;
             need_timer   =   1 ;
           }   else   {
             /* We purged the entire slot, anything left?  */   
             if   ( twdr -> tw_count )
               need_timer   =   1 ;
             //这句话位置的变动引起TIME_WAIT状态增多
         +     twdr -> slot   =   ( ( twdr -> slot   +   1 )   &   ( INET_TWDR_TWKILL_SLOTS   -   1 ) ) ;
           }   
         -   twdr -> slot   =   ( ( twdr -> slot   +   1 )   &   ( INET_TWDR_TWKILL_SLOTS   -   1 ) ) ;
           if   ( need_timer )
             mod_timer ( & twdr -> tw_timer ,   jiffies   +   twdr -> period ) ;
         out :
     

    导致TIME_WAIT状态增多,正是由于

    twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));

    位置的改变。

    具体分析

    1. 关键数据结构

    inet_timewait_death_row: 用于管理timewait控制块的数据结构,位置:include/net/inet_timewait_sock.h

    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    struct   inet_timewait_death_row   {
         /* Short-time timewait calendar */
         int           twcal_hand ;
         int           twcal_jiffie ;
         struct   timer_list    twcal_timer ;
         struct   hlist_head    twcal_row [ INET_TWDR_RECYCLE_SLOTS ] ;
     
         spinlock_t       death_lock ;
         int           tw_count ;
         int           period ;
         u32          thread_slots ;
         struct   work_struct   twkill_work ;
         struct   timer_list    tw_timer ;
         int           slot ;
         //INET_TWDR_TWKILL_SLOTS 值为 8
         struct   hlist_head    cells [ INET_TWDR_TWKILL_SLOTS ] ;
         struct   inet_hashinfo      * hashinfo ;
         int           sysctl_tw_recycle ;
         int           sysctl_max_tw_buckets ;
    } ;
     

    此数据结构,可以分为两部分看,一部分处理 tw_recycle 开启时timewait块的快速回收,另一部分为未开启时用于等待时间较长的timewait块的回收。由于系统没有开启 tw_recycle , 因此我们主要关注等待时间较长的timewait块回收。

    用于等待时间较长的主要成员变量:

    int period : tw_timer 定时器的超时时间固定值为 TCP_TIMEWAIT_LEN / INET_TWDR_TWKILL_SLOTS,其中 TCP_TIMEWAIT_LEN 为 60 * HZ (60s),INET_TWDR_TWKILL_SLOTS 为 8。

    u32 thread_slots : 用于标识未完成的timewait块的位图。

    struct work_struct twkill_work : 分批删除(默认值为每次删除100个)cells中timewait块时的工作队列。

    struct timer_list tw_timer : 定时器,每过 period,触发一次 inet_twdr_hangman()

    以下是此数据结构的初始化:

    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct   inet_timewait_death_row  tcp_death_row   =   {
         . sysctl_max_tw_buckets   =   NR_FILE   *   2 ,
         . period     =   TCP_TIMEWAIT_LEN   /   INET_TWDR_TWKILL_SLOTS ,
         . death_lock   =   __SPIN_LOCK_UNLOCKED ( tcp_death_row . death_lock ) ,
         . hashinfo   =   & tcp_hashinfo ,
         . tw_timer   =   TIMER_INITIALIZER ( inet_twdr_hangman ,   0 ,
                     ( unsigned   long ) & tcp_death_row ) ,
         . twkill_work   =   __WORK_INITIALIZER ( tcp_death_row . twkill_work ,
                      inet_twdr_twkill_work ,
                      & tcp_death_row ) ,
         /* Short-time timewait calendar */
     
         . twcal_hand   =   - 1 ,
         . twcal_timer   =   TIMER_INITIALIZER ( inet_twdr_twcal_tick ,   0 ,
                     ( unsigned   long ) & tcp_death_row ) ,
    } ;
     

    inet_timewait_sock: 用于组成 tcp_timewait_sock 结构,其前部是 sock_common 的前部。位置: include/net/inet_timewait_sock.h

    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    struct   inet_timewait_sock   {
         /*  
         * Now struct sock also uses sock_common, so please just
         * don’t add nothing before this first member (__tw_common) –acme
         */
         struct   sock_common   __tw_common ;
    #define tw_family   __tw_common.skc_family
    #define tw_state    __tw_common.skc_state
    #define tw_reuse    __tw_common.skc_reuse
    #define tw_bound_dev_if   __tw_common.skc_bound_dev_if
    #define tw_node     __tw_common.skc_node
    #define tw_bind_node    __tw_common.skc_bind_node
    #define tw_refcnt   __tw_common.skc_refcnt
    #define tw_hash     __tw_common.skc_hash
    #define tw_prot     __tw_common.skc_prot
         volatile   unsigned   char    tw_substate ;
         /* 3 bits hole, try to pack */
         unsigned   char     tw_rcv_wscale ;
         /* Socket demultiplex comparisons on incoming packets. */
         /* these five are in inet_sock */
         __u16      tw_sport ;
         __u32      tw_daddr  __attribute__ ( ( aligned ( INET_TIMEWAIT_ADDRCMP_ALIGN_BYTES ) ) ) ;
         __u32      tw_rcv_saddr ;
         __u16      tw_dport ;
         __u16      tw_num ;
         /* And these are ours. */
         __u8       tw_ipv6only : 1 ;
         /* 15 bits hole, try to pack */
         __u16      tw_ipv6_offset ;
         int       tw_timeout ;
         unsigned   long     tw_ttd ;
         struct   inet_bind_bucket   * tw_tb ;
         struct   hlist_node  tw_death_node ;
    } ;
     

    此数据结构暂时只需要知道 tw_substate 即可。 tw_substate : TCP状态迁移到 FIN_WAIT2 或TIME_WAIT 状态时,协议栈会用 timewait 块取代 tcp_sock 块,因为这两种状态都需要由定时器处理,超时立即释放。其对外状态都表现为 TIME_WAIT , 但其内部状态还是有分别,通过 tw_substate进行区分。


    2. timewait块释放时的逻辑

    inet_twdr_hangman() 此函数是定时器到期时执行的函数,用于释放timewait块。

    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    void   inet_twdr_hangman ( unsigned   long   data )
    {
         struct   inet_timewait_death_row   * twdr ;
         int   unsigned   need_timer ;
     
         twdr   =   ( struct   inet_timewait_death_row   * ) data ;
         spin_lock ( & twdr -> death_lock ) ;
     
         if   ( twdr -> tw_count   ==   0 )
             goto   out ;
     
         need_timer   =   0 ;
     
         //inet_twdr_do_twkill_work 释放timewait块的具体函数,每次释放100个
         //释放完成返回0, 否则返回1
         if   ( inet_twdr_do_twkill_work ( twdr ,   twdr -> slot ) )   {
             //一次遍历未完全删除timewait块时,剩余的time块放入twkill_work的工作队列中处理。
             //thread_slots标识未完成的timewait块
             twdr -> thread_slots   |=   ( 1   <<   twdr -> slot ) ;
             mb ( ) ;
             schedule_work ( & twdr -> twkill_work ) ;
             //未删除所有timewait块,需要重新调度定时器
             need_timer   =   1 ;
         }   else   {
           /* We purged the entire slot, anything left?  */
           if   ( twdr -> tw_count )
               need_timer   =   1 ;
         }
     
         //此句是关键,代码出自2.6.18内核,不管定时器例程一次有没有释放完timewait块,都进行 + 1 操作
         twdr -> slot   =   ( ( twdr -> slot   +   1 )   &   ( INET_TWDR_TWKILL_SLOTS   -   1 ) ) ;
         if   ( need_timer )
             mod_timer ( & twdr -> tw_timer ,   jiffies   +   twdr -> period ) ;
       out :
           spin_unlock ( & twdr -> death_lock ) ;
    }
     

    用于慢timewait块释放的逻辑可参考下图:


    3. 结论

    按照 2.6.18 中的逻辑,如果一次没有全部删除一个slot中的timewait控制块, twdr->slot 仍然会执行+ 1 操作。此时如果有一个tcp_sock进入FIN_WAIT2状态,则此时的timewait(tw_substate = fin_wait2)块会被放在上一个slot中,而此时有一个线程正在处理那个队列,因此会导致处于FIN_WAIT2状态的timewait块被提前释放,若此时对端的FIN分节到达,协议栈会回复一个RST分节。

    为了修复此BUG,2.6.32 协议栈中修改了 twdr->slot + 1 的时机,每次必须完全释放一个slot中所有的timewait块后,才会进行 + 1 操作。这也就是说协议栈不保证在 TCP_TWKILL_PERIOD 周期内,移动一个格子,所以当系统繁忙时,会导致timewait块的等待时间大于 TCP_TIMEWAIT_LEN

    您可能感兴趣的文章

    • 望闻问切诊断问题
    • Linux常用监控命令介绍
    • FUSE源码剖析
    This entry was posted in  默认分类 and tagged  Linux Kernel,  struct,  TCP/IP,  数据结构,  栈 on  2013/08/18.

    为学Linux,我看了这些书

    去年开始,抱着学习的态度开始了我的Linux学习,到现在,差不多一年了,收获很多,不敢说精通Linux,但是,还是对得起“略懂”这两个字的。这一年里我看了很多书,细细数下,大概15本左右,其中包含了两个方面,一个是Android,另一个就是Linux。当然,在学习之初,遇到了不少瓶颈,在这里,还要感谢师兄师姐的指导。写这篇文章的目的有两个:

    1.分享自己的学习经验

    2.记录下我的学习历程

    既然谈到了学习,那一定需要从看书说起,下面我就罗列下我看过的关于Linux学习的书

     

    1.《鸟哥的Linux私房菜-基础学习篇》

    Docker 介绍: 相关技术(LXC)_第10张图片

     

          首先声明,我用的Linux系统是Ubuntu11.10;大学时没接触过Linux系统,所以不知道怎么开始学习是好,请教吧,师兄告诉我看这本书,现在回想起来,他的建议不错,这本书很适合初学Linux系统的人。

          我们大概都知道,或许是听说过,Linux系统经典的地方是它的命令行操作方式,这本书的主要内容就是介绍Linux系统下的命令,教你怎么使用这个系统,并简单的管理这个系统,这本书我记得后面几章的内容是关于系统运维的,由于对这方面的管理,我还暂时不需要,所以最后几章的内容并没有涉猎。

     

    2.《Linux Shell 脚本攻略》

    Docker 介绍: 相关技术(LXC)_第11张图片

     

          这本书虽然放在第二的位置,但是确实是最近才看完的,读这本书的缘由是项目的需要,不过,现在,对这本书我又有了新的想法。

          Linux的思想是“一切皆文件”,所以,对于文件的处理是Linux最基本的功能,那么使用什么处理文件呢?使用什么效率高呢? 答案是:Shell脚本。所谓Shell脚本其实就是Linux命令的集合。这本书正是讲解这种语言的,但是内容不是很简单,需要知道一些简单的命令和参数是做什么的,所以,读这本书之前最好还是看看第一本书吧。

          我们大概都已经知道,Linux对网络的支持也是非常好的,它在内核中接入网络,所以我们需要知道和运用一些Linux系统提供给我们的命令实现网络的测试,运行状况查看等操作,这本书又派上用场了,我记得这本书有一章的内容叫做“无网不利”,听,这是对Linux系统多么贴切的描述。

     

    3.《Unix环境高级编程》

    Docker 介绍: 相关技术(LXC)_第12张图片

     

          相信知道这本书的人很多,这是讲解Unix编程的经典书籍,由于Linux属于类Unix系统,所以,学习Linux编程,这本书不可以少。

          这本书的开篇首先讲的是对文本文件的操作,对了,就是那几个我们常常看见的函数–open,read,write,seek,close,这里,是不是又对应了“一切皆文件”这个说法。当然,这本书介绍的内容很多,其中有很多非常好的章节,比如:Linux进程间通信,Socket编程,Linux管道等等,这里,我就不详细说了,想学习的朋友自己去看看这本书吧。

     

    4.《Linux系统编程》

    Docker 介绍: 相关技术(LXC)_第13张图片

     

          首先说,这本书不是很好找,是一本比较老的书,但是内容却是非常好,也是学习Linux编程不可多得的一本好书。

          从这本书的名字就能看出来,这本书所讲的内容是“系统编程”,所以,这里涉及的内容是系统调用,是我们的用户进程需要内核的帮助完成我们想要实现的功能,这本书我也是从头看到尾,由于书并不是很厚,所以没有花太久的时间,不过,需要理解的地方很多,有些地方比较难,所以,多看几遍这本书肯定没错。

     

    5.《Linux内核设计的艺术》

    Docker 介绍: 相关技术(LXC)_第14张图片

     

          这里,我首先引用这本书封面上的一句话–“本书对操作系统内核的驾驭能力和深刻理解程度达到世界顶尖级水平,是一本能真正引导你深入理解Linux内核设计思想的经典著作。”

          在学习Linux之初,我们可能会“百度一下”–怎么学习Linux?网上好多人会回答你–看源代码。如果这时你真的照着做了,那么,你可能走上了一条学习Linux的不归路。为什么呢?因为,Linux有好多个版本,我们看哪个版本?源代码都在几百万行左右,怎么去看?所以,这是不切实际的。那么,为什么会有那么多的网友这样说呢?下面我来分析下。

          这本书到现在我看完了两遍,当然,还是有很多地方理解的不够透彻,不过,还是有一些自己的想法的。这本书选择的版本是Linux0.11,书中配合插图和文字去分析源代码,起初,我只是看书上的“内容”,并没有分析源代码,不过,渐渐地,我发现一个问题,合上书之后,马上就忘了我看了些什么。开始,我觉得这是对系统的不理解,对系统不熟。就这样,带着这些疑问,看完了第一遍。大概两个星期之后我开始了第二遍的阅读,这一次,我配合源代码去阅读这本书,再合上书之后,我非常欣慰,我记得那些东西是什么,现在,我才明白为什么。你想理解这个东西,想记得牢,就去分析源代码吧。

          现在,可以回答那个问题了。其实,大家所说的阅读源代码并不是让你从头看源代码,我们应该做的是,想知道哪一部分是做什么的,是怎么实现的,为什么会是这样,我们需要做的就是把这一部分的源代码找出来,阅读,分析。

     

    6.《Linux内核设计与实现》

    Docker 介绍: 相关技术(LXC)_第15张图片

     

    这本书所讲的内容也是分析Linux内核的,内容比较深,博主现在对这本书的理解也是比较浅显,so,还需努力。

        这本书中前面几章的内容很像我们的《操作系统》教科书,不过,内容要深入的多,我记得这本书对进程调度算法的讲解非常好,非常深入,以至于第一遍阅读时我没有看懂,映像比较深的还有就是进程间的协调工作,就是IPC机制的实现,这里 ,我们知道通信的媒介是管道,可是,管道是什么?它是一种文件,没错,但是它是用什么实现的?这本书中也有非常细致的讲解。所以,要想更深入的理解Linux系统,这本书还是阅读下吧。

     

        这篇文章写了好长时间了,不过,并没有完全写下我的学习历程,因为,还有很长的路要走。。。

    This entry was posted in  默认分类 and tagged  Linux,  Linux Kernel,  源代码,  编程,  网络 on  2013/08/08.

    【推荐阅读】Linux内核的内存屏障

    原文链接 作者:David Howells、Paul E. McKenney 译者:曹姚君 校对:丁一

    内容:

    1. 抽象的内存访问模型
    • 设备操作
    • 保障
  • 什么是内存屏障?
    • 内存屏障的种类
    • 什么是内存屏障不能确保的?
    • 数据依赖屏障
    • 控制依赖
    • SMP屏障配对
    • 内存屏障顺序的例子
    • read内存屏障与load预取
    • 传递性
  • 显式内核屏障
    • 编译屏障
    • CPU内存屏障
    • MMIO write屏障
  • 隐式内核内存屏障
    • 锁功能
    • 中断禁用功能
    • 休眠和唤醒功能
    • 其它函数
  • CPU之间的锁屏障效应
    • 锁与内存访问
    • 锁与I/O访问
  • 什么地方需要内存障碍?
    • 多处理器间的交互
    • 原子操作
    • 设备访问
    • 中断
  • 内核的I/O屏障效应
  • 假想的最小执行顺序模型
  • CPU缓存的影响
    • 缓存一致性
    • 缓存一致性与DMA
    • 缓存一致性与MMIO
  • CPU能做到的
    • Alpha处理器
  • 使用示例
    • 循环缓冲区
  • 引用

    抽象的内存访问模型

    考虑下面这个系统的抽象模型:

    		            :                :
    		            :                :
    		            :                :
    		+-------+   :   +--------+   :   +-------+
    		|       |   :   |        |   :   |       |
    		|       |   :   |        |   :   |       |
    		| CPU 1 |<----->| Memory |<----->| CPU 2 |
    		|       |   :   |        |   :   |       |
    		|       |   :   |        |   :   |       |
    		+-------+   :   +--------+   :   +-------+
    		    ^       :       ^        :       ^
    		    |       :       |        :       |
    		    |       :       |        :       |
    		    |       :       v        :       |
    		    |       :   +--------+   :       |
    		    |       :   |        |   :       |
    		    |       :   |        |   :       |
    		    +---------->| Device |<----------+
    		            :   |        |   :
    		            :   |        |   :
    		            :   +--------+   :
    		            :                :
    

    每个CPU执行一个有内存访问操作的程序。在这个抽象的CPU中,内存操作的顺序是非常宽松的。假若能让程序的因果关系看起来是保持着的,CPU就可以以任意它喜欢的顺序执行内存操作。同样,只要不影响程序的结果,编译器可以以它喜欢的任何顺序安排指令。

    因此,上图中,一个CPU执行内存操作的结果能被系统的其它部分感知到,因为这些操作穿过了CPU与系统其它部分之间的接口(虚线)。

    例如,请考虑以下的事件序列:

    	CPU 1		CPU 2
    	===============	===============
    	{ A == 1; B == 2 }
    	A = 3;		x = A;
    	B = 4;		y = B;
    

    内存系统能看见的访问顺序可能有24种不同的组合:

    	STORE A=3,	STORE B=4,	x=LOAD A->3,	y=LOAD B->4
    	STORE A=3,	STORE B=4,	y=LOAD B->4,	x=LOAD A->3
    	STORE A=3,	x=LOAD A->3,	STORE B=4,	y=LOAD B->4
    	STORE A=3,	x=LOAD A->3,	y=LOAD B->2,	STORE B=4
    	STORE A=3,	y=LOAD B->2,	STORE B=4,	x=LOAD A->3
    	STORE A=3,	y=LOAD B->2,	x=LOAD A->3,	STORE B=4
    	STORE B=4,	STORE A=3,	x=LOAD A->3,	y=LOAD B->4
    	STORE B=4, ...
    	...
    

    因此,可能产生四种不同的值组合:

    	x == 1, y == 2
    	x == 1, y == 4
    	x == 3, y == 2
    	x == 3, y == 4
    

    此外,一个CPU 提交store指令到存储系统,另一个CPU执行load指令时感知到的这些store的顺序可能并不是第一个CPU提交的顺序。

    另一个例子,考虑下面的事件序列:

              CPU 1		CPU 2
    	===============	===============
    	{ A == 1, B == 2, C = 3, P == &A, Q == &C }
    	B = 4;		Q = P;
    	P = &B		D = *Q;
    

    这里有一个明显的数据依赖,D的值取决于CPU 2从P取得的地址。执行结束时,下面任一结果都是有可能的;

    	(Q == &A) and (D == 1)
    	(Q == &B) and (D == 2)
    	(Q == &B) and (D == 4)
    

    注意:CPU 2永远不会将C的值赋给D,因为CPU在对*Q发出load指令之前会先将P赋给Q。

    硬件操作

    一些硬件的控制接口,是一组存储单元,但这些控制寄存器的访问顺序是非常重要的。例如,考虑拥有一系列内部寄存器的以太网卡,它通过一个地址端口寄存器(A)和一个数据端口寄存器(D)访问。现在要读取编号为5的内部寄存器,可能要使用下列代码:

    	*A = 5;
    	x = *D;
    

    但上面代码可能表现出下列两种顺序:

    	STORE *A = 5, x = LOAD *D
     	x = LOAD *D, STORE *A = 5
    

    其中第二个几乎肯定会导致故障,因为它在读取寄存器之后才设置地址值。

    保障

    下面是CPU必须要保证的最小集合:

    • 任意CPU,有依赖的内存访问指令必须按顺序发出。这意味着对于

      			Q = P; D = *Q;
      		

      CPU会发出下列内存操作:

      			Q = LOAD P, D = LOAD *Q
      		

      并且总是以这种顺序。

    • 在一个特定的CPU中,重叠的load和store指令在该CPU中将会看起来是有序的。这意味着对于:
      		a = *X; *X = b;
      		

      CPU发出的内存操只会是下面的顺序:

      		a = LOAD *X, STORE *X = b
      		

      对于:

      		*X = c; d = *X;
      		

      CPU只会发出:

      		STORE *X = c, d = LOAD *X
      		

      (如果load和store指令的目标内存块有重叠,则称load和store重叠了。)。

    还有一些必须要一定不能假设的东西:

    • 一定不能假设无关联的load和store指令会按给定的顺序发出,这意味着对于:

      	X = *A; Y = *B; *D = Z;
      	

      我们可能得到下面的序列之一:

      	X = LOAD *A,  Y = LOAD *B,  STORE *D = Z
      	X = LOAD *A,  STORE *D = Z, Y = LOAD *B
      	Y = LOAD *B,  X = LOAD *A,  STORE *D = Z
      	Y = LOAD *B,  STORE *D = Z, X = LOAD *A
      	STORE *D = Z, X = LOAD *A,  Y = LOAD *B
      	STORE *D = Z, Y = LOAD *B,  X = LOAD *A
      
    • 必须要假定重叠的内存访问可能会被合并或丢弃。这意味着对于
      	X = *A; Y = *(A + 4);
      

      我们可能得到下面的序列之一:

      	X = LOAD *A; Y = LOAD *(A + 4);
      	Y = LOAD *(A + 4); X = LOAD *A;
      	{X, Y} = LOAD {*A, *(A + 4) };
      

      对于:

      	*A = X; Y = *A;
      

      我们可能得到下面的序列之一:

      	STORE *A = X; Y = LOAD *A;
      	STORE *A = Y = X;
      

    什么是内存屏障?

    如上所述,没有依赖关系的内存操作实际会以随机的顺序执行,但对CPU-CPU的交互和I / O来说却是个问题。我们需要某种方式来指导编译器和CPU以约束执行顺序。

    内存屏障就是这样一种干预手段。它们会给屏障两侧的内存操作强加一个偏序关系。

    这种强制措施是很重要的,因为一个系统中,CPU和其它硬件可以使用各种技巧来提高性能,包括内存操作的重排、延迟和合并;预取;推测执行分支以及各种类型的缓存。内存屏障是用来禁用或抑制这些技巧的,使代码稳健地控制多个CPU和(或)设备的交互。

    内存屏障的种类

    内存屏障有四种基本类型:

    1. write(或store)内存屏障。

      write内存屏障保证:所有该屏障之前的store操作,看起来一定在所有该屏障之后的store操作之前执行。

      write屏障仅保证store指令上的偏序关系,不要求对load指令有什么影响。

      随着时间推移,可以视CPU提交了一系列store操作到内存系统。在该一系列store操作中,write屏障之前的所有store操作将在该屏障后面的store操作之前执行。

      [!]注意,write屏障一般与read屏障或数据依赖障碍成对出现;请参阅“SMP屏障配对”小节。

    2. 数据依赖屏障。

      数据依赖屏障是read屏障的一种较弱形式。在执行两个load指令,第二个依赖于第一个的执行结果(例如:第一个load执行获取某个地址,第二个load指令取该地址的值)时,可能就需要一个数据依赖屏障,来确保第二个load指令在获取目标地址值的时候,第一个load指令已经更新过该地址。

      数据依赖屏障仅保证相互依赖的load指令上的偏序关系,不要求对store指令,无关联的load指令以及重叠的load指令有什么影响。

      如write(或store)内存屏障中提到的,可以视系统中的其它CPU提交了一些列store指令到内存系统,然后the CPU being considered就能感知到。由该CPU发出的数据依赖屏障可以确保任何在该屏障之前的load指令,如果该load指令的目标被另一个CPU的存储(store)指令修改,在屏障执行完成之后,所有在该load指令对应的store指令之前的store指令的更新都会被所有在数据依赖屏障之后的load指令感知。

      参考”内存屏障顺序实例”小节图中的顺序约束。

      [!]注意:第一个load指令确实必须有一个数据依赖,而不是控制依赖。如果第二个load指令的目标地址依赖于第一个load,但是这个依赖是通过一个条件语句,而不是实际加载的地址本身,那么它是一个控制依赖,需要一个完整的read屏障或更强的屏障。查看”控制依赖”小节,了解更多信息。

      [!]注意:数据依赖屏障一般与写障碍成对出现;看到“SMP屏障配对”章节。

    3. read(或load)内存屏障。

      read屏障是数据依赖屏障外加一个保证,保证所有该屏障之前的load操作,看起来一定在所有该屏障之后的load操作之前执行。

      read屏障仅保证load指令上的偏序关系,不要求对store指令有什么影响。

      read屏障包含了数据依赖屏障的功能,因此可以替代数据依赖屏障。

      [!]注意:read屏障通常与write屏障成对出现;请参阅“SMP屏障配对”小节。

    4. 通用内存屏障。

      通用屏障确保所有该屏障之前的load和store操作,看起来一定在所有屏障之后的load和store操作之前执行。

      通用屏障能保证load和store指令上的偏序关系。

      通用屏障包含了read屏障和write屏障,因此可以替代它们两者。

    一对隐式的屏障变种:

    1. LOCK操作。

      LOCK操作可以看作是一个单向渗透的屏障。它保证所有在LOCK之后的内存操作看起来一定在LOCK操作后才发生。

      LOCK操作之前的内存操作可能会在LOCK完成之后发生。

      LOCK操作几乎总是与UNLOCK操作成对出现。

    2. UNLOCK操作。

      这也是一个单向渗透屏障。它保证所有UNLOCK操作之前的内存操作看起来一定在UNLOCK操作之前发生。

      UNLOCK操作之后的内存操作可能会在UNLOCK完成之前发生。

      LOCK和UNLOCK操作严格保证自己对指令的顺序。

      使用了LOCK和UNLOCK操作,一般就不需要其它类型的内存屏障了(但要注意在”MMIO write屏障”一节中提到的例外情况)。

    仅当两个CPU之间或者CPU与其它设备之间有交互时才需要屏障。如果可以确保某段代码中不会有任何这种交互,那么这段代码就不需要内存屏障。

    注意,这些是最低限度的保证。不同的架构可能会提供更多的保证,但是它们不是必须的,不应该依赖其写代码(they may not be relied upon outside of arch specific code)。

    什么是内存屏障不能确保的?

    有一些事情,Linux内核的内存屏障并不保证:

    • 不能保证,任何在内存屏障之前的内存访问操作能在内存屏障指令执行完成时也执行完成;内存屏障相当于在CPU的访问队列中划了一条界线,相应类型的指令不能跨过该界线。
    • 不能保证,一个CPU发出的内存屏障能对另一个CPU或该系统中的其它硬件有任何直接影响。只会间接影响到第二个CPU看第一个CPU的存取操作发生的顺序,但请看下一条:
    • 不能保证,一个CPU看到第二个CPU存取操作的结果的顺序,即使第二个CPU使用了内存屏障,除非第一个CPU也使用与第二个CPU相匹配的内存屏障(见”SMP屏障配对”小节)。
    • 不能保证,一些CPU相关的硬件[*]不会对内存访问重排序。 CPU缓存的一致性机制会在多个CPU之间传播内存屏障的间接影响,但可能不是有序的。

      [*]总线主控DMA和一致性相关信息,请参阅:

      Documentation/PCI/pci.txt

      Documentation/PCI/PCI-DMA-mapping.txt

      Documentation/DMA-API.txt

    数据依赖屏障

    数据依赖屏障的使用条件有点微妙,且并不总是很明显。为了阐明问题,考虑下面的事件序列:

    	CPU 1		CPU 2
    	===============	===============
    	{ A == 1, B == 2, C = 3, P == &A, Q == &C }
    	B = 4;
    	
    	P = &B
    			Q = P;
    			D = *Q;
    

    这里很明显存在数据依赖,看起来在执行结束后,Q不是&A就是&B,并且:

    	(Q == &A) 意味着 (D == 1)
    	(Q == &B) 意味着 (D == 4)
    

    但是,从CPU 2可能先感知到P更新,然后才感知到B更新,这就导致了以下情况:

    	(Q == &B) and (D == 2) ????
    

    虽然这可能看起来像是一致性或因果关系维护失败,但实际并不是的,且这种行为在一些真实的CPU上也可以观察到(如DEC Alpha)。

    为了处理这个问题,需要在地址load和数据load之间插入一个数据依赖屏障或一个更强的屏障:

    	CPU 1		CPU 2
    	===============	===============
    	{ A == 1, B == 2, C = 3, P == &A, Q == &C }
    	B = 4;
    	
    	P = &B
    			Q = P;
    			
    			D = *Q;
    

    这将迫使结果为前两种情况之一,而防止了第三种可能性的出现。

    [!]注意:这种极其有违直觉的场景,在有多个独立缓存(split caches)的机器上很容易出现,比如:一个cache bank处理偶数编号的缓存行,另外一个cache bank处理奇数编号的缓存行。指针P可能存储在奇数编号的缓存行,变量B可能存储在偶数编号的缓存行中。然后,如果在读取CPU缓存的时候,偶数的bank非常繁忙,而奇数bank处于闲置状态,就会出现指针P(&B)是新值,但变量B(2)是旧值的情况。

    另外一个需要数据依赖屏障的例子是从内存中读取一个数字,然后用来计算某个数组的下标;

    	CPU 1		CPU 2
    	===============	===============
    	{ M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3 }
    	M[1] = 4;
    	
    	P = 1
    			Q = P;
    			
    			D = M[Q];
    

    数据依赖屏障对RCU系统是很重要的,如,看include/linux/ rcupdate.h的rcu_dereference()函数。这个函数允许RCU的指针被替换为一个新的值,而这个新的值还没有完全的初始化。

    更多详细的例子参见”高速缓存一致性”小节。

    控制依赖

    控制依赖需要一个完整的read内存屏障来保证其正确性,而不简单地只是数据依赖屏障。考虑下面的代码:

    	q = &a;
    	if (p)
    		q = &b;
    	
    	x = *q;
    

    这不会产生想要的结果,因为这里没有实际的数据依赖,而是一个控制依赖,CPU可能会提前预测结果而使if语句短路。在这样的情况下,实际需要的是下面的代码:

    	q = &a;
    	if (p)
    		q = &b;
    	
    	x = *q;
    

    SMP屏障配对

    当处理CPU-CPU之间的交互时,相应类型的内存屏障总应该是成对出现的。缺少相应的配对屏障几乎可以肯定是错误的。

    write屏障应始终与数据依赖屏障或者read屏障配对,虽然通用内存屏障也是可以的。同样地,read屏障或数据依赖屏障应至少始终与write屏障配对使用,虽然通用屏障仍然也是可以的:

    	CPU 1		CPU 2
    	===============	===============
    	a = 1;
    	
    	b = 2;		x = b;
    			
    			y = a;
    

    或者:

    	CPU 1		CPU 2
    	===============	===============================
    	a = 1;
    	
    	b = &a;		x = b;
    			
    			y = *x;
    

    基本上,那个位置的read屏障是必不可少的,尽管可以是“更弱“的类型。

    [!]注意:write屏障之前的store指令通常与read屏障或数据依赖屏障后的load指令相匹配,反之亦然:

    	CPU 1                           CPU 2
    	===============                 ===============
    	a = 1;           }----   --->{  v = c
    	b = 2;           }    \ /    {  w = d
    	        \        
    	c = 3;           }    / \    {  x = a;
    	d = 4;           }----   --->{  y = b;
    

    内存屏障序列实例

    首先,write屏障确保store操作的偏序关系。考虑以下事件序列:

    	CPU 1
    	=======================
    	STORE A = 1
    	STORE B = 2
    	STORE C = 3
    	
    	STORE D = 4
    	STORE E = 5
    

    这一连串的事件提交给内存一致性系统的顺序,可以使系统其它部分感知到无序集合{ STORE A,STORE B, STORE C } 中的操作都发生在无序集合{ STORE D, STORE E}中的操作之前:

    	+-------+       :      :
    	|       |       +------+
    	|       |------>| C=3  |     }     /\
    	|       |  :    +------+     }-----  \  -----> Events perceptible to
    	|       |  :    | A=1  |     }        \/       the rest of the system
    	|       |  :    +------+     }
    	| CPU 1 |  :    | B=2  |     }
    	|       |       +------+     }
    	|       |   wwwwwwwwwwwwwwww }   <--- At this point the write barrier
    	|       |       +------+     }        requires all stores prior to the
    	|       |  :    | E=5  |     }        barrier to be committed before
    	|       |  :    +------+     }        further stores may take place
    	|       |------>| D=4  |     }
    	|       |       +------+
    	+-------+       :      :
    	                   |
    	                   | Sequence in which stores are committed to the
    	                   | memory system by CPU 1
    	                   V
    

    其次,数据依赖屏障确保于有数据依赖关系的load指令间的偏序关系。考虑以下事件序列:

    	CPU 1			CPU 2
    	=======================	=======================
    		{ B = 7; X = 9; Y = 8; C = &Y }
    	STORE A = 1
    	STORE B = 2
    	
    	STORE C = &B		LOAD X
    	STORE D = 4		LOAD C (gets &B)
    				LOAD *C (reads B)
    

    在没有其它干涉时,尽管CPU 1发出了write屏障,CPU2感知到的CPU1上事件的顺序也可能是随机的:

    	+-------+       :      :                :       :
    	|       |       +------+                +-------+  | Sequence of update
    	|       |------>| B=2  |-----       --->| Y->8  |  | of perception on
    	|       |  :    +------+     \          +-------+  | CPU 2
    	| CPU 1 |  :    | A=1  |      \     --->| C->&Y |  V
    	|       |       +------+       |        +-------+
    	|       |   wwwwwwwwwwwwwwww   |        :       :
    	|       |       +------+       |        :       :
    	|       |  :    | C=&B |---    |        :       :       +-------+
    	|       |  :    +------+   \   |        +-------+       |       |
    	|       |------>| D=4  |    ----------->| C->&B |------>|       |
    	|       |       +------+       |        +-------+       |       |
    	+-------+       :      :       |        :       :       |       |
    	                               |        :       :       |       |
    	                               |        :       :       | CPU 2 |
    	                               |        +-------+       |       |
    	    Apparently incorrect --->  |        | B->7  |------>|       |
    	    perception of B (!)        |        +-------+       |       |
    	                               |        :       :       |       |
    	                               |        +-------+       |       |
    	    The load of X holds --->    \       | X->9  |------>|       |
    	    up the maintenance           \      +-------+       |       |
    	    of coherence of B             ----->| B->2  |       +-------+
    	                                        +-------+
    	                                        :       :
    
    

    在上述的例子中,尽管load *C(可能是B)在load C之后,但CPU 2感知到的B却是7;

    然而,在CPU2中,如果数据依赖屏障放置在loadC和load *C(即:B)之间:

    	CPU 1			CPU 2
    	=======================	=======================
    		{ B = 7; X = 9; Y = 8; C = &Y }
    	STORE A = 1
    	STORE B = 2
    	
    	STORE C = &B		LOAD X
    	STORE D = 4		LOAD C (gets &B)
    				
    				LOAD *C (reads B)
    

    将发生以下情况:

    	+-------+       :      :                :       :
    	|       |       +------+                +-------+
    	|       |------>| B=2  |-----       --->| Y->8  |
    	|       |  :    +------+     \          +-------+
    	| CPU 1 |  :    | A=1  |      \     --->| C->&Y |
    	|       |       +------+       |        +-------+
    	|       |   wwwwwwwwwwwwwwww   |        :       :
    	|       |       +------+       |        :       :
    	|       |  :    | C=&B |---    |        :       :       +-------+
    	|       |  :    +------+   \   |        +-------+       |       |
    	|       |------>| D=4  |    ----------->| C->&B |------>|       |
    	|       |       +------+       |        +-------+       |       |
    	+-------+       :      :       |        :       :       |       |
    	                               |        :       :       |       |
    	                               |        :       :       | CPU 2 |
    	                               |        +-------+       |       |
    	                               |        | X->9  |------>|       |
    	                               |        +-------+       |       |
    	  Makes sure all effects --->   \   ddddddddddddddddd   |       |
    	  prior to the store of C        \      +-------+       |       |
    	  are perceptible to              ----->| B->2  |------>|       |
    	  subsequent loads                      +-------+       |       |
    	                                        :       :       +-------+
    

    第三,read屏障确保load指令上的偏序关系。考虑以下的事件序列:

    	CPU 1			CPU 2
    	=======================	=======================
    		{ A = 0, B = 9 }
    	STORE A=1
    	
    	STORE B=2
    				LOAD B
    				LOAD A
    

    在没有其它干涉时,尽管CPU1发出了一个write屏障,CPU 2感知到的CPU 1中事件的顺序也可能是随机的:

    	+-------+       :      :                :       :
    	|       |       +------+                +-------+
    	|       |------>| A=1  |------      --->| A->0  |
    	|       |       +------+      \         +-------+
    	| CPU 1 |   wwwwwwwwwwwwwwww   \    --->| B->9  |
    	|       |       +------+        |       +-------+
    	|       |------>| B=2  |---     |       :       :
    	|       |       +------+   \    |       :       :       +-------+
    	+-------+       :      :    \   |       +-------+       |       |
    	                             ---------->| B->2  |------>|       |
    	                                |       +-------+       | CPU 2 |
    	                                |       | A->0  |------>|       |
    	                                |       +-------+       |       |
    	                                |       :       :       +-------+
    	                                 \      :       :
    	                                  \     +-------+
    	                                   ---->| A->1  |
    	                                        +-------+
    	                                        :       :

    然而,如果在CPU2上的load A和load B之间放置一个read屏障:

    	CPU 1			CPU 2
    	=======================	=======================
    		{ A = 0, B = 9 }
    	STORE A=1
    	
    	STORE B=2
    				LOAD B
    				
    				LOAD A
    

    CPU1上的偏序关系将能被CPU2正确感知到:

    	+-------+       :      :                :       :
    	|       |       +------+                +-------+
    	|       |------>| A=1  |------      --->| A->0  |
    	|       |       +------+      \         +-------+
    	| CPU 1 |   wwwwwwwwwwwwwwww   \    --->| B->9  |
    	|       |       +------+        |       +-------+
    	|       |------>| B=2  |---     |       :       :
    	|       |       +------+   \    |       :       :       +-------+
    	+-------+       :      :    \   |       +-------+       |       |
    	                             ---------->| B->2  |------>|       |
    	                                |       +-------+       | CPU 2 |
    	                                |       :       :       |       |
    	                                |       :       :       |       |
    	  At this point the read ---->   \  rrrrrrrrrrrrrrrrr   |       |
    	  barrier causes all effects      \     +-------+       |       |
    	  prior to the storage of B        ---->| A->1  |------>|       |
    	  to be perceptible to CPU 2            +-------+       |       |
    	                                        :       :       +-------+
    

    为了更彻底说明这个问题,考虑read屏障的两侧都有load A将发生什么:

    	CPU 1			CPU 2
    	=======================	=======================
    		{ A = 0, B = 9 }
    	STORE A=1
    	
    	STORE B=2
    				LOAD B
    				LOAD A [first load of A]
    				
    				LOAD A [second load of A]
    

    即使两个load A都发生在loadB之后,它们仍然可能获得不同的值:

    	+-------+       :      :                :       :
    	|       |       +------+                +-------+
    	|       |------>| A=1  |------      --->| A->0  |
    	|       |       +------+      \         +-------+
    	| CPU 1 |   wwwwwwwwwwwwwwww   \    --->| B->9  |
    	|       |       +------+        |       +-------+
    	|       |------>| B=2  |---     |       :       :
    	|       |       +------+   \    |       :       :       +-------+
    	+-------+       :      :    \   |       +-------+       |       |
    	                             ---------->| B->2  |------>|       |
    	                                |       +-------+       | CPU 2 |
    	                                |       :       :       |       |
    	                                |       :       :       |       |
    	                                |       +-------+       |       |
    	                                |       | A->0  |------>| 1st   |
    	                                |       +-------+       |       |
    	  At this point the read ---->   \  rrrrrrrrrrrrrrrrr   |       |
    	  barrier causes all effects      \     +-------+       |       |
    	  prior to the storage of B        ---->| A->1  |------>| 2nd   |
    	  to be perceptible to CPU 2            +-------+       |       |
    	                                        :       :       +-------+
    

    但是,在read屏障完成之前,CPU1对A的更新就可能被CPU2看到:

    	+-------+       :      :                :       :
    	|       |       +------+                +-------+
    	|       |------>| A=1  |------      --->| A->0  |
    	|       |       +------+      \         +-------+
    	| CPU 1 |   wwwwwwwwwwwwwwww   \    --->| B->9  |
    	|       |       +------+        |       +-------+
    	|       |------>| B=2  |---     |       :       :
    	|       |       +------+   \    |       :       :       +-------+
    	+-------+       :      :    \   |       +-------+       |       |
    	                             ---------->| B->2  |------>|       |
    	                                |       +-------+       | CPU 2 |
    	                                |       :       :       |       |
    	                                 \      :       :       |       |
    	                                  \     +-------+       |       |
    	                                   ---->| A->1  |------>| 1st   |
    	                                        +-------+       |       |
    	                                    rrrrrrrrrrrrrrrrr   |       |
    	                                        +-------+       |       |
    	                                        | A->1  |------>| 2nd   |
    	                                        +-------+       |       |
    	                                        :       :       +-------+
    

    如果load B == 2,可以保证第二次load A总是等于 1。但是不能保证第一次load A的值,A == 0或A == 1都可能会出现。

    read内存屏障与load预加载

    许多CPU都会预测并提前加载:即,当系统发现它即将需要从内存中加载一个条目时,系统会寻找没有其它load指令占用总线资源的时候提前加载 —— 即使还没有达到指令执行流中的该点。这使得实际的load指令可能会立即完成,因为CPU已经获得了值。

    也可能CPU根本不会使用这个值,因为执行到了另外的分支而绕开了这个load – 在这种情况下,它可以丢弃该值或仅是缓存该值供以后使用。

    考虑下面的场景:

    	CPU 1	   		CPU 2
    	=======================	=======================
    	 	   		LOAD B
    	 	   		DIVIDE		} Divide instructions generally
    	 	   		DIVIDE		} take a long time to perform
    	 	   		LOAD A
    

    可能出现:

    	                                        :       :       +-------+
    	                                        +-------+       |       |
    	                                    --->| B->2  |------>|       |
    	                                        +-------+       | CPU 2 |
    	                                        :       : DIVIDE|       |
    	                                        +-------+       |       |
    	The CPU being busy doing a --->     --->| A->0  |~~~~   |       |
    	division speculates on the              +-------+   ~   |       |
    	LOAD of A                               :       :   ~   |       |
    	                                        :       : DIVIDE|       |
    	                                        :       :   ~   |       |
    	Once the divisions are complete -->     :       :   ~-->|       |
    	the CPU can then perform the            :       :       |       |
    	LOAD with immediate effect              :       :       +-------+
    

    在第二个LOAD指令之前,放置一个read屏障或数据依赖屏障:

    	CPU 1	   		CPU 2
    	=======================	=======================
    	 	   		LOAD B
    	 	   		DIVIDE
    	 	   		DIVIDE
    				
    	 	   		LOAD A
    

    是否强制重新获取预取的值,在一定程度上依赖于使用的屏障类型。如果值没有发送变化,将直接使用预取的值:

    	                                        :       :       +-------+
    	                                        +-------+       |       |
    	                                    --->| B->2  |------>|       |
    	                                        +-------+       | CPU 2 |
    	                                        :       : DIVIDE|       |
    	                                        +-------+       |       |
    	The CPU being busy doing a --->     --->| A->0  |~~~~   |       |
    	division speculates on the              +-------+   ~   |       |
    	LOAD of A                               :       :   ~   |       |
    	                                        :       : DIVIDE|       |
    	                                        :       :   ~   |       |
    	                                        :       :   ~   |       |
    	                                    rrrrrrrrrrrrrrrr~   |       |
    	                                        :       :   ~   |       |
    	                                        :       :   ~-->|       |
    	                                        :       :       |       |
    	                                        :       :       +-------+
    

    但如果另一个CPU有更新该值或者使该值失效,就必须重新加载该值:

    	                                        :       :       +-------+
    	                                        +-------+       |       |
    	                                    --->| B->2  |------>|       |
    	                                        +-------+       | CPU 2 |
    	                                        :       : DIVIDE|       |
    	                                        +-------+       |       |
    	The CPU being busy doing a --->     --->| A->0  |~~~~   |       |
    	division speculates on the              +-------+   ~   |       |
    	LOAD of A                               :       :   ~   |       |
    	                                        :       : DIVIDE|       |
    	                                        :       :   ~   |       |
    	                                        :       :   ~   |       |
    	                                    rrrrrrrrrrrrrrrrr   |       |
    	                                        +-------+       |       |
    	The speculation is discarded --->   --->| A->1  |------>|       |
    	and an updated value is                 +-------+       |       |
    	retrieved                               :       :       +-------+
    

    传递性

    传递性是有关顺序的一个非常直观的概念,但是真实的计算机系统往往并不保证。下面的例子演示传递性(也可称为“积累律(cumulativity)”):

    	CPU 1			CPU 2			CPU 3
    	=======================	=======================	=======================
    		{ X = 0, Y = 0 }
    	STORE X=1		LOAD X			STORE Y=1
    					
    				LOAD Y			LOAD X
    

    假设CPU 2 的load X返回1、load Y返回0。这表明,从某种意义上来说,CPU 2的LOAD X在CPU 1 store X之后,CPU 2的load y在CPU 3的store y 之前。问题是“CPU 3的 load X是否可能返回0?”

    因为,从某种意义上说,CPU 2的load X在CPU 1的store之后,我们很自然地希望CPU 3的load X必须返回1。这就是传递性的一个例子:如果在CPU B上执行了一个load指令,随后CPU A 又对相同位置进行了load操作,那么,CPU A load的值要么和CPU B load的值相同,要么是个更新的值。

    在Linux内核中,使用通用内存屏障能保证传递性。因此,在上面的例子中,如果从CPU 2的load X指令返回1,且其load Y返回0,那么CPU 3的load X也必须返回1。

    但是,read或write屏障不保证传递性。例如,将上述例子中的通用屏障改为read屏障,如下所示:

    	CPU 1			CPU 2			CPU 3
    	=======================	=======================	=======================
    		{ X = 0, Y = 0 }
    	STORE X=1		LOAD X			STORE Y=1
    						
    				LOAD Y			LOAD X
    

    这就破坏了传递性:在本例中,CPU 2的load X返回1,load Y返回0,但是CPU 3的load X返回0是完全合法的。

    关键点在于,虽然CPU 2的read屏障保证了CPU2上的load指令的顺序,但它并不能保证CPU 1上的store顺序。因此,如果这个例子运行所在的CPU 1和2共享了存储缓冲区或某一级缓存,CPU 2可能会提前获得到CPU 1写入的值。因此,需要通用屏障来确保所有的CPU都遵守CPU1和CPU2的访问组合顺序。

    要重申的是,如果你的代码需要传递性,请使用通用屏障。

    显式内核屏障

    Linux内核有多种不同的屏障,工作在不同的层上:

    • 编译器屏障。
    • CPU内存屏障。
    • MMIO write屏障。

    编译器屏障

    Linux内核有一个显式的编译器屏障函数,用于防止编译器将内存访问从屏障的一侧移动到另一侧:

    barrier();
    

    这是一个通用屏障 – 不存在弱类型的编译屏障。

    编译屏障并不直接影响CPU,CPU依然可以按照它所希望的顺序进行重排序。

    CPU内存屏障

    Linux内核有8个基本的CPU内存屏障:

    	TYPE		MANDATORY		SMP CONDITIONAL
    	===============	=======================	===========================
    	GENERAL		mb()			smp_mb()
    	WRITE		wmb()			smp_wmb()
    	READ		rmb()			smp_rmb()
    	DATA DEPENDENCY	read_barrier_depends()	smp_read_barrier_depends()
    

    除了数据依赖屏障之外,其它所有屏障都包含了编译器屏障的功能。数据依赖屏障不强加任何额外的编译顺序。

    旁白:在数据依赖的情况下,可能希望编译器以正确的顺序发出load指令(如:’a[b]‘,将会在load a[b]之前load b),但在C规范下并不能保证如此,编译器可能不会预先推测b的值(即,等于1),然后在load b之前先load a(即,tmp = a [1];if(b!= 1)tmp = a[b];)。还有编译器重排序的问题,编译器load a[b]之后重新load b,这样,b就拥有比a[b]更新的副本。关于这些问题尚未形成共识,然而ACCESS_ONCE宏是解决这个问题很好的开始。

    在单处理器编译系统中,SMP内存屏障将退化为编译屏障,因为它假定CPU可以保证自身的一致性,并且可以正确的处理重叠访问。

    [!]注意:SMP内存屏障必须用在SMP系统中来控制引用共享内存的顺序,使用锁也可以满足需求。

    强制性屏障不应该被用来控制SMP,因为强制屏障在UP系统中会产生过多不必要的开销。但是,它们可以用于控制在通过松散内存I / O窗口访问的MMIO操作。即使在非SMP系统中,这些也是必须的,因为它们可以禁止编译器和CPU的重排从而影响内存操作的顺序。

    下面是些更高级的屏障函数:

    (*) set_mb(var, value)
    

    这个函数将值赋给变量,然后在其后插入一个完整的内存屏障,根据不同的实现。在UP编译器中,不能保证插入编译器屏障之外的屏障。

     (*) smp_mb__before_atomic_dec();
     (*) smp_mb__after_atomic_dec();
     (*) smp_mb__before_atomic_inc();
     (*) smp_mb__after_atomic_inc();
    

    这些都是用于原子加,减,递增和递减而不用返回值的,主要用于引用计数。这些函数并不包含内存屏障。

    例如,考虑下面的代码片段,它标记死亡的对象, 然后将该对象的引用计数减1:

    	obj->dead = 1;
    	smp_mb__before_atomic_dec();
    	atomic_dec(&obj->ref_count);
    

    这可以确保设置对象的死亡标记是在引用计数递减之前;

    更多信息参见Documentation/atomic_ops.txt ,“Atomic operations” 章节介绍了它的使用场景。

     (*) smp_mb__before_clear_bit(void);
     (*) smp_mb__after_clear_bit(void);
    

    这些类似于用于原子自增,自减的屏障。他们典型的应用场景是按位解锁操作,必须注意,因为这里也没有隐式的内存屏障。

    考虑通过清除一个lock位来实现解锁操作。 clear_bit()函数将需要像下面这样使用内存屏障:

    	smp_mb__before_clear_bit();
    	clear_bit( ... );
    

    这可以防止在clear之前的内存操作跑到clear后面。UNLOCK的参考实现见”锁的功能”小节。

    更多信息见Documentation/atomic_ops.txt , “Atomic operations“章节有关于使用场景的介绍;

    MMIO write屏障

    对于内存映射I / O写操作,Linux内核也有个特殊的障碍;

    mmiowb();
    

    这是一个强制性写屏障的变体,保证对弱序I / O区的写操作有偏序关系。其影响可能超越CPU和硬件之间的接口,且能实际地在一定程度上影响到硬件。

    更多信息参见”锁与I / O访问”章节。

    隐式内核内存屏障

    Linux内核中的一些其它的功能暗含着内存屏障,主要是锁和调度功能。

    该规范是一个最低限度的保证,任何特定的体系结构都可能提供更多的保证,但是在特定体系结构之外不能依赖它们。

    锁功能

    Linux内核有很多锁结构:

    • 自旋锁
    • R / W自旋锁
    • 互斥
    • 信号量
    • R / W信号量
    • RCU

    所有的情况下,它们都是LOCK操作和UNLOCK操作的变种。这些操作都隐含着特定的屏障:

    1. LOCK操作的含义:

      在LOCK操作之后的内存操作将会在LOCK操作结束之后完成;

      在LOCK操作之前的内存操作可能在LOCK操作结束之后完成;

    2. UNLOCK操作的含义:

      在UNLOCK操作之前的内存操作将会在UNLOCK操作结束之前完成;

      在UNLOCK操作之后的内存操作可能在UNLOCK操作结束之前完成;

    3. LOCK与LOCK的含义:

      在一个LOCK之前的其它LOCK操作一定在该LOCK结束之前完成;

    4. LOCK与UNLOCK的含义:

      在某个UNLOCK之前的所有其它LOCK操作一定在该UNLOCK结束之前完成;

      在某个LOCK之前的所有其它UNLOCK操作一定在该LOCK结束之前完成;

    5. 失败的有条件锁的含义:

      某些锁操作的变种可能会失败,要么是由于无法立即获得锁,要么是在休眠等待锁可用的同时收到了一个解除阻塞的信号。失败的锁操作并不暗含任何形式的屏障。

    因此,根据(1),(2)和(4),一个无条件的LOCK后面跟着一个UNLOCK操作相当于一个完整的屏障,但一个UNLOCK后面跟着一个LOCK却不是。

    [!]注意:将LOCK和UNLOCK作为单向屏障的一个结果是,临界区外的指令可能会移到临界区里。

    LOCK后跟着一个UNLOCK并不认为是一个完整的屏障,因为存在LOCK之前的存取发生在LOCK之后,UNLOCK之后的存取在UNLOCK之前发生的可能性,这样,两个存取操作的顺序就可能颠倒:

    	*A = a;
    	LOCK
    	UNLOCK
    	*B = b;
    

    可能会发生:

    	LOCK, STORE *B, STORE *A, UNLOCK
    

    锁和信号量在UP编译系统中不保证任何顺序,所以在这种情况下根本不能考虑为屏障 —— 尤其是对于I / O访问 —— 除非结合中断禁用操作。

    更多信息请参阅”CPU之间的锁屏障”章节。

    考虑下面的例子:

    	*A = a;
    	*B = b;
    	LOCK
    	*C = c;
    	*D = d;
    	UNLOCK
    	*E = e;
    	*F = f;
    

    以下的顺序是可以接受的:

    	LOCK, {*F,*A}, *E, {*C,*D}, *B, UNLOCK
    

    [+] Note that {*F,*A} indicates a combined access.

    但下列情形的,是不能接受的:

    	{*F,*A}, *B,	LOCK, *C, *D,	UNLOCK, *E
    	*A, *B, *C,	LOCK, *D,	UNLOCK, *E, *F
    	*A, *B,		LOCK, *C,	UNLOCK, *D, *E, *F
    	*B,		LOCK, *C, *D,	UNLOCK, {*F,*A}, *E
    

    中断禁用功能

    禁止中断(等价于LOCK)和允许中断(等价于UNLOCK)仅可充当编译屏障。所以,如果某些场景下需要内存或I / O屏障,必须通过其它的手段来提供。

    休眠和唤醒功能

    一个全局数据标记的事件上的休眠和唤醒,可以被看作是两块数据之间的交互:正在等待的任务的状态和标记这个事件的全局数据。为了确保正确的顺序,进入休眠的原语和唤醒的原语都暗含了某些屏障。

    首先,通常一个休眠任务执行类似如下的事件序列:

    	for (;;) {
    		set_current_state(TASK_UNINTERRUPTIBLE);
    		if (event_indicated)
    			break;
    		schedule();
    	}
    

    set_current_state()会在改变任务状态后自动插入一个通用内存屏障;

    	CPU 1
    	===============================
    	set_current_state();
    	  set_mb();
    	    STORE current->state
    	    
    	LOAD event_indicated
    

    set_current_state()可能包含在下面的函数中:

    	prepare_to_wait();
    	prepare_to_wait_exclusive();
    

    因此,在设置状态后,这些函数也暗含了一个通用内存屏障。上面的各个函数又被封装在其它函数中,所有这些函数都在对应的地方插入了内存屏障;

    	wait_event();
    	wait_event_interruptible();
    	wait_event_interruptible_exclusive();
    	wait_event_interruptible_timeout();
    	wait_event_killable();
    	wait_event_timeout();
    	wait_on_bit();
    	wait_on_bit_lock();
    

    其次,执行正常唤醒的代码如下:

    	event_indicated = 1;
    	wake_up(&event_wait_queue);
    

    或:

    	event_indicated = 1;
    	wake_up_process(event_daemon);
    

    类似wake_up()的函数都暗含一个内存屏障。当且仅当他们唤醒某个任务的时候。任务状态被清除之前内存屏障执行,也即是在设置唤醒标志事件的store操作和设置TASK_RUNNING的store操作之间:

    	CPU 1				CPU 2
    	===============================	===============================
    	set_current_state();		STORE event_indicated
    	  set_mb();			wake_up();
    	    STORE current->state	  
    	    		  STORE current->state
    	LOAD event_indicated
    

    可用唤醒函数包括:

    	complete();
    	wake_up();
    	wake_up_all();
    	wake_up_bit();
    	wake_up_interruptible();
    	wake_up_interruptible_all();
    	wake_up_interruptible_nr();
    	wake_up_interruptible_poll();
    	wake_up_interruptible_sync();
    	wake_up_interruptible_sync_poll();
    	wake_up_locked();
    	wake_up_locked_poll();
    	wake_up_nr();
    	wake_up_poll();
    	wake_up_process();
    

    [!]注意:在休眠任务执行set_current_state()之后,若要load唤醒前store指令存储的值,休眠和唤醒所暗含的内存屏障都不能保证唤醒前多个store指令的顺序。例如:休眠函数如下

    	set_current_state(TASK_INTERRUPTIBLE);
    	if (event_indicated)
    		break;
    	__set_current_state(TASK_RUNNING);
    	do_something(my_data);
    

    以及唤醒函数如下:

    	my_data = value;
    	event_indicated = 1;
    	wake_up(&event_wait_queue);
    

    并不能保证休眠函数在对my_data做过修改之后能够感知到event_indicated的变化。在这种情况下,两侧的代码必须在隔离数据访问之间插入自己的内存屏障。因此,上面的休眠任务应该这样:

    	set_current_state(TASK_INTERRUPTIBLE);
    	if (event_indicated) {
    		smp_rmb();
    		do_something(my_data);
    	}
    

    以及唤醒者应该做的:

    	my_data = value;
    	smp_wmb();
    	event_indicated = 1;
    	wake_up(&event_wait_queue);
    

    其它函数

    其它暗含内存屏障的函数:

    • schedule()以及类似函数暗含了完整内存屏障。

    CPU之间的锁屏障效应

    在SMP系统中,锁原语提供了更加丰富的屏障类型:在任意特定的锁冲突的情况下,会影响其它CPU上的内存访问顺序。

    锁与内存访问

    考虑下面的场景:系统有一对自旋锁(M)、(Q)和三个CPU,然后发生以下的事件序列:

    	CPU 1				CPU 2
    	===============================	===============================
    	*A = a;				*E = e;
    	LOCK M				LOCK Q
    	*B = b;				*F = f;
    	*C = c;				*G = g;
    	UNLOCK M			UNLOCK Q
    	*D = d;				*H = h;
    

    对CPU 3来说, *A到*H的存取顺序是没有保证的,不同于单独的锁在单独的CPU上的作用。例如,它可能感知的顺序如下:

    	*E, LOCK M, LOCK Q, *G, *C, *F, *A, *B, UNLOCK Q, *D, *H, UNLOCK M
    

    但它不会看到任何下面的场景:

    	*B, *C or *D 在 LOCK M 之前
    	*A, *B or *C 在 UNLOCK M 之后
    	*F, *G or *H 在 LOCK Q 之前
    	*E, *F or *G 在 UNLOCK Q 之后
    

    但是,如果发生以下情况:

    	CPU 1				CPU 2
    	===============================	===============================
    	*A = a;
    	LOCK M		[1]
    	*B = b;
    	*C = c;
    	UNLOCK M	[1]
    	*D = d;				*E = e;
    					LOCK M		[2]
    					*F = f;
    					*G = g;
    					UNLOCK M	[2]
    					*H = h;
    

    CPU 3可能会看到:

    	*E, LOCK M [1], *C, *B, *A, UNLOCK M [1],
    		LOCK M [2], *H, *F, *G, UNLOCK M [2], *D
    

    但是,假设CPU 1先得到锁,CPU 3将不会看到任何下面的场景:

    	*B, *C, *D, *F, *G or *H 在 LOCK M [1] 之前
    	*A, *B or *C 在  UNLOCK M [1] 之后
    	*F, *G or *H 在 LOCK M [2] 之前
    	*A, *B, *C, *E, *F or *G 在 UNLOCK M [2] 之后
    

    锁与I/O访问

    在某些情况下(尤其是涉及NUMA),在两个不同CPU上的两个自旋锁区内的I / O访问,在PCI桥看来可能是交叉的,因为PCI桥不一定保证缓存一致性,此时内存屏障将失效。

    例如:

    	CPU 1				CPU 2
    	===============================	===============================
    	spin_lock(Q)
    	writel(0, ADDR)
    	writel(1, DATA);
    	spin_unlock(Q);
    					spin_lock(Q);
    					writel(4, ADDR);
    					writel(5, DATA);
    					spin_unlock(Q);
    

    PCI桥可能看到的顺序如下所示:

    	STORE *ADDR = 0, STORE *ADDR = 4, STORE *DATA = 1, STORE *DATA = 5
    

    这可能会导致硬件故障。

    这里有必要在释放自旋锁之前插入mmiowb()函数,例如:

    	CPU 1				CPU 2
    	===============================	===============================
    	spin_lock(Q)
    	writel(0, ADDR)
    	writel(1, DATA);
    	mmiowb();
    	spin_unlock(Q);
    					spin_lock(Q);
    					writel(4, ADDR);
    					writel(5, DATA);
    					mmiowb();
    					spin_unlock(Q);
    

    这将确保在CPU 1上的两次store比CPU 2上的两次store操作先被PCI感知。

    此外,相同的设备上如果store指令后跟随一个load指令,可以省去mmiowb()函数,因为load强制在load执行前store指令必须完成:

    	CPU 1				CPU 2
    	===============================	===============================
    	spin_lock(Q)
    	writel(0, ADDR)
    	a = readl(DATA);
    	spin_unlock(Q);
    					spin_lock(Q);
    					writel(4, ADDR);
    					b = readl(DATA);
    					spin_unlock(Q);
    

    更多信息参见:Documentation/DocBook/deviceiobook.tmpl

    什么地方需要内存障碍?

    在正常操作下,一个单线程代码片段中内存操作重排序一般不会产生问题,仍然可以正常工作,即使是在一个SMP内核系统中也是如此。但是,下面四种场景下,重新排序可能会引发问题:

    • 多理器间的交互。
    • 原子操作。
    • 设备访问。
    • 中断。

    多理器间的交互

    当系统具有一个以上的处理器,系统中多个CPU可能要访问同一数据集。这可能会导致同步问题,通常处理这种场景是使用锁。然而,锁是相当昂贵的,所以如果有其它的选择尽量不使用锁。在这种情况下,能影响到多个CPU的操作可能必须仔细排序,以防止出现故障。

    例如,在R / W信号量慢路径的场景。这里有一个waiter进程在信号量上排队,并且它的堆栈上的一块空间链接到信号量上的等待进程列表:

    	struct rw_semaphore {
    		...
    		spinlock_t lock;
    		struct list_head waiters;
    	};
    
    	struct rwsem_waiter {
    		struct list_head list;
    		struct task_struct *task;
    	};
    

    要唤醒一个特定的waiter进程,up_read()或up_write()函数必须做以下动作:

    1. 读取waiter记录的next指针,获取下一个waiter记录的地址;
    2. 读取waiter的task结构的指针;
    3. 清除task指针,通知waiter已经获取信号量;
    4. 在task上调用wake_up_process()函数;
    5. 释放waiter的task结构上的引用。

    换句话说,它必须执行下面的事件:

    	LOAD waiter->list.next;
    	LOAD waiter->task;
    	STORE waiter->task;
    	CALL wakeup
    	RELEASE task
    

    如果这些步骤的顺序发生任何改变,那么就会出问题。

    一旦进程将自己排队并且释放信号锁,waiter将不再获得锁,它只需要等待它的任务指针被清零,然后继续执行。由于记录是在waiter的堆栈上,这意味着如果在列表中的next指针被读取出之前,task指针被清零,另一个CPU可能会开始处理,up*()函数在有机会读取next指针之前waiter的堆栈就被修改。

    考虑上述事件序列可能发生什么:

    	CPU 1				CPU 2
    	===============================	===============================
    					down_xxx()
    					Queue waiter
    					Sleep
    	up_yyy()
    	LOAD waiter->task;
    	STORE waiter->task;
    					Woken up by other event
    	
    					Resume processing
    					down_xxx() returns
    					call foo()
    					foo() clobbers *waiter
    	
    	LOAD waiter->list.next;
    	--- OOPS ---
    

    虽然这里可以使用信号锁来处理,但在唤醒后的down_xxx()函数不必要的再次获得自旋锁。

    这个问题可以通过插入一个通用的SMP内存屏障来处理:

    	LOAD waiter->list.next;
    	LOAD waiter->task;
    	smp_mb();
    	STORE waiter->task;
    	CALL wakeup
    	RELEASE task
    

    在这种情况下,即使是在其它的CPU上,屏障确保所有在屏障之前的内存操作一定先于屏障之后的内存操作执行。但是它不能确保所有在屏障之前的内存操作一定先于屏障指令身执行完成时执行;

    在一个UP系统中, 这种场景不会产生问题 , smp_mb()仅仅是一个编译屏障,可以确保编译器以正确的顺序发出指令,而不会实际干预到CPU。因为只有一个CPU,CPU的依赖顺序逻辑会管理好一切。

    原子操作

    虽然它们在技术上考虑了处理器间的交互,但是特别注意,有一些原子操作暗含了完整的内存屏障,另外一些却没有包含,但是它们作为一个整体在内核中应用广泛。

    任一原子操作,修改了内存中某一状态并返回有关状态(新的或旧的)的信息,这意味着在实际操作(明确的lock操作除外)的两侧暗含了一个SMP条件通用内存屏障(smp_mb()),包括;

    	xchg();
    	cmpxchg();
    	atomic_cmpxchg();
    	atomic_inc_return();
    	atomic_dec_return();
    	atomic_add_return();
    	atomic_sub_return();
    	atomic_inc_and_test();
    	atomic_dec_and_test();
    	atomic_sub_and_test();
    	atomic_add_negative();
    	atomic_add_unless();	/* when succeeds (returns 1) */
    	test_and_set_bit();
    	test_and_clear_bit();
    	test_and_change_bit();
    

    它们都是用于实现诸如LOCK和UNLOCK的操作,以及判断引用计数器决定对象销毁,同样,隐式的内存屏障效果是必要的。

    下面的操作存在潜在的问题,因为它们并没有包含内存障碍,但可能被用于执行诸如解锁的操作:

    	atomic_set();
    	set_bit();
    	clear_bit();
    	change_bit();
    

    如果有必要,这些应使用恰当的显式内存屏障(例如:smp_mb__before_clear_bit())。

    下面这些也没有包含内存屏障,因此在某些场景下可能需要明确的内存屏障(例如:smp_mb__before_atomic_dec()):

    	atomic_add();
    	atomic_sub();
    	atomic_inc();
    	atomic_dec();
    

    如果将它们用于统计,那么可能并不需要内存屏障,除非统计数据之间有耦合。

    如果将它们用于对象的引用计数器来控制生命周期,也许也不需要内存屏障,因为可能引用计数会在锁区域内修改,或调用方已经考虑了锁,因此内存屏障不是必须的。

    如果将它们用于构建一个锁的描述,那么确实可能需要内存屏障,因为锁原语通常以特定的顺序来处理事情;

    基本上,每一个使用场景都必须仔细考虑是否需要内存屏障。

    以下操作是特殊的锁原语:

    	test_and_set_bit_lock();
    	clear_bit_unlock();
    	__clear_bit_unlock();
    

    这些实现了诸如LOCK和UNLOCK的操作。在实现锁原语时应当优先考虑使用它们,因为它们的实现可以在很多架构中进行优化。

    [!]注意:对于这些场景,也有特定的内存屏障原语可用,因为在某些CPU上原子指令暗含着完整的内存屏障,再使用内存屏障显得多余,在这种情况下,特殊屏障原语将是个空操作。

    更多信息见 Documentation/atomic_ops.txt。

    设备访问

    许多设备都可以映射到内存上,因此对CPU来说它们只是一组内存单元。为了控制这样的设备,驱动程序通常必须确保对应的内存访问顺序的正确性。

    然而,聪明的CPU或者聪明的编译器可能为引发潜在的问题,如果CPU或者编译器认为重排、合并、联合访问更加高效,驱动程序精心编排的指令顺序可能在实际访问设备是并不是按照这个顺序访问的 —— 这会导致设备故障。

    在Linux内核中,I / O通常需要适当的访问函数 —— 如inb() 或者 writel() —— 它们知道如何保持适当的顺序。虽然这在大多数情况下不需要明确的使用内存屏障,但是下面两个场景可能需要:

    1. 在某些系统中,I / O存储操作并不是在所有CPU上都是严格有序的,所以,对所有的通用驱动,锁是必须的,且必须在解锁临界区之前执行mmiowb().
    2. 如果访问函数是用来访问一个松散访问属性的I / O存储窗口,那么需要强制内存屏障来保证顺序。

    更多信息参见 Documentation/DocBook/deviceiobook.tmpl。

    中断

    驱动可能会被自己的中断服务例程中断,因此,驱动程序两个部分可能会互相干扰,尝试控制或访问该设备。

    通过禁用本地中断(一种锁的形式)可以缓和这种情况,这样,驱动程序中关键的操作都包含在中断禁止的区间中 。有时驱动的中断例程被执行,但是驱动程序的核心不是运行在相同的CPU上,并且直到当前的中断被处理结束之前不允许其它中断,因此,在中断处理器不需要再次加锁。

    但是,考虑一个驱动使用地址寄存器和数据寄存器跟以太网卡交互,如果该驱动的核心在中断禁用下与网卡通信,然后驱动程序的中断处理程序被调用:

    	LOCAL IRQ DISABLE
    	writew(ADDR, 3);
    	writew(DATA, y);
    	LOCAL IRQ ENABLE
    	
    	writew(ADDR, 4);
    	q = readw(DATA);
    	
    

    如果排序规则十分宽松,数据寄存器的存储可能发生在第二次地址寄存器之后:

    	STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA
    

    如果是宽松的排序规则,它必须假设中断禁止部分的内存访问可能向外泄漏,可能会和中断部分交叉访问 – 反之亦然 – 除非使用了隐式或显式的屏障。

    通常情况下,这不会产生问题,因为这种区域中的I / O访问将在严格有序的IO寄存器上包含同步load操作,形成隐式内存屏障。如果这还不够,可能需要显式地使用一个mmiowb()。

    类似的情况可能发生在一个中断例程和运行在不同CPU上进行通信的两个例程的时候。这样的情况下,应该使用中断禁用锁来保证顺序。

    内核I/O屏障效应

    访问I/O内存时,驱动应使用适当的存取函数:

    • inX(), outX():

      它们都旨在跟I / O空间打交道,而不是内存空间,但这主要是一个特定于CPU的概念。在 i386和x86_64处理器中确实有特殊的I / O空间访问周期和指令,但许多CPU没有这样的概念。

      包括PCI总线也定义了I / O空间,比如在i386和x86_64的CPU 上很容易将它映射到CPU的I / O空间上。然而,对于那些不支持IO空间的CPU,它也可能作为虚拟的IO空间被映射CPU的的内存空间。

      访问这个空间可能是完全同步的(在i386),但桥设备(如PCI主桥)可能不完全履行这一点。

      可以保证它们彼此之间的全序关系。

      对于其他类

  • 你可能感兴趣的:(Docker 介绍: 相关技术(LXC))