kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

SystemTap Beginner

http://blog.csdn.net/kafeiflynn/article/details/6429976

SystemTap

应用:

对管理员,SystemTap可用于监控系统性能,找出系统瓶颈,而对于开发者,可以查看他们的程序运行时在linux系统内核内部的运行情况。主要用于查看内核空间事件信息,对用户空间事件的探测,目前正加紧改进。

安装

1、SystemTap的安装及使用需要针对正在使用的内核安装相应的kernel-devel、kernel-debuginfo和kernel-debuginfo-common包,以插入探针。
2、安装SystemTap和SystemTap-runtime包
3、使用如下命令测试一下:

1
stap -v -e 'probe vfs.read {printf("read performed/n"); exit()}'

为目标机产生SystemTap instrumentation:

这样就可以在一台机器上为多种内核产生SystemTap instrumentation,而且目标机上只安装SystemTap-runtime即可。

操作如下:
1.在目标机上安装systemtap-runtime RPM包;
2.使用uname –r查看目标机内核;
3.在host system上安装SystemTap;
4.在host system上安装目标机内核及相关RPMs
5.在host name上运行命令:

1
stap -r kernel_version script -m module_name

6.把新产生的模块拷贝到目标机,并运行如下命令:

1
staprun module_name.ko

注意:host system和目标机架构及操作系统版本必须一致。

运行SystemTap脚本

运行stap和staprun需要被授以权限,一般用户需要运行SystemTap,则需要被加入到以下用户组的一个:
1、stapdev:用stap编译SystemTap脚本成内核模块,并加载进内核;
2、stapusr:仅能运行staprun加载/lib/modules/kernel_version/systemtap/目录下模块。

SystemTap Flight Recorder模式

该模式允许长时间运行SystemTap脚本,但仅focus on 最近的输出,有2个变种:in-memory和file模式,两种情况下SystemTap都作为后台进程运行。

In-memory模式:
1
stap -F iotime.stp

一旦脚本启动后,你可以看到以下输出信息以辅助命令重新连到运行中的脚本:

1
2
Disconnecting from systemtap module.
To reconnect, type "staprun -A stap_5dd0073edcb1f13f7565d8c343063e68_19556"

当感兴趣的事件发生时,可以重新连接到运行中的脚本,并在内存Buffer中输出最近的数据并持续输出:

1
staprun -A stap_5dd0073edcb1f13f7565d8c343063e68_19556

内存Buffer默认1MB,可以使用-S选项,例如-S2指定为2MB

1
2
File Flight Recorder
stap -F -o /tmp/pfaults.log -S 1,2  pfaults.stp

命令结果输出到/tmp/pfaults.log.[0-9],每个文件1MB,并且仅保存最近的两个文件,-S指定了第一个参数:每个输出文件大小1MB,第二个参数:仅保留最近的两个文件,systemtap在pfaults.log后面加.[0-9]后缀。

该命令的输出是systemtap脚本进程ID,使用如下命令可以终止systemtap脚本

1
kill -s SIGTERM 7590

运行

1
2
3
ls –sh /tmp/pfaults.log.*

1020K /tmp/pfaults.log.5    44K /tmp/pfaults.log.6

SystemTap如何工作

SystemTap的基本工作原理就是:event/handler,运行systemtap脚本产生的加载模块时刻监控事件的发生,一旦发生,内核就调用相关的handler处理。

一运行一个SystemTap脚本就会产生一个SystemTap session:
1.SystemTap检查脚本以及所使用的相关tapset库;
2.SystemTap将脚本转换成C语言文件,并运行C语言编译器编译之创建一个内核模块;
3.SystemTap加载该模块,从而使用所有探针(events和handlers);
4.事件发生时,执行相关handlers
5.一旦SystemTap session停止,则探针被禁止,该内核模块被卸载。

探针:event及其handler,一个SystemTap脚本可以包含多个探针。

SystemTap脚本以.stp为扩展名,其基本格式如下所示:

1
probe event {statements}

允许一个探针内多个event,以,隔开,任一个event发生时,都会执行statements,各个语句之间不需要特殊的结束符号标记。而且可以在一个statements block中包含其他的statements block。

函数编写:

1
2
3
function function_name(arguments) {statements}

probe event {function_name(arguments)}

SystemTap Event

可大致划分为synchronous和asynchronous。

同步事件:

执行到定位到内核代码中的特定位置时触发event

1.syscall.system_call
系统调用入口和exit处:syscall.system_call和syscall.system_call.return,比如对于close系统调用:syscall.close和syscall.close.return

2.vfs.file_operation
vfs.file_operation和vfs.file_operation.return

3.kernel.function(“function”)
如:kernel.function(“sys_open”)和kernel.function(“sys_open”).return

可使用*来代表wildcards:

1
2
probe kernel.function("*@net/socket.c") { }
probe kernel.function("*@net/socket.c").return { }

代表了net/socket.c中所有函数的入口和exit口。

4.kernel.trace(“tracepoint”)
2.6.30及newer为内核中的特定事件定义了instrumentation,入kernel.trace(“kfree_skb”)代表内核中每次网络buffer被释放掉时的event。

5.module(“module”).function(“function”)

1
2
probe module("ext3").function("*") { }
probe module("ext3").function("*").return { }

系统内核模块多存放在/lib/modules/kernel_version

