套接字的数据结构按照域的不同可以分为三种:用户态套接字、socket和sock,其中socket结构体是内核中的与用户态相似的套接字数据结构,可以理解为它是为用户态提供的一种接口,而sock结构体比较复杂,它是内核用来进行数据传输的数据结构,可以理解为它是套接字的实现。这三种套接字可谓息息相关。
这里的socket又被称为BSD socket
(伯克利套接字),它对应着网络模型中的表示层,其定义比较简单,只有7个字段,如下:
struct socket {
socket_state state;
short type;
unsigned long flags;
struct socket_wq *wq;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
};
proto_ops代表套接字操作函数的结构体,其函数对应关系如下:
inet_stream_ops | inet_dgram_ops | inet_sockraw_ops | |
---|---|---|---|
.family | PF_INET | PF_INET | PF_INET |
.owner | THIS_MODULE | THIS_MODULE | THIS_MODULE |
.release | inet_release | inet_release | inet_release |
.bind | inet_bind | inet_bind | inet_bind |
.connect | inet_stream_connect | inet_dgram_connect | inet_dgram_connect |
.socketpair | sock_no_socketpair | sock_no_socketpair | sock_no_socketpair |
.accept | inet_accept | sock_no_accept | sock_no_accept |
.getname | inet_getname | inet_getname | inet_getname |
.poll | tcp_poll | udp_poll | datagram_poll |
.ioctl | inet_ioctl | inet_ioctl | inet_ioctl |
.listen | inet_listen | sock_no_listen | sock_no_listen |
.shutdown | inet_shutdown | inet_shutdown | inet_shutdown |
.setsockopt | sock_common_setsockopt | sock_common_setsockopt | sock_common_setsockopt |
.getsockopt | sock_common_getsockopt | sock_common_getsockopt | sock_common_getsockopt |
.sendmsg | tcp_sendmsg | inet_sendmsg | inet_sendmsg |
.recvmsg | sock_common_recvmsg | sock_common_recvmsg | sock_common_recvmsg |
.mmap | sock_no_mmap | sock_no_mmap | sock_no_mmap |
.sendpage | tcp_sendpage | inet_sendpage | inet_sendpage |
.splice_read | tcp_splice_read |
下面我们来简单看一下数据发送时socket做了哪些工作。在用户空间创建套接字时,socket
系统调用会被调用,该系统调用会调用socket模块的sock_create
函数来进行套接字的创建。随后,sock_map_fd
函数被调用,该函数用于将socket中的file
指针与VFS建立联系,并将文件句柄返回给用户态。
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
int flags;
/* Check the SOCK_* constants for consistency. */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
goto out;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
在进行数据发送时,可以使用sendto
系统调用,这个函数首先会根据用户态传过来的文件句柄来查找对应的socket
,并构造msg
变量,这个变量可以理解为套接字所发送数据所需要的信息,包括所发送的数据内容、接收方的信息等。随后,sock_sendmsg
函数会被调用,这个函数会调用socket
的ops->sendmsg
方法。
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
unsigned int, flags, struct sockaddr __user *, addr,
int, addr_len)
{
struct socket *sock;
struct sockaddr_storage address;
int err;
struct msghdr msg;
struct iovec iov;
int fput_needed;
err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
err = sock_sendmsg(sock, &msg);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
struct sock
是网络层的套接字,从上图中我们可以看出网络协议栈各个部分都是使用该套接字作为数据结构的接口。每个sock
变量都会有一个与之关联的socket
和用户态套接字,它被用来存储连接的信息,常用的字段如下:
struct sock {
......
socket_lock_t sk_lock;
atomic_t sk_drops;
int sk_rcvlowat;
struct sk_buff_head sk_error_queue;
struct sk_buff_head sk_receive_queue;
struct sk_buff_head sk_write_queue;
struct sk_buff_head sk_error_queue;
......
struct {
atomic_t rmem_alloc;
int len;
struct sk_buff *head;
struct sk_buff *tail;
} sk_backlog;
unsigned int sk_padding : 1,
sk_kern_sock : 1,
sk_no_check_tx : 1,
sk_no_check_rx : 1,
sk_userlocks : 4,
sk_protocol : 8,
sk_type : 16;
......
struct socket *sk_socket;
void *sk_user_data;
truct page_frag sk_frag;
struct sk_buff *sk_send_head;
......
struct sock_cgroup_data sk_cgrp_data;
struct mem_cgroup *sk_memcg;
void (*sk_state_change)(struct sock *sk);
void (*sk_data_ready)(struct sock *sk);
void (*sk_write_space)(struct sock *sk);
void (*sk_error_report)(struct sock *sk);
int (*sk_backlog_rcv)(struct sock *sk,
struct sk_buff *skb);
void (*sk_destruct)(struct sock *sk);
struct sock_reuseport __rcu *sk_reuseport_cb;
struct rcu_head sk_rcu;
};
从上面的定义中我们可以看出,该结构体的所有字段都是以sk_
开头的,其:
sk_protocol
、sk_type
等字段与BSD socket
中的相同sk_socket
对应着BSD
套接字sk_receive_queue
是这个套接字接收到的skb队列sk_write_queue
是这个套接字要发送的skb链表sk_error_queue
错误队列sock
提供了三个队列:sk_receive_queue
、sk_write_queue
和sk_error_queue
,分别用来处理接收、发送的skb以及出错信息。skb_queue_tail
用于skb的入栈操作,skb_dequeue
用于skb的出栈操作。
作为协议在进行报文发送过程中所使用到的唯一用来保存协议及报文相关数据及状态的数据结构,不同的协议会根据其具体协议特性来添加新的字段。struct inet_sock
用来描述IP协议族的套接字,其中inet
指的是ip协议族,即L3和L4层的协议,该套接字是在sock
的基础上进行扩展的,其定义如下:
struct inet_sock {
struct sock sk;
#if IS_ENABLED(CONFIG_IPV6)
struct ipv6_pinfo *pinet6;
#endif
......
__be32 inet_saddr;
__s16 uc_ttl;
__u16 cmsg_flags;
__be16 inet_sport;
__u16 inet_id;
struct ip_options_rcu __rcu *inet_opt;
int rx_dst_ifindex;
__u8 tos;
__u8 min_ttl;
__u8 mc_ttl;
__u8 pmtudisc;
__u8 recverr:1,
is_icsk:1,
freebind:1,
hdrincl:1,
mc_loop:1,
transparent:1,
mc_all:1,
nodefrag:1;
__u8 bind_address_no_port:1;
__u8 rcv_tos;
__u8 convert_csum;
int uc_index;
int mc_index;
__be32 mc_addr;
struct ip_mc_socklist __rcu *mc_list;
struct inet_cork_full cork;
};
从其定义可以看出,虽然inet_sock
和sock
是不同的数据类型,单由于inet_sock
将sock
作为了其第一个数据成员,使得inet_sock
类型的变量也可以强制转换为sock
进行使用。
虽然INET
协议族使用的套接口数据结构都是struct inet_sock
,但各个协议都会对其再一次进行不同程度的扩展,以UDP协议为例,它在struct inet_sock
的基础上定义了struct udp_sock
,如下:
struct udp_sock {
struct inet_sock inet;
#define udp_port_hash inet.sk.__sk_common.skc_u16hashes[0]
#define udp_portaddr_hash inet.sk.__sk_common.skc_u16hashes[1]
#define udp_portaddr_node inet.sk.__sk_common.skc_portaddr_node
int pending; /* Any pending frames ? */
unsigned int corkflag; /* Cork is required */
__u8 encap_type; /* Is this an Encapsulation socket? */
unsigned char no_check6_tx:1,/* Send zero UDP6 checksums on TX? */
no_check6_rx:1;/* Allow zero UDP6 checksums on RX? */
__u16 len; /* total length of pending frames */
__u16 pcslen;
__u16 pcrlen;
__u8 unused[3];
int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);
void (*encap_destroy)(struct sock *sk);
struct sk_buff ** (*gro_receive)(struct sock *sk,
struct sk_buff **head,
struct sk_buff *skb);
int (*gro_complete)(struct sock *sk,
struct sk_buff *skb,
int nhoff);
};
当UDP协议收到skb包时,udp_rcv
函数会被调用,该函数随后会调用__udp4_lib_rcv
函数,在__udp4_lib_rcv
函数中会完成skb到sock
的交付。
首先我们来看一下,skb_steal_sock
函数会被调用,这个函数用来获取skb结构体中的*sock
字段(也不知道这个sock
是啥时候赋值进去的)。获取到sock
后,udp_queue_rcv_skb
会被调用,以将skb加到sock
的接收队列sk_receive_queue
中,然后调用sock_put
用来减少sock
的引用计数。注意,当sock
的引用计数为0时,该sock
会被销毁。
sk = skb_steal_sock(skb);
if (sk) {
struct dst_entry *dst = skb_dst(skb);
int ret;
if (unlikely(sk->sk_rx_dst != dst))
udp_sk_rx_dst_set(sk, dst);
ret = udp_queue_rcv_skb(sk, skb);
sock_put(sk);
/* a return value > 0 means to resubmit the input, but
* it wants the return to be -protocol, or 0
*/
if (ret > 0)
return -ret;
return 0;
}
当skb中没有找到sk,__udp4_lib_lookup_skb
函数会被调用,这个函数用于从udp_table
中进行sk的查找。udp_table
中的sk存储在两个哈希表中,一个是以dport
,即目的端口,为键值,记为Hash1
;另一个是以daddr
和dport
,即目的地址和端口,为键值,记为Hash2
。
在进行查找时,它会先从Hash1
中进行查找,当查找到的sk数量大于10的时候再从Hash2
中查找,从而加快查找的速度。通过这种方式查找,会获得一个sk的链表,通过计算链表中的sk与skb的匹配程度来选取一个最合适的sk来处理skb。当skb的sport
、saddr
、dport
、daddr
与sk一样时,认为他们完全匹配
,此时直接返回sk。
从上面的分析我们可以看出,当接收到skb时,与其sport
、saddr
、dport
、daddr
完全一致的sk会获得该skb的处理权。当找不到这样的sk时,daddr
为INADDR_ANY
且dport
与skb的dport
相同的sk会获得该skb的处理权,这种sk也就是监听dport
端口的套接字。
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk)
return udp_unicast_rcv_skb(sk, skb, uh);
在进行UDP数据发送时,其函数调用关系为:
sk->sendmsg -->inet_sendmsg -->udp_prot->udp_sendmsg
参考链接:Linux networking,Linux内核分析 - 网络[十二]:UDP模块 - socket