Category: Technology

Linux kernel学习-进程基本

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-process/ Linux 中进程通过 fork() 被创建时,它差不多是和父进程一样的,它得到父进程的地址空间拷贝,运行和父进程一样的代码,从 fork() 的后面开始执行,父进程和子进程共享代码页,但子进程的 data 页是独立的(包括 stack 和 heap)。 早期的 Linux kernel 并不支持多线程的程序,从 kernel 来看,一个多线程的程序只是一个普通的进程,它的多个执行流应该完全在 user mode 来完成创建、处理、调度等操作,例如使用 POSIX pthread 库。当然这样的实现是无法让人满意的,Linux 为此使用轻量级进程为多线程程序提供更好的支持,两个轻量级进程可以共享资源(例如:地址空间、打开的文件等等),一个比较简单的方法是将为每个线程关联一个轻量级进程,这样每个线程可以被 kernel 单独调度,使用 Linux 轻量级进程的库有:LinuxThreads、NPTL、NGPT 等。Linux kernel 同时也支持线程组(可以理解为轻量级进程组)的概念。 1、进程描述符: 进程描述符由 task_struct 结构来表示,一般来说,每个可以被独立调度的执行上下文都必须有自己的进程描述符,因此尽管轻量级进程共享了很大一部分 kernel 数据结构,它也必须有自己的 task_struct。task_struct 中包含关于一个进程的差不多所有信息,它定义在 include/linux/sched.h 文件中,你会看到这是非常大的结构,其中还包含指向其它结构的指针。访问进程自身的 task_struct 结构,使用宏操作 current。 task_struct 中的 struct mm_struct *mm 即指向进程的地址空间。task_struct 的 state 字段表示进程的运行状态,取值有 TASK_RUNNING(正在运行或正在队列中等待运行,进程如果在用户空间只能为此状态)、TASK_INTERRUPTIBLE(可响应信号)、TASK_UNINTERRUPTIBLE(不响应信号)、TASK_STOPED 等,另外 state 还有特殊的两个值是 EXIT_ZOMBIE(僵尸进程) 和 EXIT_DEAD(进程将被系统移除)。kernel 提供 set_task_state 宏修改进程状态,set_task_state 最终调用 set_mb,set_current_state 用于当前进程的状态。task_struct 的 pid 字段就是咱们喜闻乐见的进程 ID 了。 这是一个典型的 Linux 进程状态机图: POSIX 1003.1c 标准规定一个多线程程序的每个线程都应该有相同的 PID,这样的好处是例如发一个信号给一个 PID,一个线程组里的所有线程都能收到。同一线程组中的线程有相同的线程组号(Thread Group ID),线程组组号放在 task_struct 的 tgid 成员变量中,一般是线程组里的第一个轻量级进程的 PID。特别需要注意 getpid() 系统调用返回的就是 tgid 的值,而不是 pid 值,这样一个多线程程序的所有线程可以共享一个 PID。 对每个进程,kernel 在通过 slab 分配器分配 task_struct 时,通常是实际分配了两个连续的物理页面(8KB),以 thread_union 联合表示,其中包括一个 thread_info 结构(其 task 成员是指向 task_struct 的指针)以及 kernel 模式的进程堆栈。esp CPU 堆栈指针即表示此进程堆栈的栈顶地址,进程从用户模式切换到 kernel 模式时,kernel 堆栈会被清空。为了效率考虑,kernel 会将这两个连续的物理页面的第一个页面按 2^13(也就是 8KB) 对齐,为了避免内存较少时产生问题,kernel 提供配置选项(就是下面的 THREAD_SIZE 了)可以将 thread_info 和堆栈包含在一个页面也就是 4KB 的内存区域里。一般来说,8KB 的堆栈对于内核程序已经够用。 看看 Linux 2.6.34 中 thread_union 的定义: 由于 thread_info 和内核堆栈是合并在连续的页面里的,kernel 就可以从 esp 指针得到 thread_info 结构地址,这是通过 current_thread_info 函数来实现的。 假设 thread_union 是 8KB 大小,也即 2^13,将 esp 的最低 13 位屏蔽掉即可得到 thread_info 的地址,如果是 4KB 的栈大小,屏蔽掉最低 12 位即可(和上面的代码一致),这样通过 current_thread_info()->task 就能得到当前的 task_struct,这就是 current 宏的实现了。 系统中进程的列表保存在 init_task 所在的双向链表中,task_struct 的 tasks 字段就是 list_head,init_task 表示的就是 PID 为 0 的 swapper 进程(或者叫 idle 进程),其 tasks 会依次指向下一个 task_struct,PID 为 1 的进程就是 init 进程,这两个进程都由 kernel 来创建。 而关于可以运行的进程的调度,Linux 2.6.34 和 ULK 上说的已经有很大的不同了。2.6.34 上加上了 struct sched_class 结构体表示不同类型的调度算法类,目前 2.6.34 上实现了三种:Completely Fair Scheduling (CFS) Class(完全公平算法,见 kernel/sched_fair.c)、Real-Time Scheduling Class(实时算法,见 kernel/sched_rt.c)和 idle-task scheduling class(见 kernel/sched_idletask.c),这三个源文件都被 include 在 kernel/sched.c 中进行编译了。CFS Class 使用 sched_entity 结构作为调度实体,其中包含权重、运行时间等信息,比 RT Class 复杂,其中还有专门的红黑树。RT Class 使用 sched_rt_entity 作为调度实体。 每个 task_struct 中都包含了 sched_entity 和 sched_rt_entity 这两个字段,sched_class 中则有 enqueue_task、dequeue_task 等函数指针指向对应调度算法中的实现函数,enqueue_task 将进程加入运行队列,dequeue_task 将进程从队列中移除,由于这段变化较大而且比较复杂,有关这三种调度算法的具体实现以后再来介绍了。 task_struct 的 real_parent 字段指向创建该进程的进程(如果父进程已不存在则为 init 进程),parent 指向当前进程的父进程,children 为该进程子进程列表,sibling 为该进程的兄弟进程列表,group_leader 字段指向该进程的线程组长。与 ULK 不同的是,ULK 中 ptrace_children 为被调试器 trace 的该进程的子进程列表,2.6.34 中 ptraced 字段包含该进程原本的子进程和 ptrace attach 的目标进程,ptrace_list 改为 ptrace_entry。另外 2.6.34 kernel 中已经引入 namespace 的概念,获得进程组 ID 和会话期 ID 的方式也于 ULK 中的有不少区别。 kernel 中进程的 PID 散列表存在 pid_hash 中以加快根据 PID 搜索 task_struct 的速度,pidhash_init 函数初始化此 PID 散列表,由于 2.6.34 中已有 namespace,pid_hashfn 也由原来的一个参数变为两个参数(增加一个 ns 参数表示哪个 namespace)。Linux kernel 也增加了 pid 和 […]

