内核中的TCP的追踪分析-1-追踪TCP(IPV4)的socket的初始化
我们首先来解释一下什么是socket,很多初次朋友可能对此概念还不是很清楚,socket这个单词英文是插座的意思,但是在计算机领域里有时称为插口,也有时称 为套接字,无论什么叫法它的作用都不会变的,都是为应用程序提供了今天网络通讯的桥梁架构,我们举一个电话通讯的例子来看。准备通话的二部电话相当于准备好了2个进程,电话区号是它的网络地址;本地电话区域的交换机相当于一台主机,这台主机分配给每个电话一个号码相当于socket号。当我们在通话之前,首先要找到带号码的电话机,相当于申请了一个socket;同时还要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一地区内,还要加拔对方区号,相当于给出网络地址)。对方如果此时空闲或者可以响铃的话(相当于通信区域的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。我们在通话的过程,是一方向电话机发出信号而对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭socket(撤消连接)。
在这个电话例子中,我们只能感受到电话机和电话号码的存在,建立通话的过程,话音传输的过程等整个系统的技术细节都是透明的,这与 我们要讲的socket机制非常相似。socket可以利用网络间的通信设施实现二台计算机之间进程的通信,但它对通信设施的细节毫不关心,只要网络通信的条件能够满足就可以实现客户端与服务器端的计算机之间的通讯。
服务器要执行的过程,客户端的执行过程,很显然分析的方法好要分别围绕服务器端和客户端展开进行,这样即能够使我们更加清楚linux内核网络的工作路线,有这样一条主线引导着我们去旅游,何乐而不为呢?我们会经常回到这个图中来对比、来参照我们的导游地图产。好了,地图上指示我们应该先从socket的创建开始,那么回忆一下我们的应用经常要用的编程经过,我们以简写的方式列在下面,注意服务器端的工作过程与客户端的过程不同所以也会此将分别编写二个socket程序,我们先看服务器端的练习程序简代码过程
int main()/* test for network,wumingxiaozu */
{
int server_fd, client_fd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
下面创建一个网络使用的socket
/* wumingxiaozu */
server_fd = socket(AF_INET, SOCK_STREAM, 0);
为这个创建的socket指定IP地址和端口,ip地址是192.168.1.1而端口则为9266
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr("192.168.1.1");
server_address.sin_port = 9266;
server_len = sizeof(server_address);
将“电话号码”赋值给“电话”,即将设定的地址结构与socket挂起钩来,朋友们以后理解指针赋值可以理解为钩子,就象网上有种叫法“钩子函数”实际就是指针函数
bind(server_fd, (struct sockaddr *)&server_address, server_len);
创建一个socket的连接队列监听只允许接收10个连接,等待客户端处的socket来连接
listen(server_fd, 10);
while(1) {/* wumingxiaozu */
char temp;
printf("server waiting\n");
到达此处时,已经说明客户端的连接请求来到了,下面是接受它的连接请求并将客户端的“电话号码”记录在client_address中。并且通过accept克隆了一个二者保持通讯的socket,为什么要克隆,我们追踪到内核再说,这里函数返回了服务器端与客户端建立连接的socket的ID号
client_len = sizeof(client_address);
client_fd = accept(server_fd,
(struct sockaddr *)&client_address, &client_len);
使用read和write函数向已经建立的socket接收客户端的一个字符然后再发送回去
read(client_fd, &temp, 1);
temp++;
write(client_fd, &temp, 1);
close(client_fd);
}
}
同样在我们下面要分析的过程中要反复回到这个练习中参考工作的过程,对照上面的练习程序(以后我们也可能在文章中以应用程序、用户程序等名词的形式出现,所以读者朋友请注意都是指这里的练习程序),我在代码中加入了我们的注释,所以让大家阅读的很容易理解,结合代码部分上面的服务器过程也就是我们的导游地图左边所展示的那样,只不过在练习程序中我们看到发送和接收数据是通过read和write的C库函数来完成的,这其实也会终调用recv和send来实现socket的数据发送,只是为了方便我们的旅行,所以没有将C库的调用过程写出来,因为我们的目的是分析socket的tcp/ip过程,所以关于如何从C库执行到系统调用又好何到达我们下面的socket系统调用的总入口函数sys_socketcall(),我们都不会再详细的列出了,很多书籍和资料讲的都非常的清晰,那不是我们本文的重点,在本文中我们假设朋友们已经具备了那些知识,如果你没有了解过这些过程请手边放一本linux内核方面的基础书籍,可以参阅了解系统调用的实现原理后,再接下来看我们下面的分析过程,好了,这节开始我们就进入探讨IPV4的TCP的socket的创建,我们直接从socket的系统调用的总入口函数sys_socketcall()开始看重点与我们本节内容相关的部分,以下从sys_socketcall()函数中节摘了与创建相关的代码部分
asmlinkage long sys_socketcall(int call, unsigned long __user *args)
{
。。。。。。
if (copy_from_user(a, args, nargs[call]))
return -EFAULT;
。。。。。。
case SYS_SOCKET:
err = sys_socket(a0, a1, a[2]);/* wumingxiaozu */
。。。。。。
}
我们知道sys_socketcall(),这个函数是内核socket的总系统调用入口,参数call是具体的操作码,参数args是一个数组指针。另外我们需要明确从用户空间复制的参数数量,这是根据nargs[]来决定的,以call为下标将会从该数组中找到参数的个数,依据个数来把args处的参数从用户空间即我们的应用程序复制过来
/* Argument list sizes for sys_socketcall */
#define AL(x) ((x) * sizeof(unsigned long))
static const unsigned char nargs[18]={
AL(0),AL(3),AL(3),AL(3),AL(2),AL(3),
AL(3),AL(3),AL(4),AL(4),AL(4),AL(6),
AL(6),AL(2),AL(5),AL(5),AL(3),AL(3)
};/* wumingxiaozu */
#undef AL
可以看到上面的nargs数组中可以看出是规定了参数的个数,根据AL的宏解释,我们还可以看到在include/linux/net.h中规定了call的详细数字
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
这里就可以确定上面数组中call具体数字了。有可能朋友们会对注释中的(2)有疑惑,其的含义喻为这里是系统调用号。好了我们进入sys_socket()函数 ,同样我们只列出重要的部分,注意我们以后在函数前面用“-à”符号来表明调用的路径过程,来方便大家了解了掌握函数的执行路径,当然我们也会在文章中插入图片来方便大家学习与阅读
sys_socketcall()-->sys_socket()
asmlinkage long sys_socket(int family, int type, int protocol)
{
。。。。。。
retval = sock_create(family, type, protocol, &sock);/* wumingxiaozu */
。。。。。。
}
这个函数从名称可以看出是为了创建一个插口(有的资料上也称为套接字,注意下边我们有时称为插口有时可能称为套接字),对照应用程序也就是我们前的练习程序中的界面(我们称界面的意思就是练习程序中的代码语句) server_sockfd = socket(AF_INET, SOCK_STREAM, 0);传递过来的三个参数,个参数family应用程序传递过来的值为AF_INET,第二个参数是SOCK_STREAM,第三个参数是0。我们从上面的函数中可以看到是进入了sock_create()函数去创建socket。另外在2.6.26内核的net/socket.c处的300行我们可以看到
static struct vfsmount *sock_mnt __read_mostly;
/* wumingxiaozu */
static struct file_system_type sock_fs_type = {
.name = "sockfs",
.get_sb = sockfs_get_sb,
.kill_sb = kill_anon_super,
};
这里如果看过、了解文件系统的朋友,会知道file_system_type的作用是代表我们的网络文件系统,上面代码就标识着我们声明了一个名为sockfs的网络文件系统,但是这里安装过程与某些文件系统不同,实在不了解的朋友可以翻阅一下操作系统理论方面的书,在此类书中讲述的数据结构file_system_type非常清楚,关于文件系统虽然不是本文的重点但是我们还是需要了解网络文件系统的初始化经过。
我是无名小卒,本文是原创如果转载请注明出处,我们在以前没提到过这里简要介绍一下,首先是内核在初始时会执行到init/main.c,而执行到内核的初始化函数kernel_init()在其内部调用了do_basic_setup()函数,再调用do_initcalls()函数,这里会看到有一个
Main()-->kernel_init()-->do_basic_setup()-->do_initcalls()
/* wumingxiaozu */
static void __init do_initcalls(void)
{
。。。。。。
for (call = __initcall_start; call __initcall_end; call++)
do_one_initcall(*call);
。。。。。。
}
这就是称作为initcall机制,我们查看socket.c中,可以看到下面语句
core_initcall(sock_init); /* early initcall */
#define core_initcall(fn) __define_initcall("1",fn,1)
在2.6内核中已经把很多初始化函数都放在initcall中实现了。所以初始化中会执行socket.c中的sock_init()函数
static int __init sock_init(void)
{
。。。。。。
register_filesystem(&sock_fs_type);/* wumingxiaozu */
sock_mnt = kern_mount(&sock_fs_type);
。。。。。。
}
就是在sock_init()函数中成功的将我们的网络文件系统登记、安装到了linux的内核中,register_filesystem函数将我们的sockfs套接字文件系统注册到linux内核中去,kern_mount函数完成了在linux内核中的套接字文件系统的安装,在内核中建立了网络文件系统安装点。这二个函数均与具体的使用的文件系统相关,所以我们不再跟进分析了,朋友们参阅操作系统理论的文件系统内容可以了解一下。在安装的过程中,我们的套接字文件系统会调用sock_fs_type数据结构中的get_sb钩子函数,以后我们称这类初始化的结构变量为钩子结构,因为他提供了我们函数的入口,就象钩子一样,将函数挂入到结构体中,所以这样的结构体我们称之为钩子结构体,而挂入的函数称之为钩子函数。我们在上面的代码中已经看到了sock_fs_type的设置了get_sb的钩子函数为sockfs_get_sb(),所以理所当然进入函数中去完成文件系统的安装过程
sock_init()-->kern_mount()-->。。。。。。--> sockfs_get_sb()
/* wumingxiaozu */
static int sockfs_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data,
struct vfsmount *mnt)
{
return get_sb_pseudo(fs_type, "socket:", &sockfs_ops, SOCKFS_MAGIC,mnt);/* wumingxiaozu */
}
这个函数一转手交给了get_sb_pseudo()去执行,注意传递的参数一个是我们的sock_fs_type,第二个参数要求创建socket名称的根目录项,第三个参数非常重要是关于我们套接字文件系统的操作函数表,而后一个为安装点。我们看一下重点的文件系统的操作函数表
static struct super_operations sockfs_ops = {
.alloc_inode = sock_alloc_inode,
.destroy_inode =sock_destroy_inode,
.statfs = simple_statfs,
};/* wumingxiaozu */
这个函数表是对于套接字文件系统的节点和目录提供了具体的钩子函数,以后在涉及到文件系统的操作内容时,linux内核都会层层跳转到这里的sockfs_ops钩子结构中再进入具体的钩子函数。那不是我们重要的分析过程了,为了不至于跑的太远,让大家在旅行途中过于劳累,我们将严格根据导游地图去前行,所以中途中的“风景”我们也是驻足欣赏不会踏入太深,一来是时间有限,加之那样将会使我们的篇幅巨增,对于朋友们的学习旅途将是无益的,所以还是前边我们提到的,如果想涉足其中请参考相关的文件系统书籍,get_sb_pseudo()与文件系统密切相关,其代码就是完成我们套接字sockfs的安装过程。我们回到sys_socket()函数继续往下看
sys_socketcall()-->sys_socket()-->sock_create()
/* wumingxiaozu */
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);/* wumingxiaozu */
}
进入__sock_create(),然后在那里执行
sys_socketcall()-->sys_socket()-->sock_create()-->__sock_create()
/* wumingxiaozu */
static int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
。。。。。。
sock = sock_alloc();
。。。。。。
err = pf->create(net, sock, protocol);/* wumingxiaozu */
。。。。。。
}
函数的前边不是很重要所以我们删减了一些内容,只留下重要的,也是我们要分析的重点,我们首先是进入了函数sock_alloc()
sys_socketcall()-->sys_socket()-->sock_create()-->__sock_create()-->sock_alloc()
/* wumingxiaozu */
static struct socket *sock_alloc(void)
{
struct inode *inode;
struct socket *sock;
inode = new_inode(sock_mnt->mnt_sb);
if (!inode)
return NULL;
sock = SOCKET_I(inode);
/* wumingxiaozu */
inode->i_mode = S_IFSOCK | S_IRWXUGO;
inode->i_uid = current->fsuid;
inode->i_gid = current->fsgid;
get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);
return sock;
}
这里我们看到socket数据结构,这个定义位于include/linux/net.h的117行
struct socket {
socket_state state;
unsigned long flags;
const struct proto_ops *ops;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
};/* wumingxiaozu */
我们暂不对其内容加以具体的解析,以后朋友们会经常看到贴出了数据结构的定义而没有做进步的分析,其原因是我认为“用中学习”的方法是有效的,经常看到有的网站上把结构体的解晰做的很全而细,但是对于朋友学习掌握,或者在将来的再学习强化记忆过程中的作用是渺小的,就象记英语单词,如果大家天天对着英中注释来记忆,而不是先看词猜想其含义,后再看中文词义的话,可想而知记忆效果的差距有多大,正是今天很多学习方法的改进才使大家的学习效果收到了事半功倍的效果。对于上面的结构体我们特别应该注意的是sock这个结构变量,其定义非常大,我们也不想占用太多的篇幅来列了,这个结构体是因为我们使用不同的协议而挂入不同的钩子结构变量,之所以从socket中分离出这么一个重要的结构是因为socket是通用的套接字结构体,而sock而与具体使用的协议相密切,所以公共的通用部分放在socket结构中,而私有的专用的放在sock结构体,这就是为什么socket内容相对少而sock结构体庞大的原因。结构内容我们也暂且放一放,我们用时再说,有朋友说ULK也就是深入理解内核书不错,我个人感觉他里面有大量的文字来描述结构体和函数作用甚至详细的参数作用,大量的篇幅都是用文字来描述,如果是英文版的看起更加吃力,我就是读了第三版的英文版,感觉不如读代码舒服来的痛快,与其说那本书是学习资料不如说更加象一本工具字典,现在我们分析一下sock_allock()这个函数,首先这里涉及到文件系统的inode,我们看到他调用new_inode(sock_mnt->mnt_sb);可以看到sock_mnt是上一节我们看到的socket文件系统的根节点,这里是在socket文件系统中分配一个节点,关于文件系统的内容将在以后的专门来分析,这里我们只需要知道他为我们在内存中分配了一个socket的inode节点即可,然后函数中接着是SOCKET_I(),这是一个内联函数,我们看一下他的内容
sys_socketcall()-->sys_socket()-->sock_create()-->__sock_create()-->sock_alloc()-->SOCKET_I()
/* wumingxiaozu */
static inline struct socket *SOCKET_I(struct inode *inode)
{
return &container_of(inode, struct socket_alloc, vfs_inode)->socket;/* wumingxiaozu */
}
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
这里我们看到他使用了一个宏container_of,这个宏我们需要分析一下,首先我们把他转换一下成下面这样
#define container_of(inode, struct socket_alloc, vfs_inode) ({ \
const typeof( ((struct socket_alloc *)0)->vfs_inode ) *__mptr = (inode); \/* wumingxiaozu */
(struct socket_alloc *)( (char *)__mptr - offsetof(struct socket_alloc,vfs_inode) );})
上面有一个数据结构我贴在下面
struct socket_alloc {
struct socket socket;
struct inode vfs_inode;
};/* wumingxiaozu */
上面还引用了另一个宏,也贴在下面并代入翻译
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)翻译成offsetof(struct socket_alloc,vfs_inode) ((size_t)&((struct socket_alloc)0)->vfs_inode)
上面的整个宏我们分析一下,首先是(struct socket_alloc)0)->vfs_inode,这句有很多朋友不很理解,它就是假设数据结构socket_alloc在0地址处时取得vfs_inode的地址也实际上就是vfs_inode在数据结构的偏离,所以宏名称使用了offsetof(),这样我们就理解了offsetof()宏是取得vfs_inode在struct socket_alloc的相对偏离位置,即地址差,然后我们看container_of宏,这个宏首先是假设0地址处的socket_alloc结构中的vfs_inode指向我们创建的inode,然后用这个inode的地址减掉vfs_inode的偏离差,就得到了数据结构socket_alloc的起始地址,而起始地址我们看到在socket_alloc头部是socket的数据结构,因此我们也就得到了socket的地址,即指针。这个宏是非常关键的一个宏,肯定今后要经常在内核中出现,希望朋友们认真把我的描述研究一遍,做到真正的理解,如果还是不得其解那就请记住他的作用就是找到数据结构中的指定数据结构的头地址。sock_alloc()剩下的代码部分是为inode节点设置他的相关指示标记,使他在内存中标记为socket,以其创建进程的信息,还有增加内存中socket的使用记数。然后我们回到__sock_create()函数中继续往下分析,创建了一个socket以后,我们看到
if (net_families[family] == NULL)
request_module("net-pf-%d", family);
也就是检查相应的协议有没有安装,我们在实践练习中曾经在创建socket用过这句
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
由些传递下来的family参数则是AF_INET,这个值是2,也就是说我们要在net_families[2]处安装这里的tcp协议,那么在我们这里要谈到的网络协议是何时安装到数组中的呢?同样我们在net/ipv4/af_inet.c中看到
fs_initcall(inet_init);
#define fs_initcall(fn) __define_initcall("5",fn,5)
所以初始化中会执行inet_init()函数,这是我们要说的登记的重点,我们在这个inet_init()函数中看到有一句关键的
static int __init inet_init(void)
{
。。。。。。
(void)sock_register(&inet_family_ops);/* wumingxiaozu */
。。。。。。
}
会在sock_register中完成注册
inet_init()-->sock_register()
/* wumingxiaozu */
int sock_register(const struct net_proto_family *ops)
{
。。。。。。
if (net_families[ops->family])
err = -EEXIST;
else {
net_families[ops->family] = ops;
err = 0;
}
。。。。。。
}
这里会把
static struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};/* wumingxiaozu */
注册到net_families数组中,而PF_INET就是上面我们说的AF_INET
#define PF_INET AF_INET
所以这里我们的练习中会进经过pf->create(net, sock, protocol)到inet_create()中
文章来源CU社区:内核中的TCP的追踪分析-1-追踪TCP(IPV4)的socket的初始化
相关文章