#220901 socket 和 sock¶
数据结构¶
内核中一个 socket 主要用 socket
、sock
这两个结构体来表示。因为 “In Linux, everything is a file”,所以 socket 会通过 file
封装成文件给用户态程序去使用。
struct sock¶
struct sock
以及这个结构体派生出的各种 xxx_sock
,实现具体的协议。各种 sock 有以下继承关系。
(上图中左侧直接继承自 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)
为例。
首先,调用
sock_alloc
创建 socket 结构体。然后,从全局数组 net_families 中取出 AF_INET 的构建函数
inet_create
并调用。这个函数中会调用sk_alloc
创建 sock 结构体 、挂载接口方法 、初始化结构体 。接口方法和初始化函数都存在 inetsw 全局列表中,根据 type 和 protocol 查找。最后,调用
sock_map_fd
将 socket 封装成文件并返回。
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 socket
和 struct sock
的 recvmsg
方法来进行真正的消息接收。
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
加锁),如此保证网络栈下半部分接收包时的性能。