Linux中拷贝目录跳过指定文件的方法

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-copy-directory-ignore-files/ 近日在 Linux 环境中做版本迁移的时候遇到一个问题:需要将一个目录遍历拷贝到另一个目录中,但需要忽略其中的某些文件,由于目录中东西比较多,忽略的项也不好一一指定。普通的 cp 命令并没有排除某个文件或文件夹的参数,比较丑陋点可以 cp -r 拷贝完目录之后再去删除无用的,但如果做批量脚本操作就不爽了,经过实际试验之后暂时找到两个比较好的方法。 1、使用 rsync 进行拷贝: rsync 本来是文件同步备份的工具,相对于普通的 cp 命令,rsync 在控制方面就强多了,而且 rsync 对遍历目录也支持,有 --exclude 参数可以忽略指定的文件或文件夹。 rsync -vaP --exclude=”.*” --exclude=”Makefile” dir1 /home/dir2 如上面演示的就可以排除掉隐藏文件和 Makefile 文件,-a 参数已经包含遍历处理参数 -r。 2、使用 find 加 cpio 进行拷贝: 备注:此方法来自 Advanced Bash-Scripting Guide,需要了解的童鞋自己去参考了。 用过 find 的童鞋都知道,find 对文件的过滤那是非常强大的,配合 cpio 来进行目录的遍历拷贝就可以实现过滤指定的文件或文件夹,当然也可以做到只备份特定的文件或文件夹,你可以用 find 的各种过滤参数达到拷贝哪天的文件,拷贝近期更改的文件等特殊效果,而且 find 支持正则表达式,这种方式想比第一种使用 rsync 跳过文件的方式更加灵活,因此非常推荐使用此方式进行目录拷贝。 cd dir1 find . -regextype posix-egrep -mindepth 1 ! -regex ‘./(dev|tmp)($|/.*)’ ! -name Makefile -a ! -name .svn | cpio -admvp /home/dir2 小解释下: find 的 -regextype 参数指定正则表达式类型,posix-egrep 为 egrep 用的扩展正则表达式,-mindepth 使 find 的输出中不包括目录本身,-regex 参数指定过滤的文件的正则表达式,-regex 前面的感叹号表示跳过,’./(dev|tmp)($|/.*)’ 这个正则表达式即表示跳过目录中的第一层 dev 和 tmp 目录以及下面所有的文件和文件夹,最后两个 -name 指定要跳过文件名为 Makefile 和 .svn 的文件,这样在备份版本库的时候非常有用。 cpio 命令将 find 的输出文件列表依次拷贝到 /home/dir2 目标目录中,-a 表示不更新文件的访问时间,-d 指定自动创建目录,-m 指定保留文件的修改时间,-p 指定 cpio 工作在 Copy-pass 模式,这是专门用来拷贝目录树的一种模式。 PS:如果有更加简单的方法,欢迎提出指正哦~~~ ^_^

