Linux内核网络协议栈

时间:2021-08-26 02:03:28
一、注册时机 1、在内核初始化时完成; 2、内核初始化过程(init/main.c):kernel_init()->do_basic_setup()->do_initcalls()->do_one_initcall(); 3、socket文件系统注册过程(net/socket.c):core_initcall(sock_init); 1) core_initcall宏定义如下:
  1. #define core_initcall(fn) __define_initcall("1",fn,1)
  2.   
  3. #define __define_initcall(level,fn,id)\
  4.    static initcall_t __initcall_##fn##id __used \
  5.    __attribute__((__section__(".initcall" level".init")))= fn
宏定义__define_initcall(level,fn, id)对于内核的初始化很重要,他指示编译器在编译的时候,将一系列初始化函数的起始地址值按照一定的顺序放在一个section中。在内核初始化阶段,do_initcalls()将按顺序从该section中以函数指针的形式取出这些函数的起始地址,来依次完成相应的初始化。由于内核某些部分的初始化需要依赖于其他某些部分的初始化的完成,因此这个顺序排列常常很重要。该宏的作用分三部分:a) 申明一个函数指针initcall_t(即int *(void))变量__initcall_fn_id;b) 将该函数指针初始化为fn;c) 编译的时候需要把这个函数指针变量放置到名称为 ".initcall"level".init"的section中; 根据上面的解释,core_initcall(sock_init)的作用就是让编译器在编译时声明函数指针变量__initcallsock_init1,让其指向sock_init,并放到名为".initcall1.init"的section中;
二、socket文件系统注册 1、socket文件系统类型
  1. static struct file_system_type sock_fs_type = {
  2.    .name ="sockfs",
  3.    .get_sb = sockfs_get_sb,
  4.    .kill_sb = kill_anon_super,
  5. };
其中,get_sb函数指针定义了如何获取该文件系统的超级块,而kill_sb函数指针定义了如何删除该超级块; 2、sock_init主要逻辑 函数的主要代码如下:
  1. static int __init sock_init(void){
  2.    init_inodecache();
  3.    register_filesystem(&sock_fs_type);
  4.    sock_mnt = kern_mount(&sock_fs_type);
  5.    return 0;
  6. }
1) init_inodecache():创建一块用于socket相关的inode的调整缓存;后面创建inode、释放inode会使用到; 2) register_filesystem():将socket文件系统注册到内核中; 在内核中,所有的文件系统保存在全局变量file_systems中,如下: static struct file_system_type *file_systems; 不同的文件系统类型通过结构体的next字段形成一个单向链表; 这样,注册文件系统实质是将新的结构体插入到单向链表中的过程; 3) kern_mount():在内核中安装文件系统,并建立安装点; 在安装的过程中,会初始化该安装点的超级块,此时会将该超级块的操作函数指针记录下来;如:
  1. static int sockfs_get_sb(struct file_system_type*fs_type,
  2.              int flags,const char *dev_name, void*data,
  3.              struct vfsmount *mnt)
  4. {
  5.     return get_sb_pseudo(fs_type,"socket:", &sockfs_ops, SOCKFS_MAGIC,
  6.                  mnt);
  7. }
其中sockfs_ops结构变量如下:
  1. static struct super_operations sockfs_ops = {
  2.     .alloc_inode = sock_alloc_inode,
  3.     .destroy_inode =sock_destroy_inode,
  4.     .statfs = simple_statfs,
  5. };
该操作函数结构体定义了如何分配inode,如何销毁inode等;

一、socket()库函数到系统调用,再到内核1、Linux运行的C库是glibc;2、socket()调用如下:1) socket()->__socket():glibc-2.3.6/sysdept/generic/socket.c (weak_alias(name1, name2))2) __socket():glibc-2.3.6/sysdept/unix/sysv/linux/i386/socket.S3) ENTER_KERNEL: 
  1. movl $SYS_ify(socketcall), %eax /* System call number in %eax. */

  2. /* Use ## so `socket' is a separate token that might be #define'd. */
  3. movl $P(SOCKOP_,socket), %ebx /* Subcode is first arg to syscall. */
  4. lea 4(%esp), %ecx /* Address of args is 2nd arg. */

  5. /* Do the system call trap. */
  6. ENTER_KERNEL
这里,SYS_ify宏定义为:glibc-2.3.6/sysdept/unix/sysv/linux/i386/Sysdept.h 
  1. #define SYS_ify(syscall_name) __NR_##syscall_name;
P宏定义为:glibc-2.3.6/sysdept/unix/sysv/linux/i386/socket.S  
  1. #define P(a, b) P2(a, b)
  2. #define P2(a, b) a##b
其中,##为连接符号;  
  1. #define __NR_socketcall 102
  2. SOCKOP_socket:glibc-2.3.6/sysdept/unix/sysv/linux/Socketcall.h
  3.  
  4. #define SOCKOP_socket 1
因此,中断号是102,子中断号是1;4) int 0x80进入内核:glibc-2.3.6/sysdept/unix/sysv/linux/i386/Sysdept.h:
  1. # define ENTER_KERNEL int $0x80
5) system_call中断入口:kernel/arch/x86/kernel/entry_32.S:  
  1. syscall_call:
  2.     call *sys_call_table(,%eax,4)
6) 进入中断向量表:kernel/arch/x86/kernel/syscall_table_32.S中的102号中断:  
  1. .long sys_socketcall
7) 进入sys_socketcall()函数,根据子中断号以决定走哪个分支:kernel/net/Socket.c:   
  1. switch (call){
  2.     case SYS_SOCKET:
  3.         break;
  4.     case SYS_BIND:
  5.         …...
二、socket其他库函数(bind, accept...)1、对于其他库函数,都是引用上面提到的glibc-2.3.6/sysdept/unix/sysv/linux/i386/socket.S来实现的,如a) bind.S:
  1. #define socket bind
  2. #define NARGS 3
  3. #define NO_WEAK_ALIAS 1
  4. #include <socket.S>
  5. weak_alias (bind, __bind)
b) accept.S:   
  1. #define socket accept
  2. #define __socket __libc_accept
  3. #define NARGS 3
  4. #define NEED_CANCELLATION
  5. #include <socket.S>
  6. libc_hidden_def (accept)
在各个库函数调用中,设置不同的参数,如socket(用于设置子中断号), NARGS(系统调用的参数个数)等,最终由
  1. movl $P(SOCKOP_,socket), %ebx /* Subcode is first arg to syscall. */
来生成最终的子中断号,然后放到ebx寄存器中;2、所有socket系统调用的子中断号参见glibc-2.3.6/sysdept/unix/sysv/linux/Socketcall.h:    
  1. #define SOCKOP_socket 1
  2. #define SOCKOP_bind 2
  3. #define SOCKOP_connect 3
  4. #define SOCKOP_listen 4
  5. #define SOCKOP_accept 5
  6. #define SOCKOP_getsockname 6
  7. #define SOCKOP_getpeername 7
  8. #define SOCKOP_socketpair 8
  9. #define SOCKOP_send 9
  10. #define SOCKOP_recv 10
  11. #define SOCKOP_sendto 11
  12. #define SOCKOP_recvfrom 12
  13. #define SOCKOP_shutdown 13
  14. #define SOCKOP_setsockopt 14
  15. #define SOCKOP_getsockopt 15
  16. #define SOCKOP_sendmsg 16
  17. #define SOCKOP_recvmsg 17

1、示例及函数入口:
1) 示例代码如下:

  1. int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);  

2) 入口:
net/Socket.c:sys_socketcall(),根据子系统调用号,创建socket会执行sys_socket()函数;

