本文同步自(最佳显示效果请点击):https://zohead.com/archives/symantec-tape-driver/ 最近需要使用 Symantec Backup Exec、NetBackup 等备份软件测试磁带库,而且 Backup Exec 中已经自带了很多的磁带和磁带库的驱动,但是这些驱动没法独立出来用。特别是在新的 Windows 系统上,如果 Backup Exec 备份软件无法安装,而又找不到磁带驱动,这时就需要专门的 Symantec 的磁带驱动集了。 Symantec 官方网站上有最新版本的 Tape Device Drivers 更新程序,例如: http://www.symantec.com/business/support/index?page=content&id=TECH173790 这个是 Symantec Backup Exec 2010 R3 版本的最新磁带驱动集。 下载之后,你会发现这个更新程序必须在安装了其对应版本的备份软件之后才能运行,不同远程安装到别的机器上,这多少显得非常不便。为此我从 Symantec 官网最新版本的 Tape Device Drivers 程序中导出了所有的驱动和安装程序并打包,此包可以在没有安装 Symantec 软件的情况下,通过设备管理器直接找到解压缩的目录进行磁带驱动安装。如果安装了 Symantec 备份软件,你也可以直接用包中的 tapeinst.exe 程序自动安装更新磁带的驱动。 做这个驱动集合包的步骤也很简单,解压缩 Symantec 安装程序,然后从中得到 Data.cab 之类的压缩包并解压缩(新版本的安装程序中可能是 msp 类型的数据文件,可以用 Universal Extractor 之类的软件进行解压)。解压缩之后会看到一堆文件名很长的文件,例如:F1078_testfile.sy_.1B15BB01_8E2D_4A12_A3E6_175864F3EF67。 实际上这种长的文件是压缩过的驱动程序(原文件名:testfile.sys),只不过文件名上加了点处理,为此我写了个非常简单的脚本进行自动遍历文件并解压缩: 这个脚本用到了 Windows 下的命令,因此需要在 Windows 下通过 Mingw 或者 Cygwin 之类的类 Linux shell 环境来运行。此脚本已在 Symantec 几个不同版本的 Backup Exec、NetBackup 备份软件的 Tape Device Drivers 包下验证过。 脚本会自动判断遍历到的文件是否是压缩的(压缩的文件扩展名名是以下划线结尾的,例如 test.exe 压缩后变为 test.ex_),如果是压缩的,则尝试自动修改后缀名,并通过 Windows 自带的 expand 命令将压缩的文件解压缩到指定的目录中(压缩的文件名 [test.ex_] 变换为正常的文件名 [test.exe]),如果没有识别到压缩文件类型,脚本会将压缩文件名和上面的完整文件名打印输出方便用户手工处理。如果遍历到的文件不是压缩的,那直接拷贝到指定的目录中(文件名也变换为正常的文件名)。 我已经打包好的 Symantec 磁带驱动集可以在这里下载: http://miseal.googlecode.com/files/symantec-tape-drv-v1.0.7z PS: 如果有的磁带未找到驱动,但确认 Symantec 可以支持,你可以自己修改解压缩出来的 vrtstape.inf 配置文件将自己的磁带设备信息加进入。另外 extract-symantec-driver 脚本也已经在压缩包中。这个驱动集主要是方便自己测试使用的,如果有任何问题欢迎指正哦。
Category: Technology
Raspberry Pi初步上手
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/raspberry-pi-start/ 前段时间听到 Raspberry Pi 微型电脑板(中文名称:树莓派)出来的消息,这是一个只有信用卡大小的售价 35 美元的 ARM 板,运行 Linux 系统,支持高清输出和高清视频播放,适合做各种省电的小应用方案,国外现在研究这个的人也很多,不过当时都需要直接从英国订购,发货时间长而且还要扣掉有点“可观”的税费,故没有很快买。 有关 Raspberry Pi 的详细介绍参考这里: http://en.wikipedia.org/wiki/Raspberry_pi 前两天看到 e络盟 网站上已经有了中国区的 Raspberry Pi 直接网上订购服务,在 e络盟 网站上注册账户并忍受其相当不人性化的支付和下订单方式之后,昨天终于收到寄过来的 Raspberry Pi B型板(售价 + 增值税 + 运费一共 300 天朝币多一点)。包装非常简单,只有裸板。 e络盟 上的 Raspberry Pi 订购链接在此,有兴趣的朋友也可以买来折腾试试:http://cn.element14.com/raspberry-pi/raspbrry-pcba/sbc-raspberry-pi-model-b/dp/2081185 从图上也可以看到 Raspberry Pi 算是麻雀虽小五脏俱全滴,板子中间有树莓派的 LOGO。整板使用 Broadcom BCM2835 SoC(集成 CPU、GPU、DSP、SDRAM),处理器为 ARM1176JZF-S 700 MHz,内存 256 MB,有 HDMI 输出(标准接口)、两个 USB 2.0 接口(支持 USB HUB)、一个 SD 卡插槽、一个百兆以太网卡、音频输出、GPIO 调试接口等,而且整个板子的功率只有 3.5W,可通过 MicroUSB 或者 GPIO 供电,一般的 5V 700mA 或以上的充电器就可以给 Raspberry Pi 供电。 由于 Raspberry Pi 只提供裸板,不提供其它一切配件,这些都要自己来弄了。我这没有支持 HDMI 或者 DVI 的高清显示器,只有老式 VGA,故先在网上淘了一个 HDMI 转 VGA 的转接头(需要注意的是 HDMI 转 DVI 再转 VGA 的方式是行不通的哦)。HDMI 转 VGA 是由数字信号转为模拟型号,必须使用数模转换芯片,因此这种转接头经常会有兼容性问题,购买时需要注意是否兼容。我买的是淘宝上的 鑫魔王 HDMI 转 VGA 线,并且特别买了支持音频输出的转接线(免广告之嫌,不给出链接 ^_^)。 Raspberry Pi 只支持从 SD 卡启动,不支持从 USB 启动,但可以启动好之后将 rootfs 放到 U盘上,为最好地兼容和优化考虑,建议使用 Raspberry Pi 官方网站上的 Raspbian “wheezy” 系统(点击 [这里] 进入下载),为此需要 SD 卡最小得有 2G。从官方网站下载系统后可以直接拷贝到 SD 卡中(dd 或直接对拷,不需要分区)。 由于我使用了 HDMI 转 VGA 装置,为了能正确完美的显示,需要先修改 SD 卡启动分区(FAT16 格式)中的 config.txt 配置文件: hdmi_force_hotplug=1 hdmi_drive=2 hdmi_group=2 hdmi_mode=47 disable_overscan=1 hdmi_force_hotplug、hdmi_drive、hdmi_group 都是为了使 HDMI 转 VGA 能正常工作,hdmi_mode 设置显示输出的分辨率(47 为我的显示器用的 1440 x 900 的分辨率,具体请参考 Raspberry Pi 上的帮助),disable_overscan 则是为了去掉显示画面四周的黑色边框。 有关 Raspberry Pi 配置文件的介绍请参考这里:http://elinux.org/RPi_config.txt 把 Raspberry Pi 的 HDMI 通过转接线接到 VGA 显示器,接上 USB 鼠标和键盘,插上网线,安装 SD 卡,然后供电(我实际使用的就是 5V 700mA 的手机充电器,完全没有压力 ^_^),正常的话就可以看到显示器上的启动输出画面咯: 具体的接线方式见下图(比较凌乱,哈哈): 稍等片刻,就会出现终端的登录界面。 直接敲回车登录,默认就是 raspi-config 配置界面,建议先做基本的配置,特别是语言、时区、键盘布局之类的,这些比较重要,而且从 Raspberry Pi 官方下的系统里的配置全都是适合英国的配置,我分别将语言设置和时区为中文、键盘改为美式键盘。 Raspbian “wheezy” 系统是基于 Debian 系统修改的,默认使用的是 DHCP 方式获得 IP,如果没有获得 IP 请手工配置。系统中的 apt 源也已经默认设置为 Raspbian 网站上的源,直接运行 apt-get 命令就可以安装需要的软件包。 在开启图形界面前,建议先使用 apt-get 命令在线安装下面的这些软件包,这对中文显示和输出比较重要: sudo apt-get install ttf-wqy-zenhei sudo apt-get install ttf-wqy-microhei sudo apt-get install fcitx 前两个是安装中文字体(不建议安装 simsun 字体),第三个安装中文输入法。 全部配置好之后就可以运行 startx 命令进入图形界面(图中即为 1440 x 900 的分辨率): 桌面中间又是大大的树莓派 LOGO。Raspbian “wheezy” 系统没有使用 GNOME 或者 KDE 之类的桌面环境,而是使用的 LXDE 轻量级桌面,这样也比较适合 Raspberry Pi 这种小的系统环境。 Raspbian “wheezy” 系统默认也自带了一些编程环境、网络浏览器之类的。下面是浏览中文网页的效果,看起来还是比较完美的(不支持 Flash 之类的就无所谓了 ^_^): 从初步上手使用的效果来看,Raspberry Pi 还是比较令人满意,高清视频播放还没机会测试。下面有机会再来继续折腾了,网上用 Raspberry Pi 做的比较多的是下载机之类的,还有另类的牛人加上了温度传感器之类的东东。 PS:Raspberry Pi 由于只有 256 MB 的内存,因此运行大的程序还是有点卡,而且系统整体的响应速度也和使用的 SD 卡有些关系,未避免影响系统系统,建议使用 class 4 或更高速度的存储卡。
SMB 3.0 over RDMA 性能测试
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/smb3-over-rdma-performance/ Windows Server 2012 (之前的名字就是 Windows Server 8)即将到来,近日看到原来 Windows Server 8 中新增的 SMB 2.2 文件共享协议也有了新的官方名称:SMB 3.0,看看这个介绍说明: http://blogs.technet.com/b/windowsserver/archive/2012/04/19/smb-2-2-is-now-smb-3-0.aspx SMB 3.0 相对于 Windows Server 2003 以及以前的操作系统中的 SMB 1.0 协议增加了很多新的特性: 精简 SMB 1.0 中繁多的命令,减少 ACK 提高效率; 支持流水线机制,可以在上一个 SMB 命令未完成之前发新的命令; 支持符号链接,支持更大的文件块大小提高大块文件读写的性能; 更好的 oplock 机制; 更像真正的文件系统,原来不能安装在 SMB 共享中的程序(例如:SQL Server)也可以使用了; 支持通过 RDMA(Remote Direct Memory Access) 远程直接访问数据提高在 Infiniband 等环境下的性能; 多通道支持,通过多网络通道提高性能,而且支持错误容忍和集群。 刚好旁边有两张 Mellanox 的 Infiniband 卡,顺便就来看看 SMB 3.0 over RDMA 的实际读写性能怎么样咯。由于 SMB 3.0 只有 Windows Server 8 或者 Windows Server 2012 才支持,因此用 Windows Server 8 的测试 ISO 安装并拷贝了一份系统(服务器和客户端都必须支持 SMB 3.0)。 测试环境: 服务器: Intel S5500BC 服务器主板; Intel Xeon E5506 CPU * 1; Kingston DDR3 1066 4G 服务器内存 * 1; Mellanox MHQH29B ConnectX®-2 系列 32Gbps Infiniband 卡(PCI-E x 8 插槽); Adaptec RAID 51645 PCIe SAS RAID卡; WD WD10EVDS 1TB SATA 监控硬盘 * 16; Windows Server 8 Beta Datacenter Build 8250 64位中文版; IPoIP 网卡 IP 地址:192.168.3.196(MTU:4092) 客户端: TYAN S7002 服务器主板; Intel Xeon E5506 CPU * 1; Kingston DDR3 1066 2G 服务器内存 * 1; Mellanox MHQH19B ConnectX®-2 系列 32Gbps Infiniband 卡(PCI-E x 16 插槽); Windows Server 8 Beta Datacenter Build 8250 64位中文版; IPoIP 网卡 IP 地址:192.168.3.172(MTU:4092) 其它环境: 由于没有 Infiniband 交换机,故测试时服务器和客户端的 Infiniband 卡通过 Mellanox MCC4Q30C-003 QSFP 线缆直连,而且客户端的 Infiniband 卡只有一个接口,所以也只测试了单口的性能,没有测试 SMB 3.0 多通道下的性能。 测试软件: IBM Tivoli SANergy(测试大块文件连续读写); Iometer(测试大块文件并发读写); NetPIPE(测试 IPoIB (IP over Infiniband) 的纯粹网络性能); Mellanox 驱动程序中的 IB Tools(启动 SM 并测试纯粹 Infiniband 性能) 测试步骤及结果: 1、在服务器和客户端分别安装 Mellanox Infiniband 卡最新的驱动程序: 虽然 Windows Server 8 中已经自动 Mellanox Infiniband 的驱动,但为了更新 firmware 并能使用上软件 SM(没 Infiniband 交换机滴说 -_-#),必须更新官方的驱动,到下面的网址下载新驱动: http://www.mellanox.com/content/pages.php?pg=products_dyn&product_family=129&menu_section=34 安装 Mellanox OFED for Windows Server 2012,建议安装时按照下面的提示更新卡的 firmware 以免带来不必要的问题: 安装完成之后重启服务器和客户端,你会发现 “网络连接” 里已经有了 Mellanox 的 IPoIB 网卡,但是是 “未连接” 的状态,因为 Infiniband 网络里还没有配置 SM(Subnet Manager)。 先来简单了解下 Infiniband 的 Subnet Manager: Infiniband(IB) 网络中需要使用 Subnet Manager(SM)来初始化 IB 硬件并允许其在 IB 架构中通信。每个 IB 子网必须有至少一个 SM,并且每个 SM 在 IB 架构中要有清楚的 ID。IB 架构就包含已经定义好的子网。IB 交换机和 IB 卡一样有自己的 GUID,主机适配卡(HCA)的端口被称为 port GUID,当一个 HCA 或者它的端口需要与子网中的另一个进行通信就需要分配网络地址,这些网络地址被称为 LID,而 IB 中的 SM 就负责为子网中的成员分配 LID,LID 只是对子网而言的,而 GUID 则是对整个 IB 架构中所有子网相同的。 IB 交换机一般就可以充当 SM 的角色,对于我这没有 IB 交换机的环境,幸好咱们还是有穷人的方法的,使用免费的 OpenSM 软件可以让两个机器中的任意一台做软件 SM。 […]
Linux kernel学习-进程地址空间
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-process-address-space/ 看完 Linux kernel block I/O 层之后来到进程地址空间管理部分,本文中的很多知识和之前的 [进程基本]、[进程调度]、[内存管理] 等章节的知识相关。 1、基础知识: Linux kernel 给每个进程提供的进程地址空间一般是 32 位或 64 位(硬件相关)的平坦地址空间,但进程是没有权限访问这段地址空间中的所有地址的,能访问的一般是很多的内存地址区间。这种内存地址区间被称为内存区域,进程可以动态添加和删除内存区域到它的地址空间中。内存区域可以有不同的权限,相关进程必须遵守这些权限,例如可读、可写、可执行等。如果进程访问的地址不在一个有效的内存区域中,或者访问时的权限不正确,kernel 将会杀掉进程并给出常见的 “Segmentation Fault” 段错误日志。 内存区域通常包括: 可执行文件的代码段,称为 text 段; 可执行文件的已初始化全局变量段,称为 data 段; 未初始化全局变量段(通常以 0 page 填充),称为 bss 段; 进程的用户空间栈(通常以 0 page 填充); 每个共享库文件的额外 text、data、bss 段,也被装入进程的地址空间; 内存映射文件; 共享内存区域; 匿名内存映射(新版本的 malloc 函数就除了 brk 之外也通过 mmap 实现); 应用程序中的堆 2、内存描述符: kernel 使用 mm_struct 内存描述符结构来表示进程的地址空间信息,它定义在 <linux/mm_types.h> 头文件中,这也是一个非常大的结构。 结构的注释中已经包含比较多的注解了哦。mmap 为地址空间的内存区域(用 vm_area_struct 结构来表示啦,也是上面的代码中)链表,mm_rb 则将其以红黑树的形式进行存储,链表形式方便遍历,红黑树形式方便查找。mm_users 为以原子变量形式保护的使用此地址空间的进程数量值(例如:如果有 4 个线程共享此地址空间,则 mm_users 值为 4),mm_count 为引用计数(所有 mm_users 等于一个引用计数),当 mm_count 值为 0 时表示没有再被使用,可以被释放。total_vm 成员表示所有内存区域的数量。 所有的 mm_struct 结构以链表的形式存在 mm_struct 的 mmlist 成员中,该链表的第一个成员就是 init 进程的 mm_struct :init_mm,该链表被 mmlist_lock 锁保护。 进程的内存描述符是在 task_struct 的 mm 成员中的。fork() 进行创建进程时调用 copy_mm 函数将父进程的内存描述符拷贝给子进程,调用 clone() 函数时如果指定 CLONE_VM 参数将使父进程和子进程地址空间共享(实际上将 mm_users 计数加 1),这种子进程就被称为线程。mm_struct 结构一般是通过 alloc_mm 宏从名为 mm_cachep 的 Slab cache 中分配。 进程退出时调用 exit_mm 函数,该函数再调用 mmput() 函数,此函数中减小地址空间的 mm_users 计数,如果 mm_users 变为 0,调用 mmdrop() 函数减小 mm_count 计数,如果 mm_count 变为 0,则最终调用 free_mm() 宏来释放内存描述符(回归到 Slab cache 中)。 另外需要说明的是 kernel 线程是没有地址空间,也就没有对应的 mm_struct(值为 NULL),kernel 线程使用之前运行的进程的内存描述符,有关 kernel 线程请参考之前的 [进程基本] 文章。 3、VMA 概念: vm_area_struct 结构即内存区域常被称为虚拟内存区域(简写为 VMA),表示的是在一个地址空间中的一个连续内存地址区间,每个内存区域是一个惟一的对象。vm_area_struct 中的 vm_mm 成员指向关联的内存描述符,vm_ops 成员为非常重要的关联的操作函数结构,vm_start 为起始地址,vm_end 为结束地址之后第一个字节的地址,即地址范围为:[vm_start, vm_end)。每个 VMA 对于它关联的内存描述符来说是惟一的,因此如果两个单独的进程映射相同的文件到各自的地址空间,它们的 VMA 也是不同的。 VMA 中的 vm_flags 表示内存区域中的页的行为状态,常见的状态有:VM_READ(页可读)、VM_WRITE(页可写)、VM_EXEC(页可被执行)、VM_SHARED(页被共享,被设置了称为共享映射,未设置称为私有映射)、VM_SHM(此区域被用作共享内存)、VM_LOCKED(页被锁)、VM_IO(此区域用于映射设备 I/O 空间)、VM_RESERVED(表示内存区域不可被交换出去)、VM_SEQ_READ(连续读,增强 readahead)、VM_RAND_READ(随机读,减弱 readahead)等。VM_SEQ_READ 和 VM_RAND_READ 标志可以通过 madvise() 系统调用来设置。 看看 vm_ops 操作函数结构的 vm_operations_struct 的定义,它在 <linux/mm.h> 头文件中: 当指定的内存区域被添加到地址空间时,open 函数被调用,反之移除时 close 函数被调用。如果一个不在内存中的页被访问,将触发缺页异常, fault 函数被缺页异常处理函数调用。当一个只读的页变为可写的时候,page_mkwrite 函数也被缺页异常处理函数调用。 mm_struct 中的 mmap 为内存区域链表,通过 VMA 的 vm_next 成员指向下一个内存区域,而且链表中的内存区域是按地址上升排序的,链表中最后一个 VMA 值为 NULL。而对于 mm_struct 的 mm_rb 红黑树,mm_rb 为红黑树的根,每个 VMA 通过其 vm_rb 红黑树节点类型链到红黑树中。 在应用层中可以通过 cat /proc/<pid>/maps 或者 pmap 程序等方法查看应用程序的内存区域列表。 操作 VMA: kernel 提供 find_vma() 函数用于查找指定的内存地址在哪个 VMA 上,它的实现在 mm/mmap.c 文件中,输入参数为内存描述符和内存地址: 如果找不到对应的 VMA 则返回 NULL。需要注意的是返回的 VMA 的开始地址可能比指定的内存地址大。find_vma() 函数返回的结果会被缓存到内存描述符的 mmap_cache 成员中用于提高之后的查找性能,因为后续的操作很可能还是在同样的 VMA 上。如果在 mmap_cache 中找不到则通过红黑树进行查找。 find_vma_prev() 函数与 find_vma() 函数类似,不过它也会返回指定地址之前的最后一个 VMA: struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev) kernel 另外还提供了 find_vma_intersection() 函数返回符合 find_vma() 的条件并且其开始地址不在指定内存结束地址之后的 VMA。 4、mmap 和 munmap: kernel 提供 do_mmap() 函数创建新的线性地址区间,这是用户层 mmap() 函数的底层实现,它用于将一段地址区间添加到进程的地址空间中。 unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset) do_mmap 映射 file 参数指定的文件,并最终返回新创建的地址区间的初始地址。 offset 和 len 指定偏移量和长度。如果 file 为 […]
Linux kernel学习-block层
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-block-layer/ Linux 内核中的 block I/O 层又是非常重要的一个概念,它相对字符设备的实现来说复杂很多,而且在现今应用中,block 层可以说是随处可见,下面分别介绍 kernel block I/O 层的一些知识,你需要对块设备、字符设备的区别清楚,而且对 kernel 基础有一些了解哦。 1、buffer_head 的概念: buffer_head 是 block 层中一个常见的数据结构(当然和下面的 bio 之类的结构相比就差多了哦,HOHO)。 当块设备中的一个块(一般为扇区大小的整数倍,并不超过一个内存 page 的大小)通过读写等方式存放在内存中,一般被称为存在 buffer 中,每个 buffer 和一个块相关联,它就表示在内存中的磁盘块。kernel 因此需要有相关的控制信息来表示块数据,每个块与一个描述符相关联,这个描述符就被称为 buffer head,并用 struct buffer_head 来表示,其定义在 <linux/buffer_head.h> 头文件中。 b_state 字段说明这段 buffer 的状态,它可以是 bh_state_bits 联合(也在上面的代码中,注释说明状态,应该比较好明白哦)中的一个或多个与值。b_count 为 buffer 的引用计数,它通过 get_bh、put_bh 函数进行原子性的增加和减小,需要操作 buffer_head 时调用 get_bh,完成之后调用 put_bh。b_bdev 表示关联的块设备,下面会单独介绍 block_device 结构,b_blocknr 表示在 b_bdev 块设备上 buffer 所关联的块的起始地址。b_page 指向的内存页即为 buffer 所映射的页。b_data 为指向块的指针(在 b_page 中),并且长度为 b_size。 在 Linux 2.6 版本以前,buffer_head 是 kernel 中非常重要的数据结构,它曾经是 kernel 中 I/O 的基本单位(现在已经是 bio 结构),它曾被用于为一个块映射一个页,它被用于描述磁盘块到物理页的映射关系,所有的 block I/O 操作也包含在 buffer_head 中。但是这样也会引起比较大的问题:buffer_head 结构过大(现在已经缩减了很多),用 buffer head 来操作 I/O 数据太复杂,kernel 更喜欢根据 page 来工作(这样性能也更好);另一个问题是一个大的 buffer_head 常被用来描述单独的 buffer,而且 buffer 还很可能比一个页还小,这样就会造成效率低下;第三个问题是 buffer_head 只能描述一个 buffer,这样大块的 I/O 操作常被分散为很多个 buffer_head,这样会增加额外占用的空间。因此 2.6 开始的 kernel (实际 2.5 测试版的 kernel 中已经开始引入)使用 bio 结构直接处理 page 和地址空间,而不是 buffer。 2、bio: 说了一堆 buffer_head 的坏话,现在来看看它的替代者:bio,它倾向于为 I/O 请求提供一个轻量级的表示方法,它定义在 <linux/bio.h> 头文件中。 该定义中已经有详细的注释了哦。bi_sector 为以 512 字节为单位的扇区地址(即使物理设备的扇区大小不是 512 字节,bi_sector 也以 512 字节为单位)。bi_bdev 为关联的块设备。bi_rw 表示为读请求还是写请求。bi_cnt 为引用计数,通过 bio_get、bio_put 宏可以对 bi_cnt 进行增加和减小操作。当 bi_cnt 值为 0 时,bio 结构就被销毁并且后端的内存也被释放。 I/O 向量: bio 结构中最重要的是 bi_vcnt、bi_idx、bi_io_vec 等成员,bi_vcnt 为 bi_io_vec 所指向的 bio_vec 类型列表个数,bi_io_vec 表示指定的 block I/O 操作中的单独的段(如果你用过 readv 和 writev 函数那应该对这个比较熟悉),bi_idx 为当前在 bi_io_vec 数组中的索引,随着 block I/O 操作的进行,bi_idx 值被不断更新,kernel 提供 bio_for_each_segment 宏用于遍历 bio 中的 bio_vec。另外 kernel 中的 MD 软件 RAID 驱动也会使用 bi_idx 值来将一个 bio 请求分发到不同的磁盘设备上进行处理。 bio_vec 的定义也在上面的代码中,同样在 <linux/bio.h> 头文件中,每个 bio_vec 类型指向对应的 page,bv_page 表示它所在的页,bv_offset 为块相对于 page 的偏移量,bv_len 即为块的长度。 buffer_head 和 bio 总结: 因此也可以看出 block I/O 请求是以 I/O 向量的形式进行提交和处理的。 bio 相对 buffer_head 的好处有:bio 可以更方便的使用高端内存,因为它只与 page 打交道,并不直接使用地址。bio 可以表示 direct I/O(不经过 page cache,后面再详细描述)。对向量形式的 I/O(包括 sg I/O) 支持更好,防止 I/O 被打散。但是 buffer_head 还是需要的,它用于映射磁盘块到内存,因为 bio 中并没有包含 kernel 需要的 buffer 状态的成员以及一些其它信息。 3、请求队列: 块设备使用请求队列来保存等待中的 block I/O 请求,其使用 request_queue 结构来表示,定义在 <linux/blkdev.h> 头文件中,此头文件中还包含了非常重要的 request 结构: request_queue 中的很多成员和 I/O 调度器、request、bio 等息息相关。request_queue 中的 queue_head 成员为请求的双向链表。nr_requests 为请求的数量。I/O 请求被文件系统等上层的代码加入到队列中(需要经过 I/O 调度器,下面会介绍),只要队列不为空,block 设备驱动程序就需要从队列中抓取请求并提交到对应的块设备中。这个队列中的就是单独的请求,以 request 结构来表示。 每个 request 结构又可以由多个 bio 组成,一个 request 中放着顺序排列的 bio(请求在多个连续的磁盘块上)。 实际上在 request_queue 中,只有当请求队列有一定数目的请求时,I/O 调度算法才能发挥作用,否则极端情况下它将退化成 “先来先服务算法”,这就悲催了。通过对 request_queue 进行 plug 操作相当于停用,unplug 相当于恢复。请求少时将request_queue 停用,当请求达到一定数目,或者 request_queue 里最 “老” 的请求已经等待一段时间了才将 request_queue 恢复,这些见 request_queue 中的 unplug_fn、unplug_timer、unplug_thresh、unplug_delay 等成员。 4、I/O 调度器: I/O 调度器也是 block 层的大头,它肩负着非常重要的使命。由于现在的机械硬盘设备的寻道是非常慢的(常常是毫秒级),因此尽可能的减少寻道操作是提高性能的关键所在。一般 I/O 调度器要做的事情就是在完成现有请求的前提下,让磁头尽可能少移动,从而提高磁盘的读写效率。最有名的就是 “电梯算法” 了。 […]
Linux kernel学习-内存管理
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-memory-management/ 接着之前的 Linux kernel 学习步伐,来到极其重要的内存管理部分,继续本文内容,需要先了解内存寻址的基础知识,见之前的 [内存寻址] 博文。 1、内存页及内存区域: 正如之前所说,Linux kernel 使用物理页作为内存管理的基本单位,其中重要的线程地址和物理地址的转换操作由页单元 MMU 来完成,系统的页表也由 MMU 来维护。kernel 使用 struct page 来表示一个物理页,它的定义在 include/linux/mm_types.h 头文件中: 其中的 flags 用于表示页的状态(是否为脏或者被锁定等),_count 即为页的引用计数,kernel 一般使用 page_count 宏调用 atomic_read 函数原子的读取此值,page_count 返回 0 表示此页可用。如果一个页被作为 page cache 使用,则 page 的 mapping 字段指向映射的 inode 的 address_space 对象,如果页被作为私有数据(作为 buffer_heads 缓冲、buddy 系统等),则 private 常包含对应的信息。注意其中的 virtual 字段为页的虚拟地址,结合之前的知识,对于高端内存来说,其并没有被固定映射到 kernel 地址空间中,因此如果 virtual 字段为 NULL,则表示此页必须被动态映射。 kernel 使用 page 结构记录系统中的所有页,因此 struct page 的大小应该要尽量小以减少内存占用,另外 kernel 必须知道页是否空闲,如果不空闲则拥有者是谁。 由于实际硬件限制,Linux kernel 不可能使用全部的物理内存,kernel 为此将内存划分为不同的区域,一个区域中的内存属性应该也相同。kernel 中常见的内存区域有 ZONE_DMA(可用于 DMA 的页)、ZONE_DMA32(与 ZONE_DMA 类似,但只对 32 位设备可用)、ZONE_NORMAL、ZONE_HIGHMEM(并没有被固定映射的高端内存区域),这些内存区域一般都是硬件相关的,例如在 x86 架构下,ZONE_DMA 的范围为 0MB - 16MB,ZONE_HIGHMEM 为高于 896MB 的物理内存,而在 x86_64 架构下 ZONE_HIGHMEM 则为空。需要注意的是内存的分配不会跨域这些不同的内存区域。内存区域在 kernel 中由 struct zone 结构来表示,其中的 name 字段即为内存区域名称。 2、获取页: 分配和释放内存是 Linux kernel 中极其重要又用的极多的接口。先看看 kernel 提供的直接获取以内存页面为单位的 alloc_pages 函数: struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) 此函数是最基本的用于分配大小为 2^order 并且连续的物理页的函数,其返回分配到的第一个页面的 page 指针。 来看看比较重要的 gfp_t 类型的 gfp_mask 值: gfp_t 实际上就是 unsigned int 类型,gfp_mask 常用于指定行为方式、区域方式、类型等信息。常见的行为方式标志有:__GFP_WAIT(标志分配器可以睡眠,明显不适用于中断上下文中)、__GFP_IO(分配器可以启动磁盘 I/O)等。区域方式指定内存从哪里分配,对应的就有:__GFP_DMA、__GFP_DMA32、__GFP_HIGHMEM(从高端内存或普通内存中分配)。类型标志则用于简化分配时的指定操作,常见的有:GFP_ATOMIC(高优先级并不可睡眠,常用于中断、中断下半部、持有自旋锁等环境中)、GFP_NOIO(表示分配可中断但不可以发起 I/O 操作)、GFP_NOFS(分配时不可发起文件 I/O 操作)、GFP_KERNEL(最常见的分配标志,常用于可以睡眠的进程上下文中)、GFP_USER(用于分配内存给用户进程)、GFP_DMA 等。 需要注意的是对 __get_free_pages 和 kmalloc 函数(下面会分别说明)不能指定 __GFP_HIGHMEM 标志,因为它们都是直接返回的虚拟地址,而非 page 结构指针,如果指定了 __GFP_HIGHMEM,则他们可能分配到的内存并没有被映射到 kernel 地址空间,因此这样得不到虚拟地址。只有 alloc_page 函数可以分配高端内存,这个限制在下面的 __get_free_pages 函数的实现中可以看到。 使用 page_address 函数可以将 page 指针转换为虚拟地址(非物理地址)。实际使用中经常会用到 __get_free_pages 函数直接在分配页时直接得到虚拟地址,其参数为 alloc_pages 完全一样,看看它的实现就一目了然了: 另外 kernel 还 “好心” 的提供了两个只分配一个页的函数:alloc_page 和 __get_free_page,可以想象只是把 order 参数设为 0 而已。你可以使用 get_zeroed_page 函数分配一个页并自动清零(gfp_mask 指定 __GFP_ZERO)。 对应的释放页可以用 __free_pages(page 指针为参数)、free_pages(虚拟地址为参数)、free_page(只释放一个页)这些函数。 下面是常用的分配非整数倍页大小的内存的函数。首先是最常用的 kmalloc 函数: void *kmalloc(size_t size, gfp_t flags) kmalloc 用于分配最少指定的 size 字节大小的内存(实际分配的可能比 size 多),这与用户空间的 malloc 函数很相似,但需要注意的是 kmalloc 分配的内存物理地址是连续的,这非常重要。 相应的释放内存函数是 kfree: void kfree(const void *objp) kfree 用于释放 kmalloc 分配的内存,注意如果使用 kfree 在不是的 kmalloc 分配的内存地址或者已经 kfree 过的地址上,都可能导致 kernel 出错。 紧接着就是大名鼎鼎的 vmalloc 函数了。它与 kmalloc 类似,但它分配的内存只是虚拟连续的而物理地址却不一定连续,这也类似于用户空间的 malloc 函数的效果。vmalloc 由于需要做页表转换之类的操作,性能比 kmalloc 差,而且 vmalloc 得到的页还必须由单独的页来做映射,对 TLB 缓存的效率也会有影响(有关 TLB 缓存参考之前的文章 [内存寻址]),由于这些原因,vmalloc 在 kernel 中用到的机会并不是很多,其常用于分配大量的内存,常见的一个例子就是内核模块的代码就是通过 vmalloc 加载到 kernel 中的。vmalloc 的原型为: void * vmalloc(unsigned long size) 与之对应的,使用 vfree 释放分配的内存。另外 vmalloc 和 vfree 都是可以睡眠的,因此它们对中断上下文是不适用的。 3、Slab分配器: Slab 也是 Linux kernel 中非常重要的组成部分,它用于简化内存的分配和释放,它相当于一个可用内存列表,里面包含一堆已经分配好的数据结构,当 kernel 需要分配一个数据结构时,可以直接从这个可用内存列表中取出而节省分配的时间,不需要的时候又可以还给这个列表而不需要释放,因此这个列表用于缓存经常访问的某种类型的数据。为了统一管理和释放,Linux kernel 引入 Slab 分配器作为通用的数据结构缓存层给经常访问的数据结构使用。需要说明的是 kmalloc 就是在 Slab 分配器基础上实现的。 这里简单对 Slab 分配器做个介绍,有关其细节请参考这篇 PDF 文档: The Slab Allocator: An Object-Caching Kernel Memory Allocator Slab 层将不同的对象划分到名为 cache 的不同组中,每个组存储不同类型的数据,也就是每种数据类型都有一个 cache。每个 cache 然后被划分为多个 slab,slab 由一个或多个连续的物理页组成(通常只有一个页),每个 slab 又包含一些数量的对象,也就是实际缓存的数据。每个 slab 的状态可以是这三个中的一个:满、部分满、空。当 kernel 请求一个新对象时,优先从状态为 部分满 的 slab 中取,如果没有则从状态为 […]
Windows Server 8 ReFS 测试初探
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/windows-server-8-refs-basic-test/ 微软最新的 Windows Server 8(现在已改名为 Windows Server 2012,坑爹的命名,汗下)中支持了微软引入的新文件系统 ReFS 用于替换存在了 N 年了的 NTFS 文件系统。大家可能依稀还记得当前 Longhorn(其实就是后来的 Vista 操作系统的开发代号) 操作系统发布时传闻要引入的新文件系统 WinFS 被阉割的消息,到了 2012 年终于推出了新文件系统 ReFS。 ReFS 全名为 Resilient File System(弹性文件系统),按照微软官方的说明,ReFS 相对 NTFS 主要的改进有: 1、改进磁盘上的数据结构,ReFS 也跟着主流文件系统的脚步使用 B+ 树来存储元数据和实际数据了,文件大小、卷大小等的限制也还是 64 位,元数据和实际数据被以类似关系数据库形式的表组织起来,文件名和路径长度的限制也扩大为 32KB 的 Unicode 字符串; 2、元数据增加校验和,增强数据的验证和自动更正处理,支持 COW 实现可靠磁盘数据更新; 3、扩大卷大小、文件大小、文件夹大小; 4、数据条带提高性能,并支持错误容忍; 5、可以在不同的计算机间共享存储池以增强容错性和负载平衡; 6、也是非常重要的,对 NTFS API 及使用方式上的很大程度上的兼容,包括 ACL、符号链接、挂载点、oplock 等,但也有一些 NTFS 的特性例如 文件压缩、硬链接、用户磁盘配额 等没有被支持。 有关 ReFS 的详细原理及说明请参考这个 MSDN 的 blog: http://blogs.msdn.com/b/b8/archive/2012/01/16/building-the-next-generation-file-system-for-windows-refs.aspx 需要注意的是 Windows 7 及之前的系统都不支持 ReFS,而且 Windows 8 似乎也只有服务器版本才支持。借着前几天下载安装的 Windows Server 8 测试版本系统笔者就先简单测试下 ReFS 相对 NTFS 的性能,由于我这实际应用时以顺序读写为主,先做顺序读写性能对比。 测试环境: Intel S5500BC 服务器主板; Intel Xeon 5506 CPU * 1; Kingston DDR3 1066 4G 服务器内存 * 1; Adaptec RAID 51645 PCIe SAS RAID卡; WD WD10EVDS 1TB SATA 监控硬盘 * 16; Windows Server 8 Beta Datacenter Build 8250 64位中文版; Iometer 1.1.0-rc1 Windows 64位版本 16 个 1TB 的 SATA 盘建 RAID0,RAID0 的块值为 256KB,并启用 read 和 write cache。然后在 Windows Server 8 中的磁盘阵列设备上分别建 2TB 大小的简单卷,格式化为 NTFS 和 ReFS,分别测试其连续读写性能。 在磁盘管理里新建分区格式化时就可以看到 ReFS 的选项: 测试结果: 测试项目 ReFS(MB/s) NTFS(MB/s) 64KB 连续写 192.85 225.02 64KB 连续读 230.79 227.82 256KB 连续写 388.94 417.79 256KB 连续读 495.48 548.49 1MB 连续写 690.77 738.42 1MB 连续读 965.39 821.82 4MB 连续写 860.45 858.28 4MB 连续读 1129.54 1092.30 从测试结果可以看到,ReFS 相对 NTFS 来说在小一些的块值的连续读写上并没有什么优势,而在 RAID0 的整个 stripe 条带大小(256KB * 16 = 4MB)的读写性能上则确实比 NTFS 有一点改进。 另外说下 ReFS 暂时的不足之处: 1、Windows 8 虽然已经支持 ReFS,但并没有 NTFS 和 ReFS 之间转换的工具,你如果需要从 NTFS 切换到 ReFS,暂时只能手工中转拷贝了; 2、不支持从 ReFS 引导启动系统; 3、可移动磁盘或者 U 盘之类的设备不能使用 ReFS; 4、不支持文件压缩和用户磁盘配额,虽然这两个用的可能不是非常多,但还是有点不可想象; 5、NTFS 中原来支持的扩展属性 ReFS 也不再支持,有一些软件会依赖这个,感觉会有些不便。 总之现在看起来微软新引入的 ReFS 并没有像 ZFS 那样的新文件系统(支持 RAID-Z、原生快照、128位文件系统等特性)那样吸引人,如果要吸引用户转移到 ReFS 感觉还是任重道远,不知道 Windows Server 2012 最终版本中的 ReFS 到底有何改善。 本文章中介绍的只是测试了连续读写的性能,ReFS 重点支持的容错等功能下次有机会时再来测试,文中有任何问题欢迎指正咯。 ^_^
Linux kernel percpu变量解析
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-percpu-variable/ Linux 2.6 kernel 中的 percpu 变量是经常用到的东西,因为现在很多计算机都已经支持多处理器了,而且 kernel 默认都会被编译成 SMP 的,相对于原来多个处理器共享数据并进行处理的方式,用 percpu 变量在 SMP、NUMA 等架构下可以提高性能,而且很多情况下必须用 percpu 来对不同的处理器做出数据区分。 本文以 kernel 中的 softirq 为例简单说下 percpu 变量,我们先来看看 kernel 中唤醒 ksoftirqd 的实现,ksoftirqd 在 ps 命令看到的进程列表中很容易找到,是每个处理器都有一个(如果有 4 个处理器,则有 4 个 kernel 线程名称分别从 ksoftirqd/0 到 ksoftirqd/3),关于 softirq 本身的实现不在本文讨论范围内,唤醒 ksoftirqd 的实现在 kernel/softirq.c 文件中: 这里就用到了 percpu 变量 ksoftirqd,它是通过 DEFINE_PER_CPU 宏来进程定义的 percpu task_struct 列表,通过 __get_cpu_var 宏来得到相应处理器的 ksoftirqd/n 的 task_struct,然后调用 wake_up_process 函数唤醒进程(也就是 ksoftirqd/n kernel 线程),关于 wake_up_process 等进程调度的相关实现在之前的日志中有介绍的,请参考 [这里]。 __get_cpu_var、DEFINE_PER_CPU 等 percpu 宏的实现在 include/linux/percpu.h、include/asm-generic/percpu.h 等头文件中。先看看 include/asm-generic/percpu.h 中的一些定义: 通常所有的 percpu 变量是一起存放在特定的 section 里的,像上面头文件中的 .data.percpu 基础 section( 当然非 SMP 系统下就是 .data 了)、.shared_aligned、.first section。使用 objdump 可以看到编译 kernel 时的 vmlinux 文件的 section(结果没有完全显示): 可以看到 vmlinux 文件中的 .data 和 .data.percpu section。 percpu 变量的地址实际上就是其在上面说到的 section 里的偏移量,这个偏移量还要加上特定处理器的偏移量(也就是上面头文件中的 per_cpu_offset、my_cpu_offset 等)得到最终的变量地址,并最终以指针引用的方式得到值,这样访问的效果就有点类似于访问全局变量了。percpu 变量通常用于更新非常频繁而访问机会又相对比较少的场合,这样的处理方式可以避免多处理器环境下的频繁加锁等操作。 从上面的注释也可以看到 per_cpu_offset 是在一个 percpu 变量上增加的偏移量,大多数系统架构下使用 __per_cpu_offset 数组来作为偏移量,而 x86_64 等架构下处理方式则不同。my_cpu_offset 是在调用 per_cpu_offset 时使用 smp_processor_id() 得到当前处理器 ID 作为参数,__my_cpu_offset 则是用 raw_smp_processor_id() 的值作为 per_cpu_offset 的参数(smp_processor_id() 在抢占被关闭时是安全的)。SHIFT_PERCPU_PTR 宏用于给指针增加偏移量,它使用的 RELOC_HIDE 宏在不同的编译器下实现不同,在 include/linux/compiler.h 头文件中,看看 gcc 编译下的处理: 可以看到 gcc 中使用内嵌汇编先将 ptr 值赋给 __ptr(unsigned long 类型),然后在 __ptr 基础上增加偏移量,这样可以避免编译报错,ptr 值不变而且最终以 ptr 指定的类型来返回。 include/asm-generic/percpu.h 头文件中定义了 per_cpu、__get_cpu_var、__raw_get_cpu_var、this_cpu_ptr、__this_cpu_ptr 等几个常用的宏。per_cpu 就用于得到某个指定处理器的变量,__get_cpu_var 用于得到当前处理器的 percpu 变量值。 再来看看 DEFINE_PER_CPU 的实现,它在 include/linux/percpu-defs.h 头文件中: 使用 DEFINE_PER_CPU 宏可以静态的定义 percpu 变量。__PCPU_ATTRS 指定输入的 section 类型,DEFINE_PER_CPU_SECTION 用于在特定的 section 上定义特定类型的变量。__typeof__ 和 上面见到的 typeof 是一样的,都用于获取 type 的数据类型。__attribute__((section(xxx))) 表示把定义的变量存储在指定的 section 上。DEFINE_PER_CPU 就用于定义在 PER_CPU_BASE_SECTION section 上(从最开始的代码中也可以看出非 SMP 时用 .data 段,SMP 时用 .data.percpu 段)。 然后是 get_cpu_var 宏的实现,它在 include/linux/percpu.h 头文件中: get_cpu_var 会先禁止抢占然后调用 __get_cpu_var 得到 percpu 变量值。put_cpu_var 则重新启用抢占。 另外在 include/linux/percpu.h 等文件中还定义了 alloc_percpu 和 free_percpu 宏来动态定义和释放 percpu 变量,他们都是通过 percpu memory allocator 来实现的,在 mm/percpu.c 中,动态分配的 percpu 变量可以通过 per_cpu_ptr 宏来得到,为此 kernel 还引入了 this_cpu_ptr、this_cpu_read 等一系列相关机制用寄存器替代内存提高对 percpu 变量的访问速度,关于 percpu memory allocator 等信息以后再来详细分析了。 以上为个人分析结果,有任何问题欢迎指正咯 ^_^
Linux kernel kfifo分析
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-kfifo/ kfifo 是 Linux kernel 中的一个通用队列实现,对于 kernel 中常见的 FIFO 队列应用还是很有用的,本文主要简单介绍分析下 Linux kernel kfifo。实际上 ChinaUnix 上有个 kfifo 的分析文章,但已经比较老(基于 Linux 2.6.10),而且我现在用的 2.6.34 版本 kernel 中 kfifo 实现有很多改动,故自己简单写下,ChinaUnix 上的 kfifo 介绍帖子在这里: http://bbs.chinaunix.net/thread-1994832-1-1.html kfifo 定义在 include/linux/kfifo.h 头文件中,我们经常使用的就是 kfifo 结构,看看它的定义: kfifo 也像其它队列那样提供了两个主要操作:入队列(in) 和 出队列(out),对应于上面结构中的 in 和 out 两个偏移量,in 偏移量为下次入队列的位置,out 为下次出队列的位置,很容易也能想到 out 值必须小于等于 in 值,当 out 值等于 in 值时表示队列为空,kfifo 中 buffer 为队列的空间,size 为空间大小,必须为 2 的 k 次幂值(原因在下面说明)。当然如果 in 值等于队列长度了,就表示队列已经满了。 先看看 kfifo 最简单的一些操作实现,在 kernel/kfifo.c 文件中: 调用 kfifo_alloc 可以自动分配空间并初始化,你也可以调用 kfifo_init 函数使用自己的空间来初始化队列,可以看到这两个函数中都用 is_power_of_2 做了检查队列空间的操作。kfifo_free 释放队列,它会调用 _kfifo_init 函数(参数为 NULL 和 0 清空队列),调用 kfifo_reset 可以重置队列(将 in 和 out 都设为 0)。你也可以用 DECLARE_KFIFO 和 INIT_KFIFO 静态定义一个 kfifo 队列,尽管这不太会被用到。 调用 kfifo_in 函数将数据加入队列,kfifo_out 将数据从队列中取出并从队列中删除(增加 out 值),Linux 还提供了 kfifo_out_peek 函数从队列中取数据但并不删除(不增加 out 值)。kfifo_in 中会先调用 __kfifo_in_data 将输入加入队列,然后调用 __kfifo_add_in 增加 in 的值,kfifo_out 相反则调用 __kfifo_out_data 和 __kfifo_add_out 函数取出数据并删除。 kfifo 中同时提供了 kfifo_from_user 函数用户将用户空间的数据加入到队列中,它会先调用 __kfifo_from_user_data 将用户空间的数据加入队列,再调用 __kfifo_add_in 增加 in 的值。看看 __kfifo_from_user_data 的实现: 可以看到 __kfifo_from_user_data 中是直接调用 copy_from_user 将用户空间的数据拷贝到 kfifo 队列的空间中。相应的也有 kfifo_to_user 函数将队列中的数据取出到用户空间的地址,他就调用 copy_to_user 将队列中数据拷贝到用户空间。 需要注意的是 __kfifo_from_user_data 中用到的 __kfifo_off 函数: __kfifo_off 是根据指定的偏移量得到索引值,由这里也可以看出为什么队列的大小为什么必须是 2 的 k 次幂值,否则无法得到正确的值。而且从代码中可以看到 __kfifo_from_user_data、__kfifo_in_n、__kfifo_in_rec 等函数中都用到了 __kfifo_off 函数指定加入队列时的偏移量。 另外从 include/linux/kfifo.h 中你也可以看到新的 kfifo 实现中默认 EXPORT 了非常多的 API 函数给 kernel 开发者使用。 以上为个人分析结果,有任何问题欢迎指正哦 ^_^
Linux kernel学习-进程调度
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-process-scheduling/ 接着上面进程基本概念的文章,进程调度器决定系统中什么进程需要运行,运行多长时间。Linux kernel 实现的是抢占式的时间片调度方式,而不是进程主动让出时间片的方式。 Linux 从 2.5 开始使用名为 O(1) 的调度器,它解决了 2.4 及之前早期的调度器中很多设计上就存在的问题,O(1) 就表示该算法可以在常数时间内完成工作。在 ULK3 对应的 2.6.11 内核中仍然在使用此调度器,它对于很大的服务器负载的情况是很理想的,但由于 O(1) 调度器对于延迟敏感的程序(被称为交互式进程)而言却有缺陷,因此从 2.6.23 内核开始 Linux 引入一种调度器类的框架,并且默认使用一种新的调度器:Completely Fair Scheduler(CFS)完全公平调度器,鉴于历史的车轮在前进着,本文就主要讨论 CFS 调度器了。 进程通常可以分为两类:I/O密集型 和 计算密集型。I/O密集型进程花费更多的时间在等待 I/O 请求上(不一定是磁盘I/O,也可以是键盘、网络 I/O 等),大多数的 GUI 程序都是 I/O密集型进程。计算密集型的进程则要求运行频率小些但运行时间更多,像各种加解密程序和 MATLAB 这种就是典型的计算密集型进程。一个好的调度策略应该能同时满足低延迟和高吞吐量,Linux 调度器会采取偏向I/O密集型进程的策略。 Linux kernel 实现了两种独立的进程优先级:一种是 nice 值,从 -20 到 +19,默认值为 0,越大的值表示优先级越低(表示你对其它进程更加 “nice”,哈哈),nice 值在所有 Unix 系统中是一个通用的进程优先级范围,运行 ps -el 可以看到进程的 nice 值;第二种是可配置的实时优先级,范围从 0 到 99,越大的值表示优先级越高,实时进程比普通进程的优先级高,Linux 根据 POSIX.1b Unix 标准实现了实现了实时优先级,运行 ps 时增加 rtprio 参数可以在 RTPRIO 栏中看到实时优先级(如果值为 - 表示不是实时进程)。 Linux 2.6.34 默认的 CFS 完全公平调度器并不像传统调度器那样,根据 nice 绝对值为相应的进程分配固定的时间片,它没有明确的时间片概念,而是根据每个进程的 nice 相对差异值作为权重得到进程可以运行的时间在处理器时间中的比例。CFS 设置了一个预定的 targeted latency 值作为调度持续时间来根据比例计算时间片,当然此值越小越接近完全公平。假设 targeted latency 值为 20 毫秒,系统中有两个进程 nice 值分别为 0 和 5,根据权重计算出来的时间片分别为 15 和 5 毫秒,当两个进程为 10 和 15 时,计算出来的仍然为 15 和 5 毫秒,因为 nice 值的相对差异值并没有变。在系统中进程不是特别多时,CFS 调度器可以做到接近完全公平,而进程数量特别多甚至接近无限时,每个进程获得的时间片将非常小,为了避免进程切换导致的开销,CFS 又规定了一个 minimum granularity 值作为每个进程最小的时间片,默认为 1 毫秒,也即即使进程无限,每个进程也最少能运行 1 毫秒的时间,因此进程特别多时 CFS 就不会那么公平了。 1、CFS调度器: CFS 调度器实现在 kernel/sched_fair.c 文件中,这在上面一篇博文:进程基本 中有简单的介绍的。CFS 使用 sched_entity 调度实体结构,task_struct 中就有这个成员,看看 sched_entity 的定义,它定义在 include/linux/sched.h 头文件中: 可以看到此结构中下面很大一部分是开启了 CONFIG_SCHEDSTATS 之后才有用的。其中 vruntime 为进程的虚拟运行时间(实际运行时间经可运行的进程个数进行权重计算后的结果),在理想的 CFS 环境中,处理器都处于理想状态,所有同级别的进程的 vruntime 值应该都相同。但实际上多处理器不能做到完美多任务,CFS 调度器就用 vruntime 记录进程的运行时间并得到它应当还要运行多长时间。 再看到下面会用到的 cfs_rq 运行队列属性的定义,在 kernel/sched.c 中定义: cfs_rq 中的 curr 字段即指向当前队列上正在运行的实体(如果没有则为 NULL 了),rq 字段即为 CPU 运行队列。 来看看 sched_entity 的 vruntime 是如何在 update_curr 函数中更新的: update_curr 会被系统定时器周期性的调用,进程转变为可运行或不可运行状态时都会被调用,而 update_curr 本身则调用 __update_curr 增加实际运行时间和 vruntime 虚拟运行时间。 由于实际情况下,每个进程的 vruntime 并不会像理想状况那样完全一样,CFS 调度器在需要调度时从运行队列里中取 vruntime 最小的那个进程来运行。CFS 调度器使用一个红黑树来管理可运行进程的列表,并用于快速查找最小的 vruntime 进程。 红黑树在 Linux 中被称为 rbtree,可以用于存储任意数据的节点,由特定的关键字来标识。sched_entity 调度实体中的 run_node 就是一个红黑树节点,cfs_rq 中的 rb_leftmost 即是红黑树最左边的节点(缓存在 cfs_rq 结构中以加快访问速度,这样可以避免遍历红黑树),最小的 vruntime 进程就在此节点上,如果找不到此进程(返回 NULL),CFS 唤醒 idle 任务。 看看将进程加到红黑树的实现: enqueue_entity 中更新了当前进程的 vruntime,并最终调用 __enqueue_entity 将进程加到红黑树中。__enqueue_entity 中先通过遍历找到正确位置,遍历过程中就能确定红黑树中最左边的节点是什么,然后设置红黑树中节点左右信息,调用 rb_link_node 添加节点,必要时更新 cfs_rq 中保存的最左边的节点缓存。 好吧,看了添加过程再看从红黑树中删除进程: 同样 dequeue_entity 先调用 update_curr 更新当前进程的 vruntime,删除的实际操作由 __dequeue_entity 来完成。__dequeue_entity 中判断如果最左边节点正是要删除的进程,必须更新最左边的节点缓存,然后调用 rb_erase 删除节点。 Linux 中进程调度入口是 schedule() 函数,定义在 kernel/sched.c 中。对于一个进程,schedule() 会先查找最高优先级的调度器类并调用此调度器类中的函数进行调度。看看 pick_next_task 的实现: 首先由于 Linux 普通进程默认使用 CFS 调度器,pick_next_task 先判断是不是所有进程都在 CFS 调度器中,如果是就直接调用 CFS 的 pick_next_task 函数节省遍历时间。sched_class_highest 的定义在上面也可以看到,就是 RT 调度器类,for_each_class 用于遍历调度器类。而其它类中找不到时,idle 调度器类的 pick_next_task 就返回一个有效 task_struct。在 CFS 调度器中 pick_next_task 会调用 pick_next_entity 函数选择下一个运行的进程。 2、睡眠与唤醒: 进程需要睡眠时,kernel 的处理大体如下:进程将自己标记为睡眠状态,将自己加到等待队列,从记录可运行进程的红黑树上删除自己,调用 schedule() 选择新进程来运行,schedule() 会调用 deactivate_task() 函数将进程从运行队列中移除。唤醒的过程则相反:进程被设置为可运行,从等待队列删除,重新加回到红黑树中。 等待队列在 kernel 中以 wait_queue_head_t 表示,定义在 include/linux/wait.h 头文件中,它其实是一个 __wait_queue_head 结构,下面的头文件中有一些经常用到的声明: 需要注意的是每个等待队列都需要可以在中断时被修改,因此操作等待队列之前必须获得一个自旋锁。而在实际使用中等待时需要处理竟态条件,为此 kernel 定义了几个很好用的等待条件的宏,为调用者减少操作,这些宏也是定义在上面的文件中。常用的有 wait_event 根据条件在队列上无限等待,wait_event_timeout 相对加了超时处理,它是调用 schedule_timeout 进行调度,wait_event_interruptible 即在等待时进程是可以响应信号之类的唤醒。 内核中的 completion 完成量机制也是基于等待队列的,用于等待某一操作结束。__wait_queue 结构的 task_list 成员通过双链表链接到 __wait_queue_head 中,__wait_queue 中的 private 成员指向等待进程的 task_struct。__wait_queue 中 […]