Linux kernel学习-内存寻址

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-memory-addressing/ 近日在看 Understanding the Linux kernel(慢慢啃E文原版,以下简称 ULK),这本书虽然已经是第三版了,但它基于的 Linux kernel 版本却不是很新,现在 Linux kernel 都已经出到 3.4 版本了,这本书还是基于 2.6.11 的 kernel,不得不说 Linux kernel 的更迭速度太快了。 下面准备以我正在用的 2.6.34 版本的 kernel 为基础进行学习,这本书中不对应的地方我会尽量找到新 kernel 中的实现,并尽量自己做个了解,日后的相同日志如无意外也基于 2.6.34 版本 Linux kernel。 首先已完成第一章:Introduction(这一章没有 Linux kernel 代码),来到第二章 Memory Addressing,开始是介绍逻辑地址、线性地址、物理地址的对应关系,虽然之前用汇编写过 Linux 的 bootloader,用到过实模式和保护模式,但对 GDT、LDT 的概念并没有深入了解过。这一章开篇就介绍了 Intel 80X86 硬件上内存分段的实现,包括段选择子,段寄存器,段描述符。 1、段式内存管理: 每个内存段由 8 个字节的段描述符来表示段的特征。段描述符被存储在 GDT 或者 LDT 中。内存中 GDT 的地址和大小包含在 gdtr 控制寄存器中,LDT 的地址和大小包含在 ldtr 控制寄存器中。段寄存器的高 13 位为段描述符在 GDT 或者 LDT 中的索引,GDT 或者 LDT 结构中包含基地址、段长度等信息。通过检查指令地址和段长度并确定没有越界以及权限是否正确之后,由于 线性地址 = 段基指 + 偏移地址,GDT 或者 LDT 中的基地址加上指令中的偏移量就可以得到需要的线性地址。 备注:由于每个进程都可以有 LDT,而 GDT 只有一个,为满足需求 Intel 的做法是将 LDT 嵌套在 GDT 表中。 Linux kernel 中的内存分段: Linux中所有进程使用相同的段寄存器值,因此它们的线性地址集也是相同的,不管在用户模式还是内核模式,都可以使用相同的逻辑地址,32位 kernel下为 4G 的地址空间。 ULK 中介绍的 user code、user data、kernel code、kernel data 这四个段对应的段选择子的宏为:__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS,2.6.11 中这4个宏定义在 include/asm-i386/segment.h 头文件中,2.6.34 中已经挪到 arch/x86/include/asm/segment.h 里,因为 2.6.34 中 i386 和 x86_64 的代码已经尽可能的合并到 x86 目录中,而不像老版本的代码那样弄成两个目录。定义如下: 下面是 Linux kernel GDT 的实现: 由于 kernel 中每个内核需要有一个 GDT,因此就有一个 GDT table,ULK 中说的是存在 cpu_gdt_table 中,GDT 的地址和大小存在 cpu_gdt_descr 中,2.6.11 kernel 里都是放在 arch/i386/kernel/head.S,使用的地方: 到了 2.6.34 中已经改为: 可以看到 2.6.34 中去掉了原来的 cpu_gdt_table 变量(详见 kernel commit bf50467204b435421d8de33ad080fa46c6f3d50b),新增了一个 gdt_page 结构存放 GDT table,而且提供 get_cpu_gdt_table 函数取得某个 CPU 的 GDT。cpu_gdt_descr 也已去掉,新增了 desc_ptr 结构存放每个 CPU 的 GDT 信息,cpu_gdt_descr 也改为 early_gdt_descr。 看下简单看下新的切换 GDT 的实现: load_gdt 最终调用 lgdt 汇编指令。 2、页式内存管理: Intel 从 80386 开始支持页式内存管理,页单元将线性地址翻译为物理地址。当 CR0 控制寄存器中的 PG 位置为 1 时,启动分页管理功能,为 0 时,禁止分页管理功能,并且把线性地址作物理地址使用。 32 位线性地址的高 10 位为页表目录的下标(指向页表),中间 10 位为页表的下标(指向页面),低 12 位为该地址在页面(通常大小为 4 KB)中的偏移量,这样的二层寻址设计主要为了减少页表本身所占用的内存,由于页表目录和页表都为 10 位,因此都最多包含 1024 个项。正在使用的页表目录的物理地址存在 cr3 控制寄存器中。 在 32 位大小的页表目录(页表)的结构中,其高 20 位为页表(页面)基地址的高 20 位,其它的 flag 中包含一个 Present 标志,如果该值为 1,表示指向的页面或者页表在内存中,如果为 0,页单元会将线性地址存在 cr2 控制寄存器中,并产生异常号 14: page fault。 页表目录结构中另外有一个 Page Size 标志(页表结构没有此标志),如果设为 1,则页面大小可以为 2MB 或者 4MB,这样可以跳过页表转换,将 cr4 寄存器的 PSE 标志启用即可启用大页面支持,此时 32 位线程地址由高 10 位页表目录下标和低 22 位的偏移量。 为满足寻址超过 4GB 的需求,Intel 从 Pentium Pro 处理器开始,将处理器的地址引脚数量由原来的 32 个提升为 36 个,处理器的寻址空间也从 4GB 增到 64GB,并增加 PAE 页面机制(设置 cr4 寄存器的 PAE 标志启用):64G内存可以划分为 2^24 个页面,页表中的基地址由 20 位增为 24 位,页表结构的大小由 32 位增为 64 位,增加 PDDT 表从而使用三层寻址设计来解释 32 位的线性地址等等。PAE 机制稍显复杂,而且由于仍然使用 32 位线性地址,因此对于应用程序来说,仍然无法使用超过 4GB 的地址空间,64GB 只是对于 kernel 而言的。 顺带说下不同的 64 位架构下的页面寻址级别,见下表,可以看到常用的 x86_64 架构只用了 48 位的线性地址空间,但也达到了 256TB 咯 ^_^ 3、硬件cache: 由于现在 CPU 速度太快,频率已经动辄多少 GHz,而相对的 DRAM 内存频率就慢很多,而且 DRAM 由于设计上电容存在不可避免的漏电原因,DRAM 的数据只能保持很短的时间,必须隔一段时间就刷新一次,不刷新的话会造成存储的信息丢失;而 […]