2、分配socket结构:
1) 调用链:
net/Socket.c:sys_socket()->sock_create()->__sock_create()->sock_alloc();

2) 在socket文件系统中创建i节点:

  1. inode = new_inode(sock_mnt->mnt_sb);  
其中,sock_mnt为socket文件系统的根节点,是在内核初始化安装socket文件系统时赋值的,mnt_sb是该文件系统安装点的超级块对象的指针;
这里,new_inode函数是文件系统的通用函数,其作用是在相应的文件系统中创建一个inode;其主要代码如下(fs/Inode.c):

  1. struct inode *new_inode(struct super_block *sb) {  
  2.     struct inode * inode;  
  3.     inode = alloc_inode(sb);  
  4.     …...  
  5.     return inode;  
  6. }  
这里调用了alloc_inode函数分配inode结构(fs/Inode.c):

  1. static struct inode *alloc_inode(struct super_block *sb) {  
  2.     struct inode *inode;  
  3.   
  4.     if (sb->s_op->alloc_inode)  
  5.         inode = sb->s_op->alloc_inode(sb);  
  6.     else  
  7.         inode = (struct inode *) kmem_cache_alloc(inode_cachep, GFP_KERNEL);  
  8.     …...  
  9. }  
上面有个条件判断:if (sb->s_op->alloc_inode),意思是说如果当前文件系统的超级块有自己分配inode的操作函数,则调用它自己的函数分配inode,否则从公用的高速缓存区中分配一块inode;

3) 创建socket专用inode:
socket文件系统注册一文中后面提到,在安装socket文件系统时,会初始化该文件系统的超级块,此时会初始化超级块的操作指针s_op为sockfs_ops结构;因此此时分配inode会调用sock_alloc_inode函数来完成:

  1. static struct inode *sock_alloc_inode(struct super_block *sb) {  
  2.     struct socket_alloc *ei;  
  3.     ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);  
  4.     …...  
  5.     return &ei->vfs_inode;  
  6. }  
从这里可以看到,实际上分配了一个socket_alloc结构体,该结构体包含socket和inode:

  1. struct socket_alloc {  
  2.     struct socket socket;  
  3.     struct inode vfs_inode;  
  4. };  
但最终返回的是该结构体中的inode成员;至此,socket结构和inode结构均分配完毕;分配inode后,应用程序便可以通过文件描述符对socket进行read()/write()之类的操作,这个是由虚拟文件系统(VFS)来完成的。

3、根据inode取得socket对象:
由于创建inode是文件系统的通用逻辑,因此其返回值是inode对象的指针;但这里在创建socket的inode后,需要根据inode得到socket对象;内联函数SOCKET_I由此而来:

  1. static inline struct socket *SOCKET_I(struct inode *inode)  
  2. {  
  3.     return &container_of(inode, struct socket_alloc, vfs_inode)->socket;  
  4. }  
再看看container_of宏(include/linux/Kernel.h):

  1. #define container_of(ptr, type, member) ({          \  
  2.     const typeof( ((type *)0)->member ) *__mptr = (ptr); \  
  3.     (type *)( (char *)__mptr - offsetof(type,member) );})  
和offsetof宏(include/linux/Stddef.h):

  1. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  

1) offerset(TYPE, MEMBER)宏的作用:返回MEMBER成员在结构体TYPE中的偏移量;
先看一下例子,假设有个结构体A如下:

  1. struct struct_A {  
  2.     char a;  
  3.     int b;  
  4. }  
其中,成员a相对于结构的偏移量为0,成员b相对于结构体的偏移量为1;结构体struct_A的变量m在内存中地址结构如下:
Linux内核网络协议栈
我们再来看offset宏:

  1. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  
可以这样来理解,把0地址强制转化为TYPE结构的指针,然后再拿到MEMBER成员的地址,该地址正好等于MEMBER成员在结构体TYPE中的偏移量;
还是拿上面的例子来说吧,如下图,offset(struct_A, b)的值为1,正好等于其偏移量;
如下图所示:
Linux内核网络协议栈

2) container_of(ptr, type, member)宏的作用:返回ptr指针所在的结构体;其中ptr为结体体type的变量中member成员的指针;
再来看看它的实现:

  1. #define container_of(ptr, type, member) ({          \  
  2.     const typeof( ((type *)0)->member ) *__mptr = (ptr); \  
  3.     (type *)( (char *)__mptr - offsetof(type,member) );})  
将ptr指针转化为char *,然后减去其在结构体中的偏移量,得到的是ptr所在的结构体的地址,最后强制转换成type *;

回到sock_alloc函数,SOCKET_I根据inode取得socket变量后,记录当前进程的一些信息,如fsuid, fsgid,并增加sockets_in_use的值(该变量表示创建socket的个数);创建后socket变量后,在__sock_create()函数中设置其type为应用程序传递下来的type,上面的例子中即为SOCK_STREAM;

4、使用协议族来初始化socket:
1) 协议族的概念:
协议族是由多个协议组成的一个通信协议栈,如我们最熟悉的TCP/IP(AF_INET因特网协议族)包括TCP,IP,ICMP,ARP等协议;

2) Linux支持的协议族:

Linux2.6.26中支持33个协议域,在net/Socket.c中定义全局变量:


  1. static const struct net_proto_family *net_families[NPROTO] __read_mostly;  
在/include/linux/socket.h中定义了每个协议域的宏,每个协议域占用该数组的一项,如AF_INET占用net_families[2],如果net_families[2]=null,则说明当前内核没有注册AF_INET模块;

3) 注册AF_INET协议域:

 

socket文件系统注册中提到系统初始化的工作,AF_INET的注册也正是通过这个来完成的;

初始化入口net/ipv4/Af_inet.c:


  1. fs_initcall(inet_init);  
  2. static int __init inet_init(void) {  
  3.     …...  
  4.     // 为不同的套接字分配高速缓冲区  
  5.     rc = proto_register(&tcp_prot, 1);  
  6.     rc = proto_register(&udp_prot, 1);  
  7.     rc = proto_register(&raw_prot, 1);  
  8.     …...  
  9.     (void)sock_register(&inet_family_ops);  
  10.     …...  
  11.     /* 将所有的socket类型按type通过inetsw管理起来 */  
  12.     for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)  
  13.         INIT_LIST_HEAD(r);  
  14.   
  15.     for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)  
  16.         inet_register_protosw(q);  
  17.     …...  
  18. }  

 

这里调用sock_register函数来完成注册:

 

  1. int sock_register(const struct net_proto_family *ops) {  
  2.     int err;  
  3.     …...  
  4.     if (net_families[ops->family])  
  5.         err = -EEXIST;  
  6.     else {  
  7.         net_families[ops->family] = ops;  
  8.         err = 0;  
  9.     }  
  10.     …...  
  11. }  
根据family将AF_INET协议域inet_family_ops注册到内核中的net_families数组中;下面是其定义:

  1. static struct net_proto_family inet_family_ops = {  
  2.     .family = PF_INET,   
  3.     .create = inet_create,  
  4.     .owner  = THIS_MODULE,  
  5. };  
其中,family指定协议域的类型,create指向相应协议域的socket的创建函数;

4) 套接字类型

在相同的协议域下,可能会存在多个套接字类型;如AF_INET域下存在流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW),在这三种类型的套接字上建立的协议分别是TCP, UDP,ICMP/IGMP等。

在Linux内核中,结构体struct proto表示域中的一个套接字类型,它提供该类型套接字上的所有操作及相关数据(在内核初始化时会分配相应的高速缓冲区,见上面提到的inet_init函数)。

