分类: Programming

fatcache源码浅析

毕业后荒废了很多时间. 最近收拾心情, 想找几个存储的开源项目学习下. 先看了fatcache的代码, 它是twitter开源的缓存服务, 可以认为是SSD版的memcache(索引还是在内存). 本文简单分析下, 做个备忘.

generic queue

fatcache使用generic queue维护资源使用情况, generic queue的实现在fc_queue.c. 和常见思路一致:fatcache在启动时, 预先分配一大块内存, 划分为多个元素, 并将多个元素组织成一个队列, 作为空闲队列. 需分配元素时, 将元素从空闲队列中弹出压入已使用队列. 当归还元素时, 将元素从已使用队列弹出压入到空闲队列.

slab allocator

fatcache底层采用slab allocator内存模型, 并且使用hash表快速索引key所在的item.

结构定义

如上所述, fatcache采用slab allocator内存管理模型. slab所占内存大小相同,但slab上item的大小可以不同, fatcache按照item大小将slab分级, 称为slabclass, 每种class有不同的id, 记做cid. slabclass的结构为:

47 struct slabclass {
48     uint32_t         nitem;           /* # item per slab (const) */
49     size_t           size;            /* item size (const) */
50     size_t           slack;           /* unusable slack space (const) */
51     struct slabhinfo partial_msinfoq; /* partial slabinfo q */
52 };

其中,nitem表示该class下每个slab有多少item, size表示每个item的大小, partial_msinfoq作为队列头指针, 指向已使用但未满的slabs所组成的队列.

slab的结构为:

21 struct slab {
22     uint32_t  magic;     /* slab magic (const) */
23     uint32_t  sid;       /* slab id */
24     uint8_t   cid;       /* slab class id */
25     uint8_t   unused[3]; /* unused */
26     uint8_t   data[1];   /* opaque data */
27 };

magic是魔数, 做校验用. sid表示该slab的id, cid表示该slab的级别. unused作为padding未使用. data便是item数组. 同一个slab在不同的周期可以属于不同slabclass(由cid确定).

slab可以在内存中, 也可以在磁盘上. 每个slab一一对应一个摘要结构体, 称为slabinfo. slabinfo是永在内存中的. 其结构为:

35 struct slabinfo {
36     uint32_t              sid;    /* slab id (const) */
37     uint32_t              addr;   /* address as slab_size offset from memory / disk base */
38     TAILQ_ENTRY(slabinfo) tqe;    /* link in free q / partial q / full q */
39     uint32_t              nalloc; /* # item alloced (monotonic) */
40     uint32_t              nfree;  /* # item freed (monotonic) */
41     uint8_t               cid;    /* class id */
42     unsigned              mem:1;  /* memory? */
43 };

其中,sid为slab的id, cid表示slab属于哪个级别. nalloc表示该slab当前已使用的item数, nfree表示该slab当前仍剩余的空闲item数. mem表示slab在内存中还是在磁盘上.

fatcache作为类memcache, 原理是在内存中维护hash表, 表元素记做itemx:

23 struct itemx {
24     STAILQ_ENTRY(itemx) tqe;    /* link in index / free q */
25     uint8_t             md[20]; /* sha1 message digest */               //作为key.
26     uint32_t            sid;    /* owner slab id */
27     uint32_t            offset; /* item offset from owner slab base */
28     uint64_t            cas;    /* cas */
29 } __attribute__ ((__packed__));

sid表示该item所在slab, offset表示item在slab的偏移. cas是memcache协议定义的业务相关字段, 无须理解.
需要说明的是uint8_t md[20]字段, 它表示key的摘要. 因为在fatchache中, key最长可以为256, 明显太大了, 所以fatcache认定md标志一个key, 多个不同的key如果md相同, fatcache将认为这些key相同.

itemx只是item在内存中的摘要信息. 真正的item既可以保存在内存slab中, 也可以在磁盘slab中. item结构为:

 23 struct item {
 24     uint32_t          magic;      /* item magic (const) */
 25     uint32_t          offset;     /* raw offset from owner slab base (const) */
 26     uint32_t          sid;        /* slab id (const) */
 27     uint8_t           cid;        /* slab class id (const) */
 28     uint8_t           unused[2];  /* unused */
 29     uint8_t           nkey;       /* key length */
 30     uint32_t          ndata;      /* date length */
 31     rel_time_t        expiry;     /* expiry in secs */
 32     uint32_t          flags;      /* flags opaque to the server */
 33     uint8_t           md[20];     /* key message digest */
 34     uint32_t          hash;       /* key hash */
 35     uint8_t           end[1];     /* item data */
 36 };

fatcache将key和value按tl格式保存在end尾端.

处理流程

启动阶段:

fatcache根据参数选项得知总共有多少slab级别(uint8_t nctable), 在内存中创建slabclass数组(struct slabclass *ctable), 依次初始化. 然后在内存中创建slabinfo数组(struct slabinfo *stable), 个数(uint32_t nstable)为配置的内存slab数(uint32_t nmslab)和磁盘slab数(uint32_t ndslab)之和. 然后将slabinfo数组逐一初始化, 前nmslab个元素slabinfo.mem为1, 表示slab在内存. 后ndslab个元素slabinfo.mem为0, 表示slab在磁盘.

然后fatcache创建一大块内存, 大小为nmslab * settings.slab_size (记住每个slab大小相同), 再将内存分割为多个slab并逐一入内存slab空闲队列(struct slabhinfo free_msinfoq). 同理, fatcache将磁盘分割为多个slab并逐一入磁盘slab空闲队列(struct slabhinfo free_dsinfoq).

工作阶段:

fatcache定义了几个辅助函数, 其中 uint8_t slab_cid(size_t size) 表示size大小的item属于哪个slabclass. 当需要分配一块大小为size的item时, 首先用过slab_cid()函数获得对应的cid, 然后通过struct item *slab_get_item(uint8_t cid)函数分配item. 其处理流程为:

1. 如果内存中无空闲的itemx, 则调用slab_evict()函数,它找到最老的磁盘slab, 逐一将该slab上的所有item对应的itemx从hash表中删除.

2.1 如果slabclass.partial_msinfoq不为空, 则对头部的slabinfo, 从其对应的slab上分配一个item. 如果slab此时没有空闲item, 则将slabinfo从partial_msinfoq中移除, 插入到full_msinfoq中
2.2 如果slabclass.partial_msinfo为空, 则从free_msinfoq中弹出一个slabinfo, 从对应的slab上分配item, 并将该slab从free_msinfoq移除后插入partial_msinfoq或full_msinfoq

3. 如果2.1或2.2都未成功分配, 则调用slab_drain()函数, 它将删除磁盘上最老的slab(通过slab_evict()), 然后调用_slab_drain()函数, 找到full_msinfoq中最老的一个slabinfo对应的内存mslab, 然后从free_dsinfoq中找到一个的磁盘dslab, 将mslab数据写入dslab, 交换两个slab对应的slabinfo的标志和所管理的数据的起始地址(即slabinfo.addr和slabinfo.mem). 然后尾递归调用slab_get_item()跳回1重新干活.