Asynchronous Events

不绑定到内核的特定指令或位置处。包括:
1、begin:SystemTap session开始时触发,当SystemTap脚本开始运行时触发;
2、end :SystemTap session终止时触发;
3、timer事件:

1
2
3
4
probe timer.s(4)
{
	printf("hello world/n")
}

• timer.ms(milliseconds)
• timer.us(microseconds)
• timer.ns(nanoseconds)
• timer.hz(hertz)
• timer.jiffies(jiffies)

可查看man stapprobes来查看其它支持的events

SystemTap Handler/Body

支持的函数:
1、 printf (“format string/n”, arguments),%s:字符串,%d数字,以 , 隔开;
2、 tid():当前线程ID;
3、 uid():当前用户ID;
4、 cpu():当前CPU号;
5、 gettimeofday_s():自从Epoch开始的秒数;
6、 ctime()将从Unix Epoch开始的秒数转换成date;
7、 pp():描述当前被处理的探针点的字符串;
8、 thread_indent():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
probe kernel.function("*@net/socket.c")
{
	printf ("%s -> %s/n", thread_indent(1), probefunc())
}

probe kernel.function("*@net/socket.c").return
{
	printf ("%s <- %s/n", thread_indent(-1), probefunc())
}

0 ftp(7223): -> sys_socketcall
1159 ftp(7223):  -> sys_socket
2173 ftp(7223):   -> __sock_create
2286 ftp(7223):    -> sock_alloc_inode
2737 ftp(7223):    <- sock_alloc_inode
3349 ftp(7223):    -> sock_alloc
3389 ftp(7223):    <- sock_alloc
3417 ftp(7223):   <- __sock_create
4117 ftp(7223):   -> sock_create
4160 ftp(7223):   <- sock_create
4301 ftp(7223):   -> sock_map_fd
4644 ftp(7223):    -> sock_map_file
4699 ftp(7223):    <- sock_map_file
4715 ftp(7223):   <- sock_map_fd
4732 ftp(7223):  <- sys_socket
4775 ftp(7223): <- sys_socketcall

函数thread_indent()只有1个参数:代表对线程的”indentation counter”的增减数,即系统调用显示的步数,返回字符串(自从第一次调用thread_indent()以来的描述:进程名(进程ID))

9、name
标记系统调用的名字,仅用于syscall.system_call中。

10、target()
与stap script -x process ID or stap script -c command联合使用,如果想在脚本中获得进程ID或命令可以如此做

1
2
3
4
probe syscall.* {
	if (pid() == target())
		printf("%s/n", name)
}

SystemTap Handler构造

变量

1、不必事先声明,直接使用即可,由SystemTap自动判断其属于string还是integer,整数则默认为0,默认在probe中声明的是local变量
2、在各个probe之间共享的变量使用global声明

1
2
3
4
5
6
7
8
9
10
global count_jiffies, count_ms
probe timer.jiffies(100) { count_jiffies ++ }
probe timer.ms(100) { count_ms ++ }
probe timer.ms(12345)
{
	hz=(1000*count_jiffies) / count_ms
	printf ("jiffies:ms ratio %d:%d => CONFIG_HZ=%d/n",
		count_jiffies, count_ms, hz)
	exit()
}
Target变量

Probe event可以映射到代码的实际位置,如kernel.function(“function”)、kernel.statement(“statement”),这允许使用target变量来记录代码中指定位置处可视变量的值。

运行如下命令:可以显示指定vfs_read处可视target变量

1
stap -L 'kernel.function("vfs_read")'

显示

1
2
3
kernel.function("vfs_read@fs/read_write.c:277") $file:struct file* $buf:char* $count:size_t

$pos:loff_t*

每个target变量以$开头:变量类型。如果是结构体类型,则SystemTap可以使用->来查看其成员。对基本类型,integer或string,SystemTap有函数可以直接读取address处的值,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 好像有时对于小于8位的函数,会取出8为长度的值
kernel_char(address)
Obtain the character at address from kernel memory.

kernel_short(address)
Obtain the short at address from kernel memory.

kernel_int(address)
Obtain the int at address from kernel memory.

kernel_long(address)
Obtain the long at address from kernel memory

kernel_string(address)
Obtain the string at address from kernel memory.

kernel_string_n(address, n)
Obtain the string at address from the kernel memory and limits the string to n bytes.
打印target变量
1
2
3
4
5
6
7
$$vars:类似sprintf("parm1=%x ... parmN=%x var1=%x ... varN=%x", parm1, ..., parmN, var1, ..., varN),目的是打印probe点处的每个变量;

$$locals:$$vars子集,仅打印local变量;

$$parms:$$vars子集,仅包含函数参数;

$$return:仅在return probes存在,类似sprintf("return=%x", $return),如果没有返回值,则是空串

例子如下:

1
stap -e 'probe kernel.function("vfs_read") {printf("%s/n", $$parms); exit(); }'

函数vfs_read有4个参数:file、buf、count和pos,输出如下:

1
file=0xffff8800b40d4c80 buf=0x7fff634403e0 count=0x2004 pos=0xffff8800af96df48

如果你想知道数据结构里面的成员信息,可以在”$$params”后面加一个”$”,如下所示:

1
stap -e 'probe kernel.function("vfs_read") {printf("%s/n", $$parms$); exit(); }'