STL中const_iterator、reverse_iterator转换为iterator

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/stl-const-reverse-iterator/ STL中的容器类(Container)一般提供了4个迭代器:iterator、const_iterator、reverse_iterator、const_reverse_iterator,对于 container<T> 而言,其中 const_iterator 相当于 const T *,const_iterator 指向的元素不能做修改操作。 STL 容器的 begin() 和 end() 默认都提供了 iterator 和 const_iterator 的迭代器,相应的 rbegin() 和 rend() 则也分别提供了 reverse_iterator 和 const_reverse_iterator 的迭代器用于从容器的尾端反向遍历。 最近写的代码中刚好要用到 const_iterator 迭代器,发现由于 STL 提供的一些容器操作函数像 insert、erase 的参数必须为 iterator,这时就不能用 const_iterator 做参数(不能直接转换),以下为个人经验结果。 1、const_iterator 转换为 iterator: iterator 可以隐式转换为 const_iterator,但反过来就不行,就算祭出强制类型转换的杀招应该也会编译报错,这是可以用折中的办法解决: vector<sss> v_test; vector<sss>::iterator i_test; vector<sss>::const_iterator c_test; … c_test = …. … i_test = v_test.begin(); advance(i_test, distance<vector<sss>::const_iterator>(i_test, c_test)); i_test 指向第一个元素,先通过 distance 得到 c_test 和 i_test 的偏移量,然后用 advance 将 i_test 往后移对应的偏移量即可。注意 distance 的模板类型必须为 const_iterator 类型,否则按照 STL 的默认类型推导,distance 中的 i_test 和 c_test 类型不同还是会出现编译报错。 当然如果你想偷懒简单点,也可以这样写,道理是一样的,这时类型推导就显得很好用了: i_test = v_test.begin() + (c_test - v_test.begin()); 2、reverse_iterator 转换为 iterator: 对于 reverse_iterator 可以调用 base() 得到 “与之对应” 的 iterator(与上面的环境一样): vector<sss> v_test; vector<sss>::iterator i_test; vector<sss>::reverse_iterator r_test; … r_test = …. … i_test = r_test.base(); 但需要注意的是由于 STL 的 begin() 和 end()、rbegin() 和 rend() 是两个半闭合的区间,end() 并不是最后一个元素,rend() 也不是第一个元素,因此 end() 和 rbegin()、rend() 和 begin() 之间都是差了一个元素的。我们来看看 STL 标准上 base() 的说明: The base iterator is an iterator of the same type as the one used to construct the reverse_iterator, but pointing to the element next to the one the reverse_iterator is currently pointing to (a reverse_iterator has always an offset of -1 with regards to its base iterator). 也就是 reverse_iterator 的 base() 返回的元素都是 reverse_iterator 所指向元素的下一个元素,这也是前面 “与之对应” 加引号的原因。因此在使用时要特别注意,例如如果要删除 reverse_iterator 指向的元素就需要这样(因为 erase 必须要 iterator 类型的参数): v_test.erase((++r_test).base()); 3、关于随机访问容器: STL 中随机访问容器是一个迭代器类型为随机访问迭代器的可逆容器,它提供常量缓冲时间来访问随机元素。而可逆容器是一个有双向迭代器的前向容器,它可以向后向后迭代通过容器。 常用到的顺序容器是将单一类型的元素聚集起来,然后根据位置来存储和访问这些元素。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。 平时经常用的 string、vector、deque 之类就是典型的随机访问容器,map、set、list 之类的就不是。 因此需要注意 STL 中的 sort 等函数的参数必须是随机访问迭代器,所以对 map、set、list 等容器是无效的。 以上为个人见解,有任何问题欢迎指正交流咯 ^_^。

