怎样诊断Machine-check Exception

Machine Check Exception (MCE) 是CPU发现硬件错误时触发的异常(exception),中断号是18,异常的类型是abort:

MCE18

导致MCE的原因主要有:总线故障、内存ECC校验错、cache错误、TLB错误、内部时钟错误,等等。不仅硬件故障会引起MCE,不恰当的BIOS配置、firmware bug、软件bug也有可能引起MCE。

在 Linux 系统上,如果发生的MCE错误属于可以自动纠正的类型,那么系统保持继续运行,MCE错误日志会记录在一个ring buffer中(这个ring buffer通过设备文件/dev/mcelog来访问),用 mcelog(8) 命令可以读取MCE日志,系统通常会通过cron任务或者mcelog.service把ring buffer中的MCE日志写入/var/log/mcelog文件中。如果发生的MCE错误属于无法恢复的类型,那么系统会panic,错误信息会输出在终端上和message buffer里。

分析MCE需要参考Intel手册第3卷,15章Machine-Check Architecture和16章Interpreting Machine-Check Error Codes。由于MCE在不同型号的CPU上有差异,解读的方法也有不同,第16章是专门解释在不同的CPU型号上如何解读MCE错误码。

每个CPU上有一组寄存器称为 Machine-Check MSR (Model-Specific Register),用于Machine-Check的控制与记录,分为全局寄存器和若干Bank寄存器(CPU的硬件单元分成若干组,每一组称为一个Bank)。当发生MCE时,错误信息记录在全局状态寄存器 MCG_STATUS MSR 和Bank寄存器 MCi_STATUS MSR 中,如下图黄色框所示:

MCE_MSR

分析MCE的方法,就是根据Intel手册解读上述寄存器中记录的错误信息。Linux内核把MCE的信息保存在下面的结构体中:

 

下面的MCE错误信息截取自一台因MCE而crash的机器,我们以此为例来解读一下MCE信息。
注:产生以下信息的内核函数是:
static void print_mce(struct mce *m)
源程序:arch/x86/kernel/cpu/mcheck/mce.c
如果需要的话,阅读源程序可以理解输出的信息与原始数据的对应关系。

其中CPU和Bank是MCE的接收者:

  • CPU 3 – 表示检测到MCE错误的是3号CPU,对应struct mce的extcpu字段;
  • Bank 5 – 一组硬件单元称为一个bank,每个bank对应一组machine-check寄存器;

MCE的错误代码包括两部分:

  • Machine Check Exception: 4 – 表示 IA32_MCG_STATUS MSR寄存器的状态码是4(含义见后文),对应 mcgstatus字段;
  • be00000000800400 – 表示 IA32_MCi_STATUS MSR寄存器中的错误码(含义见后文),对应status字段。

Machine Check Excheption: 4 的含义

它来自全局状态寄存器 IA32_MCG_STATUS MSR,(对应struct mce的 mcgstatus字段),只用到三个bit,如下所示。4表示machine-check in progress。

MCG_status

  • Bit 0: Restart IP Valid. 表示程序的执行是否可以在被异常中断的指令处重新开始。
  • Bit 1: Error IP Valid. 表示被中断的指令是否与MCE错误直接相关。
  • Bit 2: Machine Check In Progress. 表示 machine check 正在进行中。

be00000000800400 的含义

它来自bank寄存器IA32_MCi_STATUS MSR,(对应struct mce的status字段)。

MCi_status

be00000000800400 的二进制位如下:

Bit 63: VAL. 表示本寄存器中包含有效的错误码
Bit 61: UC. 表示是无法纠正的MCE
Bit 60: EN. 表示处于允许报告错误的状态
Bit 59: MISCV. 表示MCi_MISC寄存器中含有对该错误的补充信息
Bit 58: ADDRV. 表示MCi_ADDR寄存器含有发生错误的内存地址
Bit 57: PCC. 表示该CPU的上下文状态已被该错误破坏,无法恢复软件代码的运行
Bits [16:31] 包含特定CPU型号相关的扩展错误码. 本例中是0x0080.
Bits [0:15] 包含MCE错误码,该错误码是所有CPU型号通用的,分为两类:simple error codes(简单错误码) 和 compound error codes(复合错误码),本例中0x0400表示Internal timer error:

– Simple Error Codes:
0000 0000 0000 0000 – 没有错误.
0000 0000 0000 0001 – Unclassified. 未分类的错误类型.
0000 0000 0000 0010 – ROM微码校验错
0000 0000 0000 0011 – MCE是由于别的CPU的BINT# 引起的.
0000 0000 0000 0100 – Functional redundancy check (FRC) master/slave error.
0000 0000 0000 0101 – Internal parity error.
0000 0100 0000 0000 – Internal timer error.
0000 01xx xxxx xxxx – Internal unclassified error. 至少有一个x等于1

– Compound Error Codes:
000F 0000 0000 11LL – Generic cache hierarchy errors.
000F 0000 0001 TTLL – TLB errors.
000F 0000 1MMM CCCC – Memory controller errors (Intel-only).
000F 0001 RRRR TTLL – Memory errors in the cache hierarchy.
000F 1PPT RRRR IILL – Bus and interconnect errors.

