socket()接口与内核协议栈的挂接

来源:互联网 发布:seo主管 编辑:程序博客网 时间:2024/06/05 08:59

1、socket()到系统调用

    Linux下,用户空间的调用的socket()接口由glibc实现,man socket如下,三个参数domain,type和protocol:

1
2
3
4
5
6
7
NAME
       socket - create an endpoint forcommunication
 
SYNOPSIS
       #include <sys/socket.h>
 
       intsocket(intdomain, inttype, intprotocol);

    libc实现系统调用同名函数通常使用INT 0x80 + 系统调用号的方式陷入内核,一般来说,read对应sys_read,write对应sys_write.但是,socket系列却不是这样,为了节约系统调用号,将所有的socket系列的接口使用同一个系统调用号陷入内核(叫socketcall),glibc中通过socket.S陷入内核:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.globl __socket
ENTRY (__socket)
 
    /* Save registers.  */
    movl %ebx, %edx
    cfi_register (3, 2)
 
   /* System call number in %eax.  */
    movl $SYS_ify(socketcall), %eax
 
    /* Use ## so `socket' is a separate
    token that might be #define'd.  */
    /* Subcode is first arg to syscall.  */
    movl $P(SOCKOP_,socket), %ebx 
    /* Address of args is 2nd arg.  */
    lea 4(%esp), %ecx     
 
        /* Do the system call trap.  */
    ENTER_KERNEL
 
    /* Restore registers.  */
    movl %edx, %ebx
    cfi_restore (3)
 
    /* %eax is &lt; 0 if there was an error.  */
    cmpl $-125, %eax
    jae SYSCALL_ERROR_LABEL
 
    /* Successful; return the syscall's value.  */
    ret

    eax存系统调用号,应该是198,ebx存参数1,ecx存参数2(这个地方的参数1和参数2不是domian/type/protocol,而是参数1是socket systemcall的类型,参数二是栈顶减4,ecx存domain/type/protocol的整体指针),ENTER_KERNEL即通过INT 0x80陷入内核:

1
# define ENTER_KERNEL int $0x80