未能看到整个森林-编程学习中所犯的错误

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/fail-to-see-the-big-picture/ 备注:本文根据 pongba 大哥的这篇E文文章翻译并结合自己体会总结而来,pongba 的E文原文请猛击这里: http://blog.csdn.net/pongba/article/details/2143245 人类的一个普遍的天性是容易被自己感兴趣的东西所吸引。 不论是本文要说的编程学习还是日常事务都是这样,包括美女之类(哈哈),这似乎是一个难以打破的公理。人类自文明开始以来就对非凡的自然现象下的本质非常好奇,我们渴望理解,渴望知道原因。人类天性就是用来解决问题的,我们热衷于解决问题,热衷于发现问题的本质。不过悲哀的是,我们也是问题的主要创造者。 具体说到编程学习这一块,pongba 的原文中用 interesting(感兴趣) 和 mundane(平凡普通的)这两个词来区分编程学习中的两类知识。 我们最开始学习编程时用到的最经典的 hello world 就是 interesting 的一种,看到自己敲的一段字符能让计算机打印出来 hello world 确实能激发我们的兴趣。这就是所谓的 Under the Hood,这是一个在英文技术文章里经常见到的词,原意是钻进魔术师的帐篷,屏住呼吸,瞪大眼睛,把那些奇妙的魔法看个通透,让自己的理解和技艺获得巨幅的提升,在IT界里就是深入理解的意思。 你在学会设计程序和了解程序能正确运行的原因之后,接下来你会做什么?你会继续写程序,发现你所用的编程语言的越来越多的细节。你会越来越了解你用的编程语言,能知道该语言能方便的做些什么,哪些不能很方便的实现。接下来各种语言的窍门就开始进入你的脑海,注意这些窍门最吸引人的地方在于能让你做到本来做不到的事情,能让你荷尔蒙迸发,让你很 happy。 从程序设计语言的角度来看,我们热衷于解决的问题往往是我们自己创造的。例如,最近有一种争论关于设计模式是语言中缺失的一种特性。首先我们创造一门程序设计语言,由于一些设计时没有预料到的缺点,在使用中发现了,我们使用包括设计模式在内的一些语言窍门来解决它。但随着时间推移,我们就会发现这些模式不但没有价值,反而变成一种沉重的负担,这时通常会把这些作为新特性加入到语言中。 通常我们在解决以前的语言造成的问题过程中,我们通常又会造成新的问题。例如,现在总有 DSL 和 GPL 的争论,注意这里的 GPL 不是 GPL 开放授权协议,而是 Gerneral-purpose Language(通用语言),DSL 是 Domain-specific Language(领域专用语言)。DSL在很多人心目中是“非程序员的编程语言”,其首要目的是使程序尽可能接近领域中遇到的问题,消除不必要的间接性和复杂性,而其最终受众一般不是普通的程序员。一方面,将领域专用的一些特性加入到语言中,对那些需要对特定领域编程的人来说会非常便利;而另一方面,它会限制语言的使用范围。相对于DSL,GPL的最大优势在于可以为理论上无限的应用领域服务。GPL最大的妥协在于当面对领域相关的问题时,它只相当于一个 second-class language,这是为什么微软要搞一个CLR(通用语言运行时,Common Language Runtime)运行环境,也是为什么 Martin Fowler 要倡导面向语言编程LOP(Language-oriented Programming)了。 因此,在这总结一下,我们创造了各种语言抽象概念以使语言更加易用,但周尔反复的是我们总是在解决一个问题时创造时另一个新的问题。由于我们的程序设计语言都存在着不可避免的缺陷,这样语言窍门之类的东西就会登堂入室,并偷走我们的关注点(原文如此,嘿嘿),这也是为什么市面上有如此多的编程语言技巧书,语言陷阱介绍之类的,而且销量似乎都甚好。你可以看看任何C++编程学习的推荐书列表中,都不乏这样的例子。 然而到底是什么导致我们在编程学习时如在一堆树木里迷失,而导致没有看到整个森林?为了什么我们要学习这些奇淫技巧呢?实际上我们不是真正的需要,但我们内心里趋向于学习这些技巧,因为像文章开头说的,我们天生就是问题解决者,我们喜欢解决问题,即使这些问题是我们自己创造的。但这些奇淫技巧实际上只要在真正需要时按需学习即可,我们被这些东西吸引的原因在于: 1、我们喜欢新事物,如果什么东西是新事物就很有趣; 2、我们喜欢赶时髦(jump on the bandwagon)。 这就引出人类的第二个普遍的天性:赶时髦,如果所有人都做一件事,那我无论如何也得做。 不光是一些公司或者团体使用这个策略引诱我们去赶时髦,我们还热衷于创造自己的潮流。每当有新的语言或者技巧出来的时候,我们总是欢呼雀跃,总是被这些新带来的特性的光晕笼罩,而忘了它实际包含的问题,我们总是把它当做是万能灵药,开始万般饥渴地学习它。程序员是一种聪明的动物(原谅我如此直白,哈哈),不过有时显得过于聪明。他们总是渴望于新的事物(在任何编程论坛上找一圈就能得到验证,你会发现成千上万的程序语言技术细节的问题,学习这些东西是永无止境的,但程序员就是如此地欲罢不能),就像野兽永远不能停止对于食物的饥渴一样。 下面说说程序员普遍不爱的平凡的东西,什么是大多数程序员不喜欢的东西? 大多数程序员不爱的东西包括:编程原则,从小的编码规则(例如:永远给变量起一个有意义的名字)到大的项目设计原则(例如:在写代码之前先写测试文档)都有,还有文档的编制之类的,这些都是比较乏味的,不会显得古怪有味道,显得没有挑战性,显得没有那么酷。我们无法向外界展示遵从一些愚蠢的规则是多么聪明的一件事。我们尤其钟爱的是写一些疯狂的技巧代码或使用一些耀眼的模式以使别人都不知道我们在做什么(或者每个人都知道我们在做什么)。 接下来是人类的第三个天性:自私的偏见,我们热爱我们所做的,或者我们是谁,我们讨厌与之对抗或相反的东西。 不管你是否愿意承认这点,我们都有过这个体验。当我们对某些语言或平台非常熟悉时,我们就容易产生自私的偏见,它会影响我们想学习和不想学习的东西。你应该可以在一些论坛上感受到关于编程语言的争论是如此普遍。我们总是被蒙蔽了双眼,没有看到自己所用的语言或平台上的缺点和其它语言上的优点。我们限制了学习新的语言的能力,从某种意义上来说,我们限制了自己的潜能。 翻译的总结: 一方面,大多数时候我们学的东西有点太多了。我们像飞蛾扑火般被新事物吸引,我们经常是在学习周围的人在学习的,或者是别人告诉我们要学习的。但如果我们抓牢了一些本质的知识,其它的东西就完全可以按需学习。别再沉迷于技术技巧,除非它是必备的或者你马上就要用到的。因为要学习的技术技巧总是无穷无尽的,你应该将你的时间花在更有用的东西上(学些本质知识,学习编程思想,或者学习另一门编程语言)。 但另一方面,我们学的东西又太少了。我们总是忽视看起来乏味但又非常重要的东西,例如以下观点(可能很多人都有过): 测试? --- 就像做爱前戴套一样不爽; 重构? --- 为什么要做这种不能带来新功能也不酷的东西; 防御式编程? --- 对不起,我知道自己在写什么; API设计? --- 拜托,我在写这么华丽的代码的时候去考虑别人怎么使用我的代码也太TMD难了; 新语言? --- 你是说我现在用的这个不够好?没看到我能随意折腾这门语言来实现我想做的? 这是我首次完整翻译别人写的E文文章,也加了一些自己的观点,希望读者和我都能在未来看到编程学习中的整个森林 ^_^,有任何错误欢迎指正咯。 参考: 1、DSL领域专用语言: http://en.wikipedia.org/wiki/Domain-specific_programming_language 2、LOP面向语言编程: http://en.wikipedia.org/wiki/Language-oriented_programming

