Socket与系统调用深度分析

一.系统调用

系统调用使得操作系统从用户态进入了内核态,从而使用了内核中的一些功能,通过查阅书籍得知,linux x86-64系统通过int 80调用syscall中断来进入内核态。在使用socket编程中,系统先通过中断进入内核态,再调用底层功能来使得可以与网络通信协议产生连接,这是此实验的原理。

二.实验准备

本实验需要上次实验准备的menuOS系统,基于linux5.0.1内核,编译方式为x86-64

三.流程分析

首先分析socket函数的参数信息

int socket(int domain, int type, int protocol);
参数说明:
domain:指定通信协议族。常用的协议族有AF_INET、AF_UNIX等,对于TCP协议,该字段应为AF_INET(ipv4)或AF_INET6(ipv6)。
type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM等。SOCK_STREAM针对于面向连接的TCP服务应用。SOCK_DGRAM对应于无连接的UDP服务应用。
protocol:指定socket所使用的协议,一般我们平常都指定为0,使用type中的默认协议。严格意义上,IPPROTO_TCP(值为6)代表TCP协议。
 
x86-64Linux系统启动时依次调用以下过程:start_kernel --> trap_init --> cpu_init --> syscall_init,而syscall_init函数实现了系统调用的初始化将中断向量与服务例程进行绑定。除此之外,还要进行系统调用表的初始化。
 
 
 进入内核的方法
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;
    if (call < 1 || call > SYS_SENDMMSG)
        return -EINVAL;
    call = array_index_nospec(call, SYS_SENDMMSG + 1);
    len = nargs[call];
    if (len > sizeof(a))
        return -EINVAL;
    /* copy_from_user should be SMP safe. */
    if (copy_from_user(a, args, len))
        return -EFAULT;
    err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
    if (err)
        return err;
    a0 = a[0];
    a1 = a[1];
    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_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], 0);
        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_sendto(a0, (void __user *)a1, a[2], a[3],
                   NULL, 0);
        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_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     NULL, NULL);
        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 user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_SENDMMSG:
        err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
                     a[3], true);
        break;
    case SYS_RECVMSG:
        err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_RECVMMSG:
        if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3],
                         (struct __kernel_timespec __user *)a[4],
                         NULL);
        else
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3], NULL,
                         (struct old_timespec32 __user *)a[4]);
        break;
    case SYS_ACCEPT4:
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], a[3]);
        break;
    default:
        err = -EINVAL;
        break;
    }
    return err;
}

 这里通过syscall进入了内核中,可以看到除了一些合法性检验外,通过switch语句来实现不通的调用,也是一种函数内部多态的情况

上面我们介绍了进入内核的一些方法,下面我们从socket的参数入手来分析到如何连接TCP/IP协议。

1.函数原型

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

  

参数说明:

domain:指定通信协议族。常用的协议族有AF_INET、AF_UNIX等,对于TCP协议,该字段应为AF_INET(ipv4)或AF_INET6(ipv6)。

type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM等。SOCK_STREAM针对于面向连接的TCP服务应用。SOCK_DGRAM对应于无连接的UDP服务应用。

protocol:指定socket所使用的协议,一般我们平常都指定为0,使用type中的默认协议。严格意义上,IPPROTO_TCP(值为6)代表TCP协议。

2.内核实现源码分析

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;

...
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;

    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    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函数主要由sock_create和sock_map_fd这两个函数完成,因此我们主要分析这两个函数,看它们是怎么在内核一步步创建和管理我们使用的socket

2.1 sock_create函数

sock_create() 实际调用的是 __sock_create()。

int __sock_create(struct net *net, int family, int type, int protocol,
                  struct socket **res, int kern)
{
    int err;
    struct socket *sock;
    const struct net_proto_family *pf;
...
    err = security_socket_create(family, type, protocol, kern);//SElinux相关,暂不关注
    /*
     *  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();//创建struct socket结构体

    sock->type = type;//设置套接字的类型
...
    pf = rcu_dereference(net_families[family]);//获取对应协议族的协议实例对象
...
    err = pf->create(net, sock, protocol, kern);
    if (err < 0)
        goto out_module_put;

...
    err = security_socket_post_create(sock, family, type, protocol, kern);//SElinux相关,暂不关注

    *res = sock;
...
}

  这里比较重要的是sock_alloc()和pf->create()这两个函数。其中sock_alloc()里体现了linux一切皆文件(Everything is a file)理念,即使用文件系统来管理socket,这也是VFS所要达到的效果。

  sock_alloc()函数

  总的来说,sock_alloc()函数分配一个struct socket_alloc结构体,将sockfs相关属性填充在socket_alloc结构体的vfs_inode变量中,以限定后续对这个sock文件允许的操作。同时sock_alloc()最终返回socket_alloc结构体的socket变量,用于后续操作。
2.2  pf->create()函数

下面我们继续看下pf->create()这个函数。

pf由net_families[]数组获得,我们先看下net_families[]数组的定义:

#define AF_MAX      41  /* For now.. */
#define NPROTO      AF_MAX

static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;