AF_IENT域的这三种套接字类型定义用结构体inet_protosw(net/ipv4/Af_inet.c)来表示,如下:


  1. static struct inet_protosw inetsw_array[] =  
  2. {  
  3.     {  
  4.         .type =       SOCK_STREAM,  
  5.         .protocol =   IPPROTO_TCP,  
  6.         .prot =       &tcp_prot,  
  7.         .ops =        &inet_stream_ops,  
  8.         .capability = -1,  
  9.         .no_check =   0,  
  10.         .flags =      INET_PROTOSW_PERMANENT |  
  11.                   INET_PROTOSW_ICSK,  
  12.     },  
  13.   
  14.     {  
  15.         .type =       SOCK_DGRAM,  
  16.         .protocol =   IPPROTO_UDP,  
  17.         .prot =       &udp_prot,  
  18.         .ops =        &inet_dgram_ops,  
  19.         .capability = -1,  
  20.         .no_check =   UDP_CSUM_DEFAULT,  
  21.         .flags =      INET_PROTOSW_PERMANENT,  
  22.        },  
  23.   
  24.   
  25.        {  
  26.            .type =       SOCK_RAW,  
  27.            .protocol =   IPPROTO_IP,    /* wild card */  
  28.            .prot =       &raw_prot,  
  29.            .ops =        &inet_sockraw_ops,  
  30.            .capability = CAP_NET_RAW,  
  31.            .no_check =   UDP_CSUM_DEFAULT,  
  32.            .flags =      INET_PROTOSW_REUSE,  
  33.        }  
  34. };  
其中,tcp_prot(net/ipv4/Tcp_ipv4.c)、udp_prot(net/ipv4/Udp.c)、raw_prot(net/ipv4/Raw.c)分别表示三种类型的套接字,分别表示相应套接字的操作和相关数据;ops成员提供该协议域的全部操作集合,针对三种不同的套接字类型,有三种不同的域操作inet_stream_ops、inet_dgram_ops、inet_sockraw_ops,其定义均位于net/ipv4/Af_inet.c下;

内核初始化时,在inet_init中,会将不同的套接字存放到全局变量inetsw中统一管理;inetsw是一个链表数组,每一项都是一个struct inet_protosw结构体的链表,总共有SOCK_MAX项,在inet_init函数对AF_INET域进行初始化的时候,调用函数inet_register_protosw把数组inetsw_array中定义的套接字类型全部注册到inetsw数组中;其中相同套接字类型,不同协议类型的套接字通过链表存放在到inetsw数组中,以套接字类型为索引,在系统实际使用的时候,只使用inetsw,而不使用inetsw_array;


5) 使用协议域来初始化socket

 

了解了上面的知识后,我们再回到net/Socket.c:sys_socket()->sock_create()->__sock_create()中:


  1. const struct net_proto_family *pf;  
  2. …...  
  3. pf = rcu_dereference(net_families[family]);  
  4. err = pf->create(net, sock, protocol);  
上面的代码中,找到内核初始化时注册的协议域,然后调用其create方法;

5、分配sock结构:

本文中的例子会调用inet_family_ops.create方法即inet_create方法完成socket的创建工作;其调用链如下:

net/Socket.c:sys_socket()->sock_create()->__sock_create()->net/ipv4/Af_inet.c:inet_create();

inet_create()主要完成以下几个工作:

1) 设置socket的状态为SS_UNCONNECTED;


  1. sock->state = SS_UNCONNECTED;  

 


2) 根据socket的type找到对应的套接字类型:


  1. list_for_each_rcu(p, &inetsw[sock->type]) {  
  2.     answer = list_entry(p, struct inet_protosw, list);  
  3.   
  4.     /* Check the non-wild match. */  
  5.     if (protocol == answer->protocol) {  
  6.         if (protocol != IPPROTO_IP)  
  7.             break;  
  8.     } else {  
  9.         /* Check for the two wild cases. */  
  10.         if (IPPROTO_IP == protocol) {  
  11.             protocol = answer->protocol;  
  12.             break;  
  13.         }  
  14.         if (IPPROTO_IP == answer->protocol)  
  15.             break;  
  16.     }  
  17.     err = -EPROTONOSUPPORT;  
  18.     answer = NULL;  
  19. }  
由于同一type不同protocol的套接字保存在inetsw中的同一链表中,因此需要遍历链表来查找;在上面的例子中,会将protocol重新赋值为answer->protocol,即IPPROTO_TCP,其值为6;

3) 使用匹配的协议族操作集初始化socket;

  1. sock->ops = answer->ops;  
  2. answer_prot = answer->prot;// 供后面使用  
结合例子,sock变量的ops指向inet_stream_ops结构体变量;

4) 分配sock结构体变量net/Socket.c:sys_socket()->sock_create()->__sock_create()->net/ipv4/Af_inet.c:inet_create()->net/core/Sock.c:sk_alloc():

  1. sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);  
其中,answer_prot指向tcp_prot结构体变量;

  1. struct sock *sk_alloc(struct net *net, int family, gfp_t priority, struct proto *prot) {  
  2.     struct sock *sk;  
  3.   
  4.     sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);  
  5.     if (sk) {  
  6.         sk->sk_family = family;  
  7.   
  8.         sk->sk_prot = sk->sk_prot_creator = prot;  
  9.         sock_lock_init(sk);  
  10.         sock_net_set(sk, get_net(net));  
  11.     }  
  12.   
  13.     return sk;  
  14. }  
其中,sk_prot_alloc分配sock结构体变量;由于在inet_init中为不同的套接字分配了高速缓冲区,因此该sock结构体变量会在该缓冲区中分配空间;分配完成后,对其做一些初始化工作:
i) 初始化sk变量的sk_prot和sk_prot_creator;
ii) 初始化sk变量的等待队列;
iii) 设置net空间结构,并增加引用计数;

6、建立socket结构与sock结构的关系:
1) socket, sock, inet_sock, tcp_sock的关系
创建完sk变量后,回到inet_create函数中:

  1. inet = inet_sk(sk);  
  2. static inline struct inet_sock *inet_sk(const struct sock *sk)  
  3. {  
  4.     return (struct inet_sock *)sk;  
  5. }  
这里是根据sk变量得到inet_sock变量的地址;细心的同学可能会问到:inet_sock是什么?之前分配的是sock变量,与inet_sock有什么关系啊?
a. struct socket:这个是基本的BSD socket,应用程序通过系统调用开始创建的socket都是该结构体,它是基于虚拟文件系统创建出来的;
类型主要有三种,即流式、数据报、原始套接字协议;
其状态比较粗粒度,如下:

  1. typedef enum {  
  2.     SS_FREE = 0,            /* not allocated        */  
  3.     SS_UNCONNECTED,         /* unconnected to any socket    */  
  4.     SS_CONNECTING,          /* in process of connecting */  
  5.     SS_CONNECTED,           /* connected to socket      */  
  6.     SS_DISCONNECTING        /* in process of disconnecting  */  
  7. } socket_state;  
b. struct sock:它是网络层的socket;对应有TCP、UDP、RAW三种;
其状态相比socket结构更精细:

  1. enum {  
  2.     TCP_ESTABLISHED = 1,  
  3.     TCP_SYN_SENT,  
  4.     TCP_SYN_RECV,  
  5.     TCP_FIN_WAIT1,  
  6.     TCP_FIN_WAIT2,  
  7.     TCP_TIME_WAIT,  
  8.     TCP_CLOSE,  
  9.     TCP_CLOSE_WAIT,  
  10.     TCP_LAST_ACK,  
  11.     TCP_LISTEN,  
  12.     TCP_CLOSING,    /* Now a valid state */  
  13.   
  14.     TCP_MAX_STATES  /* Leave at the end! */  
  15. };  
