美文网首页
socket工作机制

socket工作机制

作者: EamonXia | 来源:发表于2018-12-14 11:37 被阅读0次

      首先谈一下Socket 机制本身,socket为各种协议提供了统一接口的一种ipc机制。在linux中,它由几个部分组成。为了讨论,先讨论几个数据结构,如下所示:

    struct net_proto_family {
             int             family;
             int             (*create)(struct socket *sock, int protocol);       
             short           authentication;
             short           encryption;
             short           encrypt_net;
             struct module   *owner;
    };
    

      这个数据结构定义在linux的kernel中,在文件src/include/linux/net.h中。其中family是用来标示协议号的。而那个create函数指针则表示用来创建socket时所对应的create函数,owner则是这个协议的module结构。同时,还定义一个协议数:
        #define NPROTO 64
      再看一下socket的本身的定义:

    struct socket {
             socket_state                     state;
             unsigned long                    flags;
             struct proto_ops                 *ops;
             struct fasync_struct             *fasync_list;
             struct file                      *file;
             struct sock                      *sk;
             wait_queue_head_t                wait;
             short                            type;
    };
    

      ops指针所对应的是在这个socket上的一些操作,它的定义如下:

    struct proto_ops {
             int             family;
             struct          module         *owner;
             int             (*release)      (struct socket *sock);
             int             (*bind)         (struct socket *sock,
                                              struct sockaddr *myaddr,
                                              int sockaddr_len);
             int             (*connect)      (struct socket *sock,
                                               struct sockaddr *vaddr,
                                               int sockaddr_len, int flags);
             int             (*socketpair)    (struct socket *sock1,
                                               struct socket *sock2);
             int             (*accept)    (struct socket *sock,
                                               struct socket *newsock, int flags);
             int             (*getname)   (struct socket *sock,
                                               struct sockaddr *addr,
                                               int *sockaddr_len, int peer);
             unsigned int   (*poll)       (struct file *file, struct socket *sock,
                                               struct poll_table_struct *wait);
             int             (*ioctl)     (struct socket *sock, unsigned int cmd,
                                               unsigned long arg);
             int             (*listen)    (struct socket *sock, int len);
             int             (*shutdown)  (struct socket *sock, int flags);
             int             (*setsockopt)(struct socket *sock, int level,
                                           int optname, char __user *optval,
                                           int optlen);
             int             (*getsockopt)(struct socket *sock, int level,
                                            int optname, char __user *optval,
                                              int __user *optlen);
             int             (*sendmsg)   (struct kiocb *iocb, struct socket *sock,
                                               struct msghdr *m, size_t total_len);
             int             (*recvmsg)   (struct kiocb *iocb, struct socket *sock,
                                               struct msghdr *m, size_t total_len,
                                               int flags);
             int             (*mmap)      (struct file *file, struct socket *sock,
                                                  struct vm_area_struct * vma);
             ssize_t         (*sendpage)  (struct socket *sock, struct page *page,
                                               int offset, size_t size, int flags);
    };
    

      从这个定义可以看出它定义了很多函数指针,也就是当生成某个协议的socket时,这个协议所对应的函数可以赋给这些函数指针。这样协议的实现者和socket本身的实现机制就可以分开。
      在kernel中定义了一个静态的全局数组,如下所示:

    static struct net_proto_family * net_families[NPROTO];
    

      这个定义在kernel的socket.c中。当linux系统启动时,系统的init进程会调用sock_init函数对这个数组初始化, 在init进程中调用过程是:

    /*start_kernel   =>  
     *rest_init   =>  
     *kernel_thread(init, NULL, CLONE_FS |CLONE_SIGHAND) => 
     *init  => 
     *do_basic_setup  => 
     *sock_init:
     */
    for(int i = 0; i < NPROTO; i++)
        net_families[i]=NULL;  
    

      也就是每一个协议对应这个数组的一项。同时在这个socket.c文件中还定义了一些socket注册函数:

    int sock_register(struct net_proto_family *ops)
    {
            int err;
            if (ops->family >= NPROTO) {
                   printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", 
                          ops->family, NPROTO);
                   return -ENOBUFS;
            }
            net_family_write_lock();
            err = -EEXIST;
            if (net_families[ops->family] == NULL) {
                   net_families[ops->family]=ops;
                   err = 0;
            }
            net_family_write_unlock();
            printk(KERN_INFO "NET: Registered protocol family %d\n",
                   ops->family);
            return err;
    }
    

      从这个代码可以看出,它最主要的工作就是在net_families数组所对应的项中把协议所对应的socket操作函数的net_proto_family结构指针给赋上值,这样当给定某个协议的socket时,就能通过协议号在这个net_families数组中找对应的项,进而可以得到这个socket的实际的创建函数,从而在需要生成一个新的这个协议的socket时调用用这个创建函数。
      那么这个socket注册函数是在哪调用的呢?
      一般是在协议初始化被调用的。如tipc协议在linux中是作为一个module来实现的,那么在module的module_init(tipc_init);
      这个tipc_init调用关系如下:

    /*
    * tipc_init -> start_core -> start_core_base -> socket_init ->
    * sock_register(&tipc_family_ops);
    * 这个tipc_family_ops的定义如下:
    */
    static struct net_proto_family tipc_family_ops = {
            .owner         = THIS_MODULE,
            .family        = AF_TIPC,
            .create        = tipc_create
    };
    

      AF_TIPC就是TIPC对应的协议标示,其值是30。而tipc_create函数就是tipc的socket的创建函数。

    static int tipc_create(struct socket *sock, int protocol)
    {
            struct tipc_sock *tsock;
            struct tipc_port *port;
            struct sock *sk;
            u32 ref;
            struct task_struct *tsk;
            int size = (sizeof(tsock->comm) < sizeof(tsk->comm)) ?
                        sizeof(tsock->comm) : sizeof(tsk->comm);
     
            if ((protocol < 0) || (protocol >= MAX_TIPC_STACKS)) {
                   warn("Invalid protocol number : %d, permitted range 0 - %d.\n",
                        protocol, MAX_TIPC_STACKS);
                   return -EPROTONOSUPPORT;
            }
            if (protocol != 0) {
                   int vres = handle_protocol(sock, protocol);
                   return vres;
            }
     
            ref = tipc_createport_raw(0, &dispatch, &wakeupdispatch,
                                     TIPC_LOW_IMPORTANCE, 0);
            if (unlikely(!ref))
                   return -ENOMEM;
     
            sock->state = SS_UNCONNECTED;
     
        switch (sock->type) {
            case SOCK_STREAM:
                   sock->ops = &stream_ops;
                   break;
            case SOCK_SEQPACKET:
                   sock->ops = &packet_ops;
                   break;
            case SOCK_DGRAM:
                   tipc_set_portunreliable(ref, 1);
                  
            case SOCK_RDM:
                   tipc_set_portunreturnable(ref, 1);
                   sock->ops = &msg_ops;
                   sock->state = SS_READY;
                   break;
            default:
                   tipc_deleteport(ref);
                   return -EPROTOTYPE;
            }
     
    #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,12)
            sk = sk_alloc(AF_TIPC, GFP_KERNEL, &tipc_proto, 1);
    #else
            sk = sk_alloc(AF_TIPC, GFP_KERNEL, 1, tipc_cache);
    #endif
            if (!sk) {
                   tipc_deleteport(ref);
                   return -ENOMEM;
            }
     
            sock_init_data(sock, sk);
            init_waitqueue_head(sk->sk_sleep);
            sk->sk_rcvtimeo = 8 * HZ;  
     
            tsock = tipc_sk(sk);
            port = tipc_get_port(ref);
     
            tsock->p = port;
            port->usr_handle = tsock;
     
            init_MUTEX(&tsock->sem);
           
            memset(tsock->comm, 0, size);
            tsk = current;
            task_lock(tsk);
            tsock->pid = tsk->pid;
            memcpy(tsock->comm, tsk->comm, size);
            task_unlock(tsk);
           
            tsock->comm[size-1]=0;
     
            tsock->overload_hwm = 0;
     
            tsock->ovld_limit = tipc_persocket_overload;
     
            dbg("sock_create: %x\n",tsock);
     
            atomic_inc(&tipc_user_count);
     
            return 0;
    }
    
      从这个函数的定义中可以看出,根据这个协议的不同的类型,如SOCK_STREAM还是SOCK_SEQPACKET,这给生成socket的ops指针赋予不同的操作类型,如下所示:
    static struct proto_ops packet_ops = {
            .owner         = THIS_MODULE,
            .family        = AF_TIPC,
            .release       = release,
            .bind          = bind,
            .connect       = connect,
            .socketpair    = no_skpair,
            .accept        = accept,
            .getname       = get_name,
            .poll          = poll,
            .ioctl         = ioctl,
            .listen        = listen,
            .shutdown      = shutdown,
            .setsockopt    = setsockopt,
            .getsockopt    = getsockopt,
            .sendmsg       = send_packet,
            .recvmsg       = recv_msg,
            .mmap          = no_mmap,
            .sendpage      = no_sendpage
    };
    static struct proto_ops stream_ops = {
            .owner         = THIS_MODULE,
            .family        = AF_TIPC,
            .release       = release,
            .bind          = bind,
            .connect       = connect,
            .socketpair    = no_skpair,
            .accept        = accept,
            .getname       = get_name,
            .poll          = poll,
            .ioctl         = ioctl,
            .listen        = listen,
            .shutdown      = shutdown,
            .setsockopt    = setsockopt,
            .getsockopt    = getsockopt,
            .sendmsg       = send_stream,
            .recvmsg       = recv_stream,
            .mmap          = no_mmap,
            .sendpage      = no_sendpage
    };
    

      以上所讨论的都是linux内核当中的部分,但对于应用程序来说,使用socket编程时,并不是直接与这些内核当中的接口打交道的。由于应用程序运行在用户空间,这这些接口是需要在内核空间才可以调到。
      那么就有一个问题,应用程序是如何调用到这些接口的呢?其中的奥秘就在于glibc这个库。linux应用程序是调用glibc中的socket函数来编程的,在glibc中socket的函数只有一套,通过以上的这个机制它就可以对应各种协议的socket函数。
      那么glibc中是如何调用到内核中的函数的呢?我们先来看一下内核socket.c这个文件,在这个文件中还定义了一个如下的函数:

    #ifdef __ARCH_WANT_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
     
     
    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;
     
            if (copy_from_user(a, args, nargs[call]))
                   return -EFAULT;
     
            err = audit_socketcall(nargs[call]/sizeof(unsigned long), a);
            if (err)
                   return err;
     
            a0=a[0];
            a1=a[1];
     
            trace_socket_call(call, a0);
           
            switch(call)
            {
                   case SYS_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:
                           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;
    }
     
    #endif
    

      这个sys_socketcall是一个系统调用,所有的glibc中的socket 函数都是通过这个系统调用进入到内核空间的。我们来看accept的调用。glibc中accept的调用在:sysdeps\unix\sysv\linux\accept.S文件中:

    //glibc-2.0.111\sysdeps\unix\sysv\linux\accept.S
    #define  socket   accept
    #define  __socket __libc_accept
    #define  NARGS    3
    #define  NEED_CANCELLATION
    #include <socket.S>
    libc_hidden_def (accept)
    

      这段与socket.S是accept()从用户态进入内核态的关键代码。accept.S中将accept定义为socket,__socket定义为__libc_accpet,NARGS定义为3,表示调用参数有3个。接下来包含了socket.S文件,如下:

    The socket-oriented system calls are handled unusally in Linux.
    They are all gated through the single `socketcall' system call number.
    `socketcall' takes two arguments: the first is the subcode, specifying
    which socket function is being called; and the second is a pointer to
    the arguments to the specific function.
    The .S files for the other calls just #define socket and #include this.  
    

    翻译成中文的大概意思是:
      socket系列的系统函数经常被调用。他们都通过单一的一个socketcall系统调用号(进行调用)。Socketcall有两个参数:第一个是子调用码,指定了哪一个socket函数被调用;第二个参数是一个指向被调用的socket函数所需参数的指针。其他的(socket系列的)函数的.S文件只需要#define socket 为某个值和#include 这个文件(指此socket.S)即可。
      在socket.S中进行了进一步的调用,socket从用户态进行相应参数的设置,然后使用int指令自陷,调用操作系统提供的中断服务程序,在内核态执行相应的系统服务,我们将整个函数的代码粘贴进来,在具体的语句上进行注释解释:

     1 // glibc-2.0.111\sysdeps\unix\sysv\linux\i386\socket.S
     2 #include <sysdep.h>
     3 #include <socketcall.h>
     4 // 定义了P(a,b)与P2(a,b)两个宏,他们的作用都是将a与b连接到一起。
     5 #define P(a, b) P2(a, b)
     6 #define P2(a, b) a##b
     7 
     8     .text
     9 
    10 #ifndef __socket
    11 #ifndef NO_WEAK_ALIAS
    12 #define __socket P(__,socket)     
    13 #else
    14 #define __socket socket
    15 #endif
    16 #endif
    17 
    18 .globl __socket
    19 ENTRY (__socket)   //这里开始进行函数的处理
    20 
    21 
    22     /* 保存ebx的值  */
    23     movl %ebx, %edx
    24 
    25     // SYS_ify宏在sysdep.h中定义。一会儿详细了解它的作用
    26     // 下面一条语句的作用是将socketcall的调用号存入寄存器eax
    27     movl $SYS_ify(socketcall), %eax    /* System call number in %eax.  */
    28 
    29     /* 子调用号放入ebx中,关于下面一条语句的将在下面有详细解释  */
    30     movl $P(SOCKOP_socket), %ebx    /* Subcode is first arg to syscall.  */
    31     /* 指向调用参数的指针放入ecx中  */
    32     lea 4(%esp), %ecx        /* Address of args is 2nd arg.  */
    33 
    34         /* 0x80中断,自陷进入内核态 */
    35     int $0x80
    36 
    37     /* 恢复ebx寄存器的值 */
    38     movl %edx, %ebx
    39 
    40     /* eax是返回值,如果<0则表示调用出错,就跳到错误处理的代码中去  */
    41     cmpl $-125, %eax
    42     jae SYSCALL_ERROR_LABEL
    43 
    44     /* 成功的话就返回相应的返回值  */
    45 L(pseudo_end):
    46     ret
    47 
    48 PSEUDO_END (__socket)
    49 
    50 #ifndef NO_WEAK_ALIAS
    51 weak_alias (__socket, socket)
    52 #endif
    

      我们首先看movl $SYS_ify(socketcall), %eax这一条语句。SYS_ify在sysdep.h中定义,但是有两个不同文件夹下的sysdep.h文件。

    (1)

      按照文件层次来讲,应该是按照如下的代码进行:

    1 // glibc-2.0.111\sysdeps\unix\sysv\linux\i386\sysdep.h
    2 .....
    3 #undef SYS_ify
    4 #define SYS_ify(syscall_name)    __NR_##syscall_name
    5 .....
    

      在这段代码之前有一段注释:

    For Linux we can use the system call table in the header file 
    /usr/include/asm/unistd.hof the kernel.  But these symbols do not follow the 
    SYS_* syntax so we have to redefine the `SYS_ify' macro here. 
    
    对于Linux系统,我们可以使用在/usr/include/asm/unistd.h头文件中的内核系统调用表。
    但是这些符号并不是以SYS_符号为前缀的,所以这里我们必须重定义SYS_ify宏。
    可以看到,通过SYS_ify(socketcall),我们得到了__NR_socketcall。
    
    (2)

      按照另外一本书上所讲的,在下列位置中存在另外一套代码:

    1 // glibc-2.0.111\sysdeps\unix\sysdep.h
    2 ……
    3 #ifdef __STDC__
    4 #define SYS_ify(syscall_name) SYS_##syscall_name
    5 #else
    6 #define SYS_ify(syscall_name) SYS_/**/syscall_name
    7 #endif
    8 ……
    

      如果是经由这段代码的处理,那么我们将得到SYS_socketcall,那么这又是一个什么呢?我们查看源代码是看不到的。而在实际的操作系统(笔者所使用的是Fedora 14)中,/usr/include /bits/syscall.h中则有相应的答案,这个文件是libc在构建时候根据具体的操作系统而生成的。在其中,会有:

     1 #ifndef _SYSCALL_H
     2 # error "Never use <bits/syscall.h> directly; include <sys/syscall.h> instead."
     3 #endif
     4 
     5 #define SYS__llseek __NR__llseek
     6 #define SYS__newselect __NR__newselect
     7 #define SYS__sysctl __NR__sysctl
     8 #define SYS_access __NR_access
     9 #define SYS_acct __NR_acct
    10 ……
    11 #define SYS_socketcall __NR_socketcall
    12 ……
    

    可以看到,通过这一部分的处理之后,最后依然会得到__NR_socketcall。
      了解Linux系统的人都知道,在/linux/include/linux/unistd.h中,我们可以看到,这些内容:

     1 // linux/include/linux/unistd.h
     2 ……
     3 #define __NR_setup          0    /* used only by init, to get system going */
     4 #define __NR_exit          1
     5 #define __NR_fork          2
     6 #define __NR_read          3
     7 #define __NR_write          4
     8 ……
     9 #define __NR_socketcall        102
    10 ……
    

    我们可以看到,__NR_socketcall被定义为102,上面一行的代码即是将eax的值赋成102,即此系统调用的调用号。下面我们看movl $P(SOCKOP_socket), %ebx这一句。在socketcall.h中有相应的定义:

     1 // glibc-2.0.111\sysdeps\unix\sysv\linux\socketcall.h
     2 ……
     3 #define SOCKOP_socket        1
     4 #define SOCKOP_bind        2
     5 #define SOCKOP_connect        3
     6 #define SOCKOP_listen        4
     7 #define SOCKOP_accept        5
     8 #define SOCKOP_getsockname    6
     9 #define SOCKOP_getpeername    7
    10 #define SOCKOP_socketpair    8
    11 #define SOCKOP_send        9
    12 #define SOCKOP_recv        10
    13 #define SOCKOP_sendto        11
    14 #define SOCKOP_recvfrom        12
    15 #define SOCKOP_shutdown        13
    16 #define SOCKOP_setsockopt    14
    17 #define SOCKOP_getsockopt    15
    18 #define SOCKOP_sendmsg        16
    19 #define SOCKOP_recvmsg        17
    20 ……
    

      这一句的意思就是将相应的操作码赋予ebx,此例中是5。下面我们进入操作系统中的代码进行分析,在entry.S中,有一段中断处理函数:

     1 // linux/arch/i386/kernel/entry.S
     2 _system_call:
     3 // 保存eax的值
     4     pushl %eax            # save orig_eax
     5 // 保存所有寄存器的值
     6     SAVE_ALL
     7     movl $-ENOSYS,EAX(%esp)
     8 // 比较eax中的调用号是否超过了限定的数值,NR_syscalls,默认是256。
     9     cmpl $(NR_syscalls),%eax  # compare whether eax>NR_syscalls
    10     jae ret_from_sys_call
    11 //从系统调用表中找到对应的入口地址,放入eax中
    12     movl _sys_call_table(,%eax,4),%eax
    13     testl %eax,%eax
    14     je ret_from_sys_call
    15 // 子调用号放入ebx中
    16     movl _current,%ebx
    17     andl $~CF_MASK,EFLAGS(%esp)    # clear carry - assume no errors
    18     movl $0,errno(%ebx)
    19     movl %db6,%edx
    20     movl %edx,dbgreg6(%ebx)  # save current hardware debugging status
    21     testb $0x20,flags(%ebx)        # PF_TRACESYS
    22     jne 1f
    23 // 进行系统调用
    24     call *%eax
    25     movl %eax,EAX(%esp)        # save the return value
    26     movl errno(%ebx),%edx
    27     negl %edx
    28     je ret_from_sys_call
    29     movl %edx,EAX(%esp)
    30     orl $(CF_MASK),EFLAGS(%esp)    # set carry to indicate error
    31     jmp ret_from_sys_call
    

      具体的语句的作用已经在代码中进行了标注。我们接下来可以查看处理socket调用的系统函数socket.c:

     1 // linux/net/socket.c
     2 ……
     3 asmlinkage int sys_socketcall(int call, unsigned long *args)
     4  {
     5     int er;
     6     switch(call) 
     7     {
     8         case SYS_SOCKET:
     9             er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
    10             if(er)
    11                 return er;
    12             return(sock_socket(get_fs_long(args+0),
    13                 get_fs_long(args+1),
    14                 get_fs_long(args+2)));
    15 ……
    16         case SYS_ACCEPT:
    17             er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
    18             if(er)
    19                 return er;
    20             return(sock_accept(get_fs_long(args+0),
    21                 (struct sockaddr *)get_fs_long(args+1),
    22                 (int *)get_fs_long(args+2)));
    23 ……
    

      这个sys_socketcall函数是socket系列函数的分发函数,根据具体调用号,调用不同的处理函数进行处理,至此,我们看到了整个从应用层socket函数到BSDsocket的层的传递过程,加深了我们对于此过程的了解。

    本文非原创

    参考文献:

    http://blog.sina.com.cn/s/blog_605507340101cwaf.html
    https://www.xuebuyuan.com/910647.html
    https://segmentfault.com/a/1190000008926093

    相关文章

      网友评论

          本文标题:socket工作机制

          本文链接:https://www.haomeiwen.com/subject/gygwhqtx.html