下一步,由于Bits [16:31] 是非零值0x0080,包含的是特定CPU型号相关的扩展错误码,我们要参考Intel手册第三卷第16章。首先确定CPU型号,我们需要的是CPU faimily和model,从/proc/cpuinfo中可以找到:

根据cpu family/model,即06_1AH,找到Intel手册中对应的章节,但是没找到匹配Internal timer error和0x0080的条目。所以只能到此为止了。

总结我们的发现:
CPU 3 发现了无法纠正的MCE,是Internal timer error,被中断的指令与MCE不相关,被中断的程序指令不能恢复运行,CPU的上下文已被MCE破坏。

 

怎样禁用MCE

可以在 /boot/grub/grub.conf 中加入以下内容:mce=off。

还有其它的MCE选项,比如禁用CMCI(Corrected Machine Check Interrupt),或者禁用MCE日志,等等,例如:
mce=off  — Disable machine check
mce=no_cmci  — Disable CMCI(Corrected Machine Check Interrupt)
参见文档:Documentation/x86/x86_64/boot-options.txt

 

每个 CPU 都有一个sysfs接口:
/sys/devices/system/machinecheck/machinecheckN
注:(N = CPU number)

其中包含的可调参数参见:Documentation/x86/x86_64/machinecheck

参考资料:
Intel – Intel 64 and IA-32 Architectures Software Developer’s Manual . Chapters 15 and 16.
AMD – AMD64 Architecture Programmer’s Manual Volume 2: System Programming . Chapter 9.

 

NMI是什么

NMI(non-maskable interrupt),就是不可屏蔽的中断。根据Intel的Software Developer手册Volume 3,NMI的来源有两个:
– NMI pin
– delivery mode NMI messages through system bus or local APIC serial bus

NMI通常用于通知操作系统发生了无法恢复的硬件错误,也可以用于系统调试与采样,大多数服务器还提供了人工触发NMI的接口,比如NMI按钮或者iLO命令等。

  1. 无法恢复的硬件错误通常包括:芯片错误、内存ECC校验错、总线数据损坏等等。
  2. 当系统挂起,失去响应的时候,可以人工触发NMI,使系统重置,如果早已配置好了kdump,那么会保存crash dump以供分析。有的服务器提供了NMI按钮,而刀片服务器通常不提供按钮,但可以用iLO命令触发。
  3. Linux还提供一种称为”NMI watchdog“的机制,用于检测系统是否失去响应(也称为lockup),可以配置为在发生lockup时自动触发panic。原理是周期性地生成NMI,由NMI handler检查hrtimer中断的发生次数,如果一定时间内这个数字停顿了,表示系统失去了响应,于是调用panic例程。NMI watchdog的开关是通过内核参数 kernel.nmi_watchdog 或者在boot parameter中加入”nmi_watchdog=1″参数实现,比如:
    在RHEL上编辑 /boot/grub/menu.lst:

    kernel /vmlinuz-2.6.18-128.el5 ro root=/dev/sda nmi_watchdog=1

    然后你会看到:
    # grep NMI /proc/interrupts
    NMI: 0 0 0 0

Linux kernel笼统地把NMI分为三大类:内存校验错 mem_parity_error(),总线数据损坏 io_check_error(),其他的全部归入 unknown_nmi_error()。kernel对NMI是不能精确定位的,对故障诊断很不利,硬件驱动程序可以注册自己的NMI处理例程,kernel会在发生NMI之后通过notify_die()调用这些第三方注册的处理例程。


参考资料:
Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3
http://wiki.osdev.org/Non_Maskable_Interrupt

 

怎样避免mysqld被oom-killer杀死?

前几天我的小小的WordPress服务器被攻击了,被五六台机器从不同的IP发起xmlrpc攻击,大约每秒80次请求的样子,虽然不算猛烈,但我的虚拟机本身太小了,单CPU仅1GB内存,于是频频出现内存紧张的情况,oom-killer会自动选择合适的进程牺牲掉,但它怎么就那么不开眼,每次都选中了最重要的mysqld进程。解决的思路很简单,就是减小mysqld进程的oom_score_adj值,因为oom-killer通过比较每个进程的oom_score来挑选要出局的进程,数值越大就越容易被选中,而手工调整oom_score是通过oom_score_adj来实现的,命令如下:

但是每次reboot或者mysql重启都需要重新设置一遍实在麻烦,怎样才能实现自动化呢?我的做法如下,虽然道理很简单,但实现过程中有好几个坑,包含了几个有用的知识点,所以有点共享价值。

要点一:如何修改systemd的服务脚本

首先,我想在启动mysql服务的时候就自动把 oom_score_adj 调整好,理论上应该通过修改启动脚本完成,问题是我用了CentOS 7,systemd的启动脚本与以前有很大的不同。还是让我们先找到它吧:

显然/usr/lib/systemd/system/mariadb.service就是我们感兴趣的,可以直接修改,但不建议这么做,因为将来升级mariadb有可能会覆盖掉你的改动。更好的方式是这样的:创建一个新目录 “/etc/systemd/system/mariadb.service.d/”,把需要改动的内容放在该目录下的”.conf”文件里,文件名可以随便,但必须以”.conf”结尾。这个文件起的作用是对systemd的service文件做出补充,systemd的service文件包括三个主要部分:[Unit], [Service]和[Install],其中[Service]段定义了服务启停的方法,[Service]段中以下三个字段是最常用的:
ExecStart 用于定义启动服务的命令,
ExecStartPre 是启动服务之前执行的命令,
ExecStartPost 是启动服务之后执行的命令。
我们修改oom_score_adj的任务应该放在ExecStartPost部分,需要注意的是:命令必须使用全路径,因为systemd不提供设置好的PATH环境变量。(注:更多关于systemd的细节可参考Red Hat的系统管理指南

我们只增加了一行有实质内容,即让mariadb.service在启动之后调用自定义的脚本oom_mysql.sh,我们在这个脚本中调整mysqld进程的oom_score_adj值,脚本如下 (因为mariadb会把mysqld的PID记录在/var/run/mariadb/mariadb.pid之中,所以在脚本中我们寻找mysqld进程号便简单了):

要点二:如何设置不用输入密码而且没有tty的sudo

为什么我要通过sudo来执行以上脚本呢?
ExecStartPost=/bin/sudo   /usr/local/bin/oom_mysql.sh
为什么不象下面这样直接执行呢?
ExecStartPost=/usr/local/bin/oom_mysql.sh

这是因为mariadb.service是以mysql用户的身份启动的,mysql用户不具备写入/proc的权限,而在oom_mysql.sh脚本中我们需要写入 /proc/<pid>oom_score_adj

为了使mysql用户能够写入/proc/<pid>/oom_score_adj,我们利用了sudo。修改sudo配置的命令是visudo,如果不加 -f 参数,它默认修改配置文件/etc/sudoers,但直接改动/etc/sudoers不太稳妥,万一改坏了什么地方就有麻烦,所以推荐的方法是:把你要增加的sudoers配置放在/etc/sudoers.d/ 目录下的文件中,文件名可以随意。

我们把要增加的配置放进了4mysql文件里,内容只有两行,需要解释一下:因为mysql用户很特别,它是不能登录的,因为它的shell是/sbin/nologin,而且在自动启动mariadb.service的时候显然也不能依赖人工输入口令去执行sudo,所以它的sudoers配置需要一种特殊的权限–即无需输入口令就可以执行sudo命令,这就是上述第二行 “mysql ALL=(ALL) NOPASSWD:” 的意思;第一行表示mysql用户执行sudo可以无需tty(终端),因为RHEL和CentOS默认情况下sudo需要tty才能执行,否则会发生下列错误:

至此,我们的工作就完成了,最后重启mariadb.service即可:

再检查mysqld,发现它的oom_score_adj已经设置为-100了,正是我们所期望的:

 

理解 %iowait (%wio)

%iowait 是 “sar -u” 等工具检查CPU使用率时显示的一个指标,在Linux上显示为 %iowait,在有的Unix版本上显示为 %wio,含义都是一样的。这个指标常常被误读,很多人把它当作I/O问题的征兆,我自己每隔一段时间就会遇到对 %iowait 紧张兮兮的客户,不得不费尽唇舌反复解释。事实上这个指标所含的信息量非常少,不能单独用来判断系统有没有I/O问题。在此我们详细探讨一下它真正的含义,先从man page上的解释开始:

 Linux:
%iowait
Percentage of time that the CPU or CPUs were idle during
which the system had an outstanding disk I/O request.

HP-UX:
%wio
idle with some process waiting for I/O (only block I/O, raw
I/O, or VM pageins/swapins indicated).

Linux和HP-UX的man page分别从两个角度描述了这个指标:Linux着眼于I/O,强调的是仍有未完成的I/O请求;而HP-UX着眼于进程,强调的是仍有进程在等待I/O。二者所说的是同一件事的两个方面,合在一起就完整了,就是:至少有一个I/O请求尚未完成,有进程因为等待它而休眠。

我们不妨采纳Linux的措辞,%iowait 表示在一个采样周期内有百分之几的时间属于以下情况:CPU空闲、并且有仍未完成的I/O请求。

对 %iowait 常见的误解有两个:一是误以为 %iowait 表示CPU不能工作的时间,二是误以为 %iowait 表示I/O有瓶颈。

第一种误解太低级了,%iowait 的首要条件就是CPU空闲,既然空闲当然就可以接受运行任务,只是因为没有可运行的进程,CPU才进入空闲状态的。那为什么没有可运行的进程呢?因为进程都处于休眠状态、在等待某个特定事件:比如等待定时器、或者来自网络的数据、或者键盘输入、或者等待I/O操作完成,等等。

第二种误解更常见,为什么人们会认为 %iowait 偏高是有I/O瓶颈的迹象呢?他们的理由是:”%iowait  的第一个条件是CPU空闲,意即所有的进程都在休眠,第二个条件是仍有未完成的I/O请求,意味着进程休眠的原因是等待I/O,而 %iowait 升高则表明因等待I/O而休眠的进程数量更多了、或者进程因等待I/O而休眠的时间更长了。“ 听上去似乎很有道理,但是不对:

首先 %iowait 升高并不能证明等待I/O的进程数量增多了,也不能证明等待I/O的总时间增加了。为什么呢?看看下面两张图就明白了。

第一张图演示的是,在I/O完全一样的情况下,CPU忙闲状态的变化就能够影响 %iowait 的大小。下图我们看到,在CPU繁忙期间发生的I/O,无论有多少,%iowait 的值都是不受影响的(因为 %iowait 的第一个前提条件就是CPU必须空闲);当CPU繁忙程度下降时,有一部分I/O落入了CPU空闲的时间段内,这就导致了 %iowait 升高。可见,I/O并没有变化,%iowait 却升高了,原因仅仅是CPU的空闲时间增加了。请记住,系统中有成百上千的进程数,任何一个进程都可以引起CPU和I/O的变化,因为 %iowait、%idle、%user、%system 等这些指标都是全局性的,并不是特指某个进程。

iowait
再往下看第二张图,它描述了另一种情形:假设CPU的繁忙状况保持不变的条件下,即使 %iowait 升高也不能说明I/O负载加重了。
如果2个I/O请求依次提交、使得整个时段内始终有I/O在进行,那么 %iowait 是100%;
如果3个I/O请求同时提交,因为系统有能力同时处理多个I/O,所以3个并发的I/O从开始到结束的时间与一个I/O一样,%iowait 的结果只有50%。
2个I/O使 %iowait 达到了100%,3个I/O的 %iowait 却只有50%,显然 %iowait 的高低与I/O的多少没有必然关系,而是与I/O的并发度相关。所以,仅凭 %iowait 的上升不能得出I/O负载增加 的结论。

iowait

 

这就是为什么说 %iowait 所含的信息量非常少的原因,它是一个非常模糊的指标,如果看到 %iowait 升高,还需检查I/O量有没有明显增加,avserv/avwait/avque等指标有没有明显增大,应用有没有感觉变慢,如果都没有,就没什么好担心的。

 

PREEMPT_ACTIVE标志在内核抢占中的作用

Linux从2.6开始支持内核抢占,意味着即使进程运行在内核态也可以被抢占。为了支持内核抢占,代码中为每个进程的 thread_info 引入了 preempt_count 计数器,数值为0的时候表示可以内核抢占,每当进程持有内核锁的时候把 preempt_count 计数器加1,表示禁止内核抢占。此外还有一些其它禁止内核抢占的场景,也通过 preempt_counter 字段反应出来:preempt_counter 字段是32位的,除了抢占计数器之外还包括其他标志位,只要 preempt_counter 整体不为0,就不能进行内核抢占,这个设计一下子简化了对众多不能抢占的情况的检测:

  • Bit  0- 7: 就是上文中所讲的抢占计数器,表示显式禁用内核抢占的次数;
  • Bit  8-15: 软中断计数器,记录可延迟函数被禁用的次数;
  • Bit 16-27: 硬中断计数器,表示中断处理程序的嵌套数,irq_enter()递增它,irq_exit()递减它;
  • Bit    28: PREEMPT_ACTIVE 标志。

这篇笔记要讲的是 PREEMPT_ACTIVE 标志。PREEMPT_ACTIVE标志的本意是表明正在进行内核抢占,设置了之后preempt_counter就不再为0,从而达到禁止内核抢占的效果,使得执行抢占工作的代码不会被再抢占。它的一个重要用途是防止非Running状态的进程被抢占过程错误地从Run Queue中移除。这句话令人十分费解,已经不处于Running状态的进程本来就不应该留在Run Queue中,为什么要防止它被移除?我用了很长时间才琢磨出来,要回答这个问题,首先要理解为什么非Running状态的进程会被抢占?所谓抢占,就是从一个正在运行的进程手上把CPU抢过来,可是既然进程已经不是Running状态了,怎么会还在CPU上,还被抢占?
这是因为进程从Running变成非Running要经过几个步骤:在把自己放进Wait Queue、状态置成非Running之后,最后调用schedule()把自己从Run Queue中移除、并把CPU交给其他进程。设想一下,一个进程恰好在调用schedule()之前就被抢占了,此时它仍然还在CPU上运行。这就是为什么非Running状态的进程也会被抢占的原因。对这样的进程,抢占流程不能擅自将之从Run Queue中移除,因为它的切换过程没有完成,应该让它有机会自己回头接着做完。比如以下的代码,是一个典型的休眠过程:

如果在第2行被抢占,刚把进程状态设置为TASK_UNINTERRUPTIBLE,本来马上就要测试条件是否满足了,这时被抢占,而抢占过程必定包含调用schedule()的步骤,导致该进程被移出运行队列,失去了运行机会,随后的条件判断语句就无法执行了,假如此时condition条件是满足的,它本来会跳出for循环、而不会去调用schedule()进入休眠,然而却被抢占过程错误地调用schedule()导致它休眠了,也因此错过了那个条件判断语句,也许就永远没有被唤醒的机会了。正确的做法是:进程被抢占后还留在Run Queue中,下次还有机会继续运行,恢复运行后继续判断condition,如果条件不满足,在随后主动调用的schedule()中会被移出运行队列,这是不能由抢占代劳的。

下面我们详细看看PREEMPT_ACTIVE是如何帮助实现上述的正确做法的。在内核里,进程从运行态进入休眠态的最后一步是呼叫调度器schedule()——把自己从Run Queue中移除,把CPU交给其他进程,这在不支持内核抢占的时代没有问题,因为整个过程不会被打断,然而内核抢占的出现使情况变复杂了,现在从运行态进入休眠态的过程可能会被抢占所打断,而抢占过程中会调用schedule(),导致schedule()的调用提前发生,有可能形成race condition。为了避免这种情况,内核抢占过程中不能直接呼叫schedule()调度器,而是呼叫preempt_schedule(),再通过它来调用schedule(),preempt_schedule()会在调用schedule()之前设置PREEMPT_ACTIVE标志,调用之后再清除这个标志。而schedule()会检查这个标志,如果设置了PREEMPT_ACTIVE标志,意味着这是从抢占过程中进入schedule(),对于不是TASK_RUNNING(state != 0)的进程,就不会调用deactivate_task()把进程从Run Queue移除。源码如下:

这段代码的逻辑含义是这样的:
如果设置了PREEMPT_ACTIVE,说明该进程是由于内核抢占而被调离CPU的,这时不把它从Run Queue里移除;如果PREEMPT_ACTIVE没被设置(进程不是由于内核抢占而被调离),还要看一下它有没有未处理的信号,如果有的话,也不把它从Run Queue移除。
总之,只要不是主动呼叫schedule(),而是因被抢占而调离CPU的,进程就还在运行队列中,还有机会运行。

 

 

借助perf工具分析CPU使用率

如果CPU的使用率突然暴涨,如何迅速定位是哪个进程、哪段代码引起的呢?我们需要一个profiling工具,对CPU上执行的代码进行采样、统计,告诉我们CPU到底在忙些什么。

perf 就是这样的工具。我们举个例子看看 perf 是怎样工作的。

首先我们用以下命令模拟出CPU利用率暴涨的现象:

然后我们看到 CPU 1 的 %system 飙升到95%:

现在我们用 perf 工具采样:

注:”-a”表示对所有CPU采样,如果只需针对特定的CPU,可以使用”-C”选项。

把采样的数据生成报告:

我们很清楚地看到,CPU利用率有75%来自 cat 进程 的 sys_read 系统调用,perf 甚至精确地告诉了我们是消耗在 read_zero 这个 kernel routine 上。

POSIX信号量

提起信号量,大多数人首先想到SystemV 信号量,熟悉POSIX信号量的人则相对少一点,下面就对POSIX信号量做一个简单介绍,但在此之前,我们先简单回顾一下SystemV 信号量,以便比较它们的异同。SystemV 信号量是内核对象,由内核统一管理,用户程序通过semget(2)向内核提出申请,内核根据需要创建新的信号量或者关联已存在的信号量,每组信号量都有唯一的ID标识,可以被多个进程共享。PV操作通过调用semop(2)或者semtimedop(2)来进行。因为SystemV 信号量是内核对象,所以它的存续不依赖于进程,而是由内核管理,即使进程退出也仍然存在,要删除信号量必须显式地调用semctl(2)。有两条命令用来管理System V 信号量:ipcs(1)ipcrm(1)ipcs -s 用于查看,ipcrm -s 用于删除。

POSIX信号量与SystemV 不太一样,它分为两类:无名信号量(unnamed semaphore) 和 有名信号量(named semaphore)

有名信号量与System V 信号量类似,它也是由内核统一管理的,但不是用ID作标识,而是用名字作标识,有名信号量的名字有点像文件名,必须是以”/”开头的字符串,但不能包含字符”.”。用户程序通过调用sem_open(3)创建或打开有名信号量。有名信号量的存续也是内核决定的,与进程是否退出没有关系,必须显式调用sem_unlink(3)才能删除。在Linux系统上,有名信号量跟共享内存一样,会在/dev/shm 目录下产生文件,管理有名信号量通过这些文件进行,查看的命令如下:

删除的命令如下:
$ rm /dev/shm/sem.mysem

无名信号量是基于user memory的,通过sem_init(3)创建,调用参数中需要指定放置信号量的内存地址,这个地址可以是私有内存,也可以是共享内存。如果要在多个进程之间共享无名信号量的话,就必须使用共享内存。在实践中,无名信号量一般用于线程之间、或者父子进程之间,否则编程有点麻烦:因为要事先约定好无名信号量的地址才行。由于无名信号量建立在user memory上,它的存续与内存的存续直接相关,如果无名信号量位于进程的私有内存(比如进程的全局变量),那么当该进程终止时无名信号量也会消失;如果无名信号量是在共享内存区中,那么只要该共享内存区还存在,该信号量就存在,而与进程是否中止没有关系。
注:好像没有命令用来查看或管理无名信号量。

POSIX信号量的PV操作通过调用sem_wait(3)和sem_post(3)进行,对无名信号量和有名信号量都一样。

POSIX信号量的API是库函数,而SystemV信号量的API是系统调用(system calls)。如果你用strace(1)跟踪一下,会看到SystemV信号量使用的系统调用是semget(2)/semctl(2)/semop(2)等,而POSIX信号量使用的系统调用是futex(2)。使用POSIX信号量的程序需要链接librt.xxx库,编译时使用-lrt参数。

dmar 与 IOMMU

在分析Linux kernel dump的时候经常会看到一个叫做dmar的东西,查看中断信息的时候也时常见到一个名称为dmar0的设备,到底什么是dmar呢?

大家知道,I/O设备可以直接存取内存,称为DMA(Direct Memory Access);DMA要存取的内存地址称为DMA地址(也可称为BUS address)。在DMA技术刚出现的时候,DMA地址都是物理内存地址,简单直接,但缺点是不灵活,比如要求物理内存必须是连续的一整块而且不能是高位地址等等,也不能充分满足虚拟机的需要。后来dmar就出现了。 dmar意为DMA remapping,是Intel为支持虚拟机而设计的I/O虚拟化技术,I/O设备访问的DMA地址不再是物理内存地址,而要通过DMA remapping硬件进行转译,DMA remapping硬件会把DMA地址翻译成物理内存地址,并检查访问权限等等。负责DMA remapping操作的硬件称为IOMMU。做个类比:大家都知道MMU是支持内存地址虚拟化的硬件,MMU是为CPU服务的;而IOMMU是为I/O设备服务的,是将DMA地址进行虚拟化的硬件。   IOMMUIOMMA不仅将DMA地址虚拟化,还起到隔离、保护等作用,如下图所示意,详细请参阅Intel Virtualization Technology for Directed I/O DMA remapping现在我们知道了dmar的概念,那么Linux中断信息中出现的dmar0又是什么呢? 还是用MMU作类比吧,便于理解:当CPU访问一个在地址翻译表中不存在的地址时,就会触发一个fault,Linux kernel的fault处理例程会判断这是合法地址还是非法地址,如果是合法地址,就分配相应的物理内存页面并建立从物理地址到虚拟地址的翻译表项,如果是非法地址,就给进程发个signal,产生core dump。IOMMU也类似,当I/O设备进行DMA访问也可能触发fault,有些fault是recoverable的,有些是non-recoverable的,这些fault都需要Linux kernel进行处理,所以IOMMU就利用中断(interrupt)的方式呼唤内核,这就是我们在/proc/interrupts中看到的dmar0那一行的意思。 我们看到的中断号48,据此还可以进一步发掘更多的信息:

上面最有意思的信息是ACTION的handler,表示IOMMU发生fault之后的中断处理例程,我们看到的例程名是dmar_fault,源代码如下:

请注意行号1343,dmar_fault_do_one()会报告fault的具体信息,包括对应设备的物理位置。由于一个dmar对应着很多个I/O设备,这条信息可以帮助定位到具体哪一个设备。源代码如下:

 

dmar的初始化是kernel根据ACPI中的dmar table进行的,每一个表项对应一个dmar设备,名称从dmar0开始依次递增,涉及取名的代码如下:

上面也揭示了boot过程留在dmesg中信息的来历:

附注: 过去的AMD64芯片也提供一个功能有限的地址转译模块——GART (Graphics Address Remapping Table),有时候它也可以充当IOMMU,这导致了人们对GART和新的IOMMU的混淆。最初设计GART是为了方便图形芯片直接读取内存:使用地址转译功能将收集到内存中的数据映射到一个图形芯片可以“看”到的地址。后来GART被Linux kernel用来帮助传统的32位PCI设备访问可寻址范围之外的内存区域。这件事新的IOMMU当然也可以做到,而且没有GART的局限性(它仅限于显存的范围之内),IOMMU可以将I/O设备的任何DMA地址转换为物理内存地址。


参考资料:
Linux Kernel的Intel-IOMMU.txt
Intel’s Virtualization for Directed I/O (a.k.a IOMMU)
Wikipedia IOMMU
理解IOMMU、北桥、MMIO和ioremap
Intel Virtualization Technology for Directed I/O

ssh端口转发

ssh是个多用途的工具,不仅可以远程登录,还可以搭建socks代理、进行内网穿透,这是利用它的端口转发功能来实现的。

所谓ssh端口转发,就是在ssh连接的基础上,指定 ssh client 或 ssh server 的某个端口作为源地址,所有发至该端口的数据包都会透过ssh连接被转发出去;至于转发的目标地址,既可以指定,也可以不指定,如果指定了目标地址,称为定向转发,如果不指定目标地址则称为动态转发:

  • 定向转发
    定向转发把数据包转发到指定的目标地址。目标地址不限定是ssh client 或 ssh server,既可以是二者之一,也可以是二者以外的其他机器
  • 动态转发
    动态转发不指定目标地址,数据包转发的目的地是动态决定的

因为ssh端口转发是基于ssh连接的,所以如果ssh连接断开,那么设置好的端口转发也会随之停止。

在设置端口转发之前,必须确认ssh的端口转发功能是打开的。

怎样打开ssh的端口转发功能?

ssh端口转发功能默认是打开的。控制它的开关叫做 AllowTcpForwarding,位于ssh server的配置文件 /etc/ssh/sshd_config 里:
    AllowTcpForwarding yes
如果修改的话需要重启sshd服务才会生效。

怎样设置端口转发?

设置端口转发之前要注意 iptables 设置,确保相应的端口未被屏蔽,如果嫌麻烦的话也可以临时禁用 iptables:
# service iptables stop

定向转发和动态转发的设置方法是不一样的,以下分别介绍。

设置定向转发

定向转发可以把一个 IP:Port 定向映射到另一个 IP:Port,源和目的都必须指定。源地址既可以是 ssh client 的某个端口,也可以是 ssh server 的某个端口:

  • 如果源地址是 ssh client 的某个端口,称为本地转发(Local Port Forwarding),发往 ssh client 指定端口的数据包会经过 ssh server 进行转发;
  • 如果源地址是 ssh server 的某个端口,则称为远程转发(Remote Port Forwarding),发往 ssh server 指定端口的数据包会经过 ssh client 进行转发.

ssh-port-forwarding

设置本地转发:

先看一下基本命令:

在ssh client上执行:
{ssh client}# ssh -g -N -f -o ServerAliveInterval=60 \
-L <local port>:<remote host>:<remote port> username@<ssh server>
参数的含义在后面有解释。

我们以下面的示意图为例:你想telnet连接{remote host},但是无法直达,你只能直接连接ssh client,于是试图通过{ssh client}到{ssh server}这条通道中转:

{you} — {ssh client} — {ssh server} — {remote host}

我们要做的是在{ssh client}上执行以下命令:

{ssh client} # ssh -g -L 2323:<remote-host>:23 username@<ssh-server>

输入口令之后,就跟普通的ssh登录一样,我们进入了shell,在shell中可以正常操作,不同之处是,它同时还把 {ssh client} 的2323端口映射到了{remote host} 的23端口——亦即telnet端口,此后执行”telnet <ssh client> 2323″就相当于”telnet <remote-host>”,只要shell不退出,这个定向转发就一直有效。

  • 注1:如果以上命令不加”-g”选项,那么SSH Client上的监听端口2323会绑定在127.0.0.1上,意味着只有SSH Client自己才能连上。加上”-g”选项之后,SSH Client才允许网络上其他机器连接2323端口。
  • 注2:以上命令会生成一个shell,有时候并不符合我们的需要,因为多数时候我们只想要一个端口转发功能,挂一个shell是个累赘,而且shell一退出,端口转发也停了。这就是为什么我们需要”-N -f”选项的原因:
    -N 告诉ssh client,这个连接不需要执行任何命令,仅做端口转发
    -f 告诉ssh client在后台运行
  • 注3:为了避免长时间空闲导致ssh连接被断开,我们可以加上”-o ServerAliveInterval=60″选项,每60秒向ssh server发送心跳信号。还有一个TCPKeepAlive选项的作用是类似的,但是不如ServerAliveInterval 好,因为TCPKeepAlive在TCP层工作,发送空的TCP ACK packet,有可能会被防火墙丢弃;而ServerAliveInterval 在SSH层工作,发送真正的数据包,更可靠些。
  • 如果不是以root身份设置端口转发的话,转发端口只能使用大于1024的端口号。
设置远程转发:

先看一下基本命令,分为两部分:

在ssh server上:
编辑 /etc/ssh/sshd_config,设置以下内容然后重启sshd服务
    GatewayPorts yes
在ssh client上执行:
{ssh client}# ssh -f -N -o ServerAliveInterval=60 \
-R <ssh server port>:<remote host>:<remote port> username@<ssh server>

这次的实例如下所示,你想用telnet连接{remote host},但是无法直达,于是试图通过{ssh server}到{ssh client}这条通道中转,注意与前面介绍的本地转发的不同之处是,本地转发的案例中你只能直接连接到 ssh client,而这里你只能直接连到 ssh server:

{you} — {ssh server} — {ssh client} — {remote host}

我们要做的是在{ssh client}上执行以下命令:

{ssh client} # ssh -f -N -R 2323:<remote-host>:23 username@<ssh-server>

输入口令之后,{ssh server}的2323端口映射到了{remote host}的23端口——亦即telnet端口,此后执行”telnet <ssh server> 2323″就相当于”telnet <remote-host>”。

本地转发与远程转发的区别与适用场景

定向转发(包括本地转发和远程转发)通常用于内网穿透,本地转发和远程转发的区别就在于监听端口是开在ssh client上还是ssh server上。常见的使用场景是:

  • 如果ssh client在内网里面,ssh server在Internet上,你想让Internet上的机器穿进内网之中,那就使用远程转发;
  • 如果ssh server在内网里面,ssh client在外面,你想穿进内网就应该使用本地转发。
设置动态转发

定向转发(包括本地转发和远程转发)的局限性是必须指定某个目标地址,如果我们需要借助一台中间服务器访问很多目标地址,一个一个地定向转发显然不是好办法,这时我们要用的是ssh动态端口转发,它相当于建立一个SOCKS服务器。

先看一下基本命令:

在ssh client上执行:
{ssh client}# ssh -f -N -o ServerAliveInterval=60 \
-D <ssh client port> username@<ssh server>

实际使用时有两种常见场景:

  • 你把自己的机器(127.0.0.1)当作 sock5 代理服务器:
    {you / 
    ssh client} — {ssh server} — {other hosts}

命令如下:

{ssh client} # ssh -f -N -D 1080 username@<ssh-server>

这种情况下,我们得到的socks5代理服务器是:127.0.0.1:1080,仅供ssh client自己使用。
然后你就可以在浏览器中或其他支持socks5代理的软件中进行设置。

  • ssh client 和 ssh server 是同一台机器,并充当socks5代理:
    {you}
     — {ssh client / ssh server} — {other hosts}

命令如下:

{ssh client} # ssh -f -N -g -D 1080 username@127.0.0.1

这种情况下,我们得到的socks5代理服务器是:
{ssh client IP}:1080,可供网络上其他机器使用,只要能连接ssh client即可。

通过SSH建立的SOCKS服务器使用的是SOCKS5协议,在为应用程序设置SOCKS代理的时候要注意。

用ping测网络延迟要注意的几个因素

ping常被用来测试网络延迟,但是有时ping的延迟并不是网络引起的,所以为了正确理解ping的结果,有必要了解影响ping延迟的几个因素。

ping的原理是通过发送ICMP echo request包,在收到ICMP echo reply包之后,计算发送时间与接收时间之间的差值,得出延迟的时间。ping的输出举例如下:

ping缺省每秒发一个echo request,发包的时候不会输出任何信息,直至收到echo reply的时候才输出一条信息,格式如上,最后一列是延迟时间,ms表示毫秒。

影响ping延迟的因素主要有:

ping延迟包含了进程调度的延迟

由于ping本身是用户态的程序,它首先会受到进程调度的影响,比方说高优先级的进程与ping争抢CPU的话,ping的执行就会遭到拖延,这个调度延迟如果是发生在发包之后、收包之前,就会被计入ping的延迟之中。

多个同时运行的ping进程之间会互相干扰,导致延迟

ping通过raw socket发送和接收ICMP包,而raw socket不仅会收到给自己的包,也会收到给别人的包,假如有多个ping进程同时在运行,你的ping就有可能会收到别人的ping的echo reply,当然,ping程序可以从中挑出给自己的包,因为包里嵌入了对应的ping进程号,但是每个包都打开看看、并判断是不是给自己的——这都要消耗时间的,所以说,多个ping进程之间会互相干扰,导致延迟加大。不同的UNIX版本由于实现方式的差异,受这个因素的影响程度也不一样,比如HP-UX受影响较大,而Linux受影响相对较小,因为Linux采用了一种过滤机制:Linux Socket Filtering,亦即Berkeley Packet Filter (BPF),ping程序利用BPF给raw socket加上一个过滤器,这样内核会只把对应的echo reply传递给ping程序,给其他ping进程的echo reply不会再传给这个ping,避免了CPU和buffer资源的浪费,也减少了ping延迟。

其他类型的ICMP包也会对ping造成干扰

ICMP包有好几种类型,ping希望收到的是ICMP_ECHOREPLY,但是其他类型的包也都会传递给ping,我们上面说过,这是因为ping使用raw socket的缘故,raw socket会看到所有的ICMP包。ping需要消耗额外的时间和资源去查看这些本来不相干的包,故而有可能会产生延迟。以下列出各种ICMP包的类型供参考:

ICMP_ECHO
ICMP_ECHOREPLY
ICMP_SOURCE_QUENCH
ICMP_REDIRECT
ICMP_DEST_UNREACH
ICMP_TIME_EXCEEDED
ICMP_PARAMETERPROB

 注1:在Linux上,虽然ping采用了BPF过滤机制,但是只过滤掉了发给其他ping进程的ICMP_ECHOREPLY包,其他类型的包是不过滤的,所以仍然会受到影响。
注2:在HP-UX上ICMP_SOURCE_QUENCH是最常见的影响ping延迟的因素。Source Quench是一种简陋的流控机制,当接收端有缓冲区满的时候,通过向发送端返回Source Quench,告知发送端降低发送速度,而满溢的缓冲区不一定与ICMP有关,更常见的事UDP的缓冲区。由于这种机制存在种种问题,有许多反对使用Source Quench的声音,比如:RFC6633Linux从2.2起就不再支持ICMP Source Quench了

还有些其它因素,比如网卡驱动,防火墙软件什么的,但比较少见,就不深入探讨了。

怎样判断ping延迟是网络延迟还是其它因素导致的呢?

如果有1秒以上的延迟的话,观察一下icmp_seq,它表示包的顺序,在下例中,第3个包的延迟是2.068秒,而第4个包的延迟只有0.183秒,如果发包的频率严格保持每秒一次的话,第4个包应该比第3个包先收到才对,而下例中,包的顺序并未颠倒,这说明第4个包的发送也被延迟了。发送延迟,那就不是网络延迟了,肯定有其它原因。

还有一个简单的方法可以一试,ping一下127.0.0.1或者ping本机的IP地址,它们不用通过网络,甚至不用进入网卡驱动程序,所以延迟应该非常小,可以作为一个基准值,如果它们的延迟比较大,那最大的可能是存在调度延迟或者ICMP包干扰之类的问题。