socket可以理解为网络协议栈向上为应用层提供的接口。通过指定协议族可以使用不同的网络机制。

socket接口 系统调用陷入内核 socket系统服务 tcp/ip协议栈。系统调用的执行流程在系统调用补充:系统调用的开销在哪两个专题进行介绍过了。这里我们直接进入到内核当中的socket系统服务与tcp/ip网络协议栈当中。

socket数据结构

每个socket都有一个struct socketstruct 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_queuesk_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结构体。还值得注意的有两点

  1. socket buffer初始化
  2. 创建虚拟文件系统
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就可以支持多协议栈。