81
Linux TCP/IP 协议栈之 Socket 的实现分析 内核版本:2.6.12 作者:kendo 版权所有,转载请注明出处[www.skynet.org.cn]说明:这仅仅是一个笔记,由于偶的水平有限,我甚至不能保证其中内容正确率超过 80%另外,我不太习惯在代码中注解来自哪个文件,第几行之类的,因为偶是直接通过 source insight 双击鼠标跳转之。 第一部份 Socket 套接字的创建 socket 并不是 TCP/IP 协议的一部份。 从广义上来讲, socket Unix/Linux 抽像的进程间通讯的一种方法。网络 socket 通讯仅仅是其若干协议中的一类。而 tcp/ip 又是网络这类中的一种。 tcp/ip 的解度看 socket,它更多地体现了用户 API 与协议栈的一个中间层接口 层。用户通过调用 socket API 将报文递交给协议栈,或者从协议栈中接收报文件。 一、系统总入口 Linux 内核为所有的与 socket 有关的操作的 API,提供了一个统一的系统调用入口, 其代码在 net/socket.c 中: asmlinkage long sys_socketcall(int call, unsigned long __user *args) { unsigned long a[6]; unsigned long a0,a1; int err; if(call<1||call>SYS_RECVMSG) return -EINVAL; /* copy_from_user should be SMP safe. */ if (copy_from_user(a, args, nargs[call])) return -EFAULT; a0=a[0]; a1=a[1]; switch(call) { case SYS_SOCKET:

Linux TCPIP协议栈之Socket的实现分析

Embed Size (px)

Citation preview

Page 1: Linux TCPIP协议栈之Socket的实现分析

Linux TCP/IP 协议栈之 Socket 的实现分析

内核版本:2.6.12

作者:kendo

版权所有,转载请注明出处[www.skynet.org.cn];

说明:这仅仅是一个笔记,由于偶的水平有限,我甚至不能保证其中内容正确率超过 80%。

另外,我不太习惯在代码中注解来自哪个文件,第几行之类的,因为偶是直接通过 source

insight双击鼠标跳转之。

第一部份 Socket 套接字的创建

socket并不是 TCP/IP协议的一部份。

从广义上来讲,socket是 Unix/Linux抽像的进程间通讯的一种方法。网络 socket

通讯仅仅是其若干协议中的一类。而 tcp/ip又是网络这类中的一种。

从 tcp/ip 的解度看 socket,它更多地体现了用户 API 与协议栈的一个中间层接口

层。用户通过调用 socket API将报文递交给协议栈,或者从协议栈中接收报文件。

一、 系统总入口

Linux内核为所有的与 socket有关的操作的 API,提供了一个统一的系统调用入口,

其代码在 net/socket.c中:

asmlinkage long sys_socketcall(int call, unsigned long __user *args)

{

unsigned long a[6];

unsigned long a0,a1;

int err;

if(call<1||call>SYS_RECVMSG)

return -EINVAL;

/* copy_from_user should be SMP safe. */

if (copy_from_user(a, args, nargs[call]))

return -EFAULT;

a0=a[0];

a1=a[1];

switch(call)

{

case SYS_SOCKET:

Page 2: Linux TCPIP协议栈之Socket的实现分析

err = sys_socket(a0,a1,a[2]);

break;

case SYS_BIND:

err = sys_bind(a0,(struct sockaddr __user *)a1,

a[2]);

break;

case SYS_CONNECT:

err = sys_connect(a0, (struct sockaddr __user

*)a1, a[2]);

break;

case SYS_LISTEN:

err = sys_listen(a0,a1);

break;

case SYS_ACCEPT:

err = sys_accept(a0,(struct sockaddr __user *)a1,

(int __user *)a[2]);

break;

case SYS_GETSOCKNAME:

err = sys_getsockname(a0,(struct sockaddr __user

*)a1, (int __user *)a[2]);

break;

case SYS_GETPEERNAME:

err = sys_getpeername(a0, (struct sockaddr

__user *)a1, (int __user *)a[2]);

break;

case SYS_SOCKETPAIR:

err = sys_socketpair(a0,a1, a[2], (int __user

*)a[3]);

break;

case SYS_SEND:

err = sys_send(a0, (void __user *)a1, a[2],

a[3]);

break;

case SYS_SENDTO:

err = sys_sendto(a0,(void __user *)a1, a[2],

a[3],

(struct sockaddr __user *)a[4],

a[5]);

break;

case SYS_RECV:

err = sys_recv(a0, (void __user *)a1, a[2],

a[3]);

break;

case SYS_RECVFROM:

Page 3: Linux TCPIP协议栈之Socket的实现分析

err = sys_recvfrom(a0, (void __user *)a1, a[2],

a[3],

(struct sockaddr __user *)a[4],

(int __user *)a[5]);

break;

case SYS_SHUTDOWN:

err = sys_shutdown(a0,a1);

break;

case SYS_SETSOCKOPT:

err = sys_setsockopt(a0, a1, a[2], (char __user

*)a[3], a[4]);

break;

case SYS_GETSOCKOPT:

err = sys_getsockopt(a0, a1, a[2], (char __user

*)a[3], (int __user *)a[4]);

break;

case SYS_SENDMSG:

err = sys_sendmsg(a0, (struct msghdr __user *)

a1, a[2]);

break;

case SYS_RECVMSG:

err = sys_recvmsg(a0, (struct msghdr __user *)

a1, a[2]);

break;

default:

err = -EINVAL;

break;

}

return err;

}

首先调用 copy_from_user将用户态参数拷贝至数组 a。但是问题在于,每个被调用

的 API的参数不尽相同,那么每次拷贝的字节如果断定?

来看其第三个参数 nargs[call],其中 call 是操作码,后面有个大大的

switch...case就是判断它。对应的操作码定义在 include/linux/net.h:

#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)

Page 4: Linux TCPIP协议栈之Socket的实现分析

*/

#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)

*/

而数组 nargs则根据操作码的不同,计算对应的参数的空间大小:

/* Argument list sizes for sys_socketcall */

#define AL(x) ((x) * sizeof(unsigned long))

static 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)};

#undef AL

当拷贝完成参数后,就进入一个 switch...case...判断操作码,跳转至对应的系统

接口。

二、 sys_socket 函数

操作码 SYS_SOCKET是由 sys_socket()实现的:

asmlinkage long sys_socket(int family, int type, int protocol)

{

int retval;

Page 5: Linux TCPIP协议栈之Socket的实现分析

struct socket *sock;

retval = sock_create(family, type, protocol, &sock);

if (retval < 0)

goto out;

retval = sock_map_fd(sock);

if (retval < 0)

goto out_release;

out:

/* It may be already another descriptor 8) Not kernel problem.

*/

return retval;

out_release:

sock_release(sock);

return retval;

}

在分析这段代码之间,首先来看,创建一个 Socket,对内核而言,究竟意味着什么?

究竟需要内核干什么事?

当用户空间要创建一个 socket接口时,会调用 API函数:

int socket(int domain, int type, int protocol);

此函数,其三个参数分别表示协议族、协议类型(面向连接或无连接)以及协议。

对于用户态而言,一个 Socket,就是一个特殊的,已经打开的文件。为了对 socket

抽像出文件的概念,内核中为 socket定义了一个专门的文件系统类型 sockfs:

static struct vfsmount *sock_mnt;

static struct file_system_type sock_fs_type = {

.name = "sockfs",

.get_sb = sockfs_get_sb,

.kill_sb = kill_anon_super,

};

在模块初始化的时候,安装该文件系统:

void __init sock_init(void)

{

„„

register_filesystem(&sock_fs_type);

sock_mnt = kern_mount(&sock_fs_type);

}

稍后还要回来继续分析安装中的一点细节。

有了文件系统后,对内核而言,创建一个 socket,就是在 sockfs文件系统中创建一

Page 6: Linux TCPIP协议栈之Socket的实现分析

个文件节点(inode),并建立起为了实现 socket 功能所需的一整套数据结构,包括

struct inode和 struct socket结构。struct socket结构在内核中,就代表了一

个"Socket",当一个 struct socket 数据结构被分配空间后,再将其与一个已打开的

文件“建立映射关系”。这样,用户态就可以用抽像的文件的概念来操作 socket了——当

然,由于网络的特殊性,至少就目前而言,这种抽像,并不如其它模块的抽像那么完美。

这里 socket的实现,和文件系统密切相关。这里就不再分析 Linux的文件系统了,

这里只分配与 socket 相关的一些细节,其它的都一一跳过,呵呵,希望也能有水平再写

一篇《Linux文件系统的设计与实现简析》。

文件系统 struct vfsmount中有一个成员指针 mnt_sb指向该文件系统的超级块,

而超级块结构 struct super_lock有一个重要的成员 s_op指向了超级块的操作函数表,

其中有函数指针 alloc_inode()即为在给定的超级块下创建并初始化一个新的索引节点

对像。也就是调用:

sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);

当然,连同相关的处理细节一起,这一操作被层层封装至一个上层函数

new_inode()。

那如何分配一个 struct socket结构呢?如前所述,一个 socket总是与一个 inode

密切相关的。当然,在 inode中,设置一个 socket成员,是完全可行的,但是这貌似浪

费了空间——毕竟,更多的文件系统没有 socket 这个东东。所以,内核引入了另一个

socket_alloc结构:

struct socket_alloc {

struct socket socket;

struct inode vfs_inode;

};

显而易见,该结构实现了 inode 和 socket 的封装。已经一个 inode,可以通过宏

SOCKET_I来获取与之对应的 socket:

sock = SOCKET_I(inode);

static inline struct socket *SOCKET_I(struct inode *inode)

{

return &container_of(inode, struct socket_alloc,

vfs_inode)->socket;

}

但是,这样做,也同时意味着,在分配一个 inode后,必须再分配一个 socket_alloc

结构,并实现对应的封装。否则,container_of又能到哪儿去找到 socket呢?现在来

简要地看一个这个流程——这是文件系统安装中的一个重要步骤:

struct vfsmount *kern_mount(struct file_system_type *type)

{

return do_kern_mount(type->name, 0, type->name, NULL);

}

struct vfsmount *

Page 7: Linux TCPIP协议栈之Socket的实现分析

do_kern_mount(const char *fstype, int flags, const char *name, void

*data)

{

struct file_system_type *type = get_fs_type(fstype);

struct super_block *sb = ERR_PTR(-ENOMEM);

„„

sb = type->get_sb(type, flags, name, data);

„„

mnt->mnt_sb = sb;

„„

}

do_kern_mount 函数中,先根据注册的文件系统类型,调用 get_fs_type 获取之,也

就是我们之前注册的 sock_fs_type,然后调用它的 get_sb成员函数指针,获取相应的

超级块 sb。最后,调用文件系统的超级块成员指针,使之指向对应的值。

这里 get_sb函数指针,指向之前初始化的 sockfs_get_sb()函数。

static struct super_block *sockfs_get_sb(struct file_system_type

*fs_type,

int flags, const char *dev_name, void *data)

{

return get_sb_pseudo(fs_type, "socket:", &sockfs_ops,

SOCKFS_MAGIC);

}

注意其第三个参数 sockfs_ops,它封装了 sockfs的功能函数表:

static struct super_operations sockfs_ops = {

.alloc_inode = sock_alloc_inode,

.destroy_inode =sock_destroy_inode,

.statfs = simple_statfs,

};

struct super_block *

get_sb_pseudo(struct file_system_type *fs_type, char *name,

struct super_operations *ops, unsigned long magic)

{

struct super_block *s = sget(fs_type, NULL, set_anon_super,

NULL);

„„

s->s_op = ops ? ops : &default_ops;

}

这里就是先获取/分配一个超级块,然后初始化超级块的各成员,包括 s_op,我们前

面提到过它,它封装了对应的功能函数表。这里 s_op 自然就指向了 sockfs_ops。那前

面提到的 new_inode()函数分配 inode时调用的:

sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);

Page 8: Linux TCPIP协议栈之Socket的实现分析

这个 alloc_inode 函数指针也就是 sockfs_ops 的 sock_alloc_inode()函数

——转了一大圈,终于指到它了。

来看看 sock_alloc_inode是如何分配一个 inode节点的:

static struct inode *sock_alloc_inode(struct super_block *sb)

{

struct socket_alloc *ei;

ei = (struct socket_alloc

*)kmem_cache_alloc(sock_inode_cachep, SLAB_KERNEL);

if (!ei)

return NULL;

init_waitqueue_head(&ei->socket.wait);

ei->socket.fasync_list = NULL;

ei->socket.state = SS_UNCONNECTED;

ei->socket.flags = 0;

ei->socket.ops = NULL;

ei->socket.sk = NULL;

ei->socket.file = NULL;

ei->socket.flags = 0;

return &ei->vfs_inode;

}

函数先分配了一个用于封装 socket和 inode的 ei,然后在高速缓存中为之申请了一

块空间。这样,inode和 socket就同时都被分配了。接下来初始化 socket的各个成员,

这些成员,在后面都会一一提到。

/**

* struct socket - general BSD socket

* @state: socket state (%SS_CONNECTED, etc)

* @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)

* @ops: protocol specific socket operations

* @fasync_list: Asynchronous wake up list

* @file: File back pointer for gc

* @sk: internal networking protocol agnostic socket representation

* @wait: wait queue for several uses

* @type: socket type (%SOCK_STREAM, etc)

*/

struct socket {

socket_state state;

unsigned long flags;

struct proto_ops *ops;

struct fasync_struct *fasync_list;

struct file *file;

struct sock *sk;

Page 9: Linux TCPIP协议栈之Socket的实现分析

wait_queue_head_t wait;

short type;

};

OK,至目前为止,分配 inode、socket以及两者如何关联,都已一一分析了。

最后一个关键问题,就是如何把 socket与一个已打开的文件,建立映射关系。

在内核中,用 struct file 结构描述一个已经打开的文件,指向该结构的指针内核

中通常用 file或 filp来描述。我们知道,内核中,可以通过全局项 current来获得当

前进程,它是一个 struct task_struct类型的指针。tastk_struct有一个成员:

struct files_struct *files;

指向一个已打开的文件。当然,由于一个进程可能打开多个文件,所以,struct

files_struct结构有

struct file * fd_array[NR_OPEN_DEFAULT];

成员,这是个数组,以文件描述符为下标,即 current->files->fd[fd],可以找

到与当前进程指定文件描述符的文件。

有了这些基础,如果要把一个 socket 与一个已打开的文件建立映射,首先要做的就

是为socket分配一个 struct file,并申请分配一个相应的文件描述符 fd。因为socket

并不支持open方法(前面说socket的文件界面的抽像并不完美,这应该是一个佐证吧?),

所以不能期望用户界面通过调用 open() API 来分配一个 struct file,而是通过调用

get_empty_filp来获取:

struct file *file = get_empty_filp();

同样地:

int fd;

fd = get_unused_fd();

获取一个空间的文件描述符,然后,让 current 的 files 指针的 fd 数组的 fd 索引

项指向该 file:

void fastcall fd_install(unsigned int fd, struct file * file)

{

struct files_struct *files = current->files;

spin_lock(&files->file_lock);

if (unlikely(files->fd[fd] != NULL))

BUG();

files->fd[fd] = file;

spin_unlock(&files->file_lock);

}

OK,做到这一步,有了一个文件描述符 fd和一个打开的文件 file,它们与当前进程

相连,但是好像与创建的 socket并无任何瓜葛。要做的映射还是没有进展。struct file

或者文件描述述 fd或 current都没有任何能够与 inode或者是 socket相关的东东。这

需要一个中间的桥梁,目录项:struct dentry结构。

因为一个文件都有与其对应的目录项:

struct file {

struct list_head f_list;

struct dentry *f_dentry;

„„

Page 10: Linux TCPIP协议栈之Socket的实现分析

而一个目录项:

struct dentry {

„„

struct inode *d_inode; /* Where the name belongs

to - NULL is

* negative */

d_inode成员指向了与之对应的 inode节点„„

而之前已经创建了一个 inode节点和与之对应的 socket。所以,现在要做的,就是:

“先为当前文件分配一个对应的目录项,再将已创建的 inode节点安装至该目录项”

这样,一个完成的映射关系:进程、文件描述符、打开文件、目录项、inode 节点、

socket就完整地串起来了。

基本要分析的一些前导的东东都一一罗列了,虽然已尽量避免陷入文件系统的细节分

析,但是还是不可避免地进入其中,因为它们关系实现太紧密了。现在可以来看套接字的创

建过程了:

asmlinkage long sys_socket(int family, int type, int protocol)

{

int retval;

struct socket *sock;

retval = sock_create(family, type, protocol, &sock);

if (retval < 0)

goto out;

retval = sock_map_fd(sock);

if (retval < 0)

goto out_release;

out:

/* It may be already another descriptor 8) Not kernel problem.

*/

return retval;

out_release:

sock_release(sock);

return retval;

}

int sock_create(int family, int type, int protocol, struct socket

**res)

{

return __sock_create(family, type, protocol, res, 0);

}

Page 11: Linux TCPIP协议栈之Socket的实现分析

三、 af_inet 协议簇的协议封装

接下来,函数调用之前已经注的 inet_family_ops 的函数指针 create,也就是

inet_create()函数,前面,可以说一个通用的 socket已经创建好了,这里要完成与协

议本身相关的一些创建 socket 的工作。这一部份的工作比较复杂,还是先来看看

af_inet.c中的模块初始化时候,做了哪些与此相关的工作。

要引入的第一个数据结构是 struct inet_protosw,它封装了一个协议类型(如

SOCK_STREAM、SOCK_DGRAM等)与 ip协议中对应的传输层协议:

/* This is used to register socket interfaces for IP protocols. */

struct inet_protosw {

struct list_head list;

/* These two fields form the lookup key. */

unsigned short type; /* This is the 2nd argument

to socket(2). */

int protocol; /* This is the L4 protocol number.

*/

struct proto *prot;

struct proto_ops *ops;

int capability; /* Which (if any) capability do

* we need to use this socket

* interface?

*/

char no_check; /* checksum on rcv/xmit/none? */

unsigned char flags; /* See INET_PROTOSW_* below.

*/

};

#define INET_PROTOSW_REUSE 0x01 /* Are ports automatically

reusable? */

#define INET_PROTOSW_PERMANENT 0x02 /* Permanent protocols are

unremovable. */

type 是协议类型,对于 ipv4 而言,就是 SOCK_STREAM、SOCK_DGRAM 或者是

SOCK_RAW之一。protocol是传输层的协议号。prot用于描述一个具体的传输层协议,

而 ops指向对应的当前协议类型的操作函数集。针对不同的协议类型,定义了不同的 ops:

struct proto_ops inet_stream_ops = {

.family = PF_INET,

.owner = THIS_MODULE,

.release = inet_release,

.bind = inet_bind,

.connect = inet_stream_connect,

.socketpair = sock_no_socketpair,

.accept = inet_accept,

Page 12: Linux TCPIP协议栈之Socket的实现分析

.getname = inet_getname,

.poll = tcp_poll,

.ioctl = inet_ioctl,

.listen = inet_listen,

.shutdown = inet_shutdown,

.setsockopt = sock_common_setsockopt,

.getsockopt = sock_common_getsockopt,

.sendmsg = inet_sendmsg,

.recvmsg = sock_common_recvmsg,

.mmap = sock_no_mmap,

.sendpage = tcp_sendpage

};

struct proto_ops inet_dgram_ops = {

.family = PF_INET,

.owner = THIS_MODULE,

.release = inet_release,

.bind = inet_bind,

.connect = inet_dgram_connect,

.socketpair = sock_no_socketpair,

.accept = sock_no_accept,

.getname = inet_getname,

.poll = udp_poll,

.ioctl = inet_ioctl,

.listen = sock_no_listen,

.shutdown = inet_shutdown,

.setsockopt = sock_common_setsockopt,

.getsockopt = sock_common_getsockopt,

.sendmsg = inet_sendmsg,

.recvmsg = sock_common_recvmsg,

.mmap = sock_no_mmap,

.sendpage = inet_sendpage,

};

/*

* For SOCK_RAW sockets; should be the same as inet_dgram_ops but without

* udp_poll

*/

static struct proto_ops inet_sockraw_ops = {

.family = PF_INET,

.owner = THIS_MODULE,

.release = inet_release,

.bind = inet_bind,

.connect = inet_dgram_connect,

.socketpair = sock_no_socketpair,

.accept = sock_no_accept,

Page 13: Linux TCPIP协议栈之Socket的实现分析

.getname = inet_getname,

.poll = datagram_poll,

.ioctl = inet_ioctl,

.listen = sock_no_listen,

.shutdown = inet_shutdown,

.setsockopt = sock_common_setsockopt,

.getsockopt = sock_common_getsockopt,

.sendmsg = inet_sendmsg,

.recvmsg = sock_common_recvmsg,

.mmap = sock_no_mmap,

.sendpage = inet_sendpage,

};

从各个函数指针的名称,我们就可以大约知道它们是做什么事的了。进一步进以看到,

它们的函数指针指向的函数差不多都是相同的。除了一些细节上的区别,例如后面两种协议

类型并不支持 listen。

socket() API 第二个参数是协议类型,第三个参数是该协议类型下的协议——不过

对于 ipv4而言,它们都是一一对应的。但是从抽像封装的角度看,数据结构的设计本身应

该满足一个协议类型下边,可能存在多个不同的协议,即一对多的情况。而一一对应,仅是

它们的特例:

/* Upon startup we insert all the elements in inetsw_array[] into

* the linked list inetsw.

*/

static struct inet_protosw inetsw_array[] =

{

{

.type = SOCK_STREAM,

.protocol = IPPROTO_TCP,

.prot = &tcp_prot,

.ops = &inet_stream_ops,

.capability = -1,

.no_check = 0,

.flags = INET_PROTOSW_PERMANENT,

},

{

.type = SOCK_DGRAM,

.protocol = IPPROTO_UDP,

.prot = &udp_prot,

.ops = &inet_dgram_ops,

.capability = -1,

.no_check = UDP_CSUM_DEFAULT,

.flags = INET_PROTOSW_PERMANENT,

},

Page 14: Linux TCPIP协议栈之Socket的实现分析

{

.type = SOCK_RAW,

.protocol = IPPROTO_IP, /* wild card */

.prot = &raw_prot,

.ops = &inet_sockraw_ops,

.capability = CAP_NET_RAW,

.no_check = UDP_CSUM_DEFAULT,

.flags = INET_PROTOSW_REUSE,

}

};

数组的每一个元素,就是支持的一种协议名称,例如 IPOROTO_TCP,但是由于 IPV4

本身协议类型跟协议是一一对应的,所以没有更多的.type= SOCK_xxx 了。这样数组实

现了对 PF_INET协议族下支持的协议类型,以及协议类型下边的协议进行了封装,虽然事

实上它们是一一对应的关系,不过理论上,完全可能存在一对多的可能。

数组内,封装的一个具体的协议,由 struct proto结构来描述:

/* Networking protocol blocks we attach to sockets.

* socket layer -> transport layer interface

* transport -> network interface is defined by struct inet_proto

*/

struct proto {

void (*close)(struct sock *sk,

long timeout);

int (*connect)(struct sock *sk,

struct sockaddr *uaddr,

int addr_len);

int (*disconnect)(struct sock *sk, int

flags);

struct sock * (*accept) (struct sock *sk, int

flags, int *err);

int (*ioctl)(struct sock *sk, int cmd,

unsigned long arg);

int (*init)(struct sock *sk);

int (*destroy)(struct sock *sk);

void (*shutdown)(struct sock *sk, int

how);

int (*setsockopt)(struct sock *sk, int

level,

int optname, char __user *optval,

int optlen);

int (*getsockopt)(struct sock *sk, int

Page 15: Linux TCPIP协议栈之Socket的实现分析

level,

int optname, char __user *optval,

int __user *option);

int (*sendmsg)(struct kiocb *iocb, struct

sock *sk,

struct msghdr *msg, size_t len);

int (*recvmsg)(struct kiocb *iocb, struct

sock *sk,

struct msghdr *msg,

size_t len, int noblock, int flags,

int *addr_len);

int (*sendpage)(struct sock *sk, struct

page *page,

int offset, size_t size, int

flags);

int (*bind)(struct sock *sk,

struct sockaddr *uaddr, int

addr_len);

int (*backlog_rcv) (struct sock *sk,

struct sk_buff *skb);

/* Keeping track of sk's, looking them up, and port selection

methods. */

void (*hash)(struct sock *sk);

void (*unhash)(struct sock *sk);

int (*get_port)(struct sock *sk, unsigned

short snum);

/* Memory pressure */

void (*enter_memory_pressure)(void);

atomic_t *memory_allocated; /* Current

allocated memory. */

atomic_t *sockets_allocated; /* Current

number of sockets. */

/*

* Pressure flag: try to collapse.

* Technical note: it is used by multiple contexts non

atomically.

* All the sk_stream_mem_schedule() is of this nature:

accounting

* is strict, actions are advisory and have some latency.

*/

int *memory_pressure;

Page 16: Linux TCPIP协议栈之Socket的实现分析

int *sysctl_mem;

int *sysctl_wmem;

int *sysctl_rmem;

int max_header;

kmem_cache_t *slab;

unsigned int obj_size;

struct module *owner;

char name[32];

struct list_head node;

struct {

int inuse;

u8 __pad[SMP_CACHE_BYTES - sizeof(int)];

} stats[NR_CPUS];

};

以 TCP协议为例,TCP协议的 sokcet操作函数都被封装在这里了。

struct proto tcp_prot = {

.name = "TCP",

.owner = THIS_MODULE,

.close = tcp_close,

.connect = tcp_v4_connect,

.disconnect = tcp_disconnect,

.accept = tcp_accept,

.ioctl = tcp_ioctl,

.init = tcp_v4_init_sock,

.destroy = tcp_v4_destroy_sock,

.shutdown = tcp_shutdown,

.setsockopt = tcp_setsockopt,

.getsockopt = tcp_getsockopt,

.sendmsg = tcp_sendmsg,

.recvmsg = tcp_recvmsg,

.backlog_rcv = tcp_v4_do_rcv,

.hash = tcp_v4_hash,

.unhash = tcp_unhash,

.get_port = tcp_v4_get_port,

.enter_memory_pressure = tcp_enter_memory_pressure,

.sockets_allocated = &tcp_sockets_allocated,

.memory_allocated = &tcp_memory_allocated,

.memory_pressure = &tcp_memory_pressure,

.sysctl_mem = sysctl_tcp_mem,

Page 17: Linux TCPIP协议栈之Socket的实现分析

.sysctl_wmem = sysctl_tcp_wmem,

.sysctl_rmem = sysctl_tcp_rmem,

.max_header = MAX_TCP_HEADER,

.obj_size = sizeof(struct tcp_sock),

};

四、 分配 struct sock(sk)

浏览到这里,看完了 PF_INET 的协议簇、协议类型和协议(也就是 socket 调用的三

个参数)的封装关系,它们通过了两个数据结构 inet_protosw、struct proto来描述,

被一个数组 inetsw_array所封装。接下来看它的初始化工作:

static struct list_head inetsw[SOCK_MAX];

static int __init inet_init(void)

{

„„

/* Register the socket-side information for inet_create. */

for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)

INIT_LIST_HEAD(r);

for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN];

++q)

inet_register_protosw(q);

„„

}

inetsw是一个数组,其每一个元素,都是一个链表首部,前面一个循环初始化之。后

一个循环就值得注意了,也就是函数

inet_register_protosw:

void inet_register_protosw(struct inet_protosw *p)

{

struct list_head *lh;

struct inet_protosw *answer;

int protocol = p->protocol;

struct list_head *last_perm;

spin_lock_bh(&inetsw_lock);

if (p->type >= SOCK_MAX)

goto out_illegal;

/* If we are trying to override a permanent protocol, bail. */

answer = NULL;

last_perm = &inetsw[p->type];

list_for_each(lh, &inetsw[p->type]) {

answer = list_entry(lh, struct inet_protosw, list);

Page 18: Linux TCPIP协议栈之Socket的实现分析

/* Check only the non-wild match. */

if (INET_PROTOSW_PERMANENT & answer->flags) {

if (protocol == answer->protocol)

break;

last_perm = lh;

}

answer = NULL;

}

if (answer)

goto out_permanent;

/* Add the new entry after the last permanent entry if any, so

that

* the new entry does not override a permanent entry when matched

with

* a wild-card protocol. But it is allowed to override any

existing

* non-permanent entry. This means that when we remove this

entry, the

* system automatically returns to the old behavior.

*/

list_add_rcu(&p->list, last_perm);

out:

spin_unlock_bh(&inetsw_lock);

synchronize_net();

return;

out_permanent:

printk(KERN_ERR "Attempt to override permanent

protocol %d.\n",

protocol);

goto out;

out_illegal:

printk(KERN_ERR

"Ignoring attempt to register invalid socket type %d.\n",

p->type);

goto out;

}

这个函数完成的工作,就是把 inetsw_array数组中,相同的协议类型下边的协议,

Page 19: Linux TCPIP协议栈之Socket的实现分析

加入到 inetsw 对应的协议类型的链表中去。因为事实上一对一的关系,所以这个函数要

简单得多:因为不存在其它成员,所以每一次 list_entry 都为空值,所以不存在覆盖和

追加的情况,直接调用 list_add_rcu(&p->list, last_perm);把协议类型节点

(struct inet_protosw 类型的数组的某个元素)添加到链表(链表首部本身是一个数

组,数组索引是协议对应的协议类型的值)的第一个成员。

来做一个假设,如果 SOCK_STREAM协议类型下边还有另一个协议,IPPROTO_123,

那么 inetsw_array数组中就会多出一个元素:

{

.type = SOCK_STREAM,

.protocol = IPPROTO_123,

.prot = &123_prot,

„„

},

这样,当遍历 inetsw_array,再次进入 inet_register_protosw函数后,因为

SOCK_STREAM类型下已经注册了 IPPROTO_TCP,所以,

list_for_each(lh, &inetsw[p->type]) {

answer = list_entry(lh, struct inet_protosw, list);

/* Check only the non-wild match. */

if (INET_PROTOSW_PERMANENT & answer->flags) {

if (protocol == answer->protocol) /* 已经注册了相同

协议号,退出循环,因为没有置 answer为 NULL,所以后面会直接退出函数 */

break;

last_perm = lh; /* 移动位置指

针,指向链表中最后一个元素 */

}

answer = NULL;

}

这个循环,answer 就会指向之前注册的 TCP 的链表节点,然后根据标志,如果是

INET_PROTOSW_PERMANENT,则 last_perm 指向链表中最后一个节点,也就是 TCP,

之后同样的道理,再把 123追加到 TCP之后,如果是 INET_PROTOSW_REUSE,因为位置

指针 last_perm没有移动,则之前注册的元素会被覆盖。

OK,绕了这么大一圈子,了解了协议的封装及链表的注册。现在回到 inet_create

中来:

/*

* Create an inet socket.

*/

static int inet_create(struct socket *sock, int protocol)

{

struct sock *sk;

struct list_head *p;

struct inet_protosw *answer;

struct inet_sock *inet;

Page 20: Linux TCPIP协议栈之Socket的实现分析

struct proto *answer_prot;

unsigned char answer_flags;

char answer_no_check;

int err;

sock->state = SS_UNCONNECTED;

socket 的初始状态设置为“未连接”,这意味着面向连接的协议类型,如 tcp,在使

用之前必须建立连接修改状态位。

answer = NULL;

rcu_read_lock();

list_for_each_rcu(p, &inetsw[sock->type]) {

answer = list_entry(p, struct inet_protosw, list);

/* 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;

}

answer = NULL;

}

这个循环,根据 socket(2)调用的 protocol,把之前在链表中注册的协议节点找出

来,一个问题是,因为一一对应关系的存在,用户态调用 socket(2)的时候,常常第三个

参数直接就置 0了。也就是这里 protocol 为 0。那内核又如何处理这一默认值呢?也就

是 protocol != answer->protocol,而是被 if (IPPROTO_IP == protocol) 所

匹配了。这样,将 protocol 置为链表中第一个协议。而当循环结束时,answer 自然也

是指向这个链表中的第一个注册节点。以刚才的例子,SOCK_STREAM 下同时注册了 TCP

和 123,那么这里默认就取 TCP了。当然,把 123在 inetsw_array数组中的位置调前,

那么就默认取 123了。

err = -ESOCKTNOSUPPORT;

if (!answer)

goto out_rcu_unlock;

err = -EPERM;

if (answer->capability > 0 && !capable(answer->capability))

goto out_rcu_unlock;

err = -EPROTONOSUPPORT;

Page 21: Linux TCPIP协议栈之Socket的实现分析

if (!protocol)

goto out_rcu_unlock;

/* 找到了组织,将创建的 socket 的 ops 函数指针集,指向协议类型的。例如

创建的是 SOCK_STREAM,那么就指向了 inet_stream_ops */

sock->ops = answer->ops;

/* answer_prot指针指向了当前要创建的 socket的协议类型下边的协议,如

上例,它就是 IPPROTO_TCP的 tcp_prot结构 */

answer_prot = answer->prot;

answer_no_check = answer->no_check;

answer_flags = answer->flags;

rcu_read_unlock();

BUG_TRAP(answer_prot->slab != NULL);

接下来一个重要的工作,就是为 socket分配一个 sock,并初始化它:

err = -ENOBUFS;

sk = sk_alloc(PF_INET, GFP_KERNEL, answer_prot, 1);

if (sk == NULL)

goto out;

err = 0;

sk->sk_no_check = answer_no_check;

if (INET_PROTOSW_REUSE & answer_flags)

sk->sk_reuse = 1;

inet = inet_sk(sk);

if (SOCK_RAW == sock->type) {

inet->num = protocol;

if (IPPROTO_RAW == protocol)

inet->hdrincl = 1;

}

if (ipv4_config.no_pmtu_disc)

inet->pmtudisc = IP_PMTUDISC_DONT;

else

inet->pmtudisc = IP_PMTUDISC_WANT;

inet->id = 0;

sock_init_data(sock, sk);

sk->sk_destruct = inet_sock_destruct;

sk->sk_family = PF_INET;

sk->sk_protocol = protocol;

Page 22: Linux TCPIP协议栈之Socket的实现分析

sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;

inet->uc_ttl = -1;

inet->mc_loop = 1;

inet->mc_ttl = 1;

inet->mc_index = 0;

inet->mc_list = NULL;

#ifdef INET_REFCNT_DEBUG

atomic_inc(&inet_sock_nr);

#endif

if (inet->num) {

/* It assumes that any protocol which allows

* the user to assign a number at socket

* creation time automatically

* shares.

*/

inet->sport = htons(inet->num);

/* Add to protocol hash chains. */

sk->sk_prot->hash(sk);

}

if (sk->sk_prot->init) {

err = sk->sk_prot->init(sk);

if (err)

sk_common_release(sk);

}

out:

return err;

out_rcu_unlock:

rcu_read_unlock();

goto out;

}

虽然 create的代码就到这儿了,不过要说清楚 sk的分配,还得费上大力气。

每一个 Socket套接字,都有一个对应的 struct socket结构来描述(内核中一般使

用名称为 sock),但是同时又一个 struct sock 结构(内核中一般使用名称为 sk)。两

者之间是一一对应的关系。在后面的 sock_init_data函数中,可以看到

sk->sk_socket = sock;

sock->sk = sk;

这样的代码。

socket结构和 sock结构实际上是同一个事物的两个方面。不妨说,socket结构是

面向进程和系统调用界面的侧面,而 sock 结构则是面向底层驱动程序的侧面。设计者把

socket套接字中,与文件系统关系比较密切的那一部份放在 socket结构中,而把与通信

Page 23: Linux TCPIP协议栈之Socket的实现分析

关系比较密切的那一部份,则单独成为一个数结结构,那就是 sock结构。由于这两部份逻

辑上本来就是一体的,所以要通过指针互相指向对方,形成一对一的关系。

再暂时回到 inet_init中来,初始化工作中,有如下代码:

rc = proto_register(&tcp_prot, 1);

if (rc)

goto out;

rc = proto_register(&udp_prot, 1);

if (rc)

goto out_unregister_tcp_proto;

rc = proto_register(&raw_prot, 1);

if (rc)

goto out_unregister_udp_proto;

这里为每个 protocol 都调用了 proto_register 函数,其重要功能之一,就是根

据协议的 obj_size成员的大小,为协议创建高速缓存:

static DEFINE_RWLOCK(proto_list_lock);

static LIST_HEAD(proto_list);

int proto_register(struct proto *prot, int alloc_slab)

{

int rc = -ENOBUFS;

if (alloc_slab) {

/* 可以看到,函数最重要的功能就是根据 prot的 obj_size成员的大小,为协

议创建高速缓存 */

prot->slab = kmem_cache_create(prot->name,

prot->obj_size, 0,

if (prot->slab == NULL) {

printk(KERN_CRIT "%s: Can't create sock SLAB

cache!\n",

prot->name);

goto out;

}

}

/* 顺便看到它的另一个重要的功能,是维护一个以 proto_list为首的链表 */

write_lock(&proto_list_lock);

list_add(&prot->node, &proto_list);

write_unlock(&proto_list_lock);

rc = 0;

out:

return rc;

Page 24: Linux TCPIP协议栈之Socket的实现分析

}

这里要注意的是:prot->obj_size的大小,它它非仅仅是一个 sk的大小!!!以 TCP

为例:.obj_size = sizeof(struct tcp_sock)。稍后再来分析这个东东。

回到 inet_create()函数中来,其调用 sk_alloc()分配一个 sk:

sk = sk_alloc(PF_INET, GFP_KERNEL, answer_prot, 1);

struct sock *sk_alloc(int family, int priority, struct proto *prot,

int zero_it)

{

struct sock *sk = NULL;

kmem_cache_t *slab = prot->slab;

if (slab != NULL)

sk = kmem_cache_alloc(slab, priority);

else

sk = kmalloc(prot->obj_size, priority);

if (sk) {

if (zero_it) {

memset(sk, 0, prot->obj_size);

sk->sk_family = family;

/*

* See comment in struct sock definition to

understand

* why we need sk_prot_creator -acme

*/

sk->sk_prot = sk->sk_prot_creator = prot;

„„

在之前创建的高速缓存中申请分配一个 slab缓存项。并清零。然后设置协议族、并把

sk中的 sk_prot与对应的协议关联起来。

五、 初始化 sk

分配完成 sk 后,另一个重要的功能就是初始化它,sk 的成员相当复杂,其主要的初

始化工作是在函数 sock_init_data()中完成的:

void sock_init_data(struct socket *sock, struct sock *sk)

{

/* 初始化其三个队列 */

skb_queue_head_init(&sk->sk_receive_queue);

skb_queue_head_init(&sk->sk_write_queue);

skb_queue_head_init(&sk->sk_error_queue);

sk->sk_send_head = NULL;

Page 25: Linux TCPIP协议栈之Socket的实现分析

/* 初始化数据包发送定时器 */

init_timer(&sk->sk_timer);

sk->sk_allocation = GFP_KERNEL;

sk->sk_rcvbuf = sysctl_rmem_default;

sk->sk_sndbuf = sysctl_wmem_default;

sk->sk_state = TCP_CLOSE;

/* 指向对应的 socket结构 */

sk->sk_socket = sock;

sock_set_flag(sk, SOCK_ZAPPED);

if(sock)

{

sk->sk_type = sock->type;

sk->sk_sleep = &sock->wait;

/* 回指对应的 scok结构 */

sock->sk = sk;

} else

sk->sk_sleep = NULL;

rwlock_init(&sk->sk_dst_lock);

rwlock_init(&sk->sk_callback_lock);

sk->sk_state_change = sock_def_wakeup;

sk->sk_data_ready = sock_def_readable;

sk->sk_write_space = sock_def_write_space;

sk->sk_error_report = sock_def_error_report;

sk->sk_destruct = sock_def_destruct;

sk->sk_sndmsg_page = NULL;

sk->sk_sndmsg_off = 0;

sk->sk_peercred.pid = 0;

sk->sk_peercred.uid = -1;

sk->sk_peercred.gid = -1;

sk->sk_write_pending = 0;

sk->sk_rcvlowat = 1;

sk->sk_rcvtimeo = MAX_SCHEDULE_TIMEOUT;

sk->sk_sndtimeo = MAX_SCHEDULE_TIMEOUT;

sk->sk_stamp.tv_sec = -1L;

sk->sk_stamp.tv_usec = -1L;

Page 26: Linux TCPIP协议栈之Socket的实现分析

atomic_set(&sk->sk_refcnt, 1);

}

sock 结 构 中 , 有 三 个 重 要 的 双 向 队 列 , 分 别 是 sk_receive_queue 、

sk_write_queue和 sk_error_queue。从它们的名字就可以看出来其作用了。

队列并非采用通用的 list_head来维护,而是使用 skb_buffer队列:

struct sk_buff_head {

/* These two members must be first. */

struct sk_buff *next;

struct sk_buff *prev;

__u32 qlen;

spinlock_t lock;

};

这样,队列中指向的每一个 skb_buffer,就是一个数据包,分别是接收、发送和投

递错误。

剩余的就是初始化其它成员变量了。后面再来专门分析这些成员的作用。

inet_create函数中,除了初始化 sk成员的值,还有一部份代码,是初始化一个 inet

的东东:

inet = inet_sk(sk);

inet->uc_ttl = -1;

inet->mc_loop = 1;

inet->mc_ttl = 1;

inet->mc_index = 0;

inet->mc_list = NULL;

inet是一个 struct inet_sock结构类型,来看它的定义:

struct inet_sock {

/* sk and pinet6 has to be the first two members of inet_sock

*/

struct sock sk;

„„

}

我们说 sock 是面向用户态调用,而 sk 是面向内核驱动调用的,那 sk 是如何与协议

栈交互的呢?对于每一个类型的协议,为了与 sk 联系起来,都定义了一个 struct

XXX_sock结构,XXX是协议名,例如:

struct tcp_sock {

/* inet_sock has to be the first member of tcp_sock */

struct inet_sock inet;

int tcp_header_len; /* Bytes of tcp header to send

*/

„„

}

struct udp_sock {

Page 27: Linux TCPIP协议栈之Socket的实现分析

/* inet_sock has to be the first member */

struct inet_sock inet;

int pending; /* Any pending frames ? */

unsigned int corkflag; /* Cork is required */

__u16 encap_type; /* Is this an

Encapsulation socket? */

/*

* Following member retains the infomation to create a UDP header

* when the socket is uncorked.

*/

__u16 len; /* total length of

pending frames */

};

struct raw_sock {

/* inet_sock has to be the first member */

struct inet_sock inet;

struct icmp_filter filter;

};

很明显,它们的结构定构是“af_inet一般属性+自己的私有属性”,因为它们的第一

个成员总是 inet。

呵呵,现在回头来照一下起初在 af_inet.c中,封装协议注册的时候,size成员,

对于 tcp而言:

.obj_size = sizeof(struct tcp_sock),

其它协议类似。

以 obj_size 来确定每个 slab 缓存项分配的大小,所以,我们就可说,每次申请分

配的,实际上是一个 struct XXX_sock结构大小的结构。因为都是定义于上层结构的第

一个成员,可以使用强制类型转换来使用这块分配的内存空间。XXX_sock第一个成员包含

了 struct inet_sock,inet_sock的第一个成员包含了 struct sock例如:

inet = inet_sk(sk);

static inline struct inet_sock *inet_sk(const struct sock *sk)

{

return (struct inet_sock *)sk;

}

struct tcp_sock *tp = tcp_sk(sk);

static inline struct tcp_sock *tcp_sk(const struct sock *sk)

{

return (struct tcp_sock *)sk;

}

Page 28: Linux TCPIP协议栈之Socket的实现分析

OK,inet_create()运行完,一个 socket 套接字基本上就创建完毕了,剩下的就

是与文件系统挂钩,回到最初的 sys_socket()函数中来,它在调用完 sock_create()

后,紧接着调用 sock_map_fd()函数:

int sock_map_fd(struct socket *sock)

{

int fd;

struct qstr this;

char name[32];

/*

* Find a file descriptor suitable for return to the user.

*/

fd = get_unused_fd();

if (fd >= 0) {

struct file *file = get_empty_filp();

if (!file) {

put_unused_fd(fd);

fd = -ENFILE;

goto out;

}

sprintf(name, "[%lu]", SOCK_INODE(sock)->i_ino);

this.name = name;

this.len = strlen(name);

this.hash = SOCK_INODE(sock)->i_ino;

file->f_dentry = d_alloc(sock_mnt->mnt_sb->s_root,

&this);

if (!file->f_dentry) {

put_filp(file);

put_unused_fd(fd);

fd = -ENOMEM;

goto out;

}

file->f_dentry->d_op = &sockfs_dentry_operations;

d_add(file->f_dentry, SOCK_INODE(sock));

file->f_vfsmnt = mntget(sock_mnt);

file->f_mapping = file->f_dentry->d_inode->i_mapping;

sock->file = file;

file->f_op = SOCK_INODE(sock)->i_fop =

&socket_file_ops;

Page 29: Linux TCPIP协议栈之Socket的实现分析

file->f_mode = FMODE_READ | FMODE_WRITE;

file->f_flags = O_RDWR;

file->f_pos = 0;

fd_install(fd, file);

}

out:

return fd;

}

这个函数的核心思想,在一开始,就已经分析过了。从进程的角度来讲,一个 socket

套接字就是一个特殊的,已打开的文件。前面分配好一个 socket 后,这里要做的就是将

它与文件系统拉上亲戚关系。首先获取一个空闲的文件描述符号和 file结构。然后在文件

系统中分配一个目录项(d_alloc),使其指向已经分配的 inode 节点(d_add),然后把

其目录项挂在 sockfs 文件系统的根目录之下。并且把目录项的指针 d_op 设置成指向

sockfs_dentry_ operations,这个数据结构通过函数指针提供他与文件路径有关的操

作:

static struct dentry_operations sockfs_dentry_operations = {

.d_delete = sockfs_delete_dentry,

};

最后一步,就是将 file 结构中的 f_op 和 sock 结构中的 i_fop 都指向

socket_file_ops,它是一个函数指针集,指向了 socket 面向文件系统的用户态调用

的一些接口函数:

static struct file_operations socket_file_ops = {

.owner = THIS_MODULE,

.llseek = no_llseek,

.aio_read = sock_aio_read,

.aio_write = sock_aio_write,

.poll = sock_poll,

.unlocked_ioctl = sock_ioctl,

.mmap = sock_mmap,

.open = sock_no_open, /* special open code

to disallow open via /proc */

.release = sock_close,

.fasync = sock_fasync,

.readv = sock_readv,

.writev = sock_writev,

.sendpage = sock_sendpage

};

OK,到这里,整个 socket套接字的创建工作,就宣告完成了。

写到这里,可以为 socket的创建下一个小结了:

1、所谓创建 socket,对内核而言,最重要的工作就是分配 sock与 sk;

2、sock面向上层系统调用,主要是与文件系统交互。通过进程的 current指针的 files,

结合创建 socket 时返回的文件描符述,可以找到内核中对应的 struct file,再根据

file的 f_dentry可以找到对应的目录项,而目录项 struct dentry中,有 d_inode

Page 30: Linux TCPIP协议栈之Socket的实现分析

指针,指向与 sock封装在一起的 inode。sock又与 sk指针互指,一一对应。在这串结

构中,有两个重要的函数集指针,一个是文件系统 struct file中的 f_op指针,它指向

了,对应的用户态调用的 read,write 等操调用,但不支持 open,另一个是 struct

socket结构,即 sock的 ops指针,它在 inet_create()中被置为

sock->ops = answer->ops;

指向具体协议类型的 ops;例如,inet_stream_ops、inet_dgram_ops 或者是

inet_sockraw_ops等等。它用来支持上层的 socket的其它 API调用。

3、sk面向内核协议栈,协议栈与它的接口数据结构是 struct protoname_sock,该结

构中包含了一般性的 inet结构和自己的私有成员,struct inet_sock的第一个成员就

是一个 sk指针,而分配的 sk,实际上空间大小是 struct protoname_sock,所以,这

三者可以通过强制类型转换来获取需要的指针。

4、由于水平有限,文件系统的一些细节被我跳过了,sk中的大多数成员变量的作用,也被

我跳出过了。呵呵,还好,终于还是把这块流程给初步分析出来了。另外,当时写的时候,

没有想到会写这么长,大大超出了每贴的字限制。所以,每个小节内容跟标题可能会有点对

不上号。

第二部份 Socket 的 bind(2),绑定地址

一、 bind(2)

当创建了一个 Socket套接字后,对于服务器来说,接下来的工作,就是调用 bind(2)

为服务器指明本地址、协议端口号,常常可以看到这样的代码:

strut sockaddr_in sin;

sin.sin_family = AF_INET;

sin.sin_addr.s_addr = xxx;

sin.sin_port = xxx;

bind(sock, (struct sockaddr *)&sin, sizeof(sin));

从这个系统调用中,可以知道当进行 SYS_BIND 操作的时候,:

1、对于 AF_INET协议簇来讲,其地址格式是 strut sockaddr_in,而对于 socket来

讲,strut sockaddr 结构表示的地址格式实现了更高层次的抽像,因为每种协议长簇的

地址不一定是相同的,所以,系统调用的第三个参数得指明该协议簇的地址格式的长度。

2、进行 bind(2)系统调用时,除了地址长度外,还得向内核提供:sock描述符、协议簇

名称、本地地址、端口这些参数;

二、 sys_bind()

操作 SYS_BIND 是由 sys_bind()实现的:

Page 31: Linux TCPIP协议栈之Socket的实现分析

asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int

addrlen)

{

struct socket *sock;

char address[MAX_SOCK_ADDR];

int err;

if((sock = sockfd_lookup(fd,&err))!=NULL)

{

if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0) {

err = security_socket_bind(sock, (struct

sockaddr *)address, addrlen);

if (err) {

sockfd_put(sock);

return err;

}

err = sock->ops->bind(sock, (struct sockaddr

*)address, addrlen);

}

sockfd_put(sock);

}

return err;

}

在 socket 的创建中,已经反复分析了 socket 与文件系统的关系,现在已知 socket 的

描述符号,要找出与之相关的 socket结构,应该是件容易的事情:

struct socket *sockfd_lookup(int fd, int *err)

{

struct file *file;

struct inode *inode;

struct socket *sock;

if (!(file = fget(fd)))

{

*err = -EBADF;

return NULL;

}

inode = file->f_dentry->d_inode;

if (!S_ISSOCK(inode->i_mode)) {

*err = -ENOTSOCK;

fput(file);

Page 32: Linux TCPIP协议栈之Socket的实现分析

return NULL;

}

sock = SOCKET_I(inode);

if (sock->file != file) {

printk(KERN_ERR "socki_lookup: socket file

changed!\n");

sock->file = file;

}

return sock;

}

fget 从当前进程的 files 指针中,根据 sock 对应的描述符号,找到已打开的文件

file,再根据文件的目录项中的 inode,利用 inode与 sock被封装在同一个结构中的事

实,调用宏 SOCKET_I找到待查的 sock结构。最后做一个小小的判断,因为正常情况下,

sock的 file指针是回指与之相关的 file。

接下来的工作是把用户态的地址拷贝至内核中来:

int move_addr_to_kernel(void __user *uaddr, int ulen, void *kaddr)

{

if(ulen<0||ulen>MAX_SOCK_ADDR)

return -EINVAL;

if(ulen==0)

return 0;

if(copy_from_user(kaddr,uaddr,ulen))

return -EFAULT;

return 0;

}

bind(2)第三个参数必须存在的原因之一,copy_from_user必须知道拷贝的字节长

度。

因为 sock 的 ops 函数指针集,在创建之初,就指向了对应的协议类型,例如如果类型是

SOCK_STREAM,那么它就指向 inetsw_array[0].ops。也就是 inet_stream_ops:

struct proto_ops inet_stream_ops = {

.family = PF_INET,

„„

.bind = inet_bind,

„„

};

sys_bind()在做完了一个通用的 socket bind应该做的事情,包括查找对应 sock结构,

拷贝地址。就调用对应协议族的对应协议类型的 bind函数,也就是 inet_bind。

Page 33: Linux TCPIP协议栈之Socket的实现分析

三、 inet_bind

说bind(2)的最重要的作用,就是为套接字绑定地址和端口,那么要分析inet_bind()

之前,要搞清楚的一件事情就是,这个绑定,是绑定到哪儿?或者说,是绑定到内核的哪个

数据结构的哪个成员变量上面??

有三个地方是可以考虑的:socket 结构,包括 sock 和 sk,inet 结构,以及

protoname_sock结构。绑定在 socket结构上是可行的,这样可以实现最高层面上的抽

像,但是因为每一类协议簇 socket 的地址及端口表现形式差异很大,这样就得引入专门

的转换处理功能。绑定在 protoname_sock 也是可行的,但是却是最笨拙的,因为例如

tcp 和 udp,它们的地址及端口表现形式是一样的,这样就浪费了空间,加大了代码处理

量。所以,inet做为一个协议类型的抽像,是最理想的地方了,再来回顾一下它的定义:

struct inet_sock {

„„

/* Socket demultiplex comparisons on incoming packets. */

__u32 daddr; /* Foreign IPv4

addr */

__u32 rcv_saddr; /* Bound local IPv4

addr */

__u16 dport; /* Destination

port */

__u16 num; /* Local port */

__u32 saddr; /* Sending

source */

„„

};

去掉了其它成员,保留了与地址及端口相关的成员变量,从注释中,可以清楚地了解它

们的作用。所以,我们说的 bind(2)之绑定,主要就是对这几个成员变量赋值的过程了

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int

addr_len)

{

/* 获取地址参数 */

struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;

/* 获取 sock对应的 sk */

struct sock *sk = sock->sk;

/* 获取 sk对应的 inet */

struct inet_sock *inet = inet_sk(sk);

/* 这个临时变量用来保存用户态传递下来的端口参数 */

unsigned short snum;

int chk_addr_ret;

int err;

/* 如果协议簇对应的协议自身还有 bind函数,调用之,例如 SOCK_RAW就还有

一个 raw_bind */

if (sk->sk_prot->bind) {

Page 34: Linux TCPIP协议栈之Socket的实现分析

err = sk->sk_prot->bind(sk, uaddr, addr_len);

goto out;

}

/* 校验地址长度 */

err = -EINVAL;

if (addr_len < sizeof(struct sockaddr_in))

goto out;

/* 判断地址类型:广播?多播?单播? */

chk_addr_ret = inet_addr_type(addr->sin_addr.s_addr);

/* ipv4 有一个 ip_nonlocal_bind 标志,表示是否绑定非本地址 IP 地址,

可以通过

* cat /proc/sys/net/ipv4/ip_nonlocal_bind 查看到。它用来解决某

些服务绑定

* 动态 IP地址的情况。作者在注释中已有详细说明:

* Not specified by any standard per-se, however it breaks too

* many applications when removed. It is unfortunate since

* allowing applications to make a non-local bind solves

* several problems with systems using dynamic addressing.

* (ie. your servers still start up even if your ISDN link

* is temporarily down)

* 这里判断,用来确认如果没有开启“绑定非本地址 IP”,地址值及类型是正确

*/

err = -EADDRNOTAVAIL;

if (!sysctl_ip_nonlocal_bind &&

!inet->freebind &&

addr->sin_addr.s_addr != INADDR_ANY &&

chk_addr_ret != RTN_LOCAL &&

chk_addr_ret != RTN_MULTICAST &&

chk_addr_ret != RTN_BROADCAST)

goto out;

/* 获取协议端口号 */

snum = ntohs(addr->sin_port);

err = -EACCES;

/* 校验当前进程有没有使用低于 1024端口的能力 */

if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))

goto out;

/* We keep a pair of addresses. rcv_saddr is the one

* used by hash lookups, and saddr is used for transmit.

*

Page 35: Linux TCPIP协议栈之Socket的实现分析

* In the BSD API these are the same except where it

* would be illegal to use them (multicast/broadcast) in

* which case the sending device address is used.

*/

lock_sock(sk);

/* 检查 socket是否已经被绑定过了:用了两个检查项,一个是 sk状态,另一

个是是否已经绑定过端口了

当然地址本来就可以为 0,所以,不能做为检查项 */

err = -EINVAL;

if (sk->sk_state != TCP_CLOSE || inet->num)

goto out_release_sock;

/* 绑定 inet的接收地址(地址服务绑定地址)和来源地址为用户态指定地址 */

inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;

/* 若地址类型为广播或多播,则将地址置 0,表示直接使用网络设备 */

if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret ==

RTN_BROADCAST)

inet->saddr = 0; /* Use device */

/*

* 调用协议的 get_port 函数,确认是否可绑定端口,若可以,则绑定在

inet->num之上,注意,这里虽然没有

* 把 inet传过去,但是第一个参数 sk,它本身和 inet是可以互相转化的

*/

if (sk->sk_prot->get_port(sk, snum)) {

inet->saddr = inet->rcv_saddr = 0;

err = -EADDRINUSE;

goto out_release_sock;

}

/* 如果端口和地址可以绑定,置标志位 */

if (inet->rcv_saddr)

sk->sk_userlocks |= SOCK_BINDADDR_LOCK;

if (snum)

sk->sk_userlocks |= SOCK_BINDPORT_LOCK;

/* inet的 sport(来源端口)成员也置为绑定端口 */

inet->sport = htons(inet->num);

inet->daddr = 0;

inet->dport = 0;

sk_dst_reset(sk);

err = 0;

out_release_sock:

release_sock(sk);

Page 36: Linux TCPIP协议栈之Socket的实现分析

out:

return err;

}

上述分析中,忽略的第一个细节是capable()函数调用,它是 Linux 安全模块(LSM)

的一部份,简单地讲其用来对权限做出检查,

检查是否有权对指定的资源进行操作。这里它的参数是 CAP_NET_BIND_SERVICE,表示

的含义是:

/* Allows binding to TCP/UDP sockets below 1024 */

/* Allows binding to ATM VCIs below 32 */

#define CAP_NET_BIND_SERVICE 10

另一个就是协议的端口绑定,调用了协议的 get_port 函数,如果是 SOCK_STREAM

的 TCP 协议,那么它就是 tcp_v4_get_port()函数。

四、 协议端口的绑定

要分配这个函数,还是得先看一些基本的东东。这里涉及到内核中提供 hash链表的操

作的 API。可以参考其它相关资料。

http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.h

tml

这里讲了链表的实现,顺道提了一个 hash链表,觉得写得还不错,收藏一下。

对于 TCP 已注册的端口,是采用一个 hash 表来维护的。hash 桶用 struct

tcp_bind_hashbucket结构来表示:

struct tcp_bind_hashbucket {

spinlock_t lock;

struct hlist_head chain;

};

hash表中的每一个 hash节点,用 struct tcp_bind_bucket结构来表示:

struct tcp_bind_bucket {

unsigned short port; /* 节点

中绑定的端口 */

signed short fastreuse;

struct hlist_node node;

struct hlist_head owners;

};

tcp_hashinfo 的 hash 表信息,都集中封装在结构 tcp_hashinfo 当中,而维护

已注册端口,只是它其中一部份:

extern struct tcp_hashinfo {

„„

/* Ok, let's try this, I give up, we do need a local binding

* TCP hash as well as the others for fast bind/connect.

*/

struct tcp_bind_hashbucket *__tcp_bhash;

Page 37: Linux TCPIP协议栈之Socket的实现分析

int __tcp_bhash_size;

„„

} tcp_hashinfo;

#define tcp_bhash (tcp_hashinfo.__tcp_bhash)

#define tcp_bhash_size (tcp_hashinfo.__tcp_bhash_size)

其使用的 hash函数是 tcp_bhashfn:

/* These are AF independent. */

static __inline__ int tcp_bhashfn(__u16 lport)

{

return (lport & (tcp_bhash_size - 1));

}

这样,如果要取得某个端口对应的 hash链的首部 hash桶节点的话,可以使用:

struct tcp_bind_hashbucket *head;

head = &tcp_bhash[tcp_bhashfn(snum)];

如果要新绑定一个端口,就是先创建一个 struct tcp_bind_bucket 结构的 hash

节点,然后把它插入到对应的 hash链中去:

struct tcp_bind_bucket *tb;

tb = tcp_bucket_create(head, snum);

struct tcp_bind_bucket *tcp_bucket_create(

struct tcp_bind_hashbucket *head, unsigned short snum)

{

struct tcp_bind_bucket *tb =

kmem_cache_alloc(tcp_bucket_cachep,

SLAB_ATOMIC);

if (tb) {

tb->port = snum;

tb->fastreuse = 0;

INIT_HLIST_HEAD(&tb->owners);

hlist_add_head(&tb->node, &head->chain);

}

return tb;

}

另外,sk中,还维护了一个类似的 hash链表,同时需要调用 tcp_bind_hash()函

数把 hash节点插入进去:

struct sock {

struct sock_common __sk_common;

#define sk_bind_node __sk_common.skc_bind_node

„„

}

/* @skc_bind_node: bind hash linkage for various protocol lookup tables

*/

Page 38: Linux TCPIP协议栈之Socket的实现分析

struct sock_common {

struct hlist_node skc_bind_node;

„„

}

if (!tcp_sk(sk)->bind_hash)

tcp_bind_hash(sk, tb, snum);

void tcp_bind_hash(struct sock *sk, struct tcp_bind_bucket *tb,

unsigned short snum)

{

inet_sk(sk)->num = snum;

sk_add_bind_node(sk, &tb->owners);

tcp_sk(sk)->bind_hash = tb;

}

这里,就顺道绑定了 inet 的 num 成员变量,并置协议的 bind_hash 指针为当前分

配的 hash节点。而 sk_add_bind_node函数,就是一个插入 hash表节点的过程:

static __inline__ void sk_add_bind_node(struct sock *sk,

struct hlist_head *list)

{

hlist_add_head(&sk->sk_bind_node, list);

}

如果要遍历 hash表的话,例如在插入之前,先判断端口是否已经在 hash表当中了。

就可以调用:

#define tb_for_each(tb, node, head) hlist_for_each_entry(tb, node,

head, node)

struct tcp_bind_hashbucket *head;

struct tcp_bind_bucket *tb;

head = &tcp_bhash[tcp_bhashfn(snum)];

spin_lock(&head->lock);

tb_for_each(tb, node, &head->chain)

if (tb->port == snum)

found,do_something;

有了这些基础知识,再来看 tcp_v4_get_port()的实现,就要容易得多了:

static int tcp_v4_get_port(struct sock *sk, unsigned short snum)

{

struct tcp_bind_hashbucket *head;

struct hlist_node *node;

struct tcp_bind_bucket *tb;

int ret;

local_bh_disable();

Page 39: Linux TCPIP协议栈之Socket的实现分析

/* 如果端口值为 0,意味着让系统从本地可用端口用选择一个,并置 snum 为分

配的值 */

if (!snum) {

int low = sysctl_local_port_range[0];

int high = sysctl_local_port_range[1];

int remaining = (high - low) + 1;

int rover;

spin_lock(&tcp_portalloc_lock);

if (tcp_port_rover < low)

rover = low;

else

rover = tcp_port_rover;

do {

rover++;

if (rover > high)

rover = low;

head = &tcp_bhash[tcp_bhashfn(rover)];

spin_lock(&head->lock);

tb_for_each(tb, node, &head->chain)

if (tb->port == rover)

goto next;

break;

next:

spin_unlock(&head->lock);

} while (--remaining > 0);

tcp_port_rover = rover;

spin_unlock(&tcp_portalloc_lock);

/* Exhausted local port range during search? */

ret = 1;

if (remaining <= 0)

goto fail;

/* OK, here is the one we will use. HEAD is

* non-NULL and we hold it's mutex.

*/

snum = rover;

} else {

/* 否则,就在 hash表中,查找端口是否已经存在 */

head = &tcp_bhash[tcp_bhashfn(snum)];

spin_lock(&head->lock);

tb_for_each(tb, node, &head->chain)

if (tb->port == snum)

Page 40: Linux TCPIP协议栈之Socket的实现分析

goto tb_found;

}

tb = NULL;

goto tb_not_found;

tb_found:

/* 稍后有对应的代码:第一次分配 tb 后,会调用 tcp_bind_hash 加入至相

应的 sk,这里先做一个判断,来确定这一步工作是否进行过*/

if (!hlist_empty(&tb->owners)) {

/* socket的 SO_REUSEADDR 选项,用来确定是否允许本地地址重用,例如同时启动多

个服务器、多个套接字绑定至同一端口等等,sk_reuse成员对应其值,因为如果一个绑定

的 hash 节点已经存在,而且不允许重用的话,那么则表示因冲突导致出错,调用

tcp_bind_conflict来处理之 */

if (sk->sk_reuse > 1)

goto success;

if (tb->fastreuse > 0 &&

sk->sk_reuse && sk->sk_state != TCP_LISTEN) {

goto success;

} else {

ret = 1;

if (tcp_bind_conflict(sk, tb))

goto fail_unlock;

}

}

tb_not_found:

/* 如果不存在,则分配 hash节点,绑定端口 */

ret = 1;

if (!tb && (tb = tcp_bucket_create(head, snum)) == NULL)

goto fail_unlock;

if (hlist_empty(&tb->owners)) {

if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)

tb->fastreuse = 1;

else

tb->fastreuse = 0;

} else if (tb->fastreuse &&

(!sk->sk_reuse || sk->sk_state == TCP_LISTEN))

tb->fastreuse = 0;

success:

if (!tcp_sk(sk)->bind_hash)

tcp_bind_hash(sk, tb, snum);

BUG_TRAP(tcp_sk(sk)->bind_hash == tb);

ret = 0;

fail_unlock:

spin_unlock(&head->lock);

Page 41: Linux TCPIP协议栈之Socket的实现分析

fail:

local_bh_enable();

return ret;

}

到这里,可以为这部份下一个小结了,所谓绑定,就是:

1、设置内核中 inet相关变量成员的值,以待后用;

2、协议中,如 TCP 协议,记录绑定的协议端口的信息,采用 hash 链表存储,sk 中也同

时维护了这么一个链表。两者的区别应该是前者给协议用。后者给 socket用。

第三部份 sys_listen

一、 sys_listen

对面向连接的协议,在调用 bind(2)后,进一步调用 listen(2),让套接字进入监

听状态:

int listen(int sockfd, int backlog);

backlog表示新建连接请求时,最大的未处理的积压请求数。

这里说到让套接字进入某种状态,也就是说,涉及到套接字的状态变迁,前面 create

和 bind时,也遇到过相应的代码。

sock和 sk都有相应的状态字段,先来看 sock的:

typedef enum {

SS_FREE = 0, /* 套接字未分配 */

SS_UNCONNECTED, /* 套接字未连接 */

SS_CONNECTING, /* 套接字正在处理连接

*/

SS_CONNECTED, /* 套接 字 已 连 接

*/

SS_DISCONNECTING /* 套接字正在处理关闭连接 */

} socket_state;

在创建套接字时,被初始化为 SS_UNCONNECTED。

对于面向连接模式的 SOCK_STREAM来讲,这样描述状态显然是不够的。这样,在 sk

中,使用 sk_state维护了一个有限状态机来描述套接字的状态:

enum {

TCP_ESTABLISHED = 1,

TCP_SYN_SENT,

TCP_SYN_RECV,

TCP_FIN_WAIT1,

TCP_FIN_WAIT2,

TCP_TIME_WAIT,

TCP_CLOSE,

TCP_CLOSE_WAIT,

TCP_LAST_ACK,

Page 42: Linux TCPIP协议栈之Socket的实现分析

TCP_LISTEN,

TCP_CLOSING, /* now a valid state */

TCP_MAX_STATES /* Leave at the end! */

};

还有一个相应的用来进行状态位运算的枚举结构:

enum {

TCPF_ESTABLISHED = (1 << 1),

TCPF_SYN_SENT = (1 << 2),

TCPF_SYN_RECV = (1 << 3),

TCPF_FIN_WAIT1 = (1 << 4),

TCPF_FIN_WAIT2 = (1 << 5),

TCPF_TIME_WAIT = (1 << 6),

TCPF_CLOSE = (1 << 7),

TCPF_CLOSE_WAIT = (1 << 8),

TCPF_LAST_ACK = (1 << 9),

TCPF_LISTEN = (1 << 10),

TCPF_CLOSING = (1 << 11)

};

值得一提的是,sk 的状态不等于 TCP 的状态,虽然 sk是面向协议栈,但它的状态并

不能同 TCP 状态一一直接划等号。虽然这些状态值都用 TCP_XXX 来表式,但是只是因为

TCP协议状态非常复杂。sk结构只是利用它的一个子集来抽像描述而已。

同样地,操作码 SYS_LISTEN的任务会落到 sys_listen()函数身上:

/* Maximum queue length specifiable by listen. */

#define SOMAXCONN 128

int sysctl_somaxconn = SOMAXCONN;

asmlinkage long sys_listen(int fd, int backlog)

{

struct socket *sock;

int err;

if ((sock = sockfd_lookup(fd, &err)) != NULL) {

if ((unsigned) backlog > sysctl_somaxconn)

backlog = sysctl_somaxconn;

err = security_socket_listen(sock, backlog);

if (err) {

sockfd_put(sock);

return err;

}

err=sock->ops->listen(sock, backlog);

Page 43: Linux TCPIP协议栈之Socket的实现分析

sockfd_put(sock);

}

return err;

}

同样地,函数会最终转向协议簇的 listen函数,也就是 inet_listen():

/*

* Move a socket into listening state.

*/

int inet_listen(struct socket *sock, int backlog)

{

struct sock *sk = sock->sk;

unsigned char old_state;

int err;

lock_sock(sk);

err = -EINVAL;

/* 在 listen之前,sock必须为未连接状态,且只有 SOCK_STREAM 类型,才

有 listen(2)*/

if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)

goto out;

/* 临时保存状态机状态 */

old_state = sk->sk_state;

/* 只有状态机处于 TCP_CLOSE 或者是 TCP_LISTEN 这两种状态时,才可能对

其调用 listen(2) ,这个判断证明了 listen(2)是可以重复调用地(当然是在转向

TCP_LISTEN后没有再进行状态变迁*/

if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))

goto out;

/* 如果接口已经处理 listen 状态,只修改其 max_backlog,否则先调用

tcp_listen_start,继续设置协议的 listen状态

*/

if (old_state != TCP_LISTEN) {

err = tcp_listen_start(sk);

if (err)

goto out;

}

sk->sk_max_ack_backlog = backlog;

err = 0;

out:

release_sock(sk);

return err;

Page 44: Linux TCPIP协议栈之Socket的实现分析

}

inet_listen函数在确认 sock->state和 sk->sk_state状态后,会进一步调用

tcp_listen_start函数,并且最后设置 sk_max_ack_backlog 。

tcp的 tcp_listen_start函数,完成两个重要的功能,一个是初始化 sk的一些相

关成员变量,另一方面是切换有限状态机的状态。sk_max_ack_backlog表示监听时最大

的 backlog 数量,它由用户空间传递的参数决定。而 sk_ack_backlog 表示当前的的

backlog数量。

当 tcp服务器收到一个 syn报文时,它表示了一个连接请求的到达。内核使用了一个

hash表来维护这个连接请求表:

struct tcp_listen_opt

{

u8 max_qlen_log; /* log_2 of maximal

queued SYNs */

int qlen;

int qlen_young;

int clock_hand;

u32 hash_rnd;

struct open_request *syn_table[TCP_SYNQ_HSIZE];

};

syn_table, 是 open_request 结构,就是连接请求表,表中的最大项,也就是最

大允许的 syn 报文的数量,由 max_qlen_log 来决定。当套接字进入 listen 状态,也

就是说可以接收 syn报文了,那么在此之前,需要先初始化这个表:

int tcp_listen_start(struct sock *sk)

{

struct inet_sock *inet = inet_sk(sk); //获取 inet

结构指针

struct tcp_sock *tp = tcp_sk(sk); //获取协议指针

struct tcp_listen_opt *lopt;

//初始化 sk相关成员变量

sk->sk_max_ack_backlog = 0;

sk->sk_ack_backlog = 0;

tp->accept_queue = tp->accept_queue_tail = NULL;

rwlock_init(&tp->syn_wait_lock);

tcp_delack_init(tp);

//初始化连接请求 hash表

lopt = kmalloc(sizeof(struct tcp_listen_opt), GFP_KERNEL);

if (!lopt)

return -ENOMEM;

Page 45: Linux TCPIP协议栈之Socket的实现分析

memset(lopt, 0, sizeof(struct tcp_listen_opt));

//初始化 hash表容量,最小为 6,其实际值由 sysctl_max_syn_backlog决

for (lopt->max_qlen_log = 6; ; lopt->max_qlen_log++)

if ((1 << lopt->max_qlen_log) >=

sysctl_max_syn_backlog)

break;

get_random_bytes(&lopt->hash_rnd, 4);

write_lock_bh(&tp->syn_wait_lock);

tp->listen_opt = lopt;

write_unlock_bh(&tp->syn_wait_lock);

/* There is race window here: we announce ourselves listening,

* but this transition is still not validated by get_port().

* It is OK, because this socket enters to hash table only

* after validation is complete.

*/

/* 修改状态机状态,表示进入 listen 状态,根据作者注释,当宣告自己进入

listening 状态后,但是这个状态转换并没有得到 get_port 的确认。所以需要调用

get_port()函数。但是对于一点,暂时还没有完全搞明白,只有留待后面再来分析它 */

sk->sk_state = TCP_LISTEN;

if (!sk->sk_prot->get_port(sk, inet->num)) {

inet->sport = htons(inet->num);

sk_dst_reset(sk);

sk->sk_prot->hash(sk);

return 0;

}

sk->sk_state = TCP_CLOSE;

write_lock_bh(&tp->syn_wait_lock);

tp->listen_opt = NULL;

write_unlock_bh(&tp->syn_wait_lock);

kfree(lopt);

return -EADDRINUSE;

}

在切换了有限状态机状态后,调用了

sk->sk_prot->hash(sk);

也就是 tcp_v4_hash()函数。这里涉到到另一个 hash表:TCP监听 hash表。

Page 46: Linux TCPIP协议栈之Socket的实现分析

二、 TCP 监听 hash 表

所谓 TCP 监听表,指的就内核维护“当前有哪些套接字在监听”的一个表,当一个数

据包进入 TCP栈的时候,内核查询这个表中对应的 sk,以找到相应的数据结构。(因为 sk

是面向网络栈调用的,找到了 sk,就找到了 tcp_sock,就找到了 inet_sock,就找到

了 sock,就找到了 fd„„就到了组织了)。

TCP所有的 hash表都用了 tcp_hashinfo来封装,前面分析 bind已见过它:

extern struct tcp_hashinfo {

……

/* All sockets in TCP_LISTEN state will be in here. This is

the only

* table where wildcard'd TCP sockets can exist. Hash function

here

* is just local port number.

*/

struct hlist_head __tcp_listening_hash[TCP_LHTABLE_SIZE];

……

spinlock_t __tcp_portalloc_lock;

} tcp_hashinfo;

#define tcp_listening_hash (tcp_hashinfo.__tcp_listening_hash)

函数 tcp_v4_hash将一个处理监听状态下的 sk加入至这个 hash表:

static void tcp_v4_hash(struct sock *sk)

{

if (sk->sk_state != TCP_CLOSE) {

local_bh_disable();

__tcp_v4_hash(sk, 1);

local_bh_enable();

}

}

因为__tcp_v4_hash不只用于监听 hash表,它也用于其它 hash表,其第二个参数

listen_possible为真的时候,表示处理的是监听 hash表:

static __inline__ void __tcp_v4_hash(struct sock *sk, const int

listen_possible)

{

struct hlist_head *list;

rwlock_t *lock;

BUG_TRAP(sk_unhashed(sk));

if (listen_possible && sk->sk_state == TCP_LISTEN) {

list = &tcp_listening_hash[tcp_sk_listen_hashfn(sk)];

lock = &tcp_lhash_lock;

tcp_listen_wlock();

Page 47: Linux TCPIP协议栈之Socket的实现分析

} else {

……

}

__sk_add_node(sk, list);

sock_prot_inc_use(sk->sk_prot);

write_unlock(lock);

if (listen_possible && sk->sk_state == TCP_LISTEN)

wake_up(&tcp_lhash_wait);

}

else中的部份用于另一个 hash表,暂时不管它。表很简单,如果确认是处理的是监听 hash

表。则先根据 sk计算一个 hash值,在 hash桶中找到入口。再调用__sk_add_node加

入至该 hash链。

tcp_sk_listen_hashfn()函数事实上是 tcp_lhashfn的包裹,前面已经分析过了。

__sk_add_node()函数也就是一个简单的内核 hash处理函数 hlist_add_head()的包

裹:

static __inline__ void __sk_add_node(struct sock *sk, struct

hlist_head *list)

{

hlist_add_head(&sk->sk_node, list);

}

小结

一个套接字的 listen,主要需要做的工作有以下几件:

1、初始化 sk相关的成员变量,最重要的是 listen_opt,也就是连接请求 hash表。

2、将 sk的有限状态机转换为 TCP_LISTEN,即监听状态;

3、将 sk加入监听 hash表;

4、设置允许的最大请求积压数,也就是 sk的成员 sk_max_ack_backlog的值。

第四部分 accept 接受一个连接

一、 tcp 栈的三次握手简述

进一步的分析,都是以 tcp 协议为例,因为 udp要相对简单得多,分析完 tcp,udp

的基本已经被覆盖了。

这里主要是分析 socket,但是因为它将与 tcp/udp传输层交互,所以不可避免地接

触到这一层面的代码,这里只是摘取其主要流程的一些代码片段,以更好地分析 accept

的实现过程。

当套接字进入 LISTEN 后,意味着服务器端已经可以接收来自客户端的请求。当一个

Page 48: Linux TCPIP协议栈之Socket的实现分析

syn包到达后,服务器认为它是一个 tcp 请求报文,根据 tcp协议,TCP网络栈将会自动

应答它一个 syn+ack报文,并且将它放入 syn_table这个 hash表中,静静地等待客户

端第三次握手报文的来到。一个 tcp的 syn报文进入 tcp堆栈后,会按以下函数调用,最

终进入 tcp_v4_conn_request:

tcp_v4_rcv

->tcp_v4_do_rcv

->tcp_rcv_state_process

->tp->af_specific->conn_request

tcp_ipv4.c中,tcp_v4_init_sock初始化时,有

tp->af_specific = &ipv4_specific;

struct tcp_func ipv4_specific = {

.queue_xmit = ip_queue_xmit,

.send_check = tcp_v4_send_check,

.rebuild_header = tcp_v4_rebuild_header,

.conn_request = tcp_v4_conn_request,

.syn_recv_sock = tcp_v4_syn_recv_sock,

.remember_stamp = tcp_v4_remember_stamp,

.net_header_len = sizeof(struct iphdr),

.setsockopt = ip_setsockopt,

.getsockopt = ip_getsockopt,

.addr2sockaddr = v4_addr2sockaddr,

.sockaddr_len = sizeof(struct sockaddr_in),

};

所以 af_specific->conn_request实际指向的是 tcp_v4_conn_request:

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)

{

struct open_request *req;

„„

/* 分配一个连接请求 */

req = tcp_openreq_alloc();

if (!req)

goto drop;

„„

/* 根据数据包的实际要素,如来源/目的地址等,初始化它*/

tcp_openreq_init(req, &tmp_opt, skb);

req->af.v4_req.loc_addr = daddr;

req->af.v4_req.rmt_addr = saddr;

req->af.v4_req.opt = tcp_v4_save_options(sk, skb);

req->class = &or_ipv4;

„„

/* 回送一个 syn+ack的二次握手报文 */

Page 49: Linux TCPIP协议栈之Socket的实现分析

if (tcp_v4_send_synack(sk, req, dst))

goto drop_and_free;

if (want_cookie) {

„„

} else {

/* 将连接请求 req加入连接监听表 syn_table */

tcp_v4_synq_add(sk, req);

}

return 0;

}

syn_table在前面分析的时候已经反复看到了。它的作用就是记录 syn请求报文,构

建一个 hash表。这里调用的 tcp_v4_synq_add()就完成了将请求添加进该表的操作:

static void tcp_v4_synq_add(struct sock *sk, struct open_request

*req)

{

struct tcp_sock *tp = tcp_sk(sk);

struct tcp_listen_opt *lopt = tp->listen_opt;

/* 计算一个 hash值 */

u32 h = tcp_v4_synq_hash(req->af.v4_req.rmt_addr,

req->rmt_port, lopt->hash_rnd);

req->expires = jiffies + TCP_TIMEOUT_INIT;

req->retrans = 0;

req->sk = NULL;

/*指针移到 hash链的未尾*/

req->dl_next = lopt->syn_table[h];

write_lock(&tp->syn_wait_lock);

/*加入当前节点*/

lopt->syn_table[h] = req;

write_unlock(&tp->syn_wait_lock);

tcp_synq_added(sk);

}

这样,所以的 syn请求都被放入这个表中,留待第三次 ack的到来的匹配。当第三次

ack来到后,会进入下列函数:

tcp_v4_rcv

->tcp_v4_do_rcv

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)

{

„„

if (sk->sk_state == TCP_LISTEN) {

Page 50: Linux TCPIP协议栈之Socket的实现分析

struct sock *nsk = tcp_v4_hnd_req(sk, skb);

„„

}

因为目前 sk还是 TCP_LISTEN状态,所以会进入 tcp_v4_hnd_req:

static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff

*skb)

{

struct tcphdr *th = skb->h.th;

struct iphdr *iph = skb->nh.iph;

struct tcp_sock *tp = tcp_sk(sk);

struct sock *nsk;

struct open_request **prev;

/* Find possible connection requests. */

struct open_request *req = tcp_v4_search_req(tp, &prev,

th->source,

iph->saddr,

iph->daddr);

if (req)

return tcp_check_req(sk, skb, req, prev);

„„

}

tcp_v4_search_req就是查找匹配 syn_table表:

static struct open_request *tcp_v4_search_req(struct tcp_sock *tp,

struct open_request

***prevp,

__u16 rport,

__u32 raddr, __u32 laddr)

{

struct tcp_listen_opt *lopt = tp->listen_opt;

struct open_request *req, **prev;

for (prev = &lopt->syn_table[tcp_v4_synq_hash(raddr, rport,

lopt->hash_rnd)];

(req = *prev) != NULL;

prev = &req->dl_next) {

if (req->rmt_port == rport &&

req->af.v4_req.rmt_addr == raddr &&

req->af.v4_req.loc_addr == laddr &&

TCP_INET_FAMILY(req->class->family)) {

BUG_TRAP(!req->sk);

*prevp = prev;

break;

}

}

Page 51: Linux TCPIP协议栈之Socket的实现分析

return req;

}

hash 表的查找还是比较简单的,调用 tcp_v4_synq_hash 计算出 hash 值,找到

hash链入口,遍历该链即可。

排除超时等意外因素,刚才加入 hash表的 req会被找到,这样,tcp_check_req()

函数将会被继续调用:

struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb,

struct open_request *req,

struct open_request **prev)

{

„„

tcp_acceptq_queue(sk, req, child);

„„

}

req 被找到,表明三次握手已经完成,连接已经成功建立,tcp_check_req 最终将

调用 tcp_acceptq_queue(),把这个建立好的连接加入至 tp->accept_queue队列,

等待用户调用 accept(2)来读取之。

static inline void tcp_acceptq_queue(struct sock *sk, struct

open_request *req,

struct sock *child)

{

struct tcp_sock *tp = tcp_sk(sk);

req->sk = child;

sk_acceptq_added(sk);

if (!tp->accept_queue_tail) {

tp->accept_queue = req;

} else {

tp->accept_queue_tail->dl_next = req;

}

tp->accept_queue_tail = req;

req->dl_next = NULL;

}

二、 sys_accept

如上,当 listen(2)调用准备就绪的时候,服务器可以通过调用 accept(2)接受或

等待(注意这个“或等待”是相当的重要)连接队列中的第一个请求:

int accept(int s, struct sockaddr * addr ,socklen_t *addrlen);

accept(2)调用,只是针对有连接模式。socket一旦经过 listen(2)调用进入监听

状态后,就被动地调用 accept(2),接受来自客户端的连接请求。accept(2)调用是阻塞

Page 52: Linux TCPIP协议栈之Socket的实现分析

的,也就是说如果没有连接请求到达,它会去睡觉,等到连接请求到来后(或者是超时),

才会返回。同样地,操作码 SYS_ACCEPT对应的是函数 sys_accept:

asmlinkage long sys_accept(int fd, struct sockaddr __user

*upeer_sockaddr, int __user *upeer_addrlen)

{

struct socket *sock, *newsock;

int err, len;

char address[MAX_SOCK_ADDR];

sock = sockfd_lookup(fd, &err);

if (!sock)

goto out;

err = -ENFILE;

if (!(newsock = sock_alloc()))

goto out_put;

newsock->type = sock->type;

newsock->ops = sock->ops;

err = security_socket_accept(sock, newsock);

if (err)

goto out_release;

/*

* We don't need try_module_get here, as the listening socket

(sock)

* has the protocol module (sock->ops->owner) held.

*/

__module_get(newsock->ops->owner);

err = sock->ops->accept(sock, newsock, sock->file->f_flags);

if (err < 0)

goto out_release;

if (upeer_sockaddr) {

if(newsock->ops->getname(newsock, (struct sockaddr

*)address, &len, 2)<0) {

err = -ECONNABORTED;

goto out_release;

}

err = move_addr_to_user(address, len, upeer_sockaddr,

upeer_addrlen);

if (err < 0)

Page 53: Linux TCPIP协议栈之Socket的实现分析

goto out_release;

}

/* File flags are not inherited via accept() unlike another OSes.

*/

if ((err = sock_map_fd(newsock)) < 0)

goto out_release;

security_socket_post_accept(sock, newsock);

out_put:

sockfd_put(sock);

out:

return err;

out_release:

sock_release(newsock);

goto out_put;

}

代码稍长了点,逐步来分析它。

一个 socket,经过 listen(2)设置成 server套接字后,就永远不会再与任何客户

端套接字建立连接了。因为一旦它接受了一个连接请求,就会创建出一个新的 socket,新

的 socket 用来描述新到达的连接,而原先的 server 套接字并无改变,并且还可以通过

下一次 accept(2)调用再创建一个新的出来,就像母鸡下蛋一样,“只取蛋,不杀鸡”,

server 套接字永远保持接受新的连接请求的能力。

函数先通过 sockfd_lookup(),根据 fd,找到对应的 sock,然后通过 sock_alloc

分配一个新的 sock。接着就调用协议簇的 accept()函数:

/*

Accept a pending connection. The TCP layer now gives BSD semantics.

*/

int inet_accept(struct socket *sock, struct socket *newsock, int

flags)

{

struct sock *sk1 = sock->sk;

int err = -EINVAL;

struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);

if (!sk2)

goto do_err;

lock_sock(sk2);

Page 54: Linux TCPIP协议栈之Socket的实现分析

BUG_TRAP((1 << sk2->sk_state) &

(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE));

sock_graft(sk2, newsock);

newsock->state = SS_CONNECTED;

err = 0;

release_sock(sk2);

do_err:

return err;

}

函数第一步工作是调用协议的 accept函数,然后调用 sock_graft()函数,接下来,

设置新的套接字的状态为 SS_CONNECTED。

/*

* This will accept the next outstanding connection.

*/

struct sock *tcp_accept(struct sock *sk, int flags, int *err)

{

struct tcp_sock *tp = tcp_sk(sk);

struct open_request *req;

struct sock *newsk;

int error;

lock_sock(sk);

/* We need to make sure that this socket is listening,

* and that it has something pending.

*/

error = -EINVAL;

if (sk->sk_state != TCP_LISTEN)

goto out;

/* Find already established connection */

if (!tp->accept_queue) {

long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

/* If this is a non blocking socket don't sleep */

error = -EAGAIN;

if (!timeo)

goto out;

error = wait_for_connect(sk, timeo);

if (error)

Page 55: Linux TCPIP协议栈之Socket的实现分析

goto out;

}

req = tp->accept_queue;

if ((tp->accept_queue = req->dl_next) == NULL)

tp->accept_queue_tail = NULL;

newsk = req->sk;

sk_acceptq_removed(sk);

tcp_openreq_fastfree(req);

BUG_TRAP(newsk->sk_state != TCP_SYN_RECV);

release_sock(sk);

return newsk;

out:

release_sock(sk);

*err = error;

return NULL;

}

tcp_accept()函数,当发现 tp->accept_queue准备就绪后,就直接调用

req = tp->accept_queue;

if ((tp->accept_queue = req->dl_next) == NULL)

tp->accept_queue_tail = NULL;

newsk = req->sk;

出队,并取得相应的 sk。否则,就在获取超时时间后,调用 wait_for_connect等

待连接的到来。这也是说,强调“或等待”的原因所在了。

OK,继续回到 inet_accept中来,当取得一个就绪的连接的 sk(sk2)后,先校验其

状态,再调用 sock_graft()函数。

在 sys_accept 中,已经调用了 sock_alloc,分配了一个新的 socket 结构(即

newsock),但 sock_alloc必竟不是 sock_create,它并不能为 newsock分配一个对

应的 sk。所以这个套接字并不完整。

另一方面,当一个连接到达到,根据客户端的请求,产生了一个新的 sk(即 sk2,但

这个分配过程没有深入 tcp栈去分析其实现,只分析了它对应的 req入队的代码)。呵呵,

将两者一关联,就 OK了,这就是 sock_graft的任务:

static inline void sock_graft(struct sock *sk, struct socket *parent)

{

write_lock_bh(&sk->sk_callback_lock);

sk->sk_sleep = &parent->wait;

parent->sk = sk;

sk->sk_socket = parent;

write_unlock_bh(&sk->sk_callback_lock);

Page 56: Linux TCPIP协议栈之Socket的实现分析

}

这样,一对一的联系就建立起来了。这个为 accept 分配的新的 socket 也大功告成

了。接下来将其状态切换为 SS_CONNECTED,表示已连接就绪,可以来读取数据了——如

果有的话。

顺便提一下,新的 sk的分配,是在:

]tcp_v4_rcv

->tcp_v4_do_rcv

->tcp_check_req

->tp->af_specific->syn_recv_sock(sk, skb,

req, NULL);

即 tcp_v4_syn_recv_sock 函数,其又调用 tcp_create_openreq_child()来

分配的。

struct sock *tcp_create_openreq_child(struct sock *sk, struct

open_request *req, struct sk_buff *skb)

{

/* allocate the newsk from the same slab of the master sock,

* if not, at sk_free time we'll try to free it from the wrong

* slabcache (i.e. is it TCPv4 or v6?), this is handled thru

sk->sk_prot -acme */

struct sock *newsk = sk_alloc(PF_INET, GFP_ATOMIC, sk->sk_prot,

0);

if(newsk != NULL) {

……

memcpy(newsk, sk, sizeof(struct tcp_sock));

newsk->sk_state = TCP_SYN_RECV;

……

}

等到分析 tcp栈的实现的时候,再来仔细分析它。但是这里新的 sk的有限状态机被切

换至了 TCP_SYN_RECV(按我的想法,似乎应进入 establshed才对呀,是不是哪儿看漏

了,只有看了后头的代码再来印证了)

回到 sys_accept 中来,如果调用者要求返回各户端的地址,则调用新的 sk 的

getname函数指针,也就是 inet_getname:

/*

* This does both peername and sockname.

*/

int inet_getname(struct socket *sock, struct sockaddr *uaddr,

int *uaddr_len, int peer)

{

struct sock *sk = sock->sk;

struct inet_sock *inet = inet_sk(sk);

struct sockaddr_in *sin = (struct sockaddr_in *)uaddr;

Page 57: Linux TCPIP协议栈之Socket的实现分析

sin->sin_family = AF_INET;

if (peer) {

if (!inet->dport ||

(((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT))

&&

peer == 1))

return -ENOTCONN;

sin->sin_port = inet->dport;

sin->sin_addr.s_addr = inet->daddr;

} else {

__u32 addr = inet->rcv_saddr;

if (!addr)

addr = inet->saddr;

sin->sin_port = inet->sport;

sin->sin_addr.s_addr = addr;

}

memset(sin->sin_zero, 0, sizeof(sin->sin_zero));

*uaddr_len = sizeof(*sin);

return 0;

}

函数的工作是构建珍上 struct sockaddr_in 结构出来,接着在 sys_accept中,

调用 move_addr_to_user()函数来拷贝至用户空间:

int move_addr_to_user(void *kaddr, int klen, void __user *uaddr, int

__user *ulen)

{

int err;

int len;

if((err=get_user(len, ulen)))

return err;

if(len>klen)

len=klen;

if(len<0 || len> MAX_SOCK_ADDR)

return -EINVAL;

if(len)

{

if(copy_to_user(uaddr,kaddr,len))

return -EFAULT;

}

/*

* "fromlen shall refer to the value before truncation.."

* 1003.1g

*/

return __put_user(klen, ulen);

Page 58: Linux TCPIP协议栈之Socket的实现分析

}

也就是调用 copy_to_user的过程了。

sys_accept的最后一步工作,是将新的 socket结构,与文件系统挂钩:

if ((err = sock_map_fd(newsock)) < 0)

goto out_release;

函数 sock_map_fd在创建 socket中已经见过了。

小结:

accept有几件事情要做

1、要 accept,需要三次握手完成,连接请求入 tp->accept_queue队列(新为客户端

分析的 sk,也在其中),其才能出队;

2、为 accept分配一个 sokcet结构,并将其与新的 sk关联;

3、如果调用时,需要获取客户端地址,即第二个参数不为 NULL,则从新的 sk中,取得其

想的地址;

4、将新的 socket结构与文件系统挂钩;

第五部分 Connect 客户端发起连接请求

一、 sys_connect

对于客户端来说,当创建了一个套接字后,就可以连接它了。

asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr,

int addrlen)

{

struct socket *sock;

char address[MAX_SOCK_ADDR];

int err;

sock = sockfd_lookup(fd, &err);

if (!sock)

goto out;

err = move_addr_to_kernel(uservaddr, addrlen, address);

if (err < 0)

goto out_put;

err = security_socket_connect(sock, (struct sockaddr *)address,

addrlen);

if (err)

goto out_put;

err = sock->ops->connect(sock, (struct sockaddr *) address,

addrlen,

Page 59: Linux TCPIP协议栈之Socket的实现分析

sock->file->f_flags);

out_put:

sockfd_put(sock);

out:

return err;

}

跟其它操作类似,sys_connect接着调用 inet_connect:

/*

* Connect to a remote host. There is regrettably still a little

* TCP 'magic' in here.

*/

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,

int addr_len, int flags)

{

struct sock *sk = sock->sk;

int err;

long timeo;

lock_sock(sk);

if (uaddr->sa_family == AF_UNSPEC) {

err = sk->sk_prot->disconnect(sk, flags);

sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;

goto out;

}

提交的协议簇不正确,则断开连接。

switch (sock->state) {

default:

err = -EINVAL;

goto out;

case SS_CONNECTED:

err = -EISCONN;

goto out;

case SS_CONNECTING:

err = -EALREADY;

/* Fall out of switch with err, set for this state */

break;

socket处于不正确的连接状态,返回相应的错误值。

case SS_UNCONNECTED:

err = -EISCONN;

if (sk->sk_state != TCP_CLOSE)

goto out;

/*调用协议的连接函数*/

Page 60: Linux TCPIP协议栈之Socket的实现分析

err = sk->sk_prot->connect(sk, uaddr, addr_len);

if (err < 0)

goto out;

/*协议方面的工作已经处理完成了,但是自己的一切工作还没有完成,

所以切换至正在连接中*/

sock->state = SS_CONNECTING;

/* Just entered SS_CONNECTING state; the only

* difference is that return value in non-blocking

* case is EINPROGRESS, rather than EALREADY.

*/

err = -EINPROGRESS;

break;

}

对于 TCP的实际的连接,是通过调用 tcp_v4_connect()函数来实现的。

二、 tcp_v4_connect 函数

对于 TCP协议来说,其连接,实际上就是发送一个 SYN报文,在服务器的应答到来时,

回答它一个 ack报文,也就是完成三次握手中的第一和第三次。

要发送 SYN 报文,也就是说,需要有完整的来源/目的地址,来源/目的端口,目的地

址/端口由用户态提交,但是问题是没有自己的地址和端口,因为并没有调用过 bind(2),

一台主机,对于端口,可以像 sys_bind()那样,从本地未用端口中动态分配一个,那地

址呢?因为一台主机可能会存在多个 IP地址,如果随机动态选择,那么有可能选择一个错

误的来源地址,将不能正确地到达目的地址。换句话说,来源地址的选择,是与路由相关的。

调用路由查找的核心函数 ip_route_output_slow(),在没有提供来源地址的情况

下,会根据实际情况,调用 inet_select_addr()函数来选择一个合适的。同时,如果路

由查找命中,会生成一个相应的路由缓存项,这个缓存项,不但对当前发送 SYN 报文有意

义,对于后续的所有数据包,都可以起到一个加速路由查找的作用。这一任务,是通过

ip_route_connect()函数完成的,它返回相应的路由缓存项(也就是说,来源地址也在

其中了):

static inline int ip_route_connect(struct rtable **rp, u32 dst,

u32 src, u32 tos, int oif, u8 protocol,

u16 sport, u16 dport, struct sock *sk)

{

struct flowi fl = { .oif = oif,

.nl_u = { .ip4_u = { .daddr = dst,

.saddr = src,

.tos = tos } },

.proto = protocol,

.uli_u = { .ports =

{ .sport = sport,

Page 61: Linux TCPIP协议栈之Socket的实现分析

.dport = dport } } };

int err;

if (!dst || !src) {

err = __ip_route_output_key(rp, &fl);

if (err)

return err;

fl.fl4_dst = (*rp)->rt_dst;

fl.fl4_src = (*rp)->rt_src;

ip_rt_put(*rp);

*rp = NULL;

}

return ip_route_output_flow(rp, &fl, sk, 0);

}

首先,构建一个搜索 key fl,在搜索要素中,来源地址/端口是不存在的。所以,当

通 过 __ip_route_output_key 进 行 查 找 时 , 第 一 次 是 不 会 命 中 缓 存 的 。

__ip_route_output_key 将继续调用 ip_route_output_slow()函数,在路由表中

搜索,并返回一个合适的来源地址,并且生成一个路由缓存项。

路由查找的更多细节,我会在另一个贴子中来分析。

/* This will initiate an outgoing connection. */

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int

addr_len)

{

struct inet_sock *inet = inet_sk(sk);

struct tcp_sock *tp = tcp_sk(sk);

struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;

struct rtable *rt;

u32 daddr, nexthop;

int tmp;

int err;

if (addr_len < sizeof(struct sockaddr_in))

return -EINVAL;

if (usin->sin_family != AF_INET)

return -EAFNOSUPPORT;

校验地址长度和协议簇。

nexthop = daddr = usin->sin_addr.s_addr;

将下一跳地址和目的地址的临时变量都暂时设为用户提交的地址。

if (inet->opt && inet->opt->srr) {

if (!daddr)

return -EINVAL;

nexthop = inet->opt->faddr;

}

Page 62: Linux TCPIP协议栈之Socket的实现分析

如果使用了来源地址路由,选择一个合适的下一跳地址。

tmp = ip_route_connect(&rt, nexthop, inet->saddr,

RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,

IPPROTO_TCP,

inet->sport, usin->sin_port, sk);

if (tmp < 0)

return tmp;

if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {

ip_rt_put(rt);

return -ENETUNREACH;

}

进行路由查找,并校验返回的路由的类型,TCP是不被允许使用多播和广播的

if (!inet->opt || !inet->opt->srr)

daddr = rt->rt_dst;。

更新目的地址临时变量——使用路由查找后返回的值。

if (!inet->saddr)

inet->saddr = rt->rt_src;

inet->rcv_saddr = inet->saddr;

如果还没有设置源地址,和本地发送地址,则使用路由中返回的值。

if (tp->rx_opt.ts_recent_stamp && inet->daddr != daddr) {

/* Reset inherited state */

tp->rx_opt.ts_recent = 0;

tp->rx_opt.ts_recent_stamp = 0;

tp->write_seq = 0;

}

if (sysctl_tcp_tw_recycle &&

!tp->rx_opt.ts_recent_stamp && rt->rt_dst == daddr) {

struct inet_peer *peer = rt_get_peer(rt);

/* VJ's idea. We save last timestamp seen from

* the destination in peer table, when entering state

TIME-WAIT

* and initialize rx_opt.ts_recent from it, when trying

new connection.

*/

if (peer && peer->tcp_ts_stamp + TCP_PAWS_MSL >=

xtime.tv_sec) {

tp->rx_opt.ts_recent_stamp =

peer->tcp_ts_stamp;

tp->rx_opt.ts_recent = peer->tcp_ts;

}

Page 63: Linux TCPIP协议栈之Socket的实现分析

}

这个更新初始状态方面的内容,还没有去分析它。

inet->dport = usin->sin_port;

inet->daddr = daddr;

保存目的地址及端口。

tp->ext_header_len = 0;

if (inet->opt)

tp->ext_header_len = inet->opt->optlen;

设置最小允许的 mss值

tp->rx_opt.mss_clamp = 536;

套接字状态被置为 TCP_SYN_SENT,

tcp_set_state(sk, TCP_SYN_SENT);

动态选择一个本地端口,并加入 hash表,与 bind(2)选择端口类似。

err = tcp_v4_hash_connect(sk);

if (err)

goto failure;

因为本地端口已经改变,使用新端口,重新查找路由,并用新的路由缓存项更新 sk中

保存的路由缓存项。

err = ip_route_newports(&rt, inet->sport, inet->dport, sk);

if (err)

goto failure;

/* OK, now commit destination to socket. */

__sk_dst_set(sk, &rt->u.dst);

tcp_v4_setup_caps(sk, &rt->u.dst);

为 TCP报文计算一个 seq值(实际使用的值是 tp->write_seq+1)。

inet->id = tp->write_seq ^ jiffies;

tcp_connect()函数用来根据 sk中的信息,构建一个完成的 syn报文,并将它发送

出去。在分析 tcp栈的实现时再来分析它。

根据 TCP协议,接下来的问题是,

1、可能收到了服务器的应答,则要回送一个 ack报文;

2、如果超时还没有应答,则使用超时重发定时器;

第六部分数据包的接收

前面了解过 sk有一个接收队列,用于存储接收到的 skb,对于 socket层面上来讲,

数据接收,就是要把数据从这个队列中取出来,交给上层用户态。这里涉及到出队操作,但

是,要了解如何出队,就得了解传输层协议如何入队。前面一直用 tcp 协议来分析,现在

还没有把整个 tcp栈分析出来,要再继续用 tcp协议来分析,就有点问题了,所以,数据

的接收和发送,都将以 udp 协议来分析。虽然它很简单,但同样也反应了 socket 层数据

与接收的全部核心内容与思路。我以希望,下一步拿下 tcp协议后,再把这部份的 tcp实

现补上来。

Page 64: Linux TCPIP协议栈之Socket的实现分析

一、 udp 层的数据接收

udp层的数据接收,对于 socket而言,就是接收队列的入队操作。在 ip层中,如果

是本地数据,则会交由 ip_local_deliver_finish()函数处理,它会根据传输层协议

的类型,交由相应的处理函数,对于 udp协议而言,就是 udp_rcv():

/*

* All we need to do is get the socket, and then do a checksum.

*/

int udp_rcv(struct sk_buff *skb)

{

struct sock *sk;

struct udphdr *uh;

unsigned short ulen;

struct rtable *rt = (struct rtable*)skb->dst;

u32 saddr = skb->nh.iph->saddr;

u32 daddr = skb->nh.iph->daddr;

int len = skb->len;

/*

* 数据包至少应有 UDP首部长度.

*/

if (!pskb_may_pull(skb, sizeof(struct udphdr)))

goto no_header;

/*获取 udp首部指针*/

uh = skb->h.uh;

/* 数据长度,含首部长度 */

ulen = ntohs(uh->len);

/* 数据包长度不够:UDP 长度比 skb 长度小,意味着数据的丢失,而 udp 长度

比 udp首部还要小,好像这个不太可能,除非封包出错 ̂ o^*/

if (ulen > len || ulen < sizeof(*uh))

goto short_packet;

/* 截去 udp报文后面的多余报文 */

if (pskb_trim(skb, ulen))

goto short_packet;

/* 开始 udp校验和计算,主要查依赖于 skb的 ip_summumed字段的设置来决

定是否需要进行校验和计算 */

if (udp_checksum_init(skb, uh, ulen, saddr, daddr) < 0)

Page 65: Linux TCPIP协议栈之Socket的实现分析

goto csum_error;

/* 转换多播或广播处理例程 */

if(rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))

return udp_v4_mcast_deliver(skb, uh, saddr, daddr);

/* 查找数据段对应的 socket结构的 sk */

sk = udp_v4_lookup(saddr, uh->source, daddr, uh->dest,

skb->dev->ifindex);

if (sk != NULL) {

/* 找到了,数据包进入 UDP的 socket的接收队列*/

int ret = udp_queue_rcv_skb(sk, skb);

sock_put(sk);

/* a return value > 0 means to resubmit the input, but

* it it wants the return to be -protocol, or 0

*/

if (ret > 0)

return -ret;

return 0;

}

if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))

goto drop;

/* 没有对应的 socket. 如果校验和错误,则丢弃它 */

if (udp_checksum_complete(skb))

goto csum_error;

/* 发送一个目的不可达报文 */

UDP_INC_STATS_BH(UDP_MIB_NOPORTS);

icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

/*

* Hmm. We got an UDP packet to a port to which we

* don't wanna listen. Ignore it.

*/

kfree_skb(skb);

return(0);

short_packet:

NETDEBUG(if (net_ratelimit())

printk(KERN_DEBUG "UDP: short packet:

Page 66: Linux TCPIP协议栈之Socket的实现分析

From %u.%u.%u.%u:%u %d/%d to %u.%u.%u.%u:%u\n",

NIPQUAD(saddr),

ntohs(uh->source),

ulen,

len,

NIPQUAD(daddr),

ntohs(uh->dest)));

no_header:

UDP_INC_STATS_BH(UDP_MIB_INERRORS);

kfree_skb(skb);

return(0);

csum_error:

/*

* RFC1122: OK. Discards the bad packet silently (as far as

* the network is concerned, anyway) as per 4.1.3.4 (MUST).

*/

NETDEBUG(if (net_ratelimit())

printk(KERN_DEBUG "UDP: bad checksum.

From %d.%d.%d.%d:%d to %d.%d.%d.%d:%d ulen %d\n",

NIPQUAD(saddr),

ntohs(uh->source),

NIPQUAD(daddr),

ntohs(uh->dest),

ulen));

drop:

UDP_INC_STATS_BH(UDP_MIB_INERRORS);

kfree_skb(skb);

return(0);

}

函数的核心思想,是根据 skb,查找到与之对应的 sk,调用 udp_v4_lookup()函数

实现——udp与 tcp一样,socket有一个 hash表,这个查找,就是查找 hash表的过程。

如果找到了对应的 sk,则进入 udp_queue_rcv_skb()函数:

/* returns:

* -1: error

* 0: success

* >0: "udp encap" protocol resubmission

*

* Note that in the success and error cases, the skb is assumed to

* have either been requeued or freed.

*/

static int udp_queue_rcv_skb(struct sock * sk, struct sk_buff *skb)

{

Page 67: Linux TCPIP协议栈之Socket的实现分析

/* 获取 sk对应的 udp_sock结构指针 */

struct udp_sock *up = udp_sk(sk);

/*

* Charge it to the socket, dropping if the queue is full.

*/

if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb)) {

kfree_skb(skb);

return -1;

}

if (up->encap_type) {

/*

* This is an encapsulation socket, so let's see if this

is

* an encapsulated packet.

* If it's a keepalive packet, then just eat it.

* If it's an encapsulateed packet, then pass it to the

* IPsec xfrm input and return the response

* appropriately. Otherwise, just fall through and

* pass this up the UDP socket.

*/

int ret;

ret = udp_encap_rcv(sk, skb);

if (ret == 0) {

/* Eat the packet .. */

kfree_skb(skb);

return 0;

}

if (ret < 0) {

/* process the ESP packet */

ret = xfrm4_rcv_encap(skb, up->encap_type);

UDP_INC_STATS_BH(UDP_MIB_INDATAGRAMS);

return -ret;

}

/* FALLTHROUGH -- it's a UDP Packet */

}

/* 如果需要校验 */

if (sk->sk_filter && skb->ip_summed != CHECKSUM_UNNECESSARY)

{

/* 那就校验它吧 */

if (__udp_checksum_complete(skb)) {

/* 结果校验出错,那就算了吧 */

Page 68: Linux TCPIP协议栈之Socket的实现分析

UDP_INC_STATS_BH(UDP_MIB_INERRORS);

kfree_skb(skb);

return -1;

}

/* 已经校验过了,就设置不用再校验了 */

skb->ip_summed = CHECKSUM_UNNECESSARY;

}

/* 设用 sock_queue_rcv_skb入队 */

if (sock_queue_rcv_skb(sk,skb)<0) {

UDP_INC_STATS_BH(UDP_MIB_INERRORS);

kfree_skb(skb);

return -1;

}

UDP_INC_STATS_BH(UDP_MIB_INDATAGRAMS);

return 0;

}

encap_type字段用于判断 udp包,是否是一个 IPSEC协议的封装报文,这里不关分

ipsec,所以接下来的工作,就是校验和计算,然后紧跟着调用 sock_queue_rcv_skb():

static inline int sock_queue_rcv_skb(struct sock *sk, struct sk_buff

*skb)

{

int err = 0;

int skb_len;

/* Cast skb->rcvbuf to unsigned... It's pointless, but reduces

number of warnings when compiling with -W --ANK

*/

if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=

(unsigned)sk->sk_rcvbuf) {

err = -ENOMEM;

goto out;

}

/* 进入 socket层的包过滤 */

err = sk_filter(sk, skb, 1);

if (err)

goto out;

/* 设置 skb的一些必要的指针和计数器变量

dev:关连备设指针;

sk:所对应的 sk结构;

destructor:函数指针可以初始化成一个在缓冲区释放时完成某些动作的函数。

如果 skb 不属于一个 socket,则其常为 NULL。反之,这个函数会被赋值为

Page 69: Linux TCPIP协议栈之Socket的实现分析

sock_rfree或 sock_wfree.

用于更新 socket的队列中的内存容量。*/

skb->dev = NULL;

skb_set_owner_r(skb, sk);

/* 在 skb入队之前,保存其长度至临时变量 skb_len,这是因为一旦 skb入队

后,它将被

其它线程处理,skb命运未知。。。。。。,而 len值后面还会用到。

*/

skb_len = skb->len;

/* 将 skb加入 sk的接收队列 */

skb_queue_tail(&sk->sk_receive_queue, skb);

/* 唤醒 socket上的接收线程? */

if (!sock_flag(sk, SOCK_DEAD))

sk->sk_data_ready(sk, skb_len);

out:

return err;

}

对 于 udp 而言,直接调 用 skb_queue_tail(), 将 skb 加入至 sk 的

sk_receive_queue队列即可。

二、 udp 层的数据出队操作

了解了数据包如何被加入队列,上层 socket 的数据接收,就是从这个队列中来取数

据。udp 对应的取数据的函数 udp_recvmsg(),后面再来看它是如何被调用的,现在先

来分析它的实现:

/*

* This should be easy, if there is something there we

* return it, otherwise we block.

*/

static int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct

msghdr *msg,

size_t len, int noblock, int flags, int *addr_len)

{

struct inet_sock *inet = inet_sk(sk); /* 取得 sk

对应的 inet_sock指针 */

struct sockaddr_in *sin = (struct sockaddr_in

*)msg->msg_name;

struct sk_buff *skb;

int copied, err;

Page 70: Linux TCPIP协议栈之Socket的实现分析

/*

* 校验地址长度

*/

if (addr_len)

*addr_len=sizeof(*sin);

/* 如果 sk队列中有错误的信息,则设用 ip_recv_error函数 */

if (flags & MSG_ERRQUEUE)

return ip_recv_error(sk, msg, len);

try_again:

/* 出队 */

skb = skb_recv_datagram(sk, flags, noblock, &err);

if (!skb)

goto out;

/* 需要拷贝的数据不包含 UDP首部 */

copied = skb->len - sizeof(struct udphdr);

/* 如果用户提供的缓存不够,则设置为只拷贝用户需要的长度,并设置截短数

据标志 */

if (copied > len) {

copied = len;

msg->msg_flags |= MSG_TRUNC;

}

if (skb->ip_summed==CHECKSUM_UNNECESSARY) {

/* 拷贝数据至用户空间 */

err = skb_copy_datagram_iovec(skb, sizeof(struct

udphdr), msg->msg_iov,

copied);

} else if (msg->msg_flags&MSG_TRUNC) {

if (__udp_checksum_complete(skb))

goto csum_copy_err;

err = skb_copy_datagram_iovec(skb, sizeof(struct

udphdr), msg->msg_iov,

copied);

} else {

err = skb_copy_and_csum_datagram_iovec(skb,

sizeof(struct udphdr), msg->msg_iov);

if (err == -EINVAL)

goto csum_copy_err;

}

if (err)

goto out_free;

Page 71: Linux TCPIP协议栈之Socket的实现分析

/* 更新 sk 的接收时间戳,根据 SOCK_RCVTSTAMP 标志的设置来决定:选择当

前时间还是 skb buffer的接收时间 */

sock_recv_timestamp(msg, sk, skb);

/* 拷贝地址等信息. */

if (sin)

{

sin->sin_family = AF_INET;

sin->sin_port = skb->h.uh->source;

sin->sin_addr.s_addr = skb->nh.iph->saddr;

memset(sin->sin_zero, 0, sizeof(sin->sin_zero));

}

/* 查看是否设置了一些控制标志,查看某些 IP socket选项是否被设置,例

如,设置了 IP_TOS socket选项,把 IP首部中的

tos字段拷贝至用户空间等等,这个工作是由 ip_cmsg_recv函数完成的 */

if (inet->cmsg_flags)

ip_cmsg_recv(msg, skb);

/* 设置拷贝的字节数,如果数据段已经被截短,则返回原始实际的长度 */

err = copied;

if (flags & MSG_TRUNC)

err = skb->len - sizeof(struct udphdr);

out_free:

skb_free_datagram(sk, skb);

out:

return err;

csum_copy_err:

UDP_INC_STATS_BH(UDP_MIB_INERRORS);

/* Clear queue. */

if (flags&MSG_PEEK) {

int clear = 0;

spin_lock_bh(&sk->sk_receive_queue.lock);

if (skb == skb_peek(&sk->sk_receive_queue)) {

__skb_unlink(skb, &sk->sk_receive_queue);

clear = 1;

}

spin_unlock_bh(&sk->sk_receive_queue.lock);

if (clear)

kfree_skb(skb);

}

Page 72: Linux TCPIP协议栈之Socket的实现分析

skb_free_datagram(sk, skb);

if (noblock)

return -EAGAIN;

goto try_again;

}

skb的出队,是通过调用 skb_recv_datagram()函数,出队操作时,队列中可能没

有数据,如果是阻塞,则需要一直睡眠等待,直到超时或队列中有数据而被唤醒。另外出队

操作,还分为两种情况,一种是将数据包从队列中取中来,将其从原有队列中删除,还有一

种可能是设置了 MSG_PEEK标志,它意味着:“查看当前数据,数据将被复制到缓冲区中,

但并不从输入队列中删除”。

struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned flags,

int noblock, int *err)

{

struct sk_buff *skb;

long timeo;

/*

* Caller is allowed not to check sk->sk_err before

skb_recv_datagram()

*/

int error = sock_error(sk);

if (error)

goto no_packet;

/* 获取超时时间*/

timeo = sock_rcvtimeo(sk, noblock);

/* 这个循环将一直等到队列中有数据包,直到超时 */

do {

/* Again only user level code calls this function, so

nothing

* interrupt level will suddenly eat the receive_queue.

*

* Look at current nfs client by the way...

* However, this function was corrent in any case. 8)

*/

if (flags & MSG_PEEK) {

unsigned long cpu_flags;

/* 如果设置了 MSG_PEEK,则设用 skb_peek,这里要对接收

队列加锁的原因在于:

*/

spin_lock_irqsave(&sk->sk_receive_queue.lock,

cpu_flags);

Page 73: Linux TCPIP协议栈之Socket的实现分析

skb = skb_peek(&sk->sk_receive_queue);

if (skb)

atomic_inc(&skb->users);

spin_unlock_irqrestore(&sk->sk_receive_queue.lock,

cpu_flags);

} else

skb = skb_dequeue(&sk->sk_receive_queue);

/* 直接出队 */

/* 找到要收获的葫芦了,摘下来 */

if (skb)

return skb;

/* 如果是非阻塞,就不等了,直接跳出循环,返回 */

error = -EAGAIN;

if (!timeo)

goto no_packet;

} while (!wait_for_packet(sk, err, &timeo));

return NULL;

no_packet:

*err = error;

return NULL;

}

继续回到 udp_recvmsg 函数中来,当取出 skb 后,需要将它拷贝至用户空间。用户

空间的数据缓存,用了一个很重要的数据结构 struct msghdr来表示:

struct msghdr {

void * msg_name; /* Socket name

*/

int msg_namelen; /* Length of name

*/

struct iovec * msg_iov; /* Data blocks

*/

__kernel_size_t msg_iovlen; /* Number of blocks

*/

void * msg_control; /* Per protocol magic

(eg BSD file descriptor passing) */

__kernel_size_t msg_controllen; /* Length of cmsg

list */

unsigned msg_flags;

Page 74: Linux TCPIP协议栈之Socket的实现分析

};

结 构 中 的 msg_name 、 msg_namelen 以 及 msg_flags 分 别 对 应 于

sys_sendto()[其它接收/发送函数类似]的参数 addr、addr_len 以及 flags。指针

msg_control可以指向一个附加的数据结构,用来提供一些附加的控制信息。后面可以看

到 ip_cmsg_recv()使用了它。最重要的是,结构中的指针 msg_iov,指向一个 iovec

结构数据,而 msg_iovlen则指明了这个数组的大小:

struct iovec

{

void __user *iov_base; /* BSD uses caddr_t (1003.1g

requires void *) */

__kernel_size_t iov_len; /* Must be size_t (1003.1g) */

};

数组中的每一个元素,都是 struct iovec 结构,称之为"io 向量",由指向数据缓

冲区的指针 iov_base 和表示缓冲区中数据长度的 iov_len 构成。这样,一个 msghdr

中的数据,就由控制信息 msg_control和若干个"io向量"组成。

一个疑问是,为什么不设置为一个缓冲区,而要分为若干个缓冲区呢?一个很重要的原

因就是,每个数据报文的大小一致,对于 ip 包而言。46-1500 都是为可能的,那么如果

是一个缓冲区的话,就得定义为一个“最大值”,但是如果绝大部份的包,都小于这个最大

值,例如为 256 或 512,那么内存空间就浪费得太多了„„。所以,一个好的办法是,用

一个个小的缓冲区将数据切分,要浪费,也浪费最后一个缓冲区的空间。——这种设计跟

Unix上著名的 mbuf好像是一致的。

OK,了解了 msghdr 的结构后,再来看数据的拷贝工作,拷贝的核心函数是

memcpy_toiovec:

/*

* Copy kernel to iovec. Returns -EFAULT on error.

*

* Note: this modifies the original iovec.

*/

int memcpy_toiovec(struct iovec *iov, unsigned char *kdata, int len)

{

while (len > 0) {

if (iov->iov_len) {

int copy = min_t(unsigned int, iov->iov_len,

len);

if (copy_to_user(iov->iov_base, kdata, copy))

return -EFAULT;

kdata += copy;

len -= copy;

Page 75: Linux TCPIP协议栈之Socket的实现分析

iov->iov_len -= copy;

iov->iov_base += copy;

}

iov++;

}

return 0;

}

因为每个 io向量的缓冲区可能很小,例如 100字节,而要拷贝的数据很长,例如 1000

字节。这样,需要在一个循环里,将数据拷贝至若干个

io向量数组元素当中去。

回到 udp_recvmsg 中来,它通过设用 skb_copy_datagram_iovec()函数完成数

据的拷贝工作。来看看它是如何调用 memcpy_toiovec的,当然,类似于这样:

int skb_copy_datagram_iovec(const struct sk_buff *skb, int offset,

struct iovec *to, int len)

{

return memcpy_toiovec(to, skb->data+offset, len);

}

这样的调用,该是多么美好呀,这样的日子曾经是有过,不过已经一去不复返了。

考虑到 skb 结构的复杂性,例如分片,拷贝工作要复杂得多:

/**

* skb_copy_datagram_iovec - Copy a datagram to an iovec.

* @skb: buffer to copy

* @offset: offset in the buffer to start copying from

* @to: io vector to copy to

* @len: amount of data to copy from buffer to iovec

*

* Note: the iovec is modified during the copy.

*/

int skb_copy_datagram_iovec(const struct sk_buff *skb, int offset,

struct iovec *to, int len)

{

int start = skb_headlen(skb); /* start表示要从 skb

的哪里开始拷贝数据 */

int i, copy = start - offset; /* offset 表示调用

者指出的缓存开始拷贝的偏移字节,一般情况下,它

与 start是重叠的,表示不需要拷

贝数据包的首部 */

/* 需要拷贝首部. */

if (copy > 0) {

if (copy > len)

Page 76: Linux TCPIP协议栈之Socket的实现分析

copy = len;

if (memcpy_toiovec(to, skb->data + offset, copy))

goto fault;

if ((len -= copy) == 0)

return 0;

offset += copy;

}

/* 遍历每一个分片 */

for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {

int end;

BUG_TRAP(start <= offset + len);

end = start + skb_shinfo(skb)->frags[i].size;

if ((copy = end - offset) > 0) {

int err;

u8 *vaddr;

skb_frag_t *frag = &skb_shinfo(skb)->frags[i];

struct page *page = frag->page;

if (copy > len)

copy = len;

vaddr = kmap(page);

err = memcpy_toiovec(to, vaddr +

frag->page_offset +

offset - start, copy);

kunmap(page);

if (err)

goto fault;

if (!(len -= copy))

return 0;

offset += copy;

}

start = end;

}

if (skb_shinfo(skb)->frag_list) {

struct sk_buff *list = skb_shinfo(skb)->frag_list;

for (; list; list = list->next) {

int end;

BUG_TRAP(start <= offset + len);

Page 77: Linux TCPIP协议栈之Socket的实现分析

end = start + list->len;

if ((copy = end - offset) > 0) {

if (copy > len)

copy = len;

if (skb_copy_datagram_iovec(list,

offset - start,

to, copy))

goto fault;

if ((len -= copy) == 0)

return 0;

offset += copy;

}

start = end;

}

}

if (!len)

return 0;

fault:

return -EFAULT;

}

现在还没有分析 ip 的分片,所以,要完全理解这个函数的代码有点难度,等到 ip 分

片分析完了,再来补充完整它。

三、 socket 层的数据接收

recv(2)、recvfrom(2)和 recvmsg(3),在 sys_socketcall()中,最终都会归

于一个统一的系统调用 sock_recvmsg。例如:

asmlinkage long sys_recv(int fd, void __user * ubuf, size_t size,

unsigned flags)

{

return sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);

}

sys_recv转向了 sys_recvfrom():

/*

* Receive a frame from the socket and optionally record the

address of the

* sender. We verify the buffers are writable and if needed move

the

* sender address from kernel to user space.

*/

asmlinkage long sys_recvfrom(int fd, void __user * ubuf, size_t size,

Page 78: Linux TCPIP协议栈之Socket的实现分析

unsigned flags,

struct sockaddr __user *addr, int __user

*addr_len)

{

struct socket *sock;

struct iovec iov;

struct msghdr msg;

char address[MAX_SOCK_ADDR];

int err,err2;

sock = sockfd_lookup(fd, &err);

if (!sock)

goto out;

msg.msg_control=NULL;

msg.msg_controllen=0;

msg.msg_iovlen=1;

msg.msg_iov=&iov;

iov.iov_len=size;

iov.iov_base=ubuf;

msg.msg_name=address;

msg.msg_namelen=MAX_SOCK_ADDR;

if (sock->file->f_flags & O_NONBLOCK)

flags |= MSG_DONTWAIT;

err=sock_recvmsg(sock, &msg, size, flags);

if(err >= 0 && addr != NULL)

{

err2=move_addr_to_user(address, msg.msg_namelen, addr,

addr_len);

if(err2<0)

err=err2;

}

sockfd_put(sock);

out:

return err;

}

函数先调用 sockfd_lookup,根据 socket描述符查找以相应的 sock结构,然后封

装了一个 msghdr结构后,接着调用 sock_recvmsg()。

int sock_recvmsg(struct socket *sock, struct msghdr *msg,

size_t size, int flags)

{

struct kiocb iocb;

struct sock_iocb siocb;

Page 79: Linux TCPIP协议栈之Socket的实现分析

int ret;

init_sync_kiocb(&iocb, NULL);

iocb.private = &siocb;

ret = __sock_recvmsg(&iocb, sock, msg, size, flags);

if (-EIOCBQUEUED == ret)

ret = wait_on_sync_kiocb(&iocb);

return ret;

}

iocb和 siocb用于内核和 socket的 io控制,函数中主要初始化了 iocb,siocb

的初始化,在__sock_recvmsg中完成。

进一步的接收动作,是通过__sock_recvmsg函数设用完成的:

static inline int __sock_recvmsg(struct kiocb *iocb, struct socket

*sock,

struct msghdr *msg, size_t size, int

flags)

{

int err;

struct sock_iocb *si = kiocb_to_siocb(iocb);

si->sock = sock;

si->scm = NULL;

si->msg = msg;

si->size = size;

si->flags = flags;

err = security_socket_recvmsg(sock, msg, size, flags);

if (err)

return err;

return sock->ops->recvmsg(iocb, sock, msg, size, flags);

}

初始化完 siocb,也就是 si 后,调用协议簇的 recvmsg 函数,对于 UDP 而言,是

sock_common_recvmsg():

int sock_common_recvmsg(struct kiocb *iocb, struct socket *sock,

struct msghdr *msg, size_t size, int flags)

{

struct sock *sk = sock->sk; /*取得 sock结构对应的 sk指针*/

int addr_len = 0;

int err;

err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags &

MSG_DONTWAIT,

flags & ~MSG_DONTWAIT, &addr_len);

Page 80: Linux TCPIP协议栈之Socket的实现分析

if (err >= 0)

msg->msg_namelen = addr_len;

return err;

}

于是,udp_recvmsg就被调用了。整个过程也就结束了。

最后再回到 sys_recvfrom中来,如果用户调用时,需要返回对方的地址信息:

if(err >= 0 && addr != NULL)

{

err2=move_addr_to_user(address, msg.msg_namelen, addr,

addr_len);

if(err2<0)

err=err2;

}

就需要调用 move_addr_to_user() 函数来完成,前面分析 msghdr 结构时已经提

到其 msg_name成员变量,它包含了地址的相应信息,msg.msg_namelen成员变量,决

定了地址的长度信息:

int move_addr_to_user(void *kaddr, int klen, void __user *uaddr, int

__user *ulen)

{

int err;

int len;

if((err=get_user(len, ulen))) //缓存长度校验

return err;

if(len>klen)

len=klen;

if(len<0 || len> MAX_SOCK_ADDR)

return -EINVAL;

if(len)

{

if(copy_to_user(uaddr,kaddr,len)) //拷贝地址信息

return -EFAULT;

}

/*

* "fromlen shall refer to the value before truncation.."

* 1003.1g

*/

return __put_user(klen, ulen);

}

Page 81: Linux TCPIP协议栈之Socket的实现分析