记一次Linux内核中socket源码走读

在熟悉TCP协议的原理后,我们知道TCP由于维护可靠性连接,其中的过程和算法是很复杂的。但是在实际开发中,一般只需要调用api提供的几个函数即可。更有甚者,现在各种框架将网络层包起来了,只留下应用层的读写调用,无疑大大降低了开发成本。

      但是,我们带着疑问“究竟在linux下是如何实现socket的?”

1、原理与使用

      一般而言,使用socket的接口创建一个socket,用如下构造函数。

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

      domain 就是指 PF_INET、PF_INET6 以及 PF_LOCAL 等,表示IPV4,IPV6或者域套接字等套接字类型。

     type 可用的值是:SOCK_STREAM: 表示的是字节流,对应 TCP;SOCK_DGRAM:表示的是数据报,对应 UDP;SOCK_RAW: 表示的是原始套接字。

      下面我们看一个建立的服务端创建的例子,首先使用socket接口创建一个socket,然后调用bind函数绑定本地端口。

int make_socket (uint16_t port){   int sock;  struct sockaddr_in name;  /* 创建字节流类型的IPV4 socket. */  sock = socket (PF_INET, SOCK_STREAM, 0);  if (sock < 0)    {      perror ("socket");      exit (EXIT_FAILURE);    }  /* 绑定到port和ip. */  name.sin_family = AF_INET; /* IPV4 */  name.sin_port = htons (port);  /* 指定端口 */  name.sin_addr.s_addr = htonl (INADDR_ANY); /* 通配地址 */  /* 把IPV4地址转换成通用地址格式,同时传递长度 */  if (bind(sock, (struct sockaddr *) &name, sizeof (name))< 0)    {      perror ("bind");      exit (EXIT_FAILURE);    }  return sock;}//然后服务端需要listen端口,accept连接

2、Linux源码走读

     Linux源码的socket是从系统调用开始,如下所示:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol){  int retval; struct socket *sock; int flags;   ......  if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))    flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;    retval = sock_create(family, type, protocol, &sock);//①    ......    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));    ......    return retval; }

      其中,sock_create函数是创建一个socket,然后调用sock_map_fd是为了与文件描述符绑定,因为在Linux下一切皆文件。      为了方便阅读下面的源码,我绘制了一个流程图,照着这个流程图的思路往下阅读。

      我们看①处的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;    ......  sock = sock_alloc();   ......  sock->type = type;   ......  pf = rcu_dereference(net_families[family]);    ......  err = pf->create(net, sock, protocol, kern);    ......  *res = sock;  return 0;}

     这里主要是调用sock_alloc函数分配了一个struct socket结构。然后调用rcu_dereference函数,看看这个函数干嘛的。其中的参数net_families的结构如下:

static const struct net_proto_family inet_family_ops = {   .family = PF_INET,   .create = inet_create,//这个用于socket系统调用创建    ......}

      到这里,也就是说net_families数组是一个协议簇的数组,每个元素对应一个协议,比如IPV4协议簇,IPV6协议簇。我们上面举例的socket中的参数传递的是PF_INET,一直到这里的net_proto_family结构。其实,也就是根据socket一路找到了应该调用的回调。因此,pf->create函数就是调用的net_proto_family中的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;  int try_loading_module = 0;  int err;  /* Look for the requested type/protocol pair. */ lookup_protocol:   list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {    err = 0;    /* Check the non-wild match. yishuihan*/    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;  answer_prot = answer->prot;  answer_flags = answer->flags;......  sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);......}

     list_for_each_entry_rcu函数用于循环查看inetsw[sock->type],也就是inetsw数组是一个协议的数组,比如tcp协议,UDP协议都对应一个协议元素。因此,这里是找到SOCK_STREAM参数对应的数组元素。最终调用到inet_init函数。

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);   //省略其他代码... yishuihan}

      这里的第一个for循环将inetsw数组组成了一个链表,因为协议的类型比较多,比如TCP,UDP,等等。第二个循环是将inetsw_array数组注册到inetsw数组里面。inetsw_array定义的结构如下所示。比如创建了很多tcp的socket,这里可以看成每个socket都要被关联到tcp协议这个inetsw元素下面。

static struct inet_protosw inetsw_array[] ={  {    .type = SOCK_STREAM,    .protocol = IPPROTO_TCP,    .prot = &tcp_prot,    .ops = &inet_stream_ops,    .flags = INET_PROTOSW_PERMANENT|            INET_PROTOSW_ICSK,  },   //省略其他协议,比如UDP等.... yishuihan}

       从inet_create 的 list_for_each_entry_rcu 循环中开始,这是在 inetsw 数组中,根据 type 找到属于这个类型的列表,然后依次比较列表中的 struct inet_protosw 的 protocol 是不是用户指定的 protocol;如果是,就得到了符合用户指定的 family->type->protocol 的 struct inet_protosw 类型的*answer 对象。
       接下来,struct socket *sock 的 ops 成员变量,被赋值为 answer 的 ops。对于 TCP 来讲,就是 inet_stream_ops。后面任何用户对于这个 socket 的操作,都是通过 inet_stream_ops 进行的。接下来,我们创建一个 struct sock *sk 对象。

      socket 和 sock 看起来几乎一样,实际上socket 是用于负责对上给用户提供接口,并且和文件系统已经关联。而sock则负责向下对接内核网络协议栈。在sk_alloc 函数中,struct inet_protosw *answer 结构的 tcp_prot 赋值给了 struct sock *sk 的 sk_prot 成员。tcp_prot 的定义如下,里面定义了很多的函数,都是 sock 之下内核协议栈的动作。tcp_prot 的回调函数如下,就是我们比较熟悉的tcp协议内容了。

struct proto tcp_prot = {  .name      = "TCP",  .owner      = THIS_MODULE,  .close      = tcp_close,  .connect    = tcp_v4_connect,  .disconnect    = tcp_disconnect,  .accept      = inet_csk_accept,  .ioctl      = tcp_ioctl,  .init      = tcp_v4_init_sock,  .destroy    = tcp_v4_destroy_sock,  .shutdown    = tcp_shutdown,  .setsockopt    = tcp_setsockopt,  .getsockopt    = tcp_getsockopt,  .keepalive    = tcp_set_keepalive,  .recvmsg    = tcp_recvmsg,  .sendmsg    = tcp_sendmsg,  .sendpage    = tcp_sendpage,  .backlog_rcv    = tcp_v4_do_rcv,  .release_cb    = tcp_release_cb,  .hash      = inet_hash,  .get_port    = inet_csk_get_port,    ......}

3 总结

    Socket 系统调用会有三级参数 family、type、protocal,通过这三级参数,分别在 net_proto_family 表中找到 type 链表,在 type 链表中找到 protocal 对应的操作。这个操作分为两层,对于 TCP 协议来讲,第一层是 inet_stream_ops 层,第二层是 tcp_prot 层。分别对应于应用层和内核层的操作。

原创文章,作者:Zhang Miao Miao,如若转载,请注明出处:https://www.yidc.net/archives/12724