  net_families[]数组的初始化在inet_init()函数,

static const struct net_proto_family inet_family_ops = {
    .family = PF_INET,
    .create = inet_create,
    .owner  = THIS_MODULE,
};

static int __init inet_init(void)
{
...
    (void)sock_register(&inet_family_ops);
...
}

int sock_register(const struct net_proto_family *ops)
{
...
    rcu_assign_pointer(net_families[ops->family], ops);
...
}

  因此,net_families[]数组里存放的是各个协议族的信息,以family字段作为下标。此处我们针对TCP协议分析,因此我们family字段是AF_INET,另外一般会有

#define PF_INET     AF_INET

  所以pf->create调用的就是inet_create()函数。

static int inet_create(struct net *net, struct socket *sock, int protocol,
                       int kern)
{
    struct sock *sk;
    struct inet_protosw *answer;
    struct inet_sock *inet;
    struct proto *answer_prot;
    unsigned char answer_flags;
    char answer_no_check;
    int try_loading_module = 0;
    int err;
...
    sock->state = SS_UNCONNECTED;

    /* Look for the requested type/protocol pair. */
lookup_protocol:
    err = -ESOCKTNOSUPPORT;
    rcu_read_lock();
    //根据socket传入的protocal在inetsw[]数组中查找对应的元素
    list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

        err = 0;
        /* 如果我们在socket的protocal传入的是6,即TCP协议,那么走这个分支 */
        if (protocol == answer->protocol) {
            if (protocol != IPPROTO_IP)
                break;
        } else {
            /* 如果socket的protocal传入的是0,那么走这个分支 */
            if (IPPROTO_IP == protocol) {
                protocol = answer->protocol;//重新给protocal赋值,因此socket中protocal传入的是0或者6,都是可以的
                break;
            }
            if (IPPROTO_IP == answer->protocol)
                break;
        }
        err = -EPROTONOSUPPORT;
    }
...
    sock->ops = answer->ops;//将查找到的对应协议族的协议函数操作集赋值给我们之前创建的socket
    answer_prot = answer->prot;
...
    //创建sock结构体,注意这里创建的结构体类型是sock,之前我们通过sock_alloc创建的结构体类型是socket
    sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
    if (sk == NULL)
        goto out;
...
    sock_init_data(sock, sk);//sock初始化

    //另一部分初始化,里面有对各个socket连接定时器的初始化
    if (sk->sk_prot->init) {
        err = sk->sk_prot->init(sk);
        if (err)
            sk_common_release(sk);
    }
...
}
}

  inet_create()函数里有几处是比较重要的,一个是通过inetsw[]数组获取对应协议类型的接口操作集信息,另一个是创建struct sock类型的变量,最后是对创建的sock进行初始化。

2.3 inetsw[]数组

inetsw[]数组存放的是各个sock_type的信息,它也是在inet_init()函数中初始化。

enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};

#define SOCK_MAX (SOCK_PACKET + 1)
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_array[]存放的就是具体的每种sock的信息,包括操作函数,协议号等,其中prot和ops两个成员是比较重要的,后续很多操作依赖于这两个成员。

static struct inet_protosw inetsw_array[] = {
    {
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .no_check =   0,
        .flags =      INET_PROTOSW_PERMANENT |
        INET_PROTOSW_ICSK,
    },

    {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_UDP,
        .prot =       &udp_prot,
        .ops =        &inet_dgram_ops,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_PERMANENT,
    },

    {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_ICMP,
        .prot =       &ping_prot,
        .ops =        &inet_dgram_ops,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_REUSE,
    },
...
};

  然后通过inet_register_protosw()函数,将上述inetsw_array[]里的元素,按照type字段挂在inetsw[]数组的链表上。因为type字段是有重复的,所以每个inetsw[]元素的链表上可能会有多个成员。

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;
...
    answer = NULL;
    last_perm = &inetsw[p->type];
    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)//如果是要覆盖已有的协议族,则失败
                break;
            last_perm = lh;
        }

        answer = NULL;
    }
    if (answer)
        goto out_permanent;

    list_add_rcu(&p->list, last_perm);//按照type的值,添加到inetsw[type]数组中的链表中
...
}

  2.4 创建struct sock类型变量

  通过sk_alloc()函数创建struct sock类型变量

struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
                      struct proto *prot) {
    struct sock *sk;

    sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
    if (sk) {
        sk->sk_family = family;
        //这里留意一下,后续会使用到sk_prot这个变量
        sk->sk_prot = sk->sk_prot_creator = prot;
...
    }

    return sk;
}

static struct sock *sk_prot_alloc(struct proto *prot, gfp_t priority,
        int family)
{
    struct sock *sk;
    struct kmem_cache *slab;

    slab = prot->slab;
    if (slab != NULL) {
        //使用对应协议的slab高速缓存分配,减小内存碎片
        sk = kmem_cache_alloc(slab, priority & ~__GFP_ZERO);
...

  由此可见,最后是通过prot->slab,也就是TCP的slab高速缓存里分配的。

到这里就分析到TCP的连接了,下面TCP的具体连接就不分析了,此次实验任务结束。

posted @ 2019-12-19 16:23  菜鸡学java  阅读(251)  评论(0)    收藏  举报