暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

码农的修炼之道 2020-08-30
937
     在熟悉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 层。分别对应于应用层和内核层的操作。
文章转载自码农的修炼之道,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论