kk Blog —— 通用基础


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

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

网络硬盘NFS的安装与配置

http://www.linuxidc.com/Linux/2014-11/109637.htm

NFS 是共享文件的服务的一种协议 下面给大家介绍一下这个服务器的的安装和配置。

安装

1
sudo apt-get install nfs-common nfs-kernel-server

配置

1
vim /etc/exprots

在正文的最下面输入一行

1
2
3
4
5
/srv/nfs_share *(rw)

/srv/nfs_share 表示的是我们要共享的文件目录
* 表示互联网上任何主机都可以访问 
(rw) 表示对服务器进行访问的主机可以进行的操作 也就是可读可写

如果我们只想让我们本地局域网上的主机对我们的服务器进行访问的话 可以这样写

1
/srv/nfs_share 192.168.*.*(rw)

访问

本机访问

1
sudo mount -o rw locahost:/srv/nfs_share /mnt/nfs

上面的意思是把本地的目录/srv/nfs_share 挂载到 目录/mnt/nfs上 ,这时候我们体会不到挂载点发生了变化 我们可以自己用相关的命令查询,我就不多介绍了

非本地的主机

1
sudo mount -o rw 域名:/srv/nfs_share /mnt/nfs

这个时候我们会发现NFS太简单了,所以系统管理员就麻烦了

假如在共享的目录中有我们的重要的文件,怎么办?
1
/srv/nfs_share/secret (noaccess)

就是任何主机都不能访问/srv/nfs_share/secret 这个子目录

如何限制root的权限
1
/srv/nfs_share 192.168.*。*(rw,root-aquash)
查看客户端挂载共享目录的状态
1
$ nfsstat -c
查看服务器的状态
1
$ nfsstat -s

http://stevenz.blog.hexun.com/16127677_d.html

服务器IP:172.0.0.1,主机名:p470-1, 通过NFS共享/disk1目录

在客户端使用 mount -t nfs p470-1:/disk1 /disk1 时出现

1
mount: mount to NFS server 'p470-1' failed: RPC Error: Program not registered.

错误提示。

出错原因:p470-1由于网络原因nfs服务被中断,重新开启p470-1的nfs服务然后在客户端重新mount disk1即可

service nfs restart 或 /etc/rc.d/init.d/nfs restart

VMware 'Host SMBus controller not enabled!'

https://www.centos.bz/faq/111/

Ubuntu/CentOS guest instances in VMware sometimes come up with the boot error message:

1
piix4_smbus 0000:00:007.3: Host SMBus controller not enabled!

This error is being caused because VMware doesn’t actually provide that level interface for CPU access, but Ubuntu try to load the kernel module anyway.

How to fix it:
在虚拟机中

1
sudo vim /etc/modprobe.d/blacklist.conf

add the line:

1
blacklist i2c-piix4

reboot


似乎这个错误在centos6 + 3.10* 的内核,有时kdump不起作用。