输出如下:

1
file={.f_u={...}, .f_path={...}, .f_op=0xffffffffa06e1d80, .f_lock={...}, .f_count={...}, .f_flags=34818, buf="" count=8196 pos=-131938753921208

仅一个”$”表示,不展开数据结构域成员,如想展开,则需使用”$$”

1
stap -e 'probe kernel.function("vfs_read") {printf("%s/n", parms); exit(); }'

输出受限于最大字符串大小:

1
file={.f_u={.fu_list={.next=0xffff8801336ca0e8, .prev=0xffff88012ded0840}, .fu_rcuhead={.next=0xffff8801336ca0e8
强制类型转换

大多数情况下,SystemTap都可以从debuginfo中获得变量类型,但对于代码中void指针则debuginfo中类型信息不可用,同样probe handler里面的类型信息在function里面也不可用,怎么办呢?

SystemTap函数参数使用long来代替typed pointer,SystemTap的@cast操作可以指出对象正确类型:

1
2
3
4
function task_state:long (task:long)
{
	return @cast(task, "task_struct", "kernel<linux/sched.h>")->state
}

第一个参数是指向对象的指针, 第二个参数是将该对象(参数1)要强制类型转换成的类型,第三个参数指出类型定义的出处,是可选的。

检查Target变量可用性

随着代码运行,变量可能失效,因此需要用@defined来判断该变量是否可用:

1
2
3
4
5
6
7
8
probe vm.pagefault = kernel.function("__handle_mm_fault@mm/memory.c") ?,

kernel.function("handle_mm_fault@mm/memory.c") ?
{
	name = "pagefault"
	write_access = (@defined($flags) ? $flags & FAULT_FLAG_WRITE : $write_access)
	address = $address
}
条件语句
1
2
3
4
if (condition)
	statement1
else
	statement2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global countread, countnonread
probe kernel.function("vfs_read"),kernel.function("vfs_write")
{
	if (probefunc()=="vfs_read")
		countread ++
	else
		countnonread ++
}

probe timer.s(5) { exit() }

probe end
{
	printf("VFS reads total %d/n VFS writes total %d/n", countread, countnonread)
}
循环语句
1
2
3
4
while (condition)
	statement

for (initialization; conditional; increment) statement
比较:
1
==、>=、<=、!=
命令行参数:

使用$标志着希望输入的是integer类型命令行参数,@:string

1
2
probe kernel.function(@1) { }
probe kernel.function(@1).return { }

关联数组

关联数组一般在multiple probes里面处理,所以必须声明为global,不管是在一个还是多个probes里面用,要读取数组成员值,可以:

1
array_name[index_expression]

如下所示:

1
2
3
foo["tom"] = 23
foo["dick"] = 24
foo["harry"] = 25

一个索引可以包含最多9个索引表达式,用 , 隔开:

1
device[pid(),execname(),uid(),ppid(),"W"] = devname
SystemTap的数组操作
赋值:
1
array_name[index_expression] = value

例子:索引和值可以使用handler function:

1
foo[tid()] = gettimeofday_s()

每次触发这个语句,多次后就会构成一个关联数组,如果tid()返回值在foo索引中已有一个,则用新值代替旧值。

读取数组值:
1
delta = gettimeofday_s() - foo[tid()]

如果无法找到指定”索引”对应的值,则数组读返回0(int)或null/empty值(string)

增加关联数组值
1
array_name[index_expression] ++

处理数组的多个成员:

1
2
3
4
5
6
7
8
9
10
11
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	foreach (count in reads)
	printf("%s : %d /n", count, reads[count])
}

这个foreach无序打印所有reads数组值,如果想升序/降序,则需要使用升序(+)、降序(-),也可以限制处理的数组数目:

1
2
3
4
5
probe timer.s(3)
{
	foreach (count in reads- limit 10)
	printf("%s : %d /n", count, reads[count])
}
Clearing/Deleting数组和数组成员
1
2
3
4
5
6
7
8
9
10
11
12
13
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	foreach (count in reads)
	printf("%s : %d /n", count, reads[count])

	delete reads
}

使用delete操作来删除数组成员或整个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
global reads, totalreads
probe vfs.read
{
	reads[execname()] ++
	totalreads[execname()] ++
}

probe timer.s(3)
{
	printf("=======/n")
	foreach (count in reads-)
		printf("%s : %d /n", count, reads[count])
	delete reads
}

probe end
{
	printf("TOTALS/n")
	foreach (total in totalreads-)
	printf("%s : %d /n", total, totalreads[total])
}

在if语句中使用数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	printf("=======/n")
	foreach (count in reads-)
	if (reads[count] >= 1024)
		printf("%s : %dkB /n", count, reads[count]/1024)
	else
		printf("%s : %dB /n", count, reads[count])
}
检查成员

可以检查是否一个指定健是数组键值:

1
if([index_expression] in array_name) statement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	printf("=======/n")
	foreach (count in reads+)
		printf("%s : %d /n", count, reads[count])
	if(["stapio"] in reads) {
		printf("stapio read detected, exiting/n")
		exit()
	}
}
计算统计集合

统计集合用于收集数值的统计信息,用于计算新值

1
2
3
4
5
global reads
probe vfs.read
{
	reads[execname()] <<< count
}

