socket可以理解为网络协议栈向上为应用层提供的接口。通过指定协议族可以使用不同的网络机制。
socket接口 → 系统调用陷入内核 → socket系统服务 → tcp/ip协议栈。系统调用的执行流程在系统调用和补充:系统调用的开销在哪两个专题进行介绍过了。这里我们直接进入到内核当中的socket系统服务与tcp/ip网络协议栈当中。
socket数据结构
每个socket都有一个struct socket和struct sock数据结构实例。这两个实例都是独立于具体的协议族的,一个用于描述socket信息,一个记录内核管理socket的信息。
除此之外还有sk_buff结构体,它们之间的作用和关系如下
socket结构体 是用户态进程和内核网络协议栈的接口。它作为网络连接的抽象体,让用户可以通过系统调用操作网络连接。sock结构体 是内核协议栈中管理网络连接的核心结构体,它代表了网络连接的状态和协议控制逻辑。socket结构体中的操作最终都会通过sock结构体进行处理。sk_buff结构体 是网络数据包的封装,管理着数据包的内存和相关信息。所有的网络数据包在内核中都是通过sk_buff传递和处理。
具体的协议族和协议实例在此基础上,加上自己协议相关的属性和操作,就形成了管理这个协议的套接字结构。
struct socket结构体
这个结构体和套接字是一一对应的。描述了套接字的属性
/**
* struct socket - general BSD socket
* @state: socket state (%SS_CONNECTED, etc)
* @type: socket type (%SOCK_STREAM, etc)
* @flags: socket flags (%SOCK_NOSPACE, etc)
* @ops: protocol specific socket operations
* @file: File back pointer for gc
* @sk: internal networking protocol agnostic socket representation
* @wq: wait queue for several uses
*/
struct socket {
// socket状态
socket_state state;
// socket类型
short type;
// socketflags
unsigned long flags;
// 套接字所属的文件描述符
struct file *file;
// 与内部网络协议无感知的socket表示
struct sock *sk;
// 协议相关的socket操作
const struct proto_ops *ops; /* Might change with IPV6_ADDRFORM or MPTCP. */
// 等待队列
struct socket_wq wq;
};struct sock结构体
socket结构体是socket最外层的保证,在其内部还有struct sock这个结构体,其内部字段非常复杂,包含了大量内核管理套接字的信息。
其中最重要的成员放在了struct sock_common当中,并放在sock结构体的首部。sock_common是socket在网络层的最小表示
struct sock_common {
union {
__addrpair skc_addrpair; // 与下面skc_daddr和skc_rcv_saddr对应的64bit表示
struct {
__be32 skc_daddr; // 远端ipv4地址
__be32 skc_rcv_saddr; // 绑定的本地ipv4地址
};
};
union {
unsigned int skc_hash; // 查询哈希表的哈希值
__u16 skc_u16hashes[2]; // 用于udp查询表的两个u16哈希值
};
/* skc_dport && skc_num must be grouped as well */
union {
__portpair skc_portpair; //skc_dport和skc_num的32bit表示
struct {
__be16 skc_dport; // inet_dport/tw_dport的占位符
__u16 skc_num; // inet_num/tw_num的占位符
};
};
unsigned short skc_family; // 网络地址族
volatile unsigned char skc_state; // 连接状态
// 下面是一个字节的位码,表示一些特殊设置,如SO_REUSEADDR,SO_REUSEPORT,IPv6,使用net引用计数
unsigned char skc_reuse:4;
unsigned char skc_reuseport:1;
unsigned char skc_ipv6only:1;
unsigned char skc_net_refcnt:1;
int skc_bound_dev_if; // 不为0则表示绑定的设备索引
union {
struct hlist_node skc_bind_node; // bind哈希表
struct hlist_node skc_portaddr_node; // udp协议的第二个哈希表
};
struct proto *skc_prot; // 网络族内部的协议处理器
possible_net_t skc_net; // socket的网络命名空间的引用
#if IS_ENABLED(CONFIG_IPV6)
struct in6_addr skc_v6_daddr; // ipv6目的地址
struct in6_addr skc_v6_rcv_saddr; // ipv6源地址
#endif
atomic64_t skc_cookie; // socket的cookie值
/* following fields are padding to force
* offset(struct sock, sk_refcnt) == 128 on 64bit arches
* assuming IPV6 is enabled. We use this padding differently
* for different kind of 'sockets'
*/
union {
unsigned long skc_flags; // sk_flags的占位符
struct sock *skc_listener; /* request_sock */
struct inet_timewait_death_row *skc_tw_dr; /* inet_timewait_sock */
};
/*
* fields between dontcopy_begin/dontcopy_end
* are not copied in sock_copy()
*/
/* private: */
int skc_dontcopy_begin[0];
/* public: */
union {
struct hlist_node skc_node; // 用于各种协议查找的主哈希表
struct hlist_nulls_node skc_nulls_node; // tcp/udp协议的主哈希表
};
unsigned short skc_tx_queue_mapping; // 连接的发送队列号
#ifdef CONFIG_SOCK_RX_QUEUE_MAPPING
unsigned short skc_rx_queue_mapping; // 连接的接手队列号
#endif
union {
int skc_incoming_cpu; // 记录cpu处理的包
u32 skc_rcv_wnd; // tcp接收窗口大小
u32 skc_tw_rcv_nxt; // tcp窗口下一个期望的序列号
};
refcount_t skc_refcnt; // 引用计数
/* private: */
int skc_dontcopy_end[0];
union {
u32 skc_rxhash;
u32 skc_window_clamp;
u32 skc_tw_snd_nxt; /* struct tcp_timewait_sock */
};
/* public: */
};在sock_common中使用哈希表将sock组织起来,就是skc_node字段,以及使用skc_hash作为哈希值。这样所有的sock都能够从skc_node找到自己的信息。而套接字接收和发送消息用到的两个等待队列sk_receive_queue和sk_write_queue则是定义在了sock结构体当中。
在下面的sock结构体中还声明了sk_data_ready函数指针,用于回调处理。这是最频繁使用的回调函数,在用户进程等待数据到达时,就会调用此函数。
struct sock {
/*
* sock还被inet_timewait_sock使用当作sock_common使用,所以要求sock_common作为第一个字段,不能改变其内存布局
*/
struct sock_common __sk_common;
// 下面则是在sock结构体内部定义的宏方便外部像访问sock字段一样访问sock_common字段
#define sk_node __sk_common.skc_node
#define sk_nulls_node __sk_common.skc_nulls_node
#define sk_refcnt __sk_common.skc_refcnt
#define sk_tx_queue_mapping __sk_common.skc_tx_queue_mapping
#ifdef CONFIG_SOCK_RX_QUEUE_MAPPING
#define sk_rx_queue_mapping __sk_common.skc_rx_queue_mapping
#endif
#define sk_dontcopy_begin __sk_common.skc_dontcopy_begin
#define sk_dontcopy_end __sk_common.skc_dontcopy_end
#define sk_hash __sk_common.skc_hash
#define sk_portpair __sk_common.skc_portpair
#define sk_num __sk_common.skc_num
#define sk_dport __sk_common.skc_dport
#define sk_addrpair __sk_common.skc_addrpair
#define sk_daddr __sk_common.skc_daddr
#define sk_rcv_saddr __sk_common.skc_rcv_saddr
#define sk_family __sk_common.skc_family
#define sk_state __sk_common.skc_state
#define sk_reuse __sk_common.skc_reuse
#define sk_reuseport __sk_common.skc_reuseport
#define sk_ipv6only __sk_common.skc_ipv6only
#define sk_net_refcnt __sk_common.skc_net_refcnt
#define sk_bound_dev_if __sk_common.skc_bound_dev_if
#define sk_bind_node __sk_common.skc_bind_node
#define sk_prot __sk_common.skc_prot
#define sk_net __sk_common.skc_net
#define sk_v6_daddr __sk_common.skc_v6_daddr
#define sk_v6_rcv_saddr __sk_common.skc_v6_rcv_saddr
#define sk_cookie __sk_common.skc_cookie
#define sk_incoming_cpu __sk_common.skc_incoming_cpu
#define sk_flags __sk_common.skc_flags
#define sk_rxhash __sk_common.skc_rxhash
__cacheline_group_begin(sock_write_rx);
atomic_t sk_drops;
__s32 sk_peek_off;
struct sk_buff_head sk_error_queue;
struct sk_buff_head sk_receive_queue;
/*
* The backlog queue is special, it is always used with
* the per-socket spinlock held and requires low latency
* access. Therefore we special case it's implementation.
* Note : rmem_alloc is in this structure to fill a hole
* on 64bit arches, not because its logically part of
* backlog.
*/
struct {
atomic_t rmem_alloc;
int len;
struct sk_buff *head;
struct sk_buff *tail;
} sk_backlog;
#define sk_rmem_alloc sk_backlog.rmem_alloc
__cacheline_group_end(sock_write_rx);
__cacheline_group_begin(sock_read_rx);
/* early demux fields */
struct dst_entry __rcu *sk_rx_dst;
int sk_rx_dst_ifindex;
u32 sk_rx_dst_cookie;
#ifdef CONFIG_NET_RX_BUSY_POLL
unsigned int sk_ll_usec;
unsigned int sk_napi_id;
u16 sk_busy_poll_budget;
u8 sk_prefer_busy_poll;
#endif
u8 sk_userlocks;
int sk_rcvbuf;
struct sk_filter __rcu *sk_filter;
union {
struct socket_wq __rcu *sk_wq;
/* private: */
struct socket_wq *sk_wq_raw;
/* public: */
};
void (*sk_data_ready)(struct sock *sk);
long sk_rcvtimeo;
int sk_rcvlowat;
__cacheline_group_end(sock_read_rx);
__cacheline_group_begin(sock_read_rxtx);
int sk_err;
struct socket *sk_socket;
struct mem_cgroup *sk_memcg;
#ifdef CONFIG_XFRM
struct xfrm_policy __rcu *sk_policy[2];
#endif
__cacheline_group_end(sock_read_rxtx);
__cacheline_group_begin(sock_write_rxtx);
socket_lock_t sk_lock;
u32 sk_reserved_mem;
int sk_forward_alloc;
u32 sk_tsflags;
__cacheline_group_end(sock_write_rxtx);
__cacheline_group_begin(sock_write_tx);
int sk_write_pending;
atomic_t sk_omem_alloc;
int sk_sndbuf;
int sk_wmem_queued;
refcount_t sk_wmem_alloc;
unsigned long sk_tsq_flags;
union {
struct sk_buff *sk_send_head;
struct rb_root tcp_rtx_queue;
};
struct sk_buff_head sk_write_queue;
u32 sk_dst_pending_confirm;
u32 sk_pacing_status; /* see enum sk_pacing */
struct page_frag sk_frag;
struct timer_list sk_timer;
unsigned long sk_pacing_rate; /* bytes per second */
atomic_t sk_zckey;
atomic_t sk_tskey;
__cacheline_group_end(sock_write_tx);
__cacheline_group_begin(sock_read_tx);
unsigned long sk_max_pacing_rate;
long sk_sndtimeo;
u32 sk_priority;
u32 sk_mark;
struct dst_entry __rcu *sk_dst_cache;
netdev_features_t sk_route_caps;
#ifdef CONFIG_SOCK_VALIDATE_XMIT
struct sk_buff* (*sk_validate_xmit_skb)(struct sock *sk,
struct net_device *dev,
struct sk_buff *skb);
#endif
u16 sk_gso_type;
u16 sk_gso_max_segs;
unsigned int sk_gso_max_size;
gfp_t sk_allocation;
u32 sk_txhash;
u8 sk_pacing_shift;
bool sk_use_task_frag;
__cacheline_group_end(sock_read_tx);
/*
* Because of non atomicity rules, all
* changes are protected by socket lock.
*/
u8 sk_gso_disabled : 1,
sk_kern_sock : 1,
sk_no_check_tx : 1,
sk_no_check_rx : 1;
u8 sk_shutdown;
u16 sk_type;
u16 sk_protocol;
unsigned long sk_lingertime;
struct proto *sk_prot_creator;
rwlock_t sk_callback_lock;
int sk_err_soft;
u32 sk_ack_backlog;
u32 sk_max_ack_backlog;
kuid_t sk_uid;
spinlock_t sk_peer_lock;
int sk_bind_phc;
struct pid *sk_peer_pid;
const struct cred *sk_peer_cred;
ktime_t sk_stamp;
#if BITS_PER_LONG==32
seqlock_t sk_stamp_seq;
#endif
int sk_disconnects;
u8 sk_txrehash;
u8 sk_clockid;
u8 sk_txtime_deadline_mode : 1,
sk_txtime_report_errors : 1,
sk_txtime_unused : 6;
void *sk_user_data;
#ifdef CONFIG_SECURITY
void *sk_security;
#endif
struct sock_cgroup_data sk_cgrp_data;
void (*sk_state_change)(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;
#ifdef CONFIG_BPF_SYSCALL
struct bpf_local_storage __rcu *sk_bpf_storage;
#endif
struct rcu_head sk_rcu;
netns_tracker ns_tracker;
};套接字接口实现
sys_socket
在定义系统调用可以知道linux提供了相关的宏来辅助系统服务的实现。创建套接字使用的上层调用是socket,通过man socket可以查到其函数参数格式为3个,因此使用SYSCALL_DEFINE3宏来辅助函数定义。最终可以找到下面的代码
int __sys_socket(int family, int type, int protocol)
{
struct socket *sock;
int flags;
// 创建socket结构体
sock = __sys_socket_create(family, type,
update_socket_protocol(family, type, protocol));
if (IS_ERR(sock))
return PTR_ERR(sock);
// 设置flags
flags = type & ~SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
// 设置socket属性
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}就是填充好上面介绍的socket和sock结构体。还值得注意的有两点
- socket buffer初始化
- 创建虚拟文件系统
static int __init sock_init(void) {
int err;
sk_init();
// 初始化sk_buff缓存
skb_init();
init_inodecache();
/* 注册文件系统类型 */
err = register_filesystem(&sock_fs_type);
if (err) goto out_fs;
sock_mnt = kern_mount(&sock_fs_type);
if (IS_ERR(sock_mnt)) {
err = PTR_ERR(sock_mnt);
goto out_mount;
}
}socket buffer
socket buffer是网络包在内核中的数据结构,贯穿了整个协议栈,是非常重要的一个结构。 包含了网络数据和管理结构两部分,在发送和接受数据的过程中,各层协议的头信息会不断从数据包中插入和去掉。sk_buffer中描述协议头信息的地址指针也会不断赋值和置位。
在初始化socket的时候也同样进行sk_buffer的初始化。
虚拟文件
socket在应用这一层就是作为文件描述符一样使用的,所以在这里将socket与文件描述符进行了关联,在内核这一层根据再将对文件描述符的操作变更为了socket的操作。
至于是如何关联的,即使在linux虚拟文件系统vfs中,每个文件都有一个inode,那么为socket分配一个inode并关联起来即可
struct inode{
struct file_operation *i_fop // 指向默认文件操作函数块
}
struct socket_slloc {
struct socket socket;
struct inode vfs_inode;
}地址族与协议交换表
socket是支持多协议族的接口。每个协议族下又可以实现多个协议实例。
使用socket发送和接受数据,套接字层必须明确哪个套接字是当前数据包的目标套接字。内核当中使用inet_protosw来描述一个协议实例
struct inet_protosw {
struct list_head list;
unsigned short type; /* AF_INET协议族套接字的类型,如TCP为SOCK_STREAM*/
unsigned short protocol; /* 协议族中某个协议实例的编号。如TCP协议的编码为IPPROTO_TCP */
struct proto *prot; // 内核协议相关的操作函数块
const struct proto_ops *ops; // 系统调用套接字的操作函数块
unsigned char flags; /* 该套接字属性的相关标志 */
}通过这个结构体,可以将socket系统调用指定的套接字操作转为对应协议实例的操作函数调用。而通过这个结构体组成的数组inetsw_array,socket就可以支持多协议栈。