#220901 socket 和 sock

数据结构

内核中一个 socket 主要用 socketsock 这两个结构体来表示。因为 “In Linux, everything is a file”,所以 socket 会通过 file 封装成文件给用户态程序去使用。

../_images/socket.svg

struct sock

struct sock 以及这个结构体派生出的各种 xxx_sock,实现具体的协议。各种 sock 有以下继承关系。

../_images/sock.svg

(上图中左侧直接继承自 sock_common 的结构体是 TCP 连接建立、关闭时用到的一些特殊 sock 类型,可以先忽略)

继承通过 embedding 的方式来实现,比如 udp_sock 展开就是下面这样:

struct udp_sock {
    struct inet_sock {
        struct sock {
            struct sock_common __sk_common,

            #define sk_prot __sk_common.skc_prot
            // sock 属性 ...
        } sk,

        // inet_sock 属性 ....
    } inet,

    // udp_sock 属性 ...
}

sk_prot 中是接口方法。接口方法的第一个参数是指向结构体的 this 指针 struct sock* sk

所有 sock 类结构体的第一个字段必须是其父结构体。这样接口方法中需要访问具体协议层 sock 的字段和方法时,通过强制转换即可。

struct inet_sock *inet = (struct inet_sock*)sk;
struct udp_sock *up = (struct udp_sock*)sk;

struct socket

struct socket,将各种协议的细节封装,向上提供一个统一的接口,屏蔽下面各种协议层面的实现细节,其中 sk 字段指向具体协议的 sock 结构体,ops 中是接口方法。

// general BSD socket
struct socket {
    socket_state    state;
    short           type;
    unsigned long   flags;
    struct file     *file;
    struct sock     *sk;
    const struct proto_ops *ops;
    struct socket_wq wq;
}

Note

socket 类型的变量名一般用 sock,sock 类型的用 sk。sock 的各种派生类会用 xxsk,比如 inet_connection_sock 变量名一般叫 icsk。

构建一个 socket 的过程

调用栈:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
|- __sys_socket
   |- socket *sock = __sys_socket_create
   |  |- sock_create
   |     |- __sock_create
   |        |- sock = sock_alloc()
   |        |- sock->type = type
   |        |- pf = net_families[family]
   |        |- pf->create(sock, protocol)   --+
   |        |- return sock                    |
   |- return sock_map_fd(sock)                |
                                              |
        +-------------------------------------+
        |
        v
inet_create(struct socket *sock, int protocol)
|- for answer in inetsw[sock->type]:
|      if answer and protocol match:
|          break
|- sock->ops = answer->ops
|
|- answer_prot = answer->prot
|- sk = sk_alloc(PF_INET, GFP_KERNEL, answer_prot)
|  |- sk_prot_alloc
|     |- kmem_cache_alloc(answer_prot->slab, ...)
|  |- sk->sk_prot = answer_prot
|
|- sock_init_data(sock, sk)
|  |- sk_init_common
|  |- sk->sk_blahblah = blahblah
|- sk->sk_protocol = protocol
|
|- sk->sk_prot->init/tcp_v4_init_sock/udp_init_sock(sk)

socket(AF_INET, SOCK_DGRAM, 0) 为例。

  1. 首先,调用 sock_alloc 创建 socket 结构体

  2. 然后,从全局数组 net_families 中取出 AF_INET 的构建函数 inet_create 并调用。这个函数中会调用 sk_alloc 创建 sock 结构体挂载接口方法初始化结构体 。接口方法和初始化函数都存在 inetsw 全局列表中,根据 type 和 protocol 查找。

  3. 最后,调用 sock_map_fd 将 socket 封装成文件并返回

../_images/socket-create.svg

inetsw 查找方法如下:

list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
    err = 0;
    if (protocol == answer->protocol) {
        if (protocol != IPPROTO_IP)
            break;
    } else {
        // 判断的是 socket 调用传的 protocol 参数为 是否为 0,
        if (0 == protocol) {
            protocol = answer->protocol;
            break;
        }
        // 判断的是 inetsw 里注册的协议是否支持通配,目前只有 type=SOCK_RAW 时,protocol 可以是任意的
        if (IPPROTO_IP == answer->protocol)
            break;
    }
    err = -EPROTONOSUPPORT;
}

从代码看,TCP 和 UDP 协议可以不用 protocol 参数,直接传 0 就行,SOCK_STREAM 默认协议是 TCP,SOCK_DGRAM 默认协议是 UDP。

一些常见使用 protocol 参数的场景:

// 使用 SOCK_DGRAM 的非默认协议
socket(AF_INET, SOCK_DGRAM, PROT_ICMP)
// 创建 raw socket
socket(AF_INET, SOCK_RAW, PROT_ICMP)

Note

ping 发送 icmp 包可以使用 raw socket,也可以使用 udp socket,大部分情况下使用 udp socket,因为不需要 root 权限,目前只有使用 -N 参数的时候会使用 raw socket。ping 代码会自己判断使用哪个。

从 socket 中接收数据

以 udp socket 的 recvfrom 系统调用来说。

首先,这个系统调用是 recvmsg 系统调用的一个包装(wrapper), __sys_recvfrom 负责参数适配、返回值转换。 recvmsg 属于 socket 特有的方法,所有没有走文件操作

SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
   unsigned int, flags, struct sockaddr __user *, addr,
   int __user *, addr_len)
|- __sys_recvfrom
  |- struct sockaddr_storage address
  |- struct msghdr msg = {
  |    .msg_name = (struct sockaddr *)&address
  |- }
  |- struct iovec iov;
  |- err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
  |
  |- struct socket *sock = sockfd_lookup_light(fd)
  |- if sock->file->f_flags & O_NONBLOCK
  |    flags |= MSG_DONTWAIT
  |- sock_recvmsg(sock, &msg, flags);
  |
  |- move_addr_to_user(&address, msg.msg_namelen, addr, addr_len)

两个系统调用底层都是调用 sock_recvmsg 函数。这个函数中进行一下安全检查,然后调用 sock_recvmsg_nosec (nosec 是 no security 的缩写)。这个函数再依次调用 struct socketstruct sockrecvmsg 方法来进行真正的消息接收。

sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
|- security_socket_recvmsg
|- sock_recvmsg_nosec
   |- sock->ops->recvmsg/inet_recvmsg/inet6_recvmsg
      |- struct sock *sk = sock->sk
      |- sk->sk_prot->recvmsg/tcp_recvmsg/udp_recvmsg

对于 udp 协议来说,最后调用的就是 udp_recv 这个函数。

udp_recvmsg
|- int off = 0;
|- skb = __skb_recv_udp(sk, flags, &off, &err)
|- if udp_skb_is_linear(skb)
|    copy_linear_skb(skb, copied, off, &msg->msg_iter)
|- else
|    skb_copy_datagram_msg(skb, off, msg, copied)
|- ...
|- skb_consume_udp

udp_recv 函数中调用 __skb_recv_udp 从接收队列 sk->sk_receive_queue 中取出一个 skb(如果队列为空,可能会阻塞,直到有新 skb 到来时被 sk->sk_data_ready() 唤醒)。然后根据是否是线性 skb 调用不同的函数将数据 copy 到用户 buffer 中,最后通过 skb_consume_udp 将消费完的 skb free 掉。

__skb_recv_udp 中为了避免频繁对 sk->sk_receive_queue 加锁,会先将 sk_receive_queue 队列中的 skb 一次性全部获取到 sk->reader_queue 中(这个过程会对 sk_receive_queue 加锁),然后再一个一个消费 sk->reader_queue 中的 skb(此时只对 reader_queue 加锁),如此保证网络栈下半部分接收包时的性能。