操作符<<<用于将count返回的值存放在read数组中execname()相关的值中,即一个键值关联多个相关值。

为计算统计信息,使用@extractor(variable/array index expression),extractor可以是如下integer extractor:

1
2
3
4
5
count:@count(writes[execname()])返回存放在writes数组中某单一键值对应的值数目;
sum:@sum(writes[execname()])返回在writes数组中某单一键值对应的值的和
min:最小值
max:最大值
avg:variable/array作为索引的统计集合中数据的平均值
1
2
3
4
5
6
7
8
9
10
11
global reads
probe vfs.read
{
	reads[execname(),pid()] <<< 1
}

probe timer.s(3)
{
	foreach([var1,var2] in reads)
	printf("%s (%d) : %d /n", var1, var2, @count(reads[var1,var2]))
}

Tapsets

Tapsets是脚本库,里面预写好了probes和functions可以被SystemTap脚本调用,tapsets也使用.stp作为后缀,默认位于:/usr/share/systemtap/tapset,但无法直接运行。

TCP三次握手源码详解

http://blog.csdn.net/qy532846454/article/details/7882819

http://m.bianceng.cn/OS/Linux/201301/35179_6.htm

内核:2.6.34

TCP是应用最广泛的传输层协议,其提供了面向连接的、可靠的字节流服务,但也正是因为这些特性,使得TCP较之UDP异常复杂,还是分两部分[创建与使用]来进行分析。这篇主要包括TCP的创建及三次握手的过程。

编程时一般用如下语句创建TCP Socket:

1
socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP)  

由此开始分析,调用接口[net/socket.c]: SYSCALL_DEFINE3(socket)

其中执行两步关键操作:sock_create()与sock_map_fd()

1
2
3
4
5
6
retval = sock_create(family, type, protocol, &sock);  
if (retval < 0)  
	goto out;  
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));  
if (retval < 0)  
	goto out_release;  

sock_create()用于创建socket,sock_map_fd()将之映射到文件描述符,使socket能通过fd进行访问,着重分析sock_create()的创建过程。

1
sock_create() -> __sock_create()

从__sock_create()代码看到创建包含两步:sock_alloc()和pf->create()。sock_alloc()分配了sock内存空间并初始化inode;pf->create()初始化了sk。

1
2
3
4
5
6
sock = sock_alloc();  
sock->type = type;  
……  
pf = rcu_dereference(net_families[family]);  
……  
pf->create(net, sock, protocol, kern);  

sock_alloc()

分配空间,通过new_inode()分配了节点(包括socket),然后通过SOCKET_I宏获得sock,实际上inode和sock是在new_inode()中一起分配的,结构体叫作sock_alloc。

1
2
inode = new_inode(sock_mnt->mnt_sb);  
sock = SOCKET_I(inode);  

设置inode的参数,并返回sock。

1
2
3
4
inode->i_mode = S_IFSOCK | S_IRWXUGO;  
inode->i_uid = current_fsuid();  
inode->i_gid = current_fsgid();  
return sock;  

继续往下看具体的创建过程:new_inode(),在分配后,会设置i_ino和i_state的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct inode *new_inode(struct super_block *sb)  
{  
	……  
	inode = alloc_inode(sb);  
	if (inode) {  
		spin_lock(&inode_lock);  
		__inode_add_to_lists(sb, NULL, inode);  
		inode->i_ino = ++last_ino;  
		inode->i_state = 0;  
		spin_unlock(&inode_lock);  
	}  
	return inode;  
}  

其中的alloc_inode() -> sb->s_op->alloc_inode(),sb是sock_mnt->mnt_sb,所以alloc_inode()指向的是sockfs的操作函数sock_alloc_inode。

1
2
3
4
5
static const struct super_operations sockfs_ops = {  
	.alloc_inode = sock_alloc_inode,  
	.destroy_inode =sock_destroy_inode,  
	.statfs = simple_statfs,  
};  

sock_alloc_inode()中通过kmem_cache_alloc()分配了struct socket_alloc结构体大小的空间,而struct socket_alloc结构体定义如下,但只返回了inode,实际上socket和inode都已经分配了空间,在之后就可以通过container_of取到socket。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static struct inode *sock_alloc_inode(struct super_block *sb)  
{  
	struct socket_alloc *ei;  
	ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);  
	......  
	return &ei->vfs_inode;  
}  
struct socket_alloc {  
	struct socket socket;  
	struct inode vfs_inode;  
};  

net_families[AF_INET]:  
static const struct net_proto_family inet_family_ops = {  
	.family = PF_INET,  
	.create = inet_create,  
	.owner = THIS_MODULE,  
};  

err = pf->create(net, sock, protocol, kern); ==> inet_create() 这段代码就是从inetsw[]中取到适合的协议类型answer,sock->type就是传入socket()函数的type参数SOCK_DGRAM,最终取得结果answer->ops==inet_stream_ops,从上面这段代码还可以看出以下问题:

socket(AF_INET, SOCK_RAW, IPPROTO_IP)这样是不合法的,因为SOCK_RAW没有默认的协议类型;同样socket(AF_INET, SOCK_DGRAM, IPPROTO_IP)与socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP)是一样的,因为TCP的默认协议类型是IPPTOTO_TCP;SOCK_STREAM与IPPROTO_UDP同上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sock->state = SS_UNCONNECTED;  
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {  
	err = 0;  
	/* Check the non-wild match. */  
	if (protocol == answer->protocol) {  
		if (protocol != IPPROTO_IP)  
			break;  
	} else {  
		/* Check for the two wild cases. */  
		if (IPPROTO_IP == protocol) {  
			protocol = answer->protocol;  
			break;  
		}  
		if (IPPROTO_IP == answer->protocol)  
			break;  
	}  
	err = -EPROTONOSUPPORT;  
}  

sock->ops指向inet_stream_ops,然后创建sk,sk->proto指向tcp_prot,注意这里分配的大小是struct tcp_sock,而不仅仅是struct sock大小

1
2
3
4
sock->ops = answer->ops;  
answer_prot = answer->prot;  
……  
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);  

然后设置inet的一些参数,这里直接将sk类型转换为inet,因为在sk_alloc()中分配的是struct tcp_sock结构大小,返回的是struct sock,利用了第一个成员的特性,三者之间的关系如下图:

1
2
3
4
inet = inet_sk(sk);  
……  
inet->inet_id = 0;  
sock_init_data(sock, sk);  

其中有些设置是比较重要的,如

1
2
3
4
sk->sk_state = TCP_CLOSE;  
sk_set_socket(sk, sock);  
sk->sk_protocol = protocol;  
sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;  

创建socket后,接下来的流程会因为客户端或服务器的不同而有所差异,下面着重于分析建立连接的三次握手过程。典型的客户端流程:
connect() -> send() -> recv()

典型的服务器流程:
bind() -> listen() -> accept() -> recv() -> send()

客户端流程

发送SYN报文,向服务器发起tcp连接

1
2
3
4
		connect(fd, servaddr, addrlen);
			-> SYSCALL_DEFINE3() 
			-> sock->ops->connect() == inet_stream_connect (sock->ops即inet_stream_ops)
			-> tcp_v4_connect()

查找到达[daddr, dport]的路由项,路由项的查找与更新与”路由表”章节所述一样。要注意的是由于是作为客户端调用,创建socket后调用connect,因而saddr, sport都是0,同样在未查找路由前,要走的出接口oif也是不知道的,因此也是0。在查找完路由表后(注意不是路由缓存),可以得知出接口,但并未存储到sk中。因此插入的路由缓存是特别要注意的:它的键值与实际值是不相同的,这个不同点就在于oif与saddr,键值是[saddr=0, sport=0, daddr, dport, oif=0],而缓存项值是[saddr, sport=0, daddr, dport, oif]。

1
2
3
4
5
6
7
8
9
tmp = ip_route_connect(&rt, nexthop, inet->inet_saddr,  
						RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,  
						IPPROTO_TCP,  
						inet->inet_sport, usin->sin_port, sk, 1);  
if (tmp < 0) {  
	if (tmp == -ENETUNREACH)  
		IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);  
	return tmp;  
}  

通过查找到的路由项,对inet进行赋值,可以看到,除了sport,都赋予了值,sport的选择复杂点,因为它要随机从未使用的本地端口中选择一个。

1
2
3
4
5
6
if (!inet->inet_saddr)  
	inet->inet_saddr = rt_rt_src;   
inet->inet_rcv_addr = inet->inet_saddr;  
……  
inet->inet_dport = usin->sin_port;  
inet->inet_daddr = daddr;  

状态从CLOSING转到TCP_SYN_SENT,也就是我们熟知的TCP的状态转移图。

1
tcp_set_state(sk, TCP_SYN_SENT);  

插入到bind链表中

1
err = inet_hash_connect(&tcp_death_row, sk); //== > __inet_hash_connect()  

当snum==0时,表明此时源端口没有指定,此时会随机选择一个空闲端口作为此次连接的源端口。low和high分别表示可用端口的下限和上限,remaining表示可用端口的数,注意这里的可用只是指端口可以用作源端口,其中部分端口可能已经作为其它socket的端口号在使用了,所以要循环1~remaining,直到查找到空闲的源端口。

1
2
3
4
5
6
7
8
if (!snum) {  
	inet_get_local_port_range(&low, &high);  
	remaining = (high - low) + 1;  
	……  
	for (i = 1; i <= remaining; i++) {  
		……// choose a valid port  
	}  
}  

下面来看下对每个端口的检查,即//choose a valid port部分的代码。这里要先了解下tcp的内核表组成,udp的表内核表udptable只是一张hash表,tcp的表则稍复杂,它的名字是tcp_hashinfo,在tcp_init()中被初始化,这个数据结构定义如下(省略了不相关的数据):

1
2
3
4
5
6
7
8
struct inet_hashinfo {  
	struct inet_ehash_bucket *ehash;  
	……  
	struct inet_bind_hashbucket *bhash;  
	……  
	struct inet_listen_hashbucket  listening_hash[INET_LHTABLE_SIZE]  
					____cacheline_aligned_in_smp;  
};  

从定义可以看出,tcp表又分成了三张表ehash, bhash, listening_hash,其中ehash, listening_hash对应于socket处在TCP的ESTABLISHED, LISTEN状态,bhash对应于socket已绑定了本地地址。三者间并不互斥,如一个socket可同时在bhash和ehash中,由于TIME_WAIT是一个比较特殊的状态,所以ehash又分成了chain和twchain,为TIME_WAIT的socket单独形成一张表。