淘汰策略

既然使用了generic queue, 淘汰策略必须是FIFO, 这是很容易实现的: slab如果有更新, 插入到queue尾部. 如果需淘汰, 从queue头部开始.

高效的读写

对于写操作:上面的流程保证了slab_get_item()返回的item必定在内存中, 所以上层可以直接将数据写入item尾部(item.end). 如果某个slab已写满, 且没有其他的空闲slab, 则上面的操作3所调用的_slab_drain()函数, 将内存slab和磁盘slab做数据交换时, 可以成片将数据写入磁盘. 可以理解为将随机写缓存在slab中转为顺序写.

对于读操作:SSD对随机读本来就有很高的IOPS.考虑到SSD读写块大小为512字节, fatcache通过下面几行代码在应用层做了对齐:

492     off = slab_to_daddr(sinfo) + addr;
493     aligned_off = ROUND_DOWN(off, 512);
494     aligned_size = ROUND_UP((c->size + (off – aligned_off)), 512);
495
496     n = pread(fd, readbuf, aligned_size, aligned_off);
497     if (n < aligned_size) {
498         log_error("pread fd %d %zu bytes at offset %"PRIu64" failed: %s", fd,
499                   aligned_size, (uint64_t)aligned_off, strerror(errno));
500         return NULL;
501     }
502     it = (struct item *)(readbuf + (off – aligned_off));

对于删除操作:SSD在hash表的层面删掉索引. 数据不做修改, 等待自然淘汰. 这种机制保证了如果数据在SSD上, 可以不发生随机写.

总结

代码虽然很短, 还是能学到一点知识, 尤其是随机写转为顺序写的思路. 最牛逼的是对删除请求, 只删索引不改数据的机制真是简单高效.

update

1. 关于删除操作: 上文没有介绍清楚, 这里详细解释下, 从item结构可以看出它没有next指针, 也就是说, 对某个slab而言, 可以认为它只提供append item操作, 新分配的item都压在slab数据尾部. 当需要删除某个key时, fatcache只在在hash表上删除对应的itemx(前文介绍过, itemx只是item的摘要, md唯一标识key), 也就是说不管item在内存slab还是在磁盘slab, 都不会发生删除item的操作. item的真正删除是以slab为单位, 从内存淘汰到磁盘后才发生的. 作为一个缓存服务, 这没有任何问题.

2. 如何将fatcache改造为永久存储服务: fatcache代码模块化不错, 改造起来应该没什么大问题, 比较不好处理的是第1点提到的未真正删除item而带来的存储空间出现空洞的问题. 这也许可以分两方面解决: 一是对item在内存的情况, 可以用对应slab上的最后一个item覆盖待删除item的空间(对同一slab, item大小是相同的), 这种方式可以保证slab作为数组类的操作, 不会出现空洞, 仍然不需要使用额外的next指针来维护链表; 二是对item在磁盘的情况, 可以通过某种机制在特定时刻触发磁盘slab的回收, 回收时可以针对该磁盘slab上的所有item, 一一查找hash表, 如果找不到itemx或者itemx指向存储空间不同(说明item已被删除, 或者删除后插入了key相同的数据), 跳过该item, 否则, 将item从fatcache中删除后重新插入. 最后将磁盘slab标记为free即可.

vim入门,进阶与折腾

作为编辑器之神,vim一直是我编辑文本的不二选择,哪怕其坎坷的学习曲线让人头疼不已.末学总结一下经验教训,以作备忘.

入门

个人习惯编译选项

./configure –with-features=huge –enable-cscope –enable-fontset –enable-multibyte –enable-perlinterp –enable-rubyinterp –enable-pythoninterp

工作目录

:pw[d] 显示当前工作目录
:cd[!] {path} 工作目录切换到path

移动: h j k l 0 ^ $ e E b B w W f F t T ; , % gg G [ ]
编辑: d y p r c o

在vim中还存在如x,s等编辑命令,这些命令只是 编辑命令 + 移动命令 的简答组合,如x X D C s S Y

简单总结大小写操作命令的区别:

作用于行首: I
作用于行尾: D,C,A,R
作用于整行: Y,S,V
作用于逆方向: X,F,P,O,N
搜索时大小写相关: \C
改变大小写: gU

关于编辑命令,需要说明一点: x,d,c这些命令会把脏数据置入vim的粘贴板(所谓脏数据,也就是被命令删除或改变的那部分数据).

利用vim的这个特性,我们可以轻易的实现文本剪切,如交换两个字符,交换两行文本,甚至是交换两段文本:

16进制编辑,码农必备,lol

:%!xxd 16进制编辑
:%!xxd -r 文本编辑

保存会话

Session可以忠实地记录vim当前的视图,如windows和tabs,甚至是高亮.真是谁用谁知道.

:mks[!] [file] 把当前视图保存到file,如file未指定,则缺省为Session.vim
vim -S Session.vim 打开Session.vim,也就是Reload view,重新打开视图.

可视化编辑

v: charwise
V: linewise

Ctrl-V: blockwise,后文介绍.

gv 重选上次选中的区域,谁用谁知道

区域

% 作用于当前打开的整个文件
‘<,'> 作用于当前选中的区域,其中,’<表示选中区域第一行,'>表示选中区域的最后一行.
{num1},{num2} 作用于从第num1行到第num2行

内置命令

. 当前行,比如说,想要作用于从当前行开始的总共8行文本,则可以8:,
$ 最后一行,

:[range] substitute/from/to/[flags] 替换文本

:[range] copy {num1} {num2} 复制文本

:[range] Tabluarize /{char1} 对齐文本

甚至可以是:
:[range] TOhtml 文本转为html

查找: / ? * #

:help pattern

/ 向前查找keyword
? 向后查找keyword

n 重做最后一次/或?
N 反方向重做最后一次/或?

\c 查找时忽略大小写
\C 查找时大小写相关

\< 表示一个词的开始 \> 表示一个词的结束

tabs

:help tabpage

:tabnew 新建tab
:tabclose 关闭当前tab
:tabedit {file} 新建tab,并在新创建的tab打开file (等价于: tabnew后:edit {file})
:tabmove {idx} 把当前tab移动到第idx tab之后

gt 切换到下一tab
gT 切换到上一tab
{idx}gt 切换到第idx tab

为了方便,我的vimrc配置如下:

536 map <A-1> 1gt
537 map <A-2> 2gt
538 map <A-3> 3gt
539 map <A-4> 4gt
540 map <A-5> 5gt
541 map <A-6> 6gt
542 map <A-7> 7gt
543 map <A-8> 8gt
544 map <A-9> 9gt

尽管很dirty,好歹能工作.

windows

:sp {file} 横向(horizontally)切割窗口,并在新窗口打开file
:vsp {file} 竖向(vertically)切割窗口,并在新窗口打开file

h 切换到左侧窗口
j 切换到下方窗口
k 切换到上方窗口
l 切换到右侧窗口