2、socketcall到协议簇的挂接

    内核中实现的socketcall代码片段如下,一个简单的逻辑分支将socketcall引向sys_socket():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SYSCALL_DEFINE2(socketcall,int, call,
                   unsignedlong__user *, args)
{
...
    switch(call) {
    caseSYS_SOCKET:
        err = sys_socket(a0, a1, a[2]);
        break;
    caseSYS_BIND:
        err = sys_bind(a0, (structsockaddr __user *)a1, a[2]);
        break;
    caseSYS_CONNECT:
        err = sys_connect(a0, (structsockaddr __user *)a1, a[2]);
        break;
    caseSYS_LISTEN:
        err = sys_listen(a0, a1);
        break;
    caseSYS_ACCEPT:
        err = sys_accept4(a0, (structsockaddr __user *)a1,
                  (int__user *)a[2], 0);
        break;
...
}

    sys_socket()的实现从代码上有些曲折(net/socket.c),通过SYSCALL_DEFINE3实现sys_socket:

1
SYSCALL_DEFINE3(socket,int, family, int, type, int, protocol)

    SYSCALL_DEFINE3即定义一个3个参数的系统调用,具体细节不研究了。

    sys_socket()主要有两个操作,sock_create()和sock_map_fd(),前者用于创建具体的socket结构,后者用于为socket结构分配file sttuct和file fd。我们主要关注sock_create()。sock_create()又调用了__sock_create(),__sock_creat()仅列出主要代码:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int__sock_create(structnet *net, intfamily, inttype, intprotocol,
             structsocket **res, intkern)
{
    interr;
    structsocket *sock;
    conststruct net_proto_family *pf;
...
    err = security_socket_create(family, type, protocol, kern);
    if(err)
        returnerr;
 
    /*
     *  Allocate the socket and allow the family to set things up. if
     *  the protocol is 0, the family is instructed to select an appropriate
     *  default.
     */
    sock = sock_alloc();
...
    sock->type = type;
...
    rcu_read_lock();
    pf = rcu_dereference(net_families[family]);
...
    rcu_read_unlock();
 
    err = pf->create(net, sock, protocol, kern);
...
    err = security_socket_post_create(sock, family, type, protocol, kern);
...
}

    security_socket_*用于SELinux增强系统安全,此处不讨论了。那么__sock_create主要就调用了pf->create,因此此处也是挂接具体的内核协议簇的位置,如PF_INET,PF_NETLINK等。net_family定义如下(3.10.9内核共有39中协议簇,27,28空):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
staticconst struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
 
structnet_proto_family {
    int    family;
    int    (*create)(structnet *net, structsocket *sock,
                  intprotocol, intkern);
    structmodule   *owner;
};
 
#define NPROTO      AF_MAX
 
/* Supported address families. */
#define AF_UNSPEC   0
#define AF_UNIX     1   /* Unix domain sockets      */
#define AF_LOCAL    1   /* POSIX name for AF_UNIX   */
#define AF_INET     2   /* Internet IP Protocol     */
#define AF_AX25     3   /* Amateur Radio AX.25      */
#define AF_IPX      4   /* Novell IPX           */
#define AF_APPLETALK    5   /* AppleTalk DDP        */
#define AF_NETROM   6   /* Amateur Radio NET/ROM    */
#define AF_BRIDGE   7   /* Multiprotocol bridge     */
#define AF_ATMPVC   8   /* ATM PVCs         */
#define AF_X25      9   /* Reserved for X.25 project    */
#define AF_INET6    10  /* IP version 6         */
...
#define AF_NETLINK  16
#define AF_ROUTE    AF_NETLINK /* Alias to emulate 4.4BSD */
...
#define AF_MAX      41  /* For now.. */

    协议簇,Protocol Family,在内核中使用net_familes[]树组维护,目前3.10.9内核中共有39个协议簇。协议簇代表socket的第一个参数domain,而不是第二个type或者第三个protocol,协议簇的意思是一簇协议的集合,根据ISO/OSI的7层模型,应该都包含,而非其中某一层上的一种协议。而对于具体的create函数,就是会调用具体的协议簇自己的create,如unix_create,inet_craete,netlink_create等。至此,完成了协议簇和应用层socket的挂接工作:

socket_syscall

 3、socket结构体初始化 

    sock_create()函数的目的是为了socket结构分配空间,并根据family/type/protocol对其进行初始化。先看一下socket结构(include/linux/net.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */
structsocket {
    socket_state        state;
 
    kmemcheck_bitfield_begin(type);
    short          type;
    kmemcheck_bitfield_end(type);
 
    unsignedlong      flags;
 
    structsocket_wq __rcu  *wq;
 
    structfile     *file;
    structsock     *sk;
    conststruct proto_ops  *ops;
};

    其中后三个指针比较重要,分别为文件指针、sock指针、协议ops指针,sock_create()主要也就是初始化这三个指针。*file指针用于指向VFS创建的文件结构体,而*ops则为具体的协议的操作结构体,内含有bind、accept、recvmsg、sendmsg、setsockopt、getsockopt等具体与协议相关的函数指针。而*sk,具体负责传输层的工作,非常复杂。

1)socket->file的初始化 

    首先,前面提到sock_map_fd()主要用于对socket的*file指针初始化,经过sock_map_fd()操作后,socket就通过其*file指针与VFS管理的文件进行了关联,便可以进行文件的各种操作,如read、write、lseek、ioctl等,sock_map_fd()主要有两个调用:get_unused_fd_flags用于获取一个无用的fd号,sock_alloc_file用于将分配并初始化*file,通过将file->fop指针挂接到socket_file_ops的地址完成该操作(通过alloc_file接口完成,但此时其实仍为完全完成对file->fop的赋值,因为socket_file_ops本身也有需要挂接的地方):

sock_map_fd

    当然了,alloc_file同时还做了很多其他的工作,这里就不关心了,那么socket_file_ops结构体指针就存储了socket的文件操作指针(任何内核对象要想以文件的方式进行处理,都需要定义file_operations结构题变量):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
staticconst 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,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_sock_ioctl,
#endif
    .mmap =     sock_mmap,
    .open =     sock_no_open,   /* special open code to disallow open via /proc */
    .release =  sock_close,
    .fasync =   sock_fasync,
    .sendpage = sock_sendpage,
    .splice_write = generic_splice_sendpage,
    .splice_read =  sock_splice_read,
};

    具体的sock_*函数就是将socket当成文件时的具体操作,对于一些没有的操作,例如open,就定义为sock_no_xxx,直接返回错误。

    至此,socket->file->fop已经初始化为socket_file_ops。当然,一个fop指针的初始化并不能算是一个file结构体变量完成了初始化(inode等等都需要做初始化,VFS一系列的),但是这里暂时就不关注了。

    另外,VFS的三个比较重要的结构inode,file,dentry也相互关联,而file结构又与socket结构相互管理,因此通过socket也可以索引到相关的file结构,反之也可以;通过socket索引到的file结构是一个通用的file结构,具备文件的通用功能和属性,这也是为什么在Linux操作系统中socket能够被当作文件使用的原因:

此处应该有一张inode,dentry,file,socket相互关联的图,等到写VFS时再补充吧。

2)socket->ops的初始化 

    除了socket->file->fop初始化为socket_file_ops,socket自身的操作集也需要初始化,即socket作为一个socket理所应当的操作(BSD socket)。由于socket的具体操作,如send,recv,setsockopt,getsockopt等与具体的协议簇、类型、协议有关系,那么这个socket->ops的初始化过程也应该是一个逐层挂接的过程,即种协议簇自己的xxx_create()接口实现的socket->ops的初始化。以PF_INET为例,具体分析一下inet_create如何初始化socket->ops指针。

    首先,有几种与协议簇相关的结构先介绍一下:

a)struct net_proto_family,用于描述协议簇,也即glibc的socket()接口的第一个参数domain,目前内核工支持39个协议簇,PF_INET协议簇为其定义了变量inet_family_ops

1
2
3
4
5
6
structnet_proto_family {
    int    family;
    int    (*create)(structnet *net, structsocket *sock,
                  intprotocol, intkern);
    structmodule   *owner;
};

b)struct proto_ops,用于描述每种协议簇的不同类型协议集合,即glibc的socket()接口的第二个参数type,PF_INET协议簇工支持三种type:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW,Linux内核中关于sock_type的定义如下,共7种(include/linux/net.h):

1
2
3
4
5
6
7
8
9
enumsock_type {
    SOCK_STREAM = 1,
    SOCK_DGRAM  = 2,
    SOCK_RAW    = 3,
    SOCK_RDM    = 4,
    SOCK_SEQPACKET  = 5,
    SOCK_DCCP   = 6,
    SOCK_PACKET = 10,
};

    proto_ops结构体定义如下,INET定义了它的变量inet_stream_opsinet_dgram_opsinet_sockraw_ops,分别代表SOCK_STREAM、SOCK_DGRAM、SOCK_RAW类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
structproto_ops {
    int    family;
    structmodule   *owner;
    int    (*release)   (structsocket *sock);
    int    (*bind)      (...);
    int    (*connect)      (...);
    int    (*socketpair)      (...);
    int    (*accept)      (...);
    int    (*getname)      (...);
    int    (*ioctl)      (...);
   ...
    int    (*listen)      (...);
    int    (*setsockopt)      (...);
    int    (*getsockopt)      (...);
    int    (*sendmsg)      (...);  
    int    (*mmap)      (...);  
    ...
}

    其次,PF_INET为了维护方便定义了自己的一些结构体和变量:

a)struct inet_protosw,用于INET注册自己的子协议使用,也即代表glibc的socket()的第三个参数,protocol。使用type表示该protocol所属的类型,使用protocol表示该协议,使用ops指针用于具体的该protocol的操作集。相关变量为inetsw_array[]inetsw[](inetsw[]为inet_init时将inetsw_array[]按照type作为数组下标重新赋值的变量):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* This is used to register socket interfaces for IP protocols.  */
structinet_protosw {
    structlist_head list;
 
        /* These two fields form the lookup key.  */
    unsignedshort  type;     /* This is the 2nd argument to socket(2). */
    unsignedshort  protocol; /* This is the L4 protocol number.  */
 
    structproto     *prot;
    conststruct proto_ops *ops;
 
    char            no_check;   /* checksum on rcv/xmit/none? */
    unsignedchar   flags;      /* See INET_PROTOSW_* below.  */
};

    struct proto结构与struct proto_ops有些类似,不过定义的是传输层的操作集,比较多,这里就不列举了。

    写到这个位置,Linux系统对网络这部分的分层是非常明显的,一些理解应该是这样的:

  • linux kernel遵从ISO/OSI的7层模型,但是仅仅实现其中的三层链路层、网络层、传输层(物理层由硬件负责,绘画层、表示层、应用层由用户空间的具体应用负责)。
  • socket的三个参数domain,type,protocol分别对应协议簇、协议类型、具体协议号;而这个协议号,是指传输层的协议号。三者之间属于包含关系,即协议簇下面分为具体不同类型的子协议集合,不同类型的子协议集合中又包含具体的协议。
  • Linux系统有一些结构体分别用于定义不同层次的通信句柄,用户空间使用int fd作为通信句柄,Linux内核中的socket层使用struct socket作为通信的句柄,Linux内核中传输层使用struct sock作为通信的句柄。
  • 不同层上对应了不同的操作集合,用户空间应用使用glibc的接口,如socket().bind(),recv(),send()等;socket层使用struct proto_ops指针作为操作集,传输层使用struct proto指针作为具体的协议相关的操作集合。

    OK,回到inet_create()。inet_create()通过以sock->type作为数组inetsw[]的下标,找到相关inet_protosw指针,并为socket->ops赋值为inet_protosw->ops,具体过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
staticint inet_create(structnet *net, structsocket *sock, intprotocol,
               intkern)
{
    structsock *sk;
    structinet_protosw *answer;
 
    ...
    rcu_read_lock();
    list_for_each_entry_rcu(answer, &amp;inetsw[sock->type], list) {
 
        err = 0;
        /* Check the non-wild match. */
        if(protocol == answer->protocol) {
            if(protocol != IPPROTO_IP)
                break;
        }else{
            /* Check for the two wild cases. */
            if(IPPROTO_IP == protocol) {
                protocol = answer->protocol;
                break;
            }
            if(IPPROTO_IP == answer->protocol)
                break;
        }
        err = -EPROTONOSUPPORT;
    }
    ...
    sock->ops = answer->ops;
    ...
    rcu_read_unlock();
    ...
}

    当然,inet_create()接口还做了很多其他工作,在这里就不讨论了。

3)socket->sk的初始化 

    socket->sk指针为struct sock指针,而struct sock是传输层的的句柄。在PF_INET协议簇中,仍然使用inet_create()接口为socket->sk指针做初始化的工作,下面选择另一种协议簇PF_UNIX进行socket->sk指针初始化的分析,类似与PF_INET,PF_UNIX也定义了协议簇变量和具体协议类型变量,他们分别是unix_family_opsunix_stream_opsunix_dgram_opsunix_seqpacket_ops(PF_UNIX也支持三种类型的协议,分别是SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET)。

    unix_create()接口调用了unix_create1()为socket->sk赋值,摘录unix_create1()接口代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
staticstruct sock *unix_create1(structnet *net, structsocket *sock)
{
    structsock *sk = NULL;
    ...
    sk = sk_alloc(net, PF_UNIX, GFP_KERNEL, unix_proto);
    ...
    sock_init_data(sock, sk);
    ...
}
 
voidsock_init_data(structsocket *sock, structsock *sk)
    some init operations;
    ...
    sk_set_socket(sk, sock);
    ...
    other init operations;
}
 
staticinline void sk_set_socket(structsock *sk, structsocket *sock)
{
    sk_tx_queue_clear(sk);
    sk->sk_socket = sock;
}

    大量的具体的初始化工作都在sock_init_data()接口中,本文中就不讨论了,重点在sock_init_data()为struct sock结构分配了内存,并做了初始化工作,sock_init_data()接口负责与具体协议簇无关的初始化,而其他初始化在family_create接口中其他代码完成,如:

1
2
3
4
5
6
lockdep_set_class(&sk->sk_receive_queue.lock,
            &af_unix_sk_receive_queue_lock_key);
 
sk->sk_write_space  = unix_write_space;
sk->sk_max_ack_backlog  = net->unx.sysctl_max_dgram_qlen;
sk->sk_destruct     = unix_sock_destructor;

unix_create

4、总结 

network 

0 0