回到刚才的代码,现在还只是建立socket连接,使用的就应该是tcp表中的bhash。首先取得内核tcp表的bind表 – bhash,查看是否已有socket占用:
如果没有,则调用inet_bind_bucket_create()创建一个bind表项tb,并插入到bind表中,跳转至goto ok代码段;
如果有,则跳转至goto ok代码段。
进入ok代码段表明已找到合适的bind表项(无论是创建的还是查找到的),调用inet_bind_hash()赋值源端口inet_num。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for (i = 1; i <= remaining; i++) {  
	port = low + (i + offset) % remaining;  
	head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];  
	……  
	inet_bind_bucket_for_each(tb, node, &head->chain) {  
		if (net_eq(ib_net(tb), net) && tb->port == port) {  
			if (tb->fastreuse >= 0)  
				goto next_port;  
			WARN_ON(hlist_empty(&tb->owners));  
			if (!check_established(death_row, sk, port, &tw))  
				goto ok;  
			goto next_port;  
		}  
	}  
		
	tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net, head, port);  
	……  
	next_port:  
		spin_unlock(&head->lock);  
}  
		
ok:  
	……  
inet_bind_hash(sk, tb, port);  
	……  
	goto out;  

在获取到合适的源端口号后,会重建路由项来进行更新:

1
err = ip_route_newports(&rt, IPPROTO_TCP, inet->inet_sport, inet->inet_dport, sk);  

函数比较简单,在获取sport前已经查找过一次路由表,并插入了key=[saddr=0, sport=0, daddr, dport, oif=0]的路由缓存项;现在获取到了sport,调用ip_route_output_flow()再次更新路由缓存表,它会添加key=[saddr=0, sport, daddr, dport, oif=0]的路由缓存项。这里可以看出一个策略选择,查询路由表->获取sport->查询路由表,为什么不是获取sport->查询路由表的原因可能是效率的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (sport != (*rp)->fl.fl_ip_sport ||  
				dport != (*rp)->fl.fl_ip_dport) {  
	struct flowi fl;  
		
	memcpy(&fl, &(*rp)->fl, sizeof(fl));  
	fl.fl_ip_sport = sport;  
	fl.fl_ip_dport = dport;  
	fl.proto = protocol;  
	ip_rt_put(*rp);  
	*rp = NULL;  
	security_sk_classify_flow(sk, &fl);  
	return ip_route_output_flow(sock_net(sk), rp, &fl, sk, 0);  
}  

write_seq相当于第一次发送TCP报文的ISN,如果为0,则通过计算获取初始值,否则延用上次的值。在获取完源端口号,并查询过路由表后,TCP正式发送SYN报文,注意在这之前TCP状态已经更新成了TCP_SYN_SENT,而在函数最后才调用tcp_connect(sk)发送SYN报文,这中间是有时差的。

1
2
3
4
5
6
7
if (!tp->write_seq)  
	tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,  
									inet->inet_daddr,  
									inet->inet_sport,  
									usin->sin_port);  
inet->inet_id = tp->write_seq ^ jiffies;  
err = tcp_connect(sk);  
tcp_connect() 发送SYN报文

几步重要的代码如下,tcp_connect_init()中设置了tp->rcv_nxt=0,tcp_transmit_skb()负责发送报文,其中seq=tcb->seq=tp->write_seq,ack_seq=tp->rcv_nxt。

1
2
3
4
tcp_connect_init(sk);  
tp->snd_nxt = tp->write_seq;  
……  
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);  
收到服务端的SYN+ACK,发送ACK
tcp_rcv_synsent_state_process()

此时已接收到对方的ACK,状态变迁到TCP_ESTABLISHED。最后发送对方SYN的ACK报文。

1
2
tcp_set_state(sk, TCP_ESTABLISHED);  
tcp_send_ack(sk);  

服务端流程

bind() -> inet_bind()

bind操作的主要作用是将创建的socket与给定的地址相绑定,这样创建的服务才能公开的让外部调用。当然对于socket服务器的创建来说,这一步不是必须的,在listen()时如果没有绑定地址,系统会选择一个随机可用地址作为服务器地址。

一个socket地址分为ip和port,inet->inet_saddr赋值了传入的ip,snum是传入的port,对于端口,要检查它是否已被占用,这是由sk->sk_prot->get_port()完成的(这个函数前面已经分析过,在传入port时它检查是否被占用;传入port=0时它选择未用的端口)。如果没有被占用,inet->inet_sport被赋值port,因为是服务监听端,不需要远端地址,inet_daddr和inet_dport都置0。

注意bind操作不会改变socket的状态,仍为创建时的TCP_CLOSE。

1
2
3
4
5
6
7
8
9
10
11
12
snum = ntohs(addr->sin_port);  
……  
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;  
if (sk->sk_prot->get_port(sk, snum)) {  
	inet->inet_saddr = inet->inet_rcv_saddr = 0;  
	err = -EADDRINUSE;  
	goto out_release_sock;  
}  
……  
inet->inet_sport = htons(inet->inet_num);  
inet->inet_daddr = 0;  
inet->inet_dport = 0;  
listen() -> inet_listen()