如果对所有vimer的配置做统计,下面这段配置绝对是出现频率最高的:

577 map <C-h> <C-w>h
578 map <C-j> <C-w>j
579 map <C-k> <C-w>k
580 map <C-l> <C-w>l

省下的绝对不仅是一次w按键,vimer你懂的.

关于窗口的扩大缩小, :help window-resize

marks

:help marks

我用得最多的marks操作是:

‘[ 跳到上一次被改变(changed)或者复制的文本段的第一个字符
” 跳回上一次跳转的地方
‘^ 跳到插入模式最后一次结束的地方
‘. 跳到上一次文本被修改的地方

缩进与对齐

:help indent.txt

> 向右缩进shiftwidth个字符大小
< 同上,但是向左缩进 = 对齐文本 >,<.=这三个命令的作用域既可以是选中的一段文本,也可以是一个文本对象(后文进阶部门会解释).

进阶

[N]<command> 执行<command>N次

NOTE: [N]<command><range>和<command>[N]<range>是不一样的,如:d3w和3dw两个操作虽然看似一样,但实际上它们在vim内部的行为是有本质区别的:
d3w表示一次删除3个w,而3dw表示一次删除一个w,重复3次.

[start pos]<command>[end pos] 从start pos开始执行<command>到end pos,[]表示其内部的命令不是必须的,也就是说,start pos和end pos都不是必须的.

gg=G和^y$是两个极好的例子:

gg=G 对齐整个文件(gg跳到第一行,=对齐,G最后一行)
^y$ 从当前行行首复制至行尾(^行首,y复制,$行尾),你能看出^y$和Y的区别吗?

NOTE: start pos和end pos仅仅表示一个位置(黑话叫锚点),至于如何从光标移动到start pos或者end pos,vim并没有做出要求.于是,我们可以轻松地敲出如下命令,大大提高文本编辑的效率:

df=, yf=, cf=, vf= 从当前字符开始删除(复制,改变,选中),直到遇到=之后
dt”, yt”, ct”, vt” 从当前字符开始删除(复制,改变,选中),直到遇到”之前

text object

:help text-objects

<action>i<object> 作用于对象内部(i: inner)
<action>a<object> 作用于整个对象(a: an)

其中,action可以是v,d,c,y,甚至可以是>,<等 而object可以是w,W,s,p,b,B,以及各种成对符号,如',",<,{,(,[等 有了文本对象,写起代码来更是得心应手,如: 向右缩进一段代码: >i{
删除(复制,改变,选中)光标所在单词: diw, yiw, ciw, viw
删除(复制,改变,选中)””内所有文本: di”, yi”, ci”, vi”
删除(复制,改变,选中)””号内所有文本,包括引号本身: da”, ya”, ca”, va”

visual block

${select region}<commands>

块操作可以一次编辑多行文本,对有规律的编辑需要实在是一大利器.如:

所谓宏,就是一段录制好的操作.

q${register}<commands>q 录制commands到寄存器register
[N]@register 重放寄存器register中的宏N次

看起来,宏和块操作的区别非常明显: 宏“可以认为”是linewise,而块操作是blockwise,也就是说,宏对应的是几行文本,块操作对应的则是选中的block.

从这明显的区别中我们可以推出一个重要的结论: 宏中的锚点有相对的概念,而块操作是绝对的.比如说,行尾就是一个最简单的相对概念,每一行的行尾所在的锚点可能都不一样,但这丝毫不影响宏正确的在所有行行尾插入一段文本.而在块操作中,命令A(在行尾插入)对应的语义却变成了block的尾部,显然,”block的尾部”这一概念对block中的所有行都是相同的,也就是所谓绝对的位置.

折腾

配色

:help syntax

vim自带了许多配色方案(在这里有各种预览),可以用colorscheme命令选择,如: colorscheme desertEx

哪怕再性感的配色,看久了也会生烦,所幸vim自带了synIDattr函数,在vimrc中加入如下脚本:

215 nmap <C-S-P> :call <SID>SynStack()<CR>
216 function! <SID>SynStack()
217    if !exists("*synstack")
218       return
219    endif
220    echo map(synstack(line(‘.’), col(‘.’)), ‘synIDattr(v:val, "name")’)
221 endfunc

:so %安装后就可以通过Ctrl-Shift-P组合键方便地查看某段文本的ID了.

得到文本ID之后,需要通过指定颜色来实现自定义.vim支持rgb配色(#445599之流),可是,身为毫无艺术感的二逼后台开发,我自然更偏爱skyblue这类见其名即可知其意的配色方案了:).为了让生活容易些,可以:runtime syntax/colortest.vim直接预览,谁用谁知道.

vim的强大总是让人爱不释手,我们甚至可以自定义ID,比如说,我的代码中TODO横行,为了更直观的显示TODO项,于是便有了这段配置:

60 highlight RipGroup ctermbg=yellow cterm=none ctermfg=black
61 match RipGroup /TODO/

btw,在终端下切记要打开256色: set t_Co=256

再次btw,简单回答一个可以很好的区分vim新手和老鸟的问题,对Alt键的map为何在终端模式下如此虐心? 因为终端他妈的自动在Alt前面加了Esc前缀,这该让人多胸闷啊.

代码折叠

:help fold.txt

vim支持多种折叠方法(fold methods),如indent,expr,marker,syntax等.我偏向于按syntax折叠,配置如下:

716 set foldenable           " enable folden
717 set foldmethod=syntax    " manual : Folds are created manually.
718                          " indent : Lines with equal indent form a fold.
719                          " expr   : ‘foldexpr’ gives the fold level of a line.
720                          " marker : Markers are used to specify folds.
721                          " syntax : Syntax highlighting items specify folds.
722                          " diff   : Fold text that is not changed.
728
729 "set foldclose=all
730 " use space to folden
731 nnoremap <space> @=((foldclosed(line(‘.’)) < 0) ? ‘zc’ : ‘zo’)<CR>

其中,.表示当前行,zo表示展开,zc表示折叠,整行配置的意思就是通过空格键折叠代码,效果如下:

vim默认在搜索和undo时会展开你辛辛苦苦设置好的折叠,这是让人非常难受的,所以我会追加这么一段配置:

726 set foldopen-=search     " dont open folds when I search into thm
727 set foldopen-=undo       " dont open folds when I undo stuff

编码

249 set encoding=utf-8
250 set fileencodings=ucs-bom,utf-8,cp936,gb18030,big5,gbk,euc-jp,euc-kr,latin1
251 if has("win32")
252     set fileencoding=chinese
253     " fix menu gibberish
254     source $VIMRUNTIME/delmenu.vim
255     source $VIMRUNTIME/menu.vim
256     " fix console gibberish
257     language messages zh_CN.utf8
258 else
259     set termencoding=utf-8
260     set fileencoding=utf-8
261 endif

gui设置

简单介绍一下gvim的设置,首先是字体,我的配置如下:

229 set guifont=Courier_New:h9:cANSI
231 set guifontwide=幼圆:h10:cGB2312

guifont对应的应为字体,guifontwide对应所谓的宽字节字体,中文就是宽字节.

我个人倾向于隐藏gvim菜单栏,工具栏,滚动条等,以最大化代码可视面积:

237 if has("gui_running")
238     " set guioptions-=m  " remove menu bar
239     set guioptions-=T  " remove toolbar
240     set guioptions-=r  " remove right-hand scroll bar
241     set guioptions-=l  " remove left-hand scroll bar
242     set guioptions-=L  " remove left-hand scroll bar even if there is a vertical split
243     set guioptions-=b  " remove bottom scroll bar
244 endif

我要吐槽的是,即便设置了set guioptions-=l,当切割了横向窗口时,左侧的滚动条还是会如幽灵般出现.各种不解后查了手册才明白,原来还要set guioptions-=L,但是,右侧滚动条却没有这个坑,简直坑爹.

tags

:help tags

tags是什么,程序员都懂.通过ctags程序可以很方便的为C++/C项目生成tags:

ctags -R –c++-kinds=+p –fields=+iaS –extra=+q .

–c++-kinds=+p 生成函数原型,该选项默认关闭.同样默认关闭的选项还有l(局部变量)和x(外部变量).
–fileds=+iaS 分别对应类的继承inheritabce,类成员访问权限(access)和routine签名(Signature, 如原型或参数列表等).
–extra=+q 为类成员生成的tag加上其所属的类信息.

这行命令敲起来太累了,不如按一下F5来的痛快:

492 map <F5> :!ctags -R –c++-kinds=+p –fields=+iaS –extra=+q .<CR>

该命令生成的文件为当前目录下的tags.

作为后台开发程序员,查阅系统源码是常有的事,不妨为/usr/include目录生成tags,然后配置vimrc,以便每次启动vim时自动加载(即便有再多的autoload,即便vim启动速度再慢,也足以秒杀emacs了…这算是降维攻击不?):

274 if has("win32")
275     set tags+=E:\workspace\linux\tags  " tags for /usr/include/
276 else
277     set tags+=~/.vim/tags/include/tags " tags for /usr/include/
278 endif
279 set tags+=tags                         " tags for current project

需要说明的是,我偶尔需要在windows上写代码,所以我把linux下的/usr/include目录拷贝到了windows上,然后用ctags windows版生成了tags,于是在windows上写代码也舒适了许多.

插件

vim什么都硬,只是有一点比较短: 扩展性,vim在这一点被咖啡机emacs拉下远远不止一条街,但是对于普通青年我,却也算是差强人意了.”Do one thing and do it well”,这恐怕是不少vim拥趸自我解脱的说辞.

下面是个人最喜欢的一些插件,排名不分先后:

Quickfix.vim

:help quickfix

Quickfix是vim的标准插件,它是一个典型的plumber: 只要输入符合error format(efm),则vim可以正确理解和识别”错误列表”,并跳转到对应行.

我们通过gcc和grep的输出来更好地理解error format:

Error: Unclassifiable statement at hello-world.f90:9.4 gcc编译器的错误提示
./sys/net/bpf.c:137: bpf_wakeup __P((struct bpf_d *)); grep的输出(加上-n)

可以看到,它们的格式是非常类似的.

虽然Quickfix插件原意是为了更方便地调试代码,可是借用error format实现文本的匹配也是非常拉轰的:

505 " search word under cursor like source insight
506 " <cword> is replaced with the word under the cursor (like |star|) (:help cmdline or :help cword)
507 map <C-F> :execute "let g:word=expand(\"<cword>\")"<Bar>execute "vimgrep /\\<" . g:word ."\\>/g **/*.[ch] **/*.cpp"<Bar>execute "cc 1"<Bar>execute "cw"<CR>
508 " next matched line
509 map <silent> <F10> :cnext<CR>
510 " previous matched line
511 map <silent> <F11> :cprevious<CR>
512 " open QuickFix
513 " :copen
514 " close QuickFix
515 " :cclose

简单说明一下这段配置,表示当前光标下的单词(:help cmdline or :help cword),也就是|,表示串联命令.cc 1表示跳到”错误列表”第一条,cw表示打开quickfix窗口,如果存在可识别的错误列表.cnext和cprevious表示在错误列表中切换.

btw,vim在匹配时默认使用自带的vimgrep插件,如果觉得不方便,可以显示指定使用grep: set grepprg=grep

A.vim

A.vim插件可以方便地切换源文件和头文件,还是那句话,谁用谁知道啊.

:A 在同一tab切换源文件/头文件
:AV 竖向切割窗口,打开对应的源文件/头文件.
:AS 横向切割窗口,打开对应的源文件/头文件.

NERDTree.vim

NERDTree插件可以清楚地展示目录树,而且支持许多快捷键.个人最喜欢的快捷键是t: 在新建的tab打开光标所对应的文件.可惜的是,NERDTree原版插件对所新建的tab的命名看起来没什么具体的含义,于是我用上了二手版,配置如下:

378 map <F6> <plug>NERDTreeTabsToggle<CR>
379
380 let g:nerdtree_tabs_open_on_gui_startup=1     " Open NERDTree on gvim/macvim startup
381 let g:nerdtree_tabs_open_on_console_startup=1 " Open NERDTree on console vim startup
382 let g:nerdtree_tabs_open_on_new_tab=1         " Open NERDTree on new tab creation
383 let g:nerdtree_tabs_meaningful_tab_names=1    " Unfocus NERDTree when leaving a tab for descriptive tab names
384 let g:nerdtree_tabs_autoclose=1               " Close current tab if there is only one window in it and it’s NERDTree
385 let g:nerdtree_tabs_synchronize_view=1        " Synchronize view of all NERDTree windows (scroll and cursor position)
386
387 " When switching into a tab, make sure that focus is on the file window, not in the NERDTree window.
388 let g:nerdtree_tabs_focus_on_files=1

OmniCppCompelete.vim

OmniCppComplete借助于tags实现智能补全,Ctrl-X Ctrl-O弹出待选择tags菜单,Ctrl-N切换至下一选项,Ctrl-P切换至上一选项.

个人配置如下:

318 " :help omnicppcomplete
319 set completeopt=longest,menu      " I really HATE the preview window!!!
320 let OmniCpp_NameSpaceSearch=1     " 0: namespaces disabled
321                                   " 1: search namespaces in the current buffer [default]
322                                   " 2: search namespaces in the current buffer and in included files
323 let OmniCpp_GlobalScopeSearch=1   " 0: disabled 1:enabled
324 let OmniCpp_ShowAccess=1          " 1: show access
325 let OmniCpp_ShowPrototypeInAbbr=1 " 1: display prototype in abbreviation
326 let OmniCpp_MayCompleteArrow=1    " autocomplete after ->
327 let OmniCpp_MayCompleteDot=1      " autocomplete after .
328 let OmniCpp_MayCompleteScope=1    " autocomplete after ::

在智能补全时,OmniCppComplete会在当前工作窗口上方横向切割出一个preview窗口,preview窗口包含当前待选项的各种说明.杯具的是,当通过Ctrl-N或者Ctrl-P切换待选tags时,该preview窗口将会随着待选说明的变化而增大缩小,如果切换速度较快,则preview窗口看起来就像抖动一般.这也让人很难受,我们可以如下配置,关闭preview特性.

319 set completeopt=longest,menu      " I really HATE the preview window!!!

另外,不要被插件名字欺骗了,OmniCppCompelete同样支持其他语言,如python,xml等.配置如下:

331 autocmd FileType python set omnifunc=pythoncomplete#Complete
332 autocmd FileType javascript set omnifunc=javascriptcomplete#CompleteJS
333 autocmd FileType html set omnifunc=htmlcomplete#CompleteTags
334 autocmd FileType css set omnifunc=csscomplete#CompleteCSS
335 autocmd FileType xml set omnifunc=xmlcomplete#CompleteTags
336 autocmd FileType php set omnifunc=phpcomplete#CompletePHP
337 autocmd FileType c set omnifunc=ccomplete#Complete

关于autocmd,找manual: help autocmd

Supertab.vim

vim在插入模式下支持13种补全方式(:help ins-completion),普通青年记不住,于是有了Supertab插件,配置如下:

312 let g:SuperTabRetainCOmpletionType=2    " 2: remember last autocomplete type, unless I use ESC to exit insert mode
313 let g:SuperTabDefaultCompletionType="<C-X><C-O>"

Taglist.vim

该插件展示当前文件对应的tags列表.配置如下:

299 if has("win32")
300     let Tlist_Ctags_Cmd=‘ctags’             " set ctags path
301 else
302     let Tlist_Ctags_Cmd=‘~/ctags-5.8/ctags’ " set ctags path
303 endif
304 let Tlist_Show_One_File=1               " only show current file’s taglist
305 let Tlist_Exit_OnlyWindow=1             " if taglist is of the last windows, exit vim
306 let Tlist_Use_Right_Window=1            " show taglist at right
307 let Tlist_File_Fold_Auto_Close=1        " hide taglist if it’s not for current file

mark.vim

这是一款高亮插件.<leader>m高亮当前光标所对应的单词,再次<leader>m清除高亮.<leader>n清除所有高亮.

mark.vim默认只有下面这6种高亮颜色.如果觉得太少,可以自由地在mark.vim中添加.

 68 " default colors/groups
 69 " you may define your own colors in you vimrc file, in the form as below:
 70 hi MarkWord1  ctermbg=Cyan     ctermfg=Black  guibg=#8CCBEA    guifg=Black
 71 hi MarkWord2  ctermbg=Green    ctermfg=Black  guibg=#A4E57E    guifg=Black
 72 hi MarkWord3  ctermbg=Yellow   ctermfg=Black  guibg=#FFDB72    guifg=Black
 73 hi MarkWord4  ctermbg=Red      ctermfg=Black  guibg=#FF7272    guifg=Black
 74 hi MarkWord5  ctermbg=Magenta  ctermfg=Black  guibg=#FFB3FF    guifg=Black
 75 hi MarkWord6  ctermbg=Blue     ctermfg=Black  guibg=#9999FF    guifg=Black

最后

推荐两大暗爽已久的神器,一是vimperator,在firefox上高度仿真了vim.一是hhkb,其惊艳的键位布局彻底释放了我被压抑多时的小拇指(码农你懂的),而奢侈的电容键盘更让人概叹“RealForce的素质,那仅仅是HHKB的起点而已”.当然,如果拿hhkb来码中文,那就是另一回事了…

初探Linux网络协议栈

一点声明
原文链接: http://www.ecsl.cs.sunysb.edu/elibrary/linux/network/LinuxKernel.pdf


译者注: 原文写于2003年,文中描述的不少内容已经发生了改变,在不影响愿意的情况下,我擅自增删了一些内容.

翻译过程中找到的好资料:

translated by ripwu, 个人主页: http://godorz.info


 

sk_buff

结构体定义

struct sk_buff

    /* These two members must be first. */ 
    struct sk_buff * next;     /* Next buffer in list */ 
    struct sk_buff * prev;     /* Previous buffer in list */ 

    struct sk_buff_head * list;   /* List we are on */ 
    struct sock *sk;                 /* Socket we are owned by */ 
    struct timeval stamp;          /* Time we arrived */ 
    struct net_device *dev;       /* Device we arrived on/are leaving by */ 

    /* Transport layer(TCP, UDP, ICMP, etc). header */ 
    union 
    { 
        struct tcphdr *th; 
        struct udphdr *uh; 
        struct icmphdr *icmph; 
        struct igmphdr *igmph; 
        struct iphdr *ipiph; 
        struct spxhdr *spxh; 
        unsigned char *raw; 
    } h; 

    /* Network layer header (IPv4, IPv6, arp, raw, etc).  */ 
    union 
    { 
        struct iphdr *iph; 
        struct ipv6hdr *ipv6h; 
        struct arphdr *arph; 
        struct ipxhdr *ipxh; 
        unsigned char *raw; 
    } nh; 

    /* Link layer header */ 
    union  
    {  
        struct ethhdr *ethernet; 
        unsigned char *raw; 
    } mac; 

    struct dst_entry *dst; 

    char cb[48];   

    unsigned int len;              /* Length of actual data */ 
    unsigned int data_len; 
    unsigned int csum;          /* Checksum */ 
    unsigned char __unused, /* Dead field, may be reused */ 
             cloned,                 /* head may be cloned (check refcnt to be sure). */ 
             pkt_type,              /* Packet class */ 
             ip_summed;          /* Driver fed us an IP checksum */ 
    __u32 priority;               /* Packet queueing priority */ 
    atomic_t users;              /* User count – see datagram.c,tcp.c */ 
    unsigned short protocol; /* Packet protocol from driver. */ 
    unsigned short security; /* Security level of packet */ 
    unsigned int truesize;     /* Buffer size */ 

    unsigned char *head;    /* Head of buffer */ 
    unsigned char *data;     /* Data head pointer */ 
    unsigned char *tail;       /* Tail pointer */ 
    unsigned char *end;      /* End pointer */ 
}

布局(Layout)

一个sk_buff对应的内存抽象图示为:

为了实现sk_buff在各层间传输时的零拷贝,采用的是将数据放在内存最后,增减报文头时在内存前面操作,然后重新设置相关指针即可.下图演示了一个UDP数据包在发送时添加UDP报头和IP报头的过程:

sk_buff的实现方式是双向链表.和所有的链表一样,sk_buff含有指向下一元素的next指针和指向前一元素的prev指针,为了更快的知道某个sk_buff元素属于哪一链表,sk_buff还包含一个指向头结点的list指针.

struct sk_buff_head {
    /* These two members must be first. */
    struct sk_buff * next;
    struct sk_buff * prev;
    _ _u32 qlen;
    spinlock_t lock;
};

如上所示,链表的头结点比较特殊,它包括了链表长度qlen和用于同步的自旋锁lock.

sk_buff元素组成的链表结构图如下:

创建和销毁

alloc_skb()负责sk_buff的创建,从sk_buff结构体的定义可以看到,当创建一个sk_buff时,总共需要申请两块内存,一块内存存储sk_buff本身(通过kmem_cache_alloc()),另一块内存存储sk_buff.data指向的数据区(通过kmalloc()).示意图如下:

其中, Padding的作用是字节对其. skb_shared_info这里不做介绍,详细信息参考<Understand Linux Network Kernel>.

sk_buff的销毁稍微复杂,但原理很简单,这里也不做介绍.

2. 网络层

本节介绍底层接收和处理packet(数据包)的流程.

接收packet的过程如下:

1. 网卡接收packet.

这些packet由DMA机制存储在最近使用的网卡的rx_ring中.rx_ring是一种工作于内核态的环形队列,其容量大小依赖于具体的网卡.有一种比较老的网卡,工作方式为轮询(PIO)模式: 由CPU将网卡数据读入内存态内存.

2. 网卡产生中断.

CPU开始执行网卡驱动的ISR代码,在2.4.19内核版本后,工作方式有些区别:

对于2.4.19版本之前(包括2.4.19)的内核:

如上图所示,中断处理程序将调用netif_rx()(net/dev/core.c). netif_rx()将接收到的数据压入被中断CPU的backlog队列,然后调度一个软中断(softirq, 一种内核行为,见 http://tldp.org/HOWTO/KernelAnalysis-HOWTO-5.html 和  http://www.netfilter.org/unreliable-guides/kernel-hacking/lk-hacking-guide.html 译注: 这里实际是指”下半段”,与硬中断的”上半段”呼应,见<Linux内核设计与实现> Update[2011-09-06] 译注错误: 软中断是bottom half的扩展,两者不应混为一谈. 操作系统之所以独立分开一个top half的概念,主要是因为中断是在CPU关中断(CLI)的情况下运行的,为了不丢失新到的中断信号,操作系统将中断服务程序一分为二,前者(top half)对其执行时间要求比较严格,一般立刻响应中断请求,为保证原子操作,它在关中断情况下执行,后者(bottom half)对时间要求没那么严格,通常在开中断下执行.但是,bottom half有一个致命的缺点,它只能在一个CPU上运行,也就是严格的串行,为了更充分的利用SMP,Linux2.4内核扩展了bottom half,成果便是所谓的”软中断(softirq)”机制,软中断最大的优点在于它可以在多CPU上运行,它在设计与实现中自始自终都贯彻了一个思想: “谁触发,谁执行(Who marks, Who runs),即触发软中断的那个CPU负责执行它所触发的软中断,而且每个 CPU 都由它自己的软中断触发与控制机制.), 由 软中断来负责packet的进一步处理(比如说: TCP/IP 协议的处理). backlog队列的 长度为300个packet大小(/proc/sys/net/core/netdev_max_backlog).

当backlog队列为满时,它转入throttle状态,在此状态等待数据清空,然后重新回到正常状态,正常状态下,backlog允许packet的入队操作.(netif_rx(), net/dev/core.c). 当backlog队列处于throttle状态时, netif_rx()将丢弃新来的packet.  我们可以通过/proc/net/softnet_stats来查看backlog队列的统计信息: 每一行对应一颗CPU,前两列分别是packet量和丢包量,第三列表示backlog队列有多少次进入了throttle状态.

对于2.4.19版本之后的内核:

如上图所示,内核采用的是NAPI新机制(见http://en.wikipedia.org/wiki/NAPI): 中断处理程序调用netif_rx_schedule()(include/linux/netdevice.h).该函数执行入队的对象不是packet,而是packet的一个引用(reference).(softnet_data>poll_list; include/linux/netdevice.h). 与旧内核机制相同的是,中断处理程序同样会调度一个软中断.为了向后兼容, 在NAPI机制中, backlog被当作像网卡一样可以处理所有到达packet的虚拟设备.内核开发者重写了netif_rx(),该函数将packet压入backlog之后,又将backlog压入CPU的poll_list链表.

3. 软中断执行net_rx_action()(net/core/dev.c).

在这一步中,新老内核处理方式同样有所不同:

对于2.4.19版本之前(包括2.4.19)的内核:

net_rx_action()为backlog队列中的所有packet调用**_rcv()函数(net/ipv4/ip_input.c),这里的**指代不同的packet类型,如ip, arp, bootp等.

对于NAPI:

CPU轮训其poll_list链表中的设备(注: NAPI中backlog为虚拟设备, process_backlog: net/core/dev.c),从设备的rx_ring环形队列中获得全部packet.CPU的轮询是通过 netif_receive_skb()(net/core/dev.c)调用ip_rcv()来完成的.

当一个packet被创建时,内核做了如下工作:

*IP packet由arp_constructor()创建.每个packet都包含dst field信息,dst提供了路由所需的目的地址,它对应一个output方法,对于IP packet而言,此方法为dev_queue_xmit().

*内核提供了多种排队原则(queueing disciplines, 简称qdisc).默认的排队原则使用FIFO队列,其缺省长度为100个packet大小.(ether_setup(): dev->tx_queue_len ; drivers/net/net_init.c),此长度可以通过带txqueuelen选项的ifconfig命令进行设置. 我们无法查看默认FIFO的统计信息,这里提供一个小技巧,通过tc 命令可以设置新的FIFO,以取代缺省的qdisc:

  • 取代缺省的qdisc: tc qdisc add dev eth0 root pfifo limit 100
  • 查看统计信息: tc -s -d qdisc show dev eth0
  • 重新设置缺省qdisc: tc qdisc del dev eth0 root

1.对从IP层(IP layer)传入的packet,调用dev_queue_xmit()(net/core/dev.c).该函数将packet压入外发网卡(output interface, 它由路由决定)相应的qdisc.如果网卡驱动正常, qdisc_restart()(net/sched/sch_generic.c)将处理qdisc中的所有packet;

2.调用hard_start_xmit().该方法实现于网卡驱动,它将packet压入tx_ring环形队列,然后网卡驱动将通知网卡有数据可发送.

3. 网卡发送packet并通知CPU. CPU调用net_tx_action()(net/core/dev.c)将packet压入completion_queue,然后调度一个负责释放packet内存的软中断.网卡与CPU之间的通信方式是硬件相关的,这里不作详细介绍.

3. 传输层

IP相关文件有:

  • ip_input.c         –  处理到达packet
  • ip_output.c      –  处理发送packet
  • ip_forward.c    –  处理由本机路由的packet

还有一些模块负责处理 IP packet fragmentation(ip_fragment.c),IP options(ip_options.c), multicast(ipmr.c) 和 IP over IP (ipip.c)

下图描述了packet在IP层的流转路径. 当一个packet到达主机后,如前文所述种种流程,net_rx_action()将它转给ip_rcv()处理. 在经过first netfilter hook后, ip_rcv_finish()验证该packet的目的地是否就是本机.如果是的话, ip_rcv_finish()将该packet传给ip_local_delivery(), 然后由ip_local_delivery()转发给合适的传输层(transport layer)处理函数(tcp, udp, etc).

如果IP packet的目的地址是其他主机,那么处理该packet的当前主机就起了router的角色(这种场景在小型局域网上很常见). 如果主机允许转发packet(通过/proc/sys/net/ipv4/ip_forward查看或设置),那么该packet将由一系列复杂但高效的函数处理.

如果路由表(它是一种hash表)中存在packet的路由信息的话,该packet的行经路径通过ip_route_input()查找,否则,通过ip_route_input_slow(). ip_route_input_slow()调用fib(Forward informationbase, 路由表)族函数,这些函数定义在fib*.c文件中. FIB结构体非常复杂.

如果packet是多播(multicast)数据包,那么内核通过ip_route_input_mc()计算出packet发往的那些设备(该情况下,packet目的地址不变).

在计算出packet的路由信息之后,内核往IP pcaket插入新的目的地址,并将其所属设备信息插入对应的sk_buff结构. 然后,packet被转发(forwarding)函数(ip_forward() 和 ip_forward_finish()).

一个packet也可以从上层发往ip层(通过 TCP or UDP 传输). 处理该packet的第一个函数ip_queue_xmit(),它通过ip_output()将packet发往output part.

output part对packet做了最后的修改. dev_queue_transmit()将packet压入输出队列(output queue),然后通过q->disc_run()来调用网络调度机制(network scheduler mechanism).该指针指向不同的函数,这取决于内核采用的调度器(默认为FIFO类型,可以通过tc工具进行设置).

调度函数(qdisc_restart() 和 dev_queue_xmit_init())独立于IP层其他函数.

4. TCP

本节介绍Linux内核网络协议栈中最为复杂的TCP.

Introduction

TCP相关文件有:

  • tcp_input.c           –  最大的一个文件,它处理网络中到达的packet
  • tcp_output.c        –  处理将发往网络的packet
  • tcp.c                         –  TCP代码
  • tcp_ipv4.c             –  IPv4具体的TCP代码
  • tcp_timer.c           – 定时器
  • tcp.h                         – 定义TCP结构体

TCP数据的处理如下两图,上图处理接收,下图处理发送.

TCP Input (mainly tcp_input.c }

TCP input在TCP实现中占了很大一部分.它处理TCP packet的接收,因为TCP实体(TCP entity, 也就是TCP协议栈)可以同时处于接收和发送两种状态,所以这两类代码混杂在了一起.

ip_local_deliver()将packet从IP层发往TCP层.它把packet传给ipproto->handler,在IPv4的实现中该handler就是tcp_v4_rcv().此函数进一步调用tcp_v4_do_rcv().

tcp_v4_do_rcv()会根据TCP连接(connection)的不同状态调用不同的函数.如果连接已建立(TCP_ESTABLISHED),它会调用tcp_rcv_established(),这是我们接下来会重点介绍的部分. 如果连接状态是TIME_WAIT,它会调用tcp_timewait_process().

对于其他的状态, tcp_v4_do_rcv()统一调用tcp_rcv_state_process().对于SYS_ENT状态的连接,该函数进一步调用tcp_rcv_sysent_state_process().

tcp_rcv_state_process()和tcp_timewait_process()必须初始化TCP结构体,这通过tcp_init_buffer_space()和tcp_init_metrics()完成. tcp_init_metrics()调用tcp_init_cwnd()来初始化其拥塞窗口(congestion window).

tcp_rcv_established()

tcp_rcv_established()有两条分支路径.我们首先介绍慢路径(slow path)分支,因为它简单清晰,另一分支留待后文介绍.

slow path

在RFC中,slow path只要有7步操作:

  • tcp_checksum_complete_user()计算校验码(checksum).如果检验码有误,packet被丢弃.
  • tcp_paws_discard()负责PAWS(Protection Against Wrapped Sequence Numbers).
  • STEP 1 – 检查packet序列号(sequence). 如果序列号不再接收端口中,接收模块(receiver)将 通过tcp_send_dupack()发回一个DUPACK.tcp_send_dupack()通过tcp_dsack_set()设置一个SACK,然后调用tcp_send_ack()进行发送.
  • STEP 2 – 检查RST位(th->rst),如果该位被置位,调用tcp_reset().
  • STEP 3 – 检查安全性(security)和优先级(precedence, RFC建议,但内核未实现)
  • STEP 4 – 检查SYN位,如果该位被置位,调用tcp_reset()…
  • 通过tcp_replace_ts_recent()估算RTT(RTTM).
  • STEP 5 -检查ACK位,如果该位被置位,调用tcp_ack().
  • STEP 6 -检查URG位,如果该位被置位,调用tcp_urg().
  • STEP 7 -通过tcp_data_queue()处理packet携带的数据.
  • 通过tcp_data_snd_check()检查是否有数据要发回.该函数将调用TCP发送模块( output sector)的tcp_write_xmit().
  • Finally, 通过tcp_ack_snd_check()检查是否有ACK要发回.ACK的发回有两种情况: 通过tcp_send_ack()直接发回; 或者通过tcp_send_delayed_ack()延时发回(译注: 这是TCP的延时确认(Delayed ACK)机制,目的为减少分组,可通过/proc/sys/net/ipv4/tcp_delack_min设置),被延时的ACK存储在tcp->ack.pending.

tcp_data_queue()

tcp_data_queue()负责处理packet数据.如果packet顺序到达(所有之前的packet已到达),它将把数据拷贝到tp->ucopy.iov (skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)).

如果packet并非顺序到达,那么它将通过tcp_ofo_queue()把packet压入乱序队列(out of order queue).

如果乱序队列满,根据RFC 2581文档4.2节, 协议栈将返回一个ACK(tp->ack.pingpong 被置0,tcp_ack_snd_check()被调用来返回ACK).

packet的到达引起各种反应,这由tcp_event_data_recv()处理. 它首先通过tcp_schedule_ack()调度一个ACK的发回,然后通过tcp_measure_rcv_mss()估算MSS(Maximum Segment Size).在特定的情况下(如: 慢启动), tcp_event_data_recv()选择调用tcp_incr_quickack(),以立刻返回ACK(即不使用延时确认机制).最后, tcp_event_data_recv()通过tcp_grow_window()增大通告窗口(advertised window, 译注: 其实也就是receive window,即接收窗口,更多参数设定见 http://140.116.72.80/~smallko/ns2/TCPparameter.htm).

tcp_data_queue()最终将检查FIN位,如果该位被置位,调用tcp_fin().

tcp_ack()

对发送模块(“sender”)而言,它每收到一个ACK,都会调用tcp_ack(),注意这一点不要和接收模块(“receiver”)通过tcp_send_ack()调用tcp_write_xmit()发送ACK的行为混淆了.

tcp_ack()首先检查ACK是否比先前收到的ACK较老还是较新,如果较老的话,内核通过执行 uninteresting_ack和old_ack无视之.

如果一切正常, tcp_ack()将通过tcp_ack_update_window()和/或tcp_update_wl()更新发送端的拥塞窗口.

如果ACK可疑(dubious) ,那么通过tcp_fastretrans_alert()进入快速重传(fast retransmit).

tcp_fast_retransmit()

tcp_fast_retransmit_alert()只由tcp_ack()在特定条件下调用.为了更好的理解这些特定条件,我们必须对
NewReno/SACK/FACK/ECN状态有深入的认识.注意这个TCP状态机本身没有什么关系,在这里连接状态几乎可以确定是TCP_ESTABLISHED.

拥塞控制状态有:

  • “Open” – 正常状态,无可疑事件,属于快速路径(fast path).
  • “Disorder” -当发送方检测到DACK(重复确认)或者SACK(selective ACK带选择的ACK, 见http://simohayha.iteye.com/blog/578744)时会进入该状态,它可以看成是Open状态,之所以会将”DisOrder”从”Open”中分离出来,主要是为了将某些处理从快速路径(fast path)移到慢速路径(slow one). (译注: 在这个状态,拥塞窗口不会被调整,每一次新的输入数据包都会触发一个新的端的传输)
  • “CWR”(拥塞窗口减小) – 某些拥塞通知事件(Congestion Notification event)将减小CWND(译注: 即congestion window拥塞窗口). 这些事件包括ECN(Explicit Congestion Notification显式拥塞通知,见http://en.wikipedia.org/wiki/Explicit_Congestion_Notification), ICMP远端抑制(ICMP source quench, 见http://en.wikipedia.org/wiki/ICMP_Source_Quench)和本机设备的拥塞.(译注:当接收到一个拥塞通知时,发送方并不立刻减小拥塞窗口,而是每隔一个新收到的ACK减小一段知道拥塞窗口大小减半为止,发送方在减小拥塞窗口的过程中不会有明显的重传. CWR状态可以被Recovery或Loss状态中断.)
  • “Recovery” – 该状态下CWND被减小,正在快速重传(fast-retransmitting). (译注: 默认情况下,进入Recovery状态的条件是三个连续的重复ACK.在Recovery状态期间,拥塞窗口的大小每隔一个新到的确认减少一个段,这和CWR状态类似.窗口减小直到刚进入Recovery状态时窗口大小的一半为止.拥塞窗口在恢复状态期间不增大,发送方重传那些被标记为丢失的段,或者根据包守恒原则在新数据上标记前向传输.发送方保持Recovery状态直到所有进入Recovery状态时正在发送的数据段都成功地被确认,之后该发送方恢复Open状态.重传超时有可能中断Recovery状态.)
  • “Loss” – 当RTO到期(RTO timeout)或者SACK拒绝(SACK reneging, 译注: 表示接收到的ACK的确认已经被先前的SACK确认过),发送方进入Loss状态.CWDN减小而后增大. (译注: Loss状态下所有正在发送的数据段标记为loss,拥塞窗口重置为1,发送方然后按慢启动算法增大拥塞窗口.Loss和Recovery状态的一个主要区别是: 在Loss状态,拥塞窗口在发送方重置为一个段后增大,而Recovery状态下拥塞窗口只能被减小. Loss状态不能被其他的状态中断,因此,发送方只能在所有Loss开始时正在传输的数据都成功得到确认ack后,才能回到Open状态,例如, 快速重传(fast retransmitting)不能在Loss状态期间被触发)

这些状态保存于tp->ca_state,对应值分别是TCP_CA_Open, TCP_CA_Disorder, TCP_CA_Cwr, TCP_CA_Recover 和 TCP_CA_Loss.

当收到ACK时状态不为Open,或者收到了奇怪的ACK(如SACK, DUPACK ECN ECE),内核会调用tcp_fastretrans_alert(). (译注: 此函数非常重要,它负责拥塞控制的处理,包括处理显式拥塞通知,判断SACK是否虚假,拥塞时记录的SND,NXT被确认时进行撤销,以及当前状态的处理等.详细见http://research.csc.ncsu.edu/netsrv/sites/default/files/hystart_techreport_2008.pdf)

fast path

在TCP处理过程中,receiver比较简单,因此它经常进入快速路径.

SACKs

Linux TCP协议栈完整实现了SACKS(带选择的ACK).SACK信息存储于tp->sack_ok field.

quickacks

某些情况下,receiver进入quickack模式,也就是说,延时确认机制(delayed ACK)被禁用.在quickack模式下,连接开始时,tc_rcv_sysent_state_process()将调用tcp_enter_quick_ack_mode().

Timeouts

定时器对TCP的正确工作起到了至关重要的作用.比如说,借助定时器,TCP可以判断packet是否在网络中丢失.

当发送一个packet时,其重传定时器(retransmit timer)会被设置. tcp_push_pending_frames()通过tcp_check_probe_timer()调度一个软中断(软中断有内核中非网络协议栈相关的代码处理).

定时器的超时事件会产生一个软中断,软中断通过timer_bh()调用run_timer_list(). run_timer_list()将调用timer->function函数指针指向的tcp_wite_timer(). tcp_wite_timer()进而调用tcp_retransmit_timer(),最终, tcp_wite_timer()调用tcp_enter_loss(). tcp_enter_loss()将拥塞控制状态设为CA_Loss,然后由fastretransmit_alert()重传packet.

ECN

ECN(Explicit Congestion Notification,即显式拥塞通知)代码比较简单, 几乎所有的代码都在/include/net目录下的tcp_ecn.h文件中. tcp_ack()调用TCP_ECN_rcv_ecn_echo()来处理ECN packet.

TCP output

这部分代码(主要在tcp_output.c)负责packet(包括sender发送的数据packet和receiver发回的ACK)的发送.

5. UDP

本节简单介绍UDP,它很重要,但比起TCP,其拥塞控制却简单多了.

UDP相关文件:

  • net/ipv4/udp.c

当packet通过ip_local_delivery()从IP层到达时,它被传给udp_rcv()(该函数角色类似于TCP的tcp_v4_rcv()). udp_rcv()调用sock_put()将packet压入socket queue.packet的传递就到此结束了.内核然后调用inet_recvmsg()(通过recvmsg()系统调用), inet_recvmsg()调用udp_recvmsg(), 而udp_recvmsg()进一步调用skb_rcv_datagram(). skb_rcv_datagram()函数从队列中获得packet,然后据此packet填充用户态将读取的结构体.

当一个packet从用户态到达时(译注: 也就是说要发送出去),处理就更加简单了. inet_sendmsg()调用udp_sendmsg(), udp_sendmsg()通过从sk结构体获取的信息(这些信息在socket被创建和被绑定(bind调用)时被设置于sk中)填充UDP数据报(UDP datagram).

当UDP数据报填充完成之后,数据报被传给ip_build_xmit(), ip_build_xmit()通过ip_build_xmit_slow()创建IP packet.

IP packet创建完成以后, packet被传给ip_output(),正如4 – Network Layer 介绍的一样, ip_output()将packet发往更低层.