page cache诊断控制工具 vmtouch 源代码分析

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/vmtouch-code-analysis/ vmtouch 是一个 portable 的 page cache 诊断和控制工具,可以查看文件或者设备中有多少在 page cache 中,知道之后对这些在 page cache 中的内存引用可以避免 page fault,支持将文件的内容从 page cache 逐出,同时还可以将文件手工 touch 到 page cache 中,支持 lock 文件部分区域到 memory 中防止被交换出去从而提高。 vmtouch 可以在 Linux、BSD 等系统上使用,在这下载编译: http://hoytech.com/vmtouch/ 今天简单看了下 vmtouch 的代码,发现还比较简单,自己写个类似的程序验证之后,将代码分析结果写下。vmtouch 的代码比较少,我只贴出最关键的一个函数 vmtouch_file(关键部分已经高亮显示),这个函数做 分析 page cache 使用、touch、lock 的操作,其它部分只是加了读了目录的遍历处理之类的。 稍微有点基础就可以看明白了,先 mmap 映射文件到当前进程,按 page size 对齐之后,调用 mincore 函数就可以得到文件中每一个 page 是否在 page cache 中,结果保存在 mincore_array 数组中,该数据中每个字节的第一位即表示是否在 page cache 中。 将文件内容逐出(指定 o_evict)出 page cache 是通过 posix_fadvise 函数调用 fadvise 系统调用来实现的(BSD通过 msync 实现,这个在 Linux 上没有效果)。fadvise 系统调用可以告诉 kernel 要操作的文件在接下来要干什么,kernel 可以提前做一些操作而提高性能,Linux kernel 里实现了以下几种控制方式: POSIX_FADV_NORMAL - 正常操作,对文件使用底层设备的默认 readahead 值; POSIX_FADV_SEQUENTIAL - 顺序I/O,对文件使用两倍的 readahead 值; POSIX_FADV_RANDOM - 随机I/O,禁用文件上的 readahead; POSIX_FADV_NOREUSE - 只使用一次 POSIX_FADV_WILLNEED - 很快需要使用,对文件使用非阻塞读到 page cache POSIX_FADV_DONTNEED - 不再需要使用文件,从 page cache 中逐出 posix_fadvise 加 POSIX_FADV_DONTNEED 参数就可以将文件从 page cache 中逐出,需要注意的是如果需要确保文件从 page cache 中逐出,还需要在调用 fadvise 之前用 fsync/fdatasync/sync_file_range 之类的函数将 dirty page 清理。 下面是我在 Linux 下用 posix_fadvise 的一个测试程序测试的结果: 从 free 命令的结果可以很明显的看到,dd 之后基本文件都在 page cache 中,fadvise 之后从 page cache 中正确逐出。 接着是 vmtouch 中的 touch 操作(指定 o_touch)就更简单了,对 mmap 到的地址直接遍历引用,不在 page cache 的内容会自动产生 page fault 到 page cache 中。 lock 内存(指定 o_lock)也则直接使用 mlock 函数来实现,mlock 对于对安全性和实时性有很高要求的程序非常有用,可以保证指定的文件区域在内存中,不被 swap 出去。 以上为个人分析结果,有任何问题欢迎指正咯 ^_^

RAID5布局方式介绍及对性能的影响

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/raid5-layout-performance 本文主要介绍 RAID5 内部的数据布局方式,不对 RAID5 的基本原理做过多介绍。磁盘阵列中的 RAID5 类型是通过 XOR 奇偶校验算法来实现阵列中的数据冗余的,可以允许在坏一块磁盘的情况下继续使用,虽然在正常使用过程中写的额外开销比读要多很多,但对于一般企业来说还是很不错的选择。 布局方式: RAID5的校验和信息是平均分布到 RAID5 的每个硬盘上的,实际使用中有以下4种分布方式: 1、左对称(Left-symmetric); 2、左不对称(Left-asymmetric); 3、右对称(Right-symmetric); 4、右不对称(Right-asymmetric) 说这几个名词肯定不好理解,下面是一个4个硬盘组成的 RAID5 内部数据和校验和(parity)在不同布局方式下的分布图(每一行表示是一个条带 stripe): RAID5 几种布局方式中的“左”、“右”表示校验和(也就是图中的红色的P)如何分布,如图中,“左”的校验和都是从最后一个磁盘开始,依次前推,“右”的校验和从第一个磁盘开始,依次后推。 “对称”、“不对称”标识数据(也就是图中的 D0、D1、D2…)的分布方式,“不对称”方式都是直接将数据按照磁盘的数据直接排下来的(图中的左不对称和右不对称的前后数据明显不对称),而“对称”方式则将每个 stripe 中的第一个数据放在校验和的后面,然后往后排列,需要时再绕回第一个磁盘,这样“对称”的数据排列方式就能始终按照磁盘的顺序依次下来。 性能影响: 在实际使用测试中,校验和的左右布局方式对 RAID5 性能没有太大影响。由于实际读写 RAID5 时命令时均匀发到每个磁盘上的,从图中也可以推断出“对称”方式在处理大块值的连续 I/O 读时有更好的性能,实际测试中对随机 I/O 性能也有提升。 需要注意的是上面所说的性能影响相对于 RAID5 中的磁盘个数以及 stripe 条带大小参数来说是比较细微的。 其它说明: 现在很多的RAID卡中都使用了 左不对称 这种 RAID5 布局方式,另外 Linux MD RAID、Windows动态磁盘(LDM)、Veritas Volume Manager(VxVM)等实现中都默认使用了 左不对称 方式。了解 RAID5 的布局方式对 RAID 的性能调试之类有一定帮助哦,HOHO。 除了 Linux MD RAID,其它 RAID5 实现中的布局方式似乎没见到可以让用户选择,Linux MD RAID 可以在使用 mdadm 命令创建时指定 --layout 参数指定布局方式,详细请参考 man mdadm。 如果上面还是看不明白,可以参考这些E文的说明,特别第一篇画的很详细: http://www.accs.com/p_and_p/RAID/LinuxRAID.html http://www.z-a-recovery.com/art-raid5-variations.htm http://sfdoccentral.symantec.com/sf/5.0MP3/solaris/html/vxvm_admin/ch01s04s09s03.htm

