Linux direct io使用

2012年5月16日 没有评论 18 次浏览

在linux 2.6内核上使用direct io不难,只需按照如下几点来做即可:
1,在open文件时加上O_DIRECT旗标,这样以通告内核我们想对该文件进行直接io操作。
2,在源文件的最顶端加上_GNU_SOURCE宏定义,或在编译时加在命令行上也可以,否则将提示:

direct_io_write_file.c: In function 'main':
direct_io_write_file.c:25: error: 'O_DIRECT' undeclared (first use in this function)
direct_io_write_file.c:25: error: (Each undeclared identifier is reported only once
direct_io_write_file.c:25: error: for each function it appears in.)

3,存放文件数据的缓存区起始位置以及每一次读写数据长度必须是磁盘逻辑块大小的整数倍,一般也就是512字节(也有可能是一内存页大小,4096),否则将导致read/write失败,perror将提示:read failed: Invalid argument或write failed: Invalid argument。
1和2很容易做到,而第3点,要满足缓存区起始位置与512对齐,这可以在进行缓存区空间申请时使用posix_memalign这样的函数指定512对齐:

ret = posix_memalign((void **)&buf, 512, BUF_SIZE);

或者进行手动调节对齐:

real_buf = malloc(BUF_SIZE + 512);
aligned_buf = ((((unsigned int)real_buf + 512 - 1) / 512) * 512);

由于要满足每一次读写数据长度必须是磁盘逻辑块大小的整数倍,所以最后一次文件操作可能无法满足,此时只能重新以cached io模式打开文件后,fseek到对应位置进行剩余数据的读写。
为什么要使用direct io?因为direct io不过文件系统缓冲,也就是说对文件进行direct io操作不会减少系统的free内存数量,这对于自己本身带有缓存的应用程序(比如数据库)比较有用。
示例:

/**
 * gcc cached_io_write_file.c -o cached_io_write_file -D_GNU_SOURCE
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#define BUF_SIZE 1024

int main(int argc, char * argv[])
{
	int fd;
	int ret;
	unsigned char *buf;
	ret = posix_memalign((void **)&buf, 512, BUF_SIZE);
	if (ret) {
		perror("posix_memalign failed");
		exit(1);
	}
	memset(buf, 'c', BUF_SIZE);

	fd = open("./direct_io.data", O_WRONLY | O_CREAT, 0755);
	if (fd < 0){
		perror("open ./direct_io.data failed");
		free(buf);
		exit(1);
	}

	do {
		ret = write(fd, buf, BUF_SIZE);
		if (ret < 0) {
			perror("write ./direct_io.data failed");
		}
	} while (1);

	free(buf);
	close(fd);
}

Linux GRUB挂起的可能修复方法

2012年5月16日 没有评论 20 次浏览

使用ghost32从gho镜像转换后生成的vmdk虚拟硬盘怎么起不来(vmware和kvm都不行),现象就是进入到GRUB后处于挂起状态,关于这个问题在http://plosquare.blogspot.com/2010/05/troubleshooting-grub-hangs.html有详细的描述,我这里的情况如下图:

根据blogspot上的解释是GRUB尝试从root分区加载stage2时失败导致。具体原因暂且不管,先尝试修复grub试试。
把这个ghost.vmdk虚拟硬盘挂载到另外的一台正常的vm虚拟机上,启动这台正常vm虚拟机后,查看ghost.vmdk对应磁盘的情况:

[root@localhost ~]# fdisk -l /dev/sda

Disk /dev/sda: 17.2 GB, 17179869184 bytes
255 heads, 63 sectors/track, 2088 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0xd97b4a8c

   Device Boot      Start         End      Blocks   Id  System
/dev/sda1   *           1           5       32768   83  Linux
Partition 1 does not end on cylinder boundary.
/dev/sda2               5           7       16384   83  Linux
Partition 2 does not end on cylinder boundary.
/dev/sda3               7         211     1638400   83  Linux
Partition 3 does not end on cylinder boundary.
/dev/sda4             211        2089    15087616    f  W95 Ext'd (LBA)
/dev/sda5             211         364     1228800   83  Linux
/dev/sda6             364         494     1048576   83  Linux
/dev/sda7             495         625     1048576   83  Linux
/dev/sda8             625        1102     3832832   83  Linux
/dev/sda9            1103        1111       65536   83  Linux
/dev/sda10           1111        1596     3895296   83  Linux
/dev/sda11           1596        2089     3960832   83  Linux
[root@localhost ~]#

把boot分区mount上来:

[root@localhost ~]# mkdir -p /home/hd/sda1
[root@localhost ~]# mount /dev/sda1 !$
mount /dev/sda1 /home/hd/sda1
[root@localhost ~]# cd !$
cd /home/hd/sda1
[root@localhost sda1]# ls
config-2.6.18-164.el5      memtest.bin                System.map-2.6.37.2.old  vmlinuz-2.6.30.ori
grub                       message                    System.map-2.6.37.6+     vmlinuz-2.6.37
initrd-2.6.18-164.el5.img  symvers-2.6.18-164.el5.gz  System.map-2.6.37.old    vmlinuz-2.6.37.2
initrd-2.6.30.img          System.map                 System.map-2.6.38.8      vmlinuz-2.6.37.2.old
initrd-2.6.37.2.img        System.map-2.6.18-164.el5  System.map-2.6.38.8.old  vmlinuz-2.6.37.6+
initrd-2.6.37.6+.img       System.map-2.6.30          vmlinuz                  vmlinuz-2.6.37.old
initrd-2.6.37.img          System.map-2.6.30.old      vmlinuz-2.6.18-164.el5   vmlinuz-2.6.38.8
initrd-2.6.38.8.img        System.map-2.6.37          vmlinuz-2.6.30           vmlinuz-2.6.38.8.old
lost+found                 System.map-2.6.37.2        vmlinuz-2.6.30.old
[root@localhost sda1]#

先查看磁盘映像:

[root@localhost sda1]# cd grub/
[root@localhost grub]# cat device.map
(fd0)	/dev/fd0
(hd0)	/dev/sda

hd0对应/dev/sda,而刚好我的ghost.vmdk对应在/dev/sda上,所以这里无需改动,如果ghost.vmdk对应在/dev/sdb(/dev/sdc、/dev/sdd类似)上,那么这里需要vi device.map,将其中的“(hd0) /dev/sda”改为“(hd0) /dev/sdb”,否则执行下一步将会提示“/dev/sda does not have any corresponding BIOS drive”这样的错误,改完并执行下一步后再改回/dev/sda即可,因为在只有一块硬盘的情况下,hd0对应的就是/dev/sda。
执行grub修复命令:

[root@localhost grub]# grub-install --no-floppy  --root-directory=/home/hd/sda1/ /dev/sda
Installation finished. No error reported.
This is the contents of the device map /home/hd/sda1//boot/grub/device.map.
Check if this is correct or not. If any of the lines is incorrect,
fix it and re-run the script `grub-install'.

(fd0)	/dev/fd0
(hd0)	/dev/sda
[root@localhost grub]#

关闭虚拟机,把ghost.vmdk挂载到原来虚拟机里重新启动,顺利进入系统,修复成功。

利用vm/kvm启动ghost镜像

2012年5月12日 没有评论 41 次浏览

目前,kvm还不能直接启动ghost镜像,因为qemu支持的磁盘格式里还没有包含ghost,不过可以看到qemu已经支持vmdk,而利用symantec ghost工具恰好又能将ghost镜像转换为vmdk格式,所以虽然有点扭曲,不过利用kvm启动ghost镜像还是可行的。
首先,下载symantec ghost 11.5以上版本,如果在windows 32位环境里,那么可以在cmd里执行如下命令

d:\gqk>ghost32 -clone,mode=restore,src=ylmf.GHO,dst=ylmf.vmdk -batch -sure

应该有人看得出来,ylmf.GHO是雨林木风的绝作,xp的ghost镜像,这里就以它为例。目标后缀必须是vmdk,貌似ghost32是以这个后缀名来做识别的。转换后的vmdk默认是ide接口的,可以通过使用-VMDKADAPTER=ide或-VMDKADAPTER=lsiLogic或-VMDKADAPTER=busLogic来指定具体接口类型,vmdk的大小也可以通过-VMDKSIZE=16384来指定(这里指定为16GB),更具体的参数可以使用如下命令查看:

d:\gqk>ghost32 -clone,mode=restore,src=ylmf.GHO,dst=ylmf.ide.vmdk -batch -sure -VMDKADAPTER=ide -VMDKSIZE=16384
d:\gqk>ghost32 -clone,mode=restore,src=ylmf.GHO,dst=ylmf.lsiLogic.vmdk -batch -sure -VMDKADAPTER=lsiLogic -VMDKSIZE=16384
d:\gqk>ghost32 -clone,mode=restore,src=ylmf.GHO,dst=ylmf.busLogic.vmdk -batch -sure -VMDKADAPTER=busLogic -VMDKSIZE=16384
d:\gqk>ghost32.exe /?


结束后,自然将生成ylmf.vmdk,试试kvm启动的情况,把ylmf.vmdk拷贝到支持kvm的Linux机器上,执行:

[root@localhost ghost]# qemu-system-x86_64 -hda ylmf.vmdk -net none -m 1024 -daemonize -cpu host -smp 2 -vnc :1

vnc远程连接查看,很不幸,在进行Windows XP安装时宕机了:


嘛,出现这种情况在所难免,不过试了试用VMware Workstation启动这个ylmf.vmdk,从安装到启动一切都OK:

可能是kvm虚拟硬件也许并不在ylmf.GHO支持之内,或其他什么原因就不过于去追踪了,不过这里至少可以看出利用这种方法是可以做到利用kvm启动ghost镜像的。
如果vmdk文件很大,可以利用qemu-img将其转换为qcow2格式,一般情况下,qcow2将会变小:

qemu-img convert ylmf.vmdk -O qcow2 ylmf.qcow2

利用KVM调试内核

2012年5月12日 没有评论 31 次浏览

虽然kvm运行的虚拟机也是host的一个进程,但是却不能像UML那样直接gdb attach到对应的进程进行调试,毕竟kvm和uml完全不同,如果那样做的话,你会发现你attach的只是qemu-system-x86进程:

(gdb) bt
#0  0x00007f8dba022ed2 in select () from /lib64/libc.so.6
#1  0x00007f8dbdd2118a in ?? () from /usr/local/bin/qemu-system-x86_64
#2  0x00007f8dbdd1a798 in main () from /usr/local/bin/qemu-system-x86_64
(gdb)

要用gdb调试kvm虚拟机内核,需要借助qemu-system-x86的两个选项:

-s              shorthand for -gdb tcp::1234
-S              freeze CPU at startup (use 'c' to start execution)

选项-s使得可以通过gdb远程连接qemu进行调试,而-S将让kvm虚拟机停止在执行第一条内核镜像代码的地方,等待gdb连接,如果没有-S选项,那么kvm不等待:

[root@localhost kvm]# qemu-system-x86_64 -hda vdisk.img -net none -m 1024 -daemonize -cpu host -smp 2 -vnc :1 -s

gdb可以通过127.0.0.1:1234或:1234(gdb在本机执行)或192.168.1.1:1234(gdb在另外的机器执行,而kvm host机器ip为192.168.1.1),假设在本host执行gdb命令:

[root@localhost kvm]# gdb
GNU gdb Fedora (6.8-37.el5)
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
(gdb) target remote :1234
Remote debugging using :1234
[New Thread 1]
Remote 'g' packet reply is too long: d85f8780ffffffff88f58680ffffffff00000000000000000000000000000000180000000000000020fb7c80ffffffff40318880ffffffff205f8780ffffffff000000000000000063c3dd712e00000072feff00000000004bb52180ffffffffb76ddbb66ddbb66d20748b80ffffffffc09c8b80ffffffff0000000000000000241c2280ffffffff4602000010000000180000001800000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f03000000000000000000000000000000000000000000000000000000000000000000000000e03f00000000000000007b14ae47e17a843f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a01f0000
(gdb)

如果出现上面这种情况,需要先执行:set architecture i386:x86-64:intel,我的kvm客户机是x86-64:

[root@localhost ~]# uname -a
Linux localhost.localdomain 2.6.30-gentoo-r8 #55 SMP Thu May 10 20:05:44 CST 2012 x86_64 x86_64 x86_64 GNU/Linux

,使得gdb知道远程系统的架构:

(gdb) set architecture i386:x86-64:intel
The target architecture is assumed to be i386:x86-64:intel
(gdb) target remote :1234
Remote debugging using :1234
[New Thread 1]
0xffffffff80221c24 in ?? ()
(gdb)

加载对应的kvm客户机内核镜像,当然是未压缩的(务必选中内核选项:[*] Compile the kernel with debug info和[*] Compile the kernel with frame pointers):

(gdb) file /tmp/vmlinux
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from /tmp/vmlinux...done.
(gdb) bt
#0  native_safe_halt () at /usr/src/linux-2.6.37.2/arch/x86/include/asm/irqflags.h:51
#1  0xffffffff80211e41 in default_idle ()
    at /usr/src/linux-2.6.37.2/arch/x86/include/asm/paravirt.h:802
#2  0xffffffff8020ab67 in cpu_idle ()
    at /usr/src/linux-2.6.37.2/arch/x86/kernel/process_64.c:149
#3  0xffffffff8061ab0d in rest_init () at /usr/src/linux-2.6.37.2/init/main.c:474
#4  0xffffffff808adcda in start_kernel () at /usr/src/linux-2.6.37.2/init/main.c:701
#5  0xffffffff808ad2a7 in x86_64_start_reservations (
    real_mode_data=<value optimized out>)
    at /usr/src/linux-2.6.37.2/arch/x86/kernel/head64.c:123
#6  0xffffffff808ad39f in x86_64_start_kernel (
    real_mode_data=0x93050 <Address 0x93050 out of bounds>)
    at /usr/src/linux-2.6.37.2/arch/x86/kernel/head64.c:94
#7  0x0000000000000000 in ?? ()
(gdb)

加个__schedule断点:

(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
native_safe_halt () at /usr/src/linux-2.6.37.2/arch/x86/include/asm/irqflags.h:51
51	/usr/src/linux-2.6.37.2/arch/x86/include/asm/irqflags.h: No such file or directory.
	in /usr/src/linux-2.6.37.2/arch/x86/include/asm/irqflags.h
(gdb) b __schedule
Breakpoint 1 at 0xffffffff80636792: file /usr/src/linux-2.6.37.2/kernel/sched.c, line 5022.
(gdb) c
Continuing.
[New Thread 4]
[Switching to Thread 4]

Breakpoint 1, __schedule () at /usr/src/linux-2.6.37.2/kernel/sched.c:5022
5022	/usr/src/linux-2.6.37.2/kernel/sched.c: No such file or directory.
	in /usr/src/linux-2.6.37.2/kernel/sched.c
(gdb) bt
#0  __schedule () at /usr/src/linux-2.6.37.2/kernel/sched.c:5022
#1  0xffffffff80636f51 in schedule () at /usr/src/linux-2.6.37.2/kernel/sched.c:5084
#2  0xffffffff8020ab88 in cpu_idle ()
    at /usr/src/linux-2.6.37.2/arch/x86/kernel/process_64.c:159
#3  0xffffffff80632a4e in start_secondary (unused=<value optimized out>)
    at /usr/src/linux-2.6.37.2/arch/x86/kernel/smpboot.c:329
#4  0x0000000000000000 in ?? ()
(gdb)

利用命令q退出gdb时,如果导致kvm虚拟机终止,此时需先执行detach命令,后再退出gdb:

(gdb) q
The program is running.  Exit anyway? (y or n) n
Not confirmed.
(gdb) detach
Ending remote debugging.
(gdb) q
[root@localhost kvm]#

对于内核调试的一个十分有利帮助是串口的使用,kvm虚拟机的串口可以这样添加:

[root@localhost kvm]# qemu-system-x86_64 -hda vdisk.img -net none -m 1024 -daemonize -cpu host -smp 2 -vnc :1 -s -serial file:serial.log

这将在当前目录下生成一个serial.log的文件,kvm虚拟机的串口输出将重定向到这个文件内,比如给kvm虚拟机的内核加上串口输出选项(console=ttyS0,115200)后,kvm虚拟机的内核信息将输出到这个文件:

[root@localhost kvm]# ls serial.log -lh
-rw-r----- 1 root root 21K May 11 16:56 serial.log
[root@localhost kvm]#

还可以将kvm虚拟机的串口重定向到一个tcp监听口:

[root@localhost kvm]# qemu-system-x86_64 -hda vdisk.img -net none -m 1024 -daemonize -cpu host -smp 2 -vnc :1 -s -serial tcp::1235,server
QEMU waiting for connection on: tcp:0.0.0.0:1235,server

执行qemu-system-x86_64后进行等待链接状态,在本机可以执行(当然,你需要另开一个终端):

[root@localhost ~]# telnet 127.0.0.1 1235

在另外的机器,那么可执行(前面已交代这里kvm host机器的ip为192.168.1.1):

[root@localhost ~]# telnet 192.168.1.1 1235

之后,kvm虚拟机的串口输出将都打印在telnet上,并且此时可通过这个串口通道登陆kvm虚拟机。
另外,发现一个问题就是通过windows上的VNC Viewer 4远程连接到kvm虚拟机,进入grub后键盘就无响应,任何对内核选项的上下选择、编辑或启动都失效,此时无法做任何操作,只能在host机器内kill qemu-system-x86_64。如果在升级内核,这非常不方便,但值得庆幸的是qemu-system-x86_64支持直接在外部指定内核镜像(具体可以参考qemu-system-x86_64 –help):

[root@localhost kvm]# qemu-system-x86_64 -hda vdisk.img -net none -m 1024 -daemonize -cpu host -smp 2 -vnc :1 -kernel vmlinuz-2.6.18-194.el5 -initrd initrd-2.6.18-194.el5.img
[root@localhost kvm]#

所以,在装好最初的kvm虚拟机后立马把这两个文件备份到host机器来,这样如果后续捣鼓其它内核出了问题还能通过这种方法进入kvm虚拟机内进行修复(也许还可以利用其它工具,比如http://libguestfs.org/来进行,不过毕竟不是直接手段而比较麻烦)。
kvm虚拟机模块的调试要麻烦一点,首先需要在gdb里主动加载对应模块的符号,并且要加载到正确的位置。模块的代码位置可以在kvm虚拟机使用如下命令查看:

[root@localhost ~]# cat /proc/modules
igb 84012 0 - Live 0xffffffffa0007000
dca 6468 1 igb, Live 0xffffffffa0000000
[root@localhost ~]#

只加载了两个模块,以igb模块为例,在host机内的gdb内执行add-symbol-file,其中/tmp/igb.ko是kvm虚拟机的igb模块文件,拷贝到host机器内的,而0xffffffffa0007000是从上面/proc/modules文件内看到的:

(gdb) add-symbol-file /tmp/igb.ko 0xffffffffa0007000
add symbol table from file "/tmp/igb.ko" at
	.text_addr = 0xffffffffa0007000
(y or n) y
Reading symbols from /tmp/igb.ko...done.
(gdb) c
Continuing.

设置一个igb模块内的igb_clean_tx_irq函数断点试试,马上断下来了(因为我这里使用了igb ssh远程连接),看来没什么问题:

(gdb) b igb_clean_tx_irq
Breakpoint 2 at 0xffffffffa000a5a8
(gdb) c
Continuing.
[New Thread 2]
[Switching to Thread 2]

Breakpoint 2, 0xffffffffa000a5a8 in igb_clean_tx_irq ()
(gdb) bt
#0  0xffffffffa000a5a8 in igb_clean_tx_irq ()
#1  0xffffffffa000c19e in igb_msix_tx ()
#2  0xffffffff8027cb92 in handle_IRQ_event (irq=27, action=0xffff88003e18bf40)
    at /usr/src/linux-2.6.37.2/kernel/irq/handle.c:371
#3  0xffffffff8027e9f0 in handle_edge_irq (irq=27, desc=0xffff88003e6a85c0)
    at /usr/src/linux-2.6.37.2/kernel/irq/chip.c:514
#4  0xffffffff8020de43 in handle_irq (irq=27, regs=<value optimized out>)
    at /usr/src/linux-2.6.37.2/include/linux/irq.h:312
#5  0xffffffff8020d6a1 in do_IRQ (regs=0xffff88003f89de18) at /usr/src/linux-2.6.37.2/arch/x86/kernel/irq.c:215
#6  0xffffffff8020c453 in common_interrupt ()
#7  0xffff88003f89de40 in ?? ()
#8  0x0000000000000000 in ?? ()
(gdb) c
Continuing.

如果不执行对应的add-symbol-file命令,那么将会这样:

(gdb) b igb_clean_tx_irq
Function "igb_clean_tx_irq" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n

就算选择y,后续也不能捕获到该断点。

Linux 2.6.36正式加入的RPS RFS特性介绍

2012年5月9日 没有评论 38 次浏览

之前写在CU的博客上的文章,今天又看了看,转过来。
Linux 2.6.35于2010年8月1号发布,新增特性比较多,而其中最引我注意的为第一点:Transparent spreading of incoming network traffic load across CPUs
关于此点改进的详细介绍可以查看LWN上的两篇文章:”Receive packet steering” and “Receive flow steering“。
下面我就自己的理解来做一下阐述,不当之处,多多包涵。
首先是Receive Packet Steering (RPS)
随着单核CPU速度已经达到极限,CPU向多核方向发展,要持续提高网络处理带宽,传统的提升硬件设备、智能处理(如GSO、TSO、UFO)处理办法已不足够。如何充分利用多核优势来进行并行处理提高网络处理速度就是RPS解决的课题。
以一个具有8核CPU和一个NIC的,连接在网络中的主机来说,对于由该主机产生并通过NIC发送到网络中的数据,CPU核的并行性是自热而然的事情:

问题主要在于当该主机通过NIC收到从网络发往本机的数据包时,应该将数据包分发给哪个CPU核来处理(有些具有多条接收队列和多重中断线路的NIC可以帮助数据包并行分发,这里考虑普通的NIC,普通的NIC通过RPS来模拟实现并行分发):

普通的NIC来分发这些接收到的数据包到CPU核处理需要一定的知识智能以帮助提升性能,如果数据包被任意的分配给某个CPU核来处理就可能会导致所谓的“CACHELINE-PINGPONG”现象:
比如DATA0数据流的第一个包被分发给CPU0来处理,第二个包分发给CPU1处理,第三个包又分发给CPU0处理;而DATA1数据流恰好相反。这样的交替轮换(8核情况交替得更随意)会导致CPU核的CACHE利用过分抖动。

RPS就是消除这种CPU核随意性分配的智能知识,它通过数据包相关的信息(比如IP地址和端口号)来创建CPU核分配的hash表项,当一个数据包从NIC转到内核网络子系统时就从该hash表内获取其对应分配的CPU核(首次会创建表项)。这样做的目的很明显,它将具有相同相关信息(比如IP地址和端口号)的数据包都被分发给同一个CPU核来处理,避免了CPU的CACHE抖动现象,提高处理性能。
有两点细节:第一,所有CPU核具有等同的被绑定几率,但管理员可以明确设置CPU核的绑定情况;第二,hash表项的计算是由NIC进行的,不消耗CPU。
RPS的性能优化结果为大致可提升3倍左右。tg3驱动的NIC性能由90,000提升到285,000,而e1000驱动的NIC性能由90,000提升到292,000,其它驱动NIC也得到类似的测试结果。

接下来是Receive flow steering (RFS)
RFS是在RPS上的改进,从上面的介绍可以看到,通过RPS已经可以把同一流的数据包分发给同一个CPU核来处理了,但是有可能出现这样的情况,即给该数据流分发的CPU核和执行处理该数据流的应用程序的CPU核不是同一个:

不仅要把同一流的数据包分发给同一个CPU核来处理,还要分发给其‘被期望’的CPU核来处理就是RFS需要解决的问题。
RFS会创建两个与数据包相关信息(比如IP地址和端口号)的CPU核映射hash表:
1.一个用于表示期望处理具有该类相关信息数据包的CPU核映射,通过recvmsg()或sendmsg()等系统调用信息来创建该hash表(称之为期望CPU表)。比如运行于CPU0核上的某应用程序调用了recvmsg()从远程机器host1上获取数据,那么NIC对从host1上发过来的数据包的分发期望CPU核就是CPU0。
2.一个用于表示最近处理过具有该类相关信息数据包的CPU核映射,称这种表为当前CPU表。该表的存在是因为有多线程的情况,比如运行在两个CPU核上的多线程程序(每个核运行一个线程)交替调用recvmsg()系统函数从同一个socket上获取远程机器host1上的数据会导致期望CPU表频繁更改。如果数据包的分发仅由期望CPU表决定则会导致数据包交替分发到这两个CPU核上,很明显,这不是我们想要的效果。
既然CPU核的分配由两个hash表值决定,那么就可以有一个算法来描述这个决定过程:
1.如果当前CPU表对应表项未设置或者当前CPU表对应表项映射的CPU核处于离线状态,那么使用期望CPU表对应表项映射的CPU核。
2.如果当前CPU表对应表项映射的CPU核和期望CPU表对应表项映射的CPU核为同一个,那么好办,就使用这一个核。
3.如果当前CPU表对应表项映射的CPU核和期望CPU表对应表项映射的CPU核不为同一个,那么:
a)如果同一流的前一段数据包未处理完,那么必须使用当前CPU表对应表项映射的CPU核,以避免乱序。
b)如果同一流的前一段数据包已经处理完,那么则可以使用期望CPU表对应表项映射的CPU核。
算法的前两步比较好理解,而对于第三步可以用下面两个图来帮助理解。应用程序APP0有两个线程THD0和THD1分别运行在CPU0核和CPU1核上,同时CPU1核上还运行有应用程序APP1。首先THD1调用recvmsg()获取远程数据(数据流称之为FLOW0),此时FLOW0的期望CPU核为CPU1,随着数据块FLOW0 : 0的到来并交给CPU1核处理,此时FLOW0的当前CPU核也为CPU1。如果此时THD0也在同一个socket上调用recvmsg()获取远程数据(数据流同样也是FLOW0),那么FLOW0的期望CPU核就改变为CPU0(当前CPU核仍然为CPU1)。与此同时,NIC收到数据块FLOW0 : 1,如何将该数据块分发给CPU核就到了上面算法的第三步。
分情况判断:
1.如果同一流的前一段数据包FLOW0 : 0未处理完,那么必须使用当前CPU核CPU1来处理新到达的数据块,以避免乱序。如下图所示(FLOW1流是应用程序APP1的):

2.如果同一流的前一段数据包FLOW0 : 0已经处理完毕,那么可以使用期望CPU核CPU0来处理新到达的数据块。如下图所示(FLOW1流是应用程序APP1的):

RFS适用于面向流的网络协议,它能更好的提升CPU的CACHE效率,不论是内核还是应用程序本身。RFS的性能优化结果在普通环境下为大致可提升3倍左右,而在多线程环境大致可提升2倍。能通过软件的形式提升机器网络带宽性能自然也是再好不过的事情了。