listen操作开始服务器的监听,此时服务就可以接受到外部连接了。在开始监听前,要检查状态是否正确,sock->state==SS_UNCONNECTED确保仍是未连接的socket,sock->type==SOCK_STREAM确保是TCP协议,old_state确保此时状态是TCP_CLOSE或TCP_LISTEN,在其它状态下进行listen都是错误的。

1
2
3
4
5
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)  
	goto out;  
old_state = sk->sk_state;  
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))  
	goto out;  

如果已是TCP_LISTEN态,则直接跳过,不用再执行listen了,而只是重新设置listen队列长度sk_max_ack_backlog,改变listen队列长也是多次执行listen的作用。如果还没有执行listen,则还要调用inet_csk_listen_start()开始监听。

inet_csk_listen_start()变迁状态至TCP_LISTEN,分配监听队列,如果之前没有调用bind()绑定地址,则这里会分配一个随机地址。

1
2
3
4
5
6
if (old_state != TCP_LISTEN) {  
	err = inet_csk_listen_start(sk, backlog);  
	if (err)  
		goto out;  
}  
sk->sk_max_ack_backlog = backlog;  
accept()

accept() -> sys_accept4() -> inet_accept() -> inet_csk_accept()

accept()实际要做的事件并不多,它的作用是返回一个已经建立连接的socket(即经过了三次握手),这个过程是异步的,accept()并不亲自去处理三次握手过程,而只是监听icsk_accept_queue队列,当有socket经过了三次握手,它就会被加到icsk_accept_queue中,所以accept要做的就是等待队列中插入socket,然后被唤醒并返回这个socket。而三次握手的过程完全是协议栈本身去完成的。换句话说,协议栈相当于写者,将socket写入队列,accept()相当于读者,将socket从队列读出。这个过程从listen就已开始,所以即使不调用accept(),客户仍可以和服务器建立连接,但由于没有处理,队列很快会被占满。

1
2
3
4
5
6
7
8
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {  
	long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);  
	……  
	error = inet_csk_wait_for_connect(sk, timeo);  
	……  
}  
		
newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);  

协议栈向队列中加入socket的过程就是完成三次握手的过程,客户端通过向已知的listen fd发起连接请求,对于到来的每个连接,都会创建一个新的sock,当它经历了TCP_SYN_RCV -> TCP_ESTABLISHED后,就会被添加到icsk_accept_queue中,而监听的socket状态始终为TCP_LISTEN,保证连接的建立不会影响socket的接收。

接收客户端发来的SYN,发送SYN+ACK

tcp_v4_do_rcv()

tcp_v4_do_rcv()是TCP模块接收的入口函数,客户端发起请求的对象是listen fd,所以sk->sk_state == TCP_LISTEN,调用tcp_v4_hnd_req()来检查是否处于半连接,只要三次握手没有完成,这样的连接就称为半连接,具体而言就是收到了SYN,但还没有收到ACK的连接,所以对于这个查找函数,如果是SYN报文,则会返回listen的socket(连接尚未创建);如果是ACK报文,则会返回SYN报文处理中插入的半连接socket。其中存储这些半连接的数据结构是syn_table,它在listen()调用时被创建,大小由sys_ctl_max_syn_backlog和listen()传入的队列长度决定。

此时是收到SYN报文,tcp_v4_hnd_req()返回的仍是sk,调用tcp_rcv_state_process()来接收SYN报文,并发送SYN+ACK报文,同时向syn_table中插入一项表明此次连接的sk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (sk->sk_state == TCP_LISTEN) {  
	struct sock *nsk = tcp_v4_hnd_req(sk, skb);  
	if (!nsk)  
		goto discard;  
	if (nsk != sk) {  
		if (tcp_child_process(sk, nsk, skb)) {  
			rsk = nsk;  
			goto reset;  
		}  
		return 0;  
	}  
}  
TCP_CHECK_TIMER(sk);  
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {  
	rsk = sk;  
	goto reset;  
}  

tcp_rcv_state_process()处理各个状态上socket的情况。下面是处于TCP_LISTEN的代码段,处于TCP_LISTEN的socket不会再向其它状态变迁,它负责监听,并在连接建立时创建新的socket。实际上,当收到第一个SYN报文时,会执行这段代码,conn_request() => tcp_v4_conn_request。

1
2
3
4
5
6
7
8
case TCP_LISTEN:  
……  
	if (th->syn) {  
		if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)  
			return 1;  
		kfree_skb(skb);  
		return 0;  
	}  

tcp_v4_conn_request()中注意两个函数就可以了:tcp_v4_send_synack()向客户端发送了SYN+ACK报文,inet_csk_reqsk_queue_hash_add()将sk添加到了syn_table中,填充了该客户端相关的信息。这样,再次收到客户端的ACK报文时,就可以在syn_table中找到相应项了。

1
2
3
if (tcp_v4_send_synack(sk, dst, req, (struct request_values *)&tmp_ext) || want_cookie)  
	goto drop_and_free;  
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);  

接收客户端发来的ACK

tcp_v4_do_rcv()

过程与收到SYN报文相同,不同点在于syn_table中已经插入了有关该连接的条目,tcp_v4_hnd_req()会返回一个新的sock: nsk,然后会调用tcp_child_process()来进行处理。在tcp_v4_hnd_req()中会创建新的sock,下面详细看下这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
if (sk->sk_state == TCP_LISTEN) {  
	struct sock *nsk = tcp_v4_hnd_req(sk, skb);  
	if (!nsk)  
		goto discard;  
	if (nsk != sk) {  
		if (tcp_child_process(sk, nsk, skb)) {  
			rsk = nsk;  
			goto reset;  
		}  
		return 0;  
	}  
}  
tcp_v4_hnd_req()

之前已经分析过,inet_csk_search_req()会在syn_table中找到req,此时进入tcp_check_req()

1
2
3
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source, iph->saddr, iph->daddr);  
if (req)  
	return tcp_check_req(sk, skb, req, prev);  
tcp_check_req()

syn_recv_sock() -> tcp_v4_syn_recv_sock()会创建一个新的sock并返回,创建的sock状态被直接设置为TCP_SYN_RECV,然后因为此时socket已经建立,将它添加到icsk_accept_queue中。

状态TCP_SYN_RECV的设置可能比较奇怪,按照TCP的状态转移图,在服务端收到SYN报文后变迁为TCP_SYN_RECV,但看到在实现中收到ACK后才有了状态TCP_SYN_RECV,并且马上会变为TCP_ESTABLISHED,所以这个状态变得无足轻重。这样做的原因是listen和accept返回的socket是不同的,而只有真正连接建立时才会创建这个新的socket,在收到SYN报文时新的socket还没有建立,就无从谈状态变迁了。这里同样是一个平衡的存在,你也可以在收到SYN时创建一个新的socket,代价就是无用的socket大大增加了。

1
2
3
4
5
6
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);  
if (child == NULL)  
	goto listen_overflow;  
inet_csk_reqsk_queue_unlink(sk, req, prev);  
inet_csk_reqsk_queue_removed(sk, req);  
inet_csk_reqsk_queue_add(sk, req, child);  
tcp_child_process()

如果此时sock: child被用户进程锁住了,那么就先添加到backlog中__sk_add_backlog(),待解锁时再处理backlog上的sock;如果此时没有被锁住,则先调用tcp_rcv_state_process()进行处理,处理完后,如果child状态到达TCP_ESTABLISHED,则表明其已就绪,调用sk_data_ready()唤醒等待在isck_accept_queue上的函数accept()。

1
2
3
4
5
6
7
if (!sock_owned_by_user(child)) {  
	ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb), skb->len);  
	if (state == TCP_SYN_RECV && child->sk_state != state)  
		parent->sk_data_ready(parent, 0);  
} else {  
	__sk_add_backlog(child, skb);  
}  

tcp_rcv_state_process()处理各个状态上socket的情况。下面是处于TCP_SYN_RECV的代码段,注意此时传入函数的sk已经是新创建的sock了(在tcp_v4_hnd_req()中),并且状态是TCP_SYN_RECV,而不再是listen socket,在收到ACK后,sk状态变迁为TCP_ESTABLISHED,而在tcp_v4_hnd_req()中也已将sk插入到了icsk_accept_queue上,此时它就已经完全就绪了,回到tcp_child_process()便可执行sk_data_ready()。

1
2
3
4
5
6
7
8
9
10
11
case TCP_SYN_RECV:  
	if (acceptable) {  
		……  
		tcp_set_state(sk, TCP_ESTABLISHED);  
		sk->sk_state_change(sk);  
		……  
		tp->snd_una = TCP_SKB_CB(skb)->ack_seq;  
		tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;  
		tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);   
		……  
}  

最后总结三次握手的过程

RHEL5/CentOS5 上支持 Ext4

  • 记住,只能改数据分区,//boot 分区不要试,至少我没成功,启动参数加rootfstype=ext4也起不来。

  • / 分区要改成ext4的话,可以直接改/etc/fstab文件,ext3支持以ext4形式挂载。

  • extents属性加上后去不掉,所以该不会ext3的,除非不加这个属性??,去掉属性tune4fs -O ^flex_bg /dev/sdb1


http://www.php-oa.com/2010/08/04/linux-rhel5-centos5-ext4.html

根据我以前的测试 Ext4 的性能好过 Ext3,在 RHEL5 上的 2.6.18-110 也有加入 Ext4 了.但默认没有让我们使用,怎么样才能不重起,能使用这个啦. 其实我们只要加入一个包e4fsprogs 就行,它其实和 e2fsprogs 是一样的功能,这 RHEL-6 中,会变成一个默认的包的.所以我们目前还只能使用这个包来调整和设置Ext4.

1
yum -y install e4fsprogs

在 RHEL 和 Centos5 中使用 Ext4 前,很多想可能想先给现有的文件系统转换成 Ext4 ,只要运行下面的命令就行了

1
tune4fs -O extents,uninit_bg,dir_index,flex_bg /dev/sdb1

记住,转换成 ext4 后必须用 fsck 扫描,否则不能 mount,-p 参数说明 “自动修复” 文件系统:

1
fsck -pf /dev/sdb1 或 fsck -y /dev/sdb1
下面这个好像不需要

在重起前,我还要让内核支持 Ext4 的文件系统,需要修改 initrd 的文件本身的内容.如下命令来生成 支持 Ext4 的 initrd.

1
mkinitrd --with=ext4 --with=ext3 -f /boot/initrd-2.6.18-404.el5.img 2.6.18-404.el5