WordPress友言近期评论Widget更新(v1.1)

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/wordpress-uyan-recent-comment-widget-v1-1 前两天写了一个友言近期评论的 WordPress Widget,初步测试还能用,但多次进行刷新,会出现不能正常登录之类的问题(Failed to authentication with uyan.cc、Invalid comments data from uyan.cc 之类的报错),而且原来每次都需要登录友言的后台管理严重影响速度,我也比较怕怕这样不断发请求会导致友言把我的脚本和博客给封掉,嘿嘿。 由于友言没有公开 API 给我等码农来调用,因此今天又花了点时间做了一些改进,弄了个 1.1 版本,原介绍文章在这: https://zohead.com/archives/wordpress-uyan-recent-comment-widget/ 1.1版本更新说明: 1、登录友言后台管理,得到近期评论等地方增加错误处理,如果服务端返回的 JSON 数据不对则报错 2、减少 PHP 本身的 error message 输出 3、将得到近期评论的功能分离为一个函数单独调用 4、登录成功之后保存得到的 cookies 到同目录下的 uyan_cookie.php 文件中方便下次直接使用,而无需再重复登录,如果 cookie 过期下次获取评论时失败则再重新登录 5、发查询近期评论请求时增加友言本身的管理地址作为 referer 此次增加了一个 uyan_cookie.php 文件用于保存 cookie,从下面的链接下载解压缩之后,请将此文件也上传到同一目录中(不上传的话 PHP 可能会报 require_once 出错),此文件默认为空,详细代码就不贴了,直接在下面下载查看吧。友言 的登录邮箱、密码、域名之类的修改请参考原始的介绍文章了。 这个是 1.1 版本的下载地址: http://miseal.googlecode.com/files/uyan_comments_v1.1.zip 当前的通过临时文件保存 cookie 方式应该还有改进空间,有任何问题欢迎指正咯 ^_^

PHP实现WordPress友言近期评论Widget

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/wordpress-uyan-recent-comment-widget/ 现在有很多人都使用 WordPress 来搭建自己的博客系统,其中有一些是像我这样使用 友言 这个社交评论插件来替代系统原始的评论框的,友言 评论框插件对一些主流的社交网站支持比较好,很是推荐,友言官网: http://uyan.cc/ 之前我写过一个修改 友言 插件实现完美与 WordPress Mobile Pack 插件配合实现移动版博客的文章,在这里仅供参考: https://zohead.com/archives/modify-plugin-wordpress-mobile-pack/ 但现在发现还是有一点不足,由于使用了友言评论框替代 WordPress 本身的,导致 WordPress 的 ”近期评论“ Widget(小工具)显示不了,因此小折腾一个晚上,靠着还依稀记得的 PHP 编程经历,写了一个简单的显示 友言 近期评论的 WordPress Widget,先看效果图(本博客右侧就有 ^_^): 原理及代码: 实现原理比较简单,先用工具分析 友言 评论的后台登录和评论显示之类的 HTTP 包信息,然后用 PHP 的 fsockopen 来自己发送 GET、POST 请求实现在 WordPress 上列举显示已经存在的友言评论(备注:默认只显示已经经过审核的评论)。 由于中间有几步 GET 和 POST 请求,就找了个现成的 post-to-host 这个很小的 PHP 脚本来发 GET 和 POST 请求,下载地址: http://code.google.com/p/post-to-host/ 下面是主程序 uyan_comments.php 的代码: 代码本身比较简单,有些 PHP 基础就可以看懂了,把 uyan_comments.php 文件最上面的 $email、$password、$domain、$maxcomments 改为实际的 友言 后台管理的登录邮箱、密码、你的域名、显示的最多评论数(备注:默认为10条,如果改为小于0的值则不限制显示的评论条数),就可以使用了。你应该已经发现这个 uyan_comments.php 其实和 WordPress 没太大关系,完全也可以直接单独使用。 需要注意的是为了避免使用明文密码而可能导致的问题(安全第一 ^_^),上面的 $password 是 友言 后台管理时实际用到的加密过的密码。这个加密过的密码可以通过 Firefox 的 Live HTTP headers 插件之类的抓取 HTTP 协议头的插件或工具来得到。 得到友言的加密登录密码: 下面以 Firefox 的 Live HTTP headers 插件为例说明如何得到 友言 的实际加密的密码,打开 Live HTTP headers,该插件会自动开始抓取,然后用正确的邮箱和密码登录 友言 的后台管理,停止 Live HTTP headers 的抓取,在输出里就能找到地址为如下格式的 GET 请求,请求参数中就有加密的密码: http://uyan.cc/index.php/youyan_login/userAutoLoginCrossDomain?callback=jsonpxxxxxxx&email=xxxxx@xxxxx.com&loginPassword=xxxxxxxxxxxxxxxxx&rem=1&domain=xxx.com 其中的 email 段就是登录邮箱,loginPassword 段即为加密的密码,保存下该密码,修改 uyan_comments.php 文件中的 $email 和 $password 值。 Live HTTP headers 的抓取截图如下所示(后面的未显示完整): 如何加入 WordPress Widget 列表中: 你如果有真正实现一个 WordPress Widget 的心思,可以用本代码加上 WordPress 的 register_widget 之类的接口来实现。无奈我是一个超级懒人,懒人就有懒人的办法,下面介绍的就是懒人的办法,哈哈。 首先下载本文最下面下载链接中的 post_to_host.php 和 uyan_comments.php,将 uyan_comments.php 中对应的 登录邮箱、密码、域名 改掉(参考上面),将这两个文件上传到 WordPress 根目录中(位置也可以自己修改),然后给 WordPress 安装 PHP Code Widget 插件,这是一个通用 Widget,添加之后,可以自行添加 文本、HTML、PHP 代码等,比较方便,插件地址: http://wordpress.org/extend/plugins/php-code-widget/ 安装之后,在 WordPress 管理后台的 外观 - 小工具 里就能看到名为 “PHP Code”  的小工具,将其托至右侧的 “第一小工具区域”,输入自定义的标题,然后加入以下代码保存即可(如果上传的位置不在 WordPress 根目录那请自行修改): 重新访问 WordPress,如果上面的 登录邮箱、密码、域名 设定都正确的话,应该就可以出现类似上面效果图的评论列表。 不足和改进: 1、uyan_comments.php 每次访问时都需要登录 友言 管理后台,请求评论列表,因此速度会有些影响,这个有空再改进了。 2、由于需要在 Web服务器中使用 PHP 的 fsockopen 来发 GET、POST 请求得到评论列表,因此可能对 WordPress 博客的访问速度造成一些影响,如果 PHP 空间在国内还可以,像我这样空间在国外的就稍微悲剧点了,所以建议安装 WP Super Cache WordPress 之类的插件来实现更好的缓存加速,将影响尽量降低。 下载地址(115网盘): http://115.com/file/beezjk3b 写博客好累,准备休息,HOHO,本文件为个人作品,有任何问题欢迎指正。 ^_^

bind2nd普通二元函数时无法使用引用类型参数的问题

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/cplusplus-bind2nd-reference/ 在使用 STL 的 find_if、count_if 等函数时会发现这些函数使用的参数不是普通的函数指针,而是 functional 函数对象,实际使用函数对象时可以自己定义一个仿函数来实现(实现带参数的 operator () 即可),这个相对比较简单就不写出来了。但有些情况需要直接使用普通的二元函数指针,这时可以使用 ptr_fun 将函数指针转换为函数对象作为 find_if、count_if 等的参数。 先看一个能正常工作的二元函数不使用引用类型参数的代码: 程序很简单,从 vector 中查找符合条件的 sss 对象,find_sss 就是要转换的二元函数指针,第一个参数是 sss 类,通过 ptr_fun 可以使本代码正常工作。 通过下面的运行输出能看出调用 find_sss 时进行了拷贝构造(本程序的编译环境为:Windows 7 32bit, Mingw gcc 3.4.5,Visual Studio 2010中稍有不同,主要在前面的拷贝次数上): ---copy 0x22ff10 to 0x552a58 ---copy 0x552a58 to 0x552ad8 ---copy 0x22ff10 to 0x552ae0 ---copy 0x552ad8 to 0x552af0 ---copy 0x552ae0 to 0x552af8 ---copy 0x22ff10 to 0x552b00 ---copy 0x22ff10 to 0x552b08 ---copy 0x552af0 to 0x552b18 ---copy 0x552af8 to 0x552b20 ---copy 0x552b00 to 0x552b28 ---copy 0x552b08 to 0x552b30 ---copy 0x22ff10 to 0x552b38 before find_if ---copy 0x552b18 to 0x22fdd0 ---copy 0x22fdd0 to 0x22fd40 ---copy 0x552b20 to 0x22fdd0 ---copy 0x22fdd0 to 0x22fd40 ---copy 0x552b28 to 0x22fdd0 ---copy 0x22fdd0 to 0x22fd40 index: 2, value: 13 接下来就是实际碰到的问题了,如果将 find_sss 的第一个参数改为 sss 的引用,即第 24 行改为: bool find_sss(sss& s_chk, int val) 上面的代码就会编译出错(以 Visual Studio 2010 的错误输出为例): C:\Program Files\Microsoft Visual Studio 10.0\VC\INCLUDE\xfunctional(341) : error C2535: “bool std::binder2nd<_Fn2>::operator ()(sss &) const”: 已经定义或声明成员函数 with [ _Fn2=std::pointer_to_binary_function<sss &,int,bool,bool (__cdecl *)(sss &,int)> ] C:\Program Files\Microsoft Visual Studio 10.0\VC\INCLUDE\xfunctional(335) : 参见“std::binder2nd<_Fn2>::operator ()”的声明 with [ _Fn2=std::pointer_to_binary_function<sss &,int,bool,bool (__cdecl *)(sss &,int)> ] test.cpp(43): 参见对正在编译的类 模板 实例化“std::binder2nd<_Fn2>”的引用 with [ _Fn2=std::pointer_to_binary_function<sss &,int,bool,bool (__cdecl *)(sss &,int)> ] 网上的码农和攻城师们基本都认为是 STL 本身的问题,传递引用类型参数会造成 reference to reference 问题。 几番尝试之后,发现的解决方法如下: 1、Visual Studio 2010下的不完美解决方法: 将第 43 行改为:     iii = find_if(vvv.begin(), vvv.end(), bind2nd(pointer_to_binary_function<sss, int, bool, bool(*)(sss&, int)>(find_sss), 13)); 这样通过自己给 pointer_to_binary_function 设置模板参数避免编译出错,实际运行中会发现 find_if 查找可以正常工作了,但第 27 行中修改引用类的值会没有效果,因为这种方式不是真正的引用,仍然有拷贝构造,由于需要用到这些二元函数的场合一般不需要修改数据,使用起来没有太大问题。 2、gcc下的不完美解决方法: 如果 gcc 下用上面的改动,你会发现由于 gcc 下 pointer_to_binary_function 只有 3 个参数,无法顺利修改编译,但可以这样折中,将第 43 行改为:     iii = find_if(vvv.begin(), vvv.end(), bind2nd(ptr_fun((bool(*)(sss, int)) find_sss), 13)); 通过强制类型转换来实现,稍显恶心,查找可以正常工作,但依然是拷贝构造。需要注意此方法如果在 Visual Studio 中使用会出错。 3、终极解决方案- 使用 boost 库: 首先头文件中增加: #include <boost/functional.hpp> 然后原来的第 43 行改为:     iii = find_if(vvv.begin(), vvv.end(), boost::bind2nd(boost::ptr_fun(find_sss), 13)); 编译运行之后发现 find_if 查找可以正常工作,而且现在是真正的引用,只能说 boost 的 functional 相比 STL 的实在是好强大,哈哈。 运行输出如下(使用 Mingw gcc 3.4.5 编译),find_if 找到的值已经被 find_sss 修改: ---copy 0x22ff10 to 0x7f2a58 ---copy 0x7f2a58 to 0x7f2ad8 ---copy 0x22ff10 to 0x7f2ae0 ---copy 0x7f2ad8 to 0x7f2af0 ---copy 0x7f2ae0 to 0x7f2af8 ---copy 0x22ff10 to 0x7f2b00 ---copy 0x22ff10 to 0x7f2b08 […]