c. struct inet_sock:它是INET域的socket表示,是对struct sock的一个扩展,提供INET域的一些属性,如TTL,组播列表,IP地址,端口等;
d. struct raw_socket:它是RAW协议的一个socket表示,是对struct inet_sock的扩展,它要处理与ICMP相关的内容;
e. sturct udp_sock:它是UDP协议的socket表示,是对struct inet_sock的扩展;
f. struct inet_connection_sock:它是所有面向连接的socket表示,是对struct inet_sock的扩展;
g. struct tcp_sock:它是TCP协议的socket表示,是对struct inet_connection_sock的扩展,主要增加滑动窗口,拥塞控制一些TCP专用属性;
h. struct inet_timewait_sock:它是网络层用于超时控制的socket表示;
i. struct tcp_timewait_sock:它是TCP协议用于超时控制的socket表示;

上面简单介绍了一下内核中不同的socket相关的结构体的作用;回到inet_create函数中:

  1. inet = inet_sk(sk);  
这里为什么能直接将sock结构体变量强制转化为inet_sock结构体变量呢?只有一种可能,那就是在分配sock结构体变量时,真正分配的是inet_sock或是其他结构体;

我们回到分配sock结构体的那块代码(参考前面的5.4小节:net/core/Sock.c):

  1. static struct sock *sk_prot_alloc(struct proto *prot, gfp_t priority, int family) {  
  2.     struct sock *sk;  
  3.     struct kmem_cache *slab;  
  4.   
  5.     slab = prot->slab;  
  6.     if (slab != NULL)  
  7.         sk = kmem_cache_alloc(slab, priority);  
  8.     else  
  9.         sk = kmalloc(prot->obj_size, priority);  
  10.   
  11.     return sk;  
  12. }  
上面的代码在分配sock结构体时,有两种途径,一是从tcp专用高速缓存中分配;二是从内存直接分配;前者在初始化高速缓存时,指定了结构体大小为prot->obj_size;后者也有指定大小为prot->obj_size,
根据这点,我们看下tcp_prot变量中的obj_size(net/ipv4/Tcp_ipv4.c):

  1. .obj_size       = sizeof(struct tcp_sock),  
也就是说,分配的真实结构体是tcp_sock;由于tcp_sock、inet_connection_sock、inet_sock、sock之间均为0处偏移量,因此可以直接将tcp_sock直接强制转化为inet_sock;这几个结构体间的关系如下:
Linux内核网络协议栈
2) 建立socket, sock的关系
创建完sock变量之后,便是初始化sock结构体,并建立sock与socket之间的引用关系;调用链如下:
net/Socket.c:sys_socket()->sock_create()->__sock_create()->net/ipv4/Af_inet.c:inet_create()->net/core/Sock.c:sock_init_data():
该函数主要工作是:
a. 初始化sock结构的缓冲区、队列等;
b. 初始化sock结构的状态为TCP_CLOSE;
c. 建立socket与sock结构的相互引用关系;

7、使用tcp协议初始化sock:
inet_create()函数最后,通过相应的协议来初始化sock结构:

  1. if (sk->sk_prot->init) {  
  2.     err = sk->sk_prot->init(sk);  
  3.     if (err)  
  4.         sk_common_release(sk);  
  5. }
例子中,这里调用的是tcp_prot的init钩子函数net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock(),它主要是对tcp_sock和inet_connection_sock进行一些初始化;

8、socket与文件系统关联:
回到net/Socket.c:sys_socket()函数:

  1. asmlinkage long sys_socket(int family, int type, int protocol)  
  2. {  
  3.     int retval;  
  4.     struct socket *sock;  
  5.   
  6.     retval = sock_create(family, type, protocol, &sock);  
  7.     if (retval < 0)  
  8.         goto out;  
  9.   
  10.     retval = sock_map_fd(sock);  
  11.     if (retval < 0)  
  12.         goto out_release;  
  13.   
  14. out:  
  15.     /* It may be already another descriptor 8) Not kernel problem. */  
  16.     return retval;  
  17.   
  18. out_release:  
  19.     sock_release(sock);  
  20.     return retval;  
  21. }  
创建好与socket相关的结构后,需要与文件系统关联,详见sock_map_fd()函数:
1) 申请文件描述符,并分配file结构和目录项结构;
2) 关联socket相关的文件操作函数表和目录项操作函数表;
3) 将file->private_date指向socket;

socket与文件系统关联后,以后便可以通过文件系统read/write对socket进行操作了;

小结:
1、socket库函数通过内核创建socket,并初始化其状态为TCP_CLOSE;
2、创建完后,与文件系统关联,其文件一般位于/proc/$pid/fd/目录下;
3、应用程序可以通过文件对socket进行操作;

一、socket绑定入口


1、示例代码
  1. struct sockaddr_in server_address;  
  2. server_address.sin_family = AF_INET;  
  3. server_address.sin_addr.s_addr = inet_addr("0.0.0.0");  
  4. server_address.sin_port = htons(9734);  
  5. server_len = sizeof(server_address);  
  6. bind(server_sockfd, (struct sockaddr *)&server_address, server_len);  

2、绑定入口
前面介绍了socket从库函数到内核的过程,其最终都是通过102号中断进入内核,所不同的是子中断号不同;对于绑定,其子中断号是2;

和创建socket一样,绑定socket的处理函数都是:

  1. asmlinkage long sys_socketcall(int call, unsigned long __user *args)  
  2. {  
  3.     unsigned long a[6];  
  4.     unsigned long a0, a1;  
  5.     int err;  
  6.     if (copy_from_user(a, args, nargs[call]))  
  7.             return -EFAULT;  
  8.     a0 = a[0];  
  9.     a1 = a[1];  
  10.   
  11.     switch (call) {  
  12.             …...  
  13.     case SYS_BIND:  
  14.             err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);  
  15.             …...  
  16. }  

根据子中断号,内核会执行sys_bind()函数来完成地址的绑定;

二、绑定的具体过程

sys_bind()函数如下,一起来分析一下它的主要过程:

  1. asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)  
  2. {  
  3.     struct socket *sock;  
  4.     char address[MAX_SOCK_ADDR];  
  5.     int err, fput_needed;  
  6.     // 1, 根据fd查找相应的socket结构  
  7.     sock = sockfd_lookup_light(fd, &err, &fput_needed);  
  8.     if (sock) {  
  9.             // 2, 将用户空间的地址结构拷贝到内核空间  
  10.             err = move_addr_to_kernel(umyaddr, addrlen, address);  
  11.             if (err >= 0) {  
  12.                     err = security_socket_bind(sock,  
  13.                                           (struct sockaddr *)address,  
  14.                                           addrlen);  
  15.                     if (!err)  
  16.                             // 3, 根据协议域及socket类型,调用相应的bind函数  
  17.                             err = sock->ops->bind(sock,  
  18.                                              (struct sockaddr *)  
  19.                                              address, addrlen);  
  20.             }  
  21.             fput_light(sock->file, fput_needed);  
  22.     }  
  23.     return err;  
  24. }  

上面的过程中:
1、根据fd找到相应的socket结构
在创建socket的最后,会将socket结构与文件系统关联,并返回给应用程序与socket相关的文件描述符;这里是根据应用程序传递过来的文件描述符取得关联的socket结构;
下面看看从fd取得socket结构的代码:

  1. static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)  
  2. {  
  3.     struct file *file;  
  4.     struct socket *sock;  
  5.   
  6.     *err = -EBADF;  
  7.     file = fget_light(fd, fput_needed);  
  8.     if (file) {  
  9.             sock = sock_from_file(file, err);  
  10.             if (sock)  
  11.                     return sock;  
  12.             fput_light(file, *fput_needed);  
  13.     }  
  14.     return NULL;  
  15. }  

再到fget_lignt()去看看:

  1. struct file *fget_light(unsigned int fd, int *fput_needed)  
  2. {  
  3.     struct file *file;  
  4.     struct files_struct *files = current->files;  
  5.     …...  
  6.     file = fcheck_files(files, fd);  
  7.     …...  
  8.     return file;  
  9. }  

这里current宏返回当前运行的进程的描述符,current->files返回当前进程的打开文件表;函数fcheck_files(files, fd)根据fd从打开文件表里取出相应的file结构变量;
在创建socket中提到,file与socket关联,是通过file->private=socket完成的,因为获取到file结构变量后,也可以通过同样的方式取得socket结构变量;sock_from_file()函数就是用来完成此工作的;

2、将地址从用户空间拷贝到内核空间
1) 用户空间和内核空间的概念:
Linux内核管理模型中,简化了分段机制,使得虚拟地址与线性地址总是一致的;因此,针对32位的机器,Linux的虚拟地址空间也为0~4G。

Linux内核将这4G字节的空间分为两部分:将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”;而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间”。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。但是从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。

2) 用户态和内核态
当进程在执行用户自己的代码时,则称其处于用户运行态(用户态);即此时处理器在特权级最低的(3级)用户代码中运行;在用户态,进程使用进程的用户栈;
当进程执行系统调用而陷入内核代码中执行时,称该进程处于内核运行态(或简称为内核态),此时处理器处于特权级最高的(0级)内核代码中执行;当进程处于内核态时,执行的内核代码会使用当前进程的内核栈;每个进程都有自己的内核堆栈。

当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于内核态,因为中断处理程序将使用当前进程的内核栈,这与处于内核态的进程的状态有些类似。

3) 用户栈和内核栈
前面提到,每个进程有2个栈,即用户栈和内核栈;用户栈的空间指向用户地址空间,内核栈的空间指向内核地址空间。当进程在用户态运行时,CPU堆栈指针寄存器esp指向用户栈地址,使用用户栈;当进程运行在内核态时,CPU堆栈指针寄存器esp指向的是内核栈空间地址,使用的是内核栈;

内核在创建一个新的进程时,在创建进程控制块的同时,即创建了内核栈;而当进程调用execve的时候,才会创建用户栈;

4) 为什么要拷贝?
如果内核直接访问用户空间的地址,或是使用memcpy来拷贝,可能会出现缺页,但是缺页后的中断处理程序需要特定的结构辅助才能正常返回到缺页中断发生的地方,因此需要使用copy_from_user来完成;

结合上面提到的用户态、内核态、用户空间和地址空间后,就不难理解为什么系统调用中,都要将一些参数从用户空间拷贝到内核空间了;

3、地址结构
示例代码中创建的是类型为struct sockaddr_in的结构体变量,在调用bind()库函数时,将地址变量强制转化为struct sockaddr结构;

大家看到这里可能会有下面的疑问:

a)这两个结构体到底是什么关系?

b)为什么要强制转化为struct sockaddr结构?

c)bind()库函数最后一个参数,为什么要把结构体长度传进去呢?


首先看看struct sockaddr_in和struct sockaddr结构体吧:

  1. struct sockaddr_in {  
  2.   sa_family_t           sin_family; /* Address family           */  
  3.   __be16            sin_port;       /* Port number                  */  
  4.   struct in_addr   sin_addr;    /* Internet address             */  
  5.    
  6.   /* Pad to size of `struct sockaddr'. */  
  7.   unsigned char         __pad[__SOCK_SIZE__ - sizeof(short int) -  
  8.                     sizeof(unsigned short int) - sizeof(struct in_addr)];  
  9. };  
  10.    
  11. struct sockaddr {  
  12.     sa_family_t  sa_family; /* address family, AF_xxx       */  
  13.     char            sa_data[14]; /* 14 bytes of protocol address    */  
  14. };  

这里struct sockaddr_in代表AF_INET域的地址,还有一个结构体struct sockaddr_un代表AF_UNIX域的地址;而struct sockaddr表示内核系统调用时使用的地址类型,内核根据不同的协议域,在处理具体地址时再转化为相应的结构体;

在struct sockaddr_in结构体中,__pad成员用于结构体的对齐,使struct sockaddr_in和struct sockaddr的大小一致;

三、根据不同的协议来完成绑定

上面代码中的第3步是根据应用程序在创建socket时传递到内核的协议域及socket类型来决定调用采用哪个方法,具体可以参考创建socket一文,这里不再赘述;下面以AF_IENT及SOCK_STREAM为例来说明绑定的过程;
1、调用链:
net/Socket.c:sys_bind()->net/ipv4/Af_inet.c:inet_bind();
2、inet_bind()逻辑:
1) 地址类型检查

  1. chk_addr_ret = inet_addr_type(sock_net(sk), addr->sin_addr.s_addr);  
  2. if (!sysctl_ip_nonlocal_bind &&  
  3.     !inet->freebind &&  
  4.     addr->sin_addr.s_addr != htonl(INADDR_ANY) &&  
  5.     chk_addr_ret != RTN_LOCAL &&  
  6.     chk_addr_ret != RTN_MULTICAST &&  
  7.     chk_addr_ret != RTN_BROADCAST)  
  8.     goto out;  

inet_addr_type()函数根据设置的ip地址检查其类型:

  1. static inline unsigned __inet_dev_addr_type(struct net *net,  
  2.                                     const struct net_device *dev,  
  3.                                     __be32 addr)  
  4. {  
  5.     ……  
  6.    
  7.     if (ipv4_is_zeronet(addr) || ipv4_is_lbcast(addr))  
  8.             return RTN_BROADCAST;  
  9.     if (ipv4_is_multicast(addr))  
  10.             return RTN_MULTICAST;  
  11.     ……  
  12.    
  13.     local_table = fib_get_table(net, RT_TABLE_LOCAL);  
  14.     if (local_table) {  
  15.             ret = RTN_UNICAST;  
  16.             if (!local_table->tb_lookup(local_table, &fl, &res)) {  
  17.                     if (!dev || dev == res.fi->fib_dev)  
  18.                             ret = res.type;  
  19.                     fib_res_put(&res);  
  20.             }  
  21.     }  
  22.     return ret;  
  23. }  

其中:

a.ipv4_is_zeronet()用于检查地址的高8位是否为0,即地址是否为0.x.x.x,这类地址称为零网地址,零网地址也属于广播地址;

b.ipv4_is_lbcast()用于检查地址是否是广播地址(广播地址有两种,一种是有限广播,即255.255.255.255,它不会被路由但是会发送到物理网段上的所有主机;另一种是直接广播,该类地址的主机字段为255,如192.168.1.255,该广播会路由到192.168.1网段的所有主机上);这里只是检查是否是有限广播地址;

c.ipv4_is_multicast()用于检查地址是否是多播地址,即224.x.x.x的D类地址;


当ip地址既不是多播,也不是广播时,需要通过查找路由表来确定地址的类型(关于路由表,后面再叙述);

拿到地址类型后,inet_bind()函数会检查地址是否是单播、多播或广播地址;否则就直接出错并返回;

2) 端口范围检查

  1. snum = ntohs(addr->sin_port);  
  2. if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))  
  3. goto out;  
  4.   
  5. /* Sockets 0-1023 can't be bound to unless you are superuser */  
  6. #define PROT_SOCK   1024  

这里检查如果端口小于1024,且具有超级用户权限,否则直接出错并返回;

3) 设置源地址和接收地址

  1. if (sk->sk_state != TCP_CLOSE || inet->num)  
  2.     goto out_release_sock;  
  3.    
  4. inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;  
  5. if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)  
  6.     inet->saddr = 0;  /* Use device */  

这里先检查sock的状态,如果不是TCP_CLOSE或端口为0,则出错返回(这里也映射到创建socket时要将sock结构体变量的状态设置为TCP_CLOSE上了);
如果地址类型是多播或广播,则源地址设置为0,而接收地址为设置的ip地址;

4) 检查端口是否被占用

  1. if (sk->sk_prot->get_port(sk, snum)) {  
  2.     inet->saddr = inet->rcv_saddr = 0;  
  3.     err = -EADDRINUSE;  
  4.     goto out_release_sock;  
  5. }  

这里根据创建socket协议族初始化时设置的sk_prot来判断端口是否被占用,如果被占用则直接出错返回;关于端口是否被占用,后面会有专门的一章来描述;

5) 初始化目标地址和端口

  1. inet->sport = htons(inet->num);  
  2. inet->daddr = 0;  
  3. inet->dport = 0;  

至此,地址绑定就完成了。

总结:

1、 根据文件描述符从进程描述符中取出相应的文件,再得到socket结构;

2、 检查ip地址的类型是否是单播、多播或广播;

3、 检查端口是否被占用;

一、前情回顾

上一节《socket地址绑定》中提到,应用程序传递过来的端口在内核中需要检查端口是否可用:


  1. if (sk->sk_prot->get_port(sk, snum)) {  
  2.     inet->saddr = inet->rcv_saddr = 0;  
  3.     err = -EADDRINUSE;  
  4.     goto out_release_sock;  
  5. }  

 

按照前面的例子来分析,这里是调用了tcp_prot结构变量中的get_prot函数指针,该函数位于net/ipv4/Inet_connection_sock.c中;这个函数比较长,也是我们今天要分析的重点;

 

二、端口的管理

1、端口管理数据结构

Linux内核将所有socket使用时的端口通过一个哈希表来管理,该哈希表存放在全局变量tcp_hashinfo中,通过tcp_prot变量的h成员引用,该成员是一个联合类型;对于tcp套接字类型,其引用存放在h. hashinfo成员中;下面是tcp_hashinfo的结构体类型:

 

  1. struct inet_hashinfo {  
  2.        struct inet_ehash_bucket  *ehash;  
  3.        rwlock_t                     *ehash_locks;  
  4.        unsigned int                ehash_size;  
  5.        unsigned int                ehash_locks_mask;  
  6.    
  7.        struct inet_bind_hashbucket    *bhash;//管理端口的哈希表  
  8.        unsigned int                bhash_size;//端口哈希表的大小  
  9.    
  10.        struct hlist_head         listening_hash[INET_LHTABLE_SIZE];  
  11.        rwlock_t                     lhash_lock ____cacheline_aligned;  
  12.        atomic_t                     lhash_users;  
  13.        wait_queue_head_t           lhash_wait;  
  14.        struct kmem_cache                 *bind_bucket_cachep;//哈希表结构高速缓存  
  15. }  
 

端口管理相关的,目前可以只关注加注释的这三个成员,其中bhash为已经哈希表结构,bhash_size为哈希表的大小;所有哈希表中的节点内存都是在bind_bucket_cachep高速缓存中分配;

 

下面看一下inet_bind_hashbucket结构体:

 

  1. struct inet_bind_hashbucket {  
  2.        spinlock_t            lock;  
  3.        struct hlist_head  chain;  
  4. };  
  5. struct hlist_head {  
  6.        struct hlist_node *first;  
  7. };  
  8. struct hlist_node {  
  9.        struct hlist_node *next, **pprev;  
  10. };  
 

inet_bind_hashbucket是哈希桶结构,lock成员是用于操作时对桶进行加锁,chain成员是相同哈希值的节点的链表;示意图如下:

Linux内核网络协议栈

 

2、默认端口的分配

当应用程序没有指定端口时(如socket客户端连接到服务端时,会由内核从可用端口中分配一个给该socket);

看看下面的代码(参见net/ipv4/Inet_connection_sock.c: inet_csk_get_port()函数):

 

  1. if (!snum) {  
  2.     int remaining, rover, low, high;  
  3.    
  4.     inet_get_local_port_range(&low, &high);  
  5.     remaining = (high - low) + 1;  
  6.     rover = net_random() % remaining + low;  
  7.    
  8.     do {  
  9.         head = &hashinfo->bhash[inet_bhashfn(rover, hashinfo->bhash_size)];  
  10.         spin_lock(&head->lock);  
  11.         inet_bind_bucket_for_each(tb, node, &head->chain)  
  12.             if (tb->ib_net == net && tb->port == rover)  
  13.                 goto next;  
  14.         break;  
  15.     next:  
  16.         spin_unlock(&head->lock);  
  17.         if (++rover > high)  
  18.             rover = low;  
  19.     } while (--remaining > 0);  
  20.    
  21.     ret = 1;  
  22.     if (remaining <= 0)  
  23.         goto fail;  
  24.    
  25.     snum = rover;  
  26. }  
 

这里,随机端口的范围是32768~61000;上面代码的逻辑如下:

1)  从[32768, 61000]中随机取一个端口rover;

2)  计算该端口的hash值,然后从全局变量tcp_hashinfo的哈希表bhash中取出相同哈希值的链表head;

3)  遍历链表head,检查每个节点的网络设备是否和当前网络设置相同,同时检查节点的端口是否和rover相同;

4)  如果相同,表明端口被占用,继续下一个端口;如果和链表head中的节点都不相同,则跳出循环,继续后面的逻辑;

 

inet_bind_bucket_foreach宏利用《创建socket》一文中提到的container_of宏来实现 的,大家可以自己看看;

 

3、端口重用

当应用程序指定端口时,参考下面的源代码:


  1. else {  
  2.     head = &hashinfo->bhash[inet_bhashfn(snum, hashinfo->bhash_size)];  
  3.     spin_lock(&head->lock);  
  4.     inet_bind_bucket_for_each(tb, node, &head->chain)  
  5.         if (tb->ib_net == net && tb->port == snum)  
  6.             goto tb_found;  
  7. }  
 

 

此时同样会检查该端口有没有被占用;如果被占用,会检查端口重用(跳转到tb_found):


  1. tb_found:  
  2.        if (!hlist_empty(&tb->owners)) {  
  3.               if (tb->fastreuse > 0 &&  
  4.                   sk->sk_reuse && sk->sk_state != TCP_LISTEN) {  
  5.                      goto success;  
  6.               } else {  
  7.                      ret = 1;  
  8.                      if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb))  
  9.                             goto fail_unlock;  
  10.               }  
  11.        }  
 

1)    端口节点结构


  1. struct inet_bind_bucket {  
  2.        struct net             *ib_net;//端口所对应的网络设置  
  3.        unsigned short            port;//端口号  
  4.        signed short         fastreuse;//是否可重用  
  5.        struct hlist_node  node;//作为bhash中chain链表的节点  
  6.        struct hlist_head  owners;//绑定在该端口上的socket链表  
  7. };  
 

 

前面提到的哈希桶结构中的chain链表中的每个节点,其宿主结构体是inet_bind_bucket,该结构体通过成员node链入链表;


2)    检查端口是否可重用

这里涉及到两个属性,一个是socket的sk_reuse,另一个是inet_bind_bucket的fastreuse;

sk_reuse可以通过setsockopt()库函数进行设置,其值为0或1,当为1时,表示当一个socket进入TCP_TIME_WAIT状态(连接关闭已经完成)后,它所占用的端口马上能够被重用,这在调试服务器时比较有用,重启程序不用进行等待;而fastreuse代表该端口是否允许被重用:

l 当该端口第一次被使用时(owners为空),如果sk_reuse为1且socket状态不为TCP_LISTEN,则设置fastreuse为1,否则设置为0;

l 当该端口同时被其他socket使用时(owners不为空),如果当前端口能被重用,但是当前socket的sk_reuse为0或其状态为TCP_LISTEN,则将fastreuse设置为0,标记为不能重用;


3)    当不能重用时,再次检查冲突

此时会调用inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb)再次检查端口是否冲突;回想《创建socket》一文中提到,创建socket成功后,要使用相应的协议来初始化socket,对于tcp协议来说,其初始化方法是net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock(),其中就做了如下一步的设置:


  1. icsk->icsk_af_ops = &ipv4_specific;  
  2.    
  3. struct inet_connection_sock_af_ops ipv4_specific = {  
  4.        .queue_xmit    = ip_queue_xmit,  
  5.        .send_check          = tcp_v4_send_check,  
  6.        .rebuild_header      = inet_sk_rebuild_header,  
  7.        .conn_request        = tcp_v4_conn_request,  
  8.        .syn_recv_sock     = tcp_v4_syn_recv_sock,  
  9.        .remember_stamp        = tcp_v4_remember_stamp,  
  10.        .net_header_len     = sizeof(struct iphdr),  
  11.        .setsockopt      = ip_setsockopt,  
  12.        .getsockopt     = ip_getsockopt,  
  13.        .addr2sockaddr      = inet_csk_addr2sockaddr,  
  14.        .sockaddr_len        = sizeof(struct sockaddr_in),  
  15.        .bind_conflict          = inet_csk_bind_conflict,  
  16. #ifdef CONFIG_COMPAT  
  17.        .compat_setsockopt = compat_ip_setsockopt,  
  18.        .compat_getsockopt = compat_ip_getsockopt,  
  19. #endif  
  20. };  
 

 

下面看看这里再次检查冲突的代码:


  1. int inet_csk_bind_conflict(const struct sock *sk,  
  2.                         const struct inet_bind_bucket *tb)  
  3. {  
  4.        const __be32 sk_rcv_saddr = inet_rcv_saddr(sk);  
  5.        struct sock *sk2;  
  6.        struct hlist_node *node;  
  7.        int reuse = sk->sk_reuse;  
  8.    
  9.        sk_for_each_bound(sk2, node, &tb->owners) {  
  10.               if (sk != sk2 &&  
  11.                   !inet_v6_ipv6only(sk2) &&  
  12.                   (!sk->sk_bound_dev_if ||  
  13.                    !sk2->sk_bound_dev_if ||  
  14.                    sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {  
  15.                      if (!reuse || !sk2->sk_reuse ||  
  16.                          sk2->sk_state == TCP_LISTEN) {  
  17.                             const __be32 sk2_rcv_saddr = inet_rcv_saddr(sk2);  
  18.                             if (!sk2_rcv_saddr || !sk_rcv_saddr ||  
  19.                                 sk2_rcv_saddr == sk_rcv_saddr)  
  20.                                    break;  
  21.                      }  
  22.               }  
  23.        }  
  24.        return node != NULL;  
  25. }  
 

上面函数的逻辑是:从owners中遍历绑定在该端口上的socket,如果某socket跟当前的socket不是同一个,并且是绑定在同一个网络设备接口上的,并且它们两个之中至少有一个的sk_reuse表示自己的端口不能被重用或该socket已经是TCP_LISTEN状态了,并且它们两个之中至少有一个没有指定接收IP地址,或者两个都指定接收地址,但是接收地址是相同的,则冲突产生,否则不冲突。

也就是说,不使用同一个接收地址的socket可以共用端口号,绑定在不同的网络设备接口上的socket可以共用端口号,或者两个socket都表示自己可以被重用,并且还不在TCP_LISTEN状态,则可以重用端口号。

 

4、新建inet_bind_bucket

当在bhash中没有找到指定的端口时,需要创建新的桶节点,然后挂入bhash中:


  1. tb_not_found:  
  2.        ret = 1;  
  3.        if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,  
  4.                                    net, head, snum)) == NULL)  
  5.               goto fail_unlock;  
  6.        if (hlist_empty(&tb->owners)) {  
  7.               if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)  
  8.                      tb->fastreuse = 1;  
  9.               else  
  10.                      tb->fastreuse = 0;  
  11.        } else if (tb->fastreuse &&  
  12.                  (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))  
  13.               tb->fastreuse = 0;  
  14. success:  
  15.        if (!inet_csk(sk)->icsk_bind_hash)  
  16.               inet_bind_hash(sk, tb, snum);  
 


有兴趣的可以自己看看这段代码的实现,这里就不再展开了。

几个问题
了解以下几个问题的同学可以直接忽略下文:

1listen库函数主要做了什么?
2、什么是最大并发连接请求数?
3、什么是等待连接队列?

Socket监听相对还是比较简单的,先看下应用程序代码:

  1. listen(server_sockfd, 5);

其中,第一个参数server_sockfd为服务端socket所对应的文件描述符,第二个参数5代表监听socket能处理的最大并发连接请求数,在2.6.26内核中,该值为256

listen库函数调用的主要工作可以分为以下几步:
1、根据socket文件描述符找到内核中对应的socket结构体变量;这个过程在《socket地址绑定》一文中描述过,这里不再重述;
2、设置socket的状态并初始化等待连接队列;
3、将socket放入listen哈希表中;

listen调用代码跟踪
下面是listen库函数对应的内核处理函数:

  1. asmlinkage long sys_listen(int fd,int backlog)
  2. {
  3.    struct socket *sock;
  4.    int err, fput_needed;
  5.    int somaxconn;

  6.    // 根据文件描述符取得内核中的socket
  7.    sock = sockfd_lookup_light(fd,&err,&fput_needed);
  8.    if (sock){
  9.        // 根据系统中的设置调整参数backlog
  10.        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
  11.        if ((unsigned)backlog> somaxconn)
  12.            backlog = somaxconn;

  13.        err = security_socket_listen(sock, backlog);
  14.        // 调用相应协议簇的listen函数
  15.        if (!err)
  16.            err = sock->ops->listen(sock, backlog);

  17.        fput_light(sock->file, fput_needed);
  18.    }
  19.    return err;
  20. }

根据《创建socket》一文的介绍,例子中,这里sock->ops->listen(sock, backlog)实际上调用的是net/ipv4/Af_inet.c:inet_listen()函数:

  1. int inet_listen(struct socket*sock, int backlog)
  2. {
  3.    struct sock *sk = sock->sk;
  4.    unsigned char old_state;
  5.    int err;

  6.    lock_sock(sk);

  7.    err =-EINVAL;
  8.    // 1 这里首先检查socket的状态和类型,如果状态或类型不正确,返回出错信息
  9.    if (sock->state!= SS_UNCONNECTED|| sock->type!= SOCK_STREAM)
  10.        goto out;

  11.    old_state = sk->sk_state;
  12.    // 2 这里检查sock的状态是否是TCP_CLOSE或TCP_LISTEN,如果不是,返回出错信息
  13.    if (!((1<< old_state)& (TCPF_CLOSE| TCPF_LISTEN)))
  14.        goto out;

  15.    /* Really,if the socket is alreadyin listen state
  16.     * we can only allow the backlog to be adjusted.
  17.     */
  18.    // 3 当sock的状态不是TCP_LISTEN时,做监听相关的初始化
  19.    if (old_state!= TCP_LISTEN){
  20.        err = inet_csk_listen_start(sk, backlog);
  21.    if (err)
  22.        goto out;
  23.    }
  24.    // 4 设置sock的最大并发连接请求数
  25.    sk->sk_max_ack_backlog= backlog;
  26.    err = 0;

  27. out:
  28.    release_sock(sk);
  29.    return err;
  30. }

上面的代码中,有点值得注意的是,当sock状态已经是TCP_LISTEN时,也可以继续调用listen()库函数,其作用是设置sock的最大并发连接请求数;
下面看看inet_csk_listen_start()函数:

  1. int inet_csk_listen_start(struct sock*sk, const int nr_table_entries)
  2. {

  3.   struct inet_sock *inet = inet_sk(sk);
  4.   struct inet_connection_sock *icsk = inet_csk(sk);
  5.   // 初始化连接等待队列
  6.   int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);

  7.   if (rc!= 0)
  8.       return rc;

  9.   sk->sk_max_ack_backlog= 0;
  10.   sk->sk_ack_backlog= 0;
  11.   inet_csk_delack_init(sk);

  12.   // 设置sock的状态为TCP_LISTEN
  13.   sk->sk_state= TCP_LISTEN;
  14.   if (!sk->sk_prot->get_port(sk, inet->num)){
  15.       inet->sport= htons(inet->num);
  16.       sk_dst_reset(sk);
  17.       sk->sk_prot->hash(sk);
  18.       return 0;
  19.   }

  20.   sk->sk_state= TCP_CLOSE;
  21.   __reqsk_queue_destroy(&icsk->icsk_accept_queue);

  22.   return -EADDRINUSE;
  23. }

这里nr_table_entries是参数backlog经过最大值调整后的值;

相关数据结构
先看下接下来的代码中提到了几个数据结构,一起来看一下:

1request_sock

  1. struct request_sock {
  2.        struct request_sock *dl_next;/* Must be first*/
  3.        u16 mss;
  4.        u8 retrans;
  5.        u8 cookie_ts; /* syncookie: encode tcpoptsin timestamp */

  6.        /* The following two fields can be easily recomputed I think-AK */
  7.        u32 window_clamp; /* window clamp at creationtime */
  8.        u32 rcv_wnd; /* rcv_wnd offered first time */
  9.        u32 ts_recent;
  10.        unsigned long expires;
  11.        const struct request_sock_ops *rsk_ops;
  12.        struct sock *sk;
  13.        u32 secid;
  14.        u32 peer_secid;
  15. };

socket在侦听的时候,那些来自其它主机的tcp socket的连接请求一旦被接受(完成三次握手协议),便会建立一个request_sock,建立与请求socket之间的一个tcp连接。该request_sock会被放在一个先进先出的队列中,等待accept系统调用的处理;

2listen_sock

  1. struct listen_sock {
  2.        u8 max_qlen_log;

  3.        /* 3 bytes hole, tryto use */
  4.        int qlen;
  5.        int qlen_young;
  6.        int clock_hand;
  7.        u32 hash_rnd;
  8.        u32 nr_table_entries;
  9.        struct request_sock *syn_table[0];
  10. };

新建立的request_sock就存放在syn_table中;这是一个哈希数组,总共有nr_table_entries项;

成员max_qlen_log以2的对数的形式表示request_sock队列的最大值;

qlen是队列的当前长度;

hash_rnd是一个随机数,计算哈希值用;

3request_sock_queue

  1. struct request_sock_queue {
  2.        struct request_sock *rskq_accept_head;
  3.        struct request_sock *rskq_accept_tail;
  4.        rwlock_t syn_wait_lock;
  5.        u16 rskq_defer_accept;

  6.        /* 2 bytes hole, tryto pack */
  7.        struct listen_sock *listen_opt;
  8. };

结构体struct request_sock_queue中的rskq_accept_head和rskq_accept_tail分别指向request_sock队列的队列头和队列尾;

 

等待连接队列初始化

先看下reqsk_queue_alloc()的源代码:

  1. int reqsk_queue_alloc(struct request_sock_queue*queue,
  2.              unsigned int nr_table_entries)
  3. {
  4.    size_t lopt_size = sizeof(struct listen_sock);
  5.    struct listen_sock *lopt;

  6.    // 1 控制nr_table_entries在8~ sysctl_max_syn_backlog之间
  7.    nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
  8.    nr_table_entries = max_t(u32, nr_table_entries, 8);
  9.    // 2 向上取2的幂
  10.    nr_table_entries = roundup_pow_of_two(nr_table_entries+ 1);
  11.    // 3 申请等待队列空间
  12.    lopt_size += nr_table_entries* sizeof(struct request_sock*);

  13.    if (lopt_size> PAGE_SIZE)
  14.        lopt = __vmalloc(lopt_size,
  15.            GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO,
  16.            PAGE_KERNEL);
  17.    else
  18.        lopt = kzalloc(lopt_size, GFP_KERNEL);

  19.    if (lopt== NULL)
  20.        return -ENOMEM;

  21.    // 4 设置listen_sock的成员max_qlen_log最小为3,最大为nr_table_entries的对数
  22.    for (lopt->max_qlen_log= 3;
  23.         (1 << lopt->max_qlen_log)< nr_table_entries;
  24.         lopt->max_qlen_log++);
  25.  
  26.    // 5 相关字段赋值
  27.    get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
  28.    rwlock_init(&queue->syn_wait_lock);
  29.    queue->rskq_accept_head= NULL;
  30.    lopt->nr_table_entries= nr_table_entries;
  31.  
  32.    write_lock_bh(&queue->syn_wait_lock);
  33.    queue->listen_opt= lopt;
  34.    write_unlock_bh(&queue->syn_wait_lock);

  35.    return 0;
  36. }

整个过程中,先计算request_sock的大小并申请空间,然后初始化request_sock_queue的相应成员的值;


TCP_LISTENsocket管理

在《端口管理》一文中提到管理socket的哈希表结构inet_hashinfo,其中的成员listening_hash[INET_LHTABLE_SIZE]用于存放处于TCP_LISTEN状态的sock

当socket通过listen()调用完成等待连接队列的初始化后,需要将当前sock放到该结构体中:

  1. if (!sk->sk_prot->get_port(sk, inet->num)){
  2.   // 这里再次判断端口是否被占用
  3.   inet->sport= htons(inet->num);
  4.   sk_dst_reset(sk);
  5.   // 将当前socket哈希到inet_hashinfo中
  6.   sk->sk_prot->hash(sk);
  7.   return 0;
  8. }

这里调用了net/ipv4/Inet_hashtables.c:inet_hash()方法:

  1. void inet_hash(struct sock*sk)
  2. {
  3.        if (sk->sk_state!= TCP_CLOSE){
  4.               local_bh_disable();
  5.               __inet_hash(sk);
  6.               local_bh_enable();
  7.        }
  8. }

  9. static void __inet_hash(struct sock *sk)
  10. {
  11.        // 取得inet_hashinfo结构
  12.        struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
  13.        struct hlist_head *list;
  14.        rwlock_t *lock;

  15.        // 状态检查
  16.        if (sk->sk_state!= TCP_LISTEN){
  17.               __inet_hash_nolisten(sk);
  18.               return;
  19.        }

  20.        BUG_TRAP(sk_unhashed(sk));
  21.        // 计算hash值,取得链表
  22.        list = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
  23.        lock = &hashinfo->lhash_lock;
  24.  
  25.        inet_listen_wlock(hashinfo);
  26.        // 将sock添加到链表中
  27.        __sk_add_node(sk, list);
  28.        sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);
  29.        write_unlock(lock);
  30.        wake_up(&hashinfo->lhash_wait);
  31. }
了解到这里,回答文初提出的3个问题,应该没什么问题了吧J