TCP实现之:套接字

TCP实现之:套接字

套接字的数据结构按照域的不同可以分为三种:用户态套接字、socket和sock,其中socket结构体是内核中的与用户态相似的套接字数据结构,可以理解为它是为用户态提供的一种接口,而sock结构体比较复杂,它是内核用来进行数据传输的数据结构,可以理解为它是套接字的实现。这三种套接字可谓息息相关。

在这里插入图片描述

struct socket

这里的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;
};
  • state:套接字的状态(不是L4连接的状态),可用值为:
    • SS_FREE
    • SS_UNCONNECTED
    • SS_CONNECTING
    • SS_CONNECTED
    • SS_DISCONNECTING
  • type:套接字类型,与用户空间的相同
  • sock:套接字所关联的INET套接字
  • ops:套接字的操作函数。根据协议的不同,其处理函数也不同

proto_ops代表套接字操作函数的结构体,其函数对应关系如下:

inet_stream_opsinet_dgram_opsinet_sockraw_ops
.familyPF_INETPF_INETPF_INET
.ownerTHIS_MODULETHIS_MODULETHIS_MODULE
.releaseinet_releaseinet_releaseinet_release
.bindinet_bindinet_bindinet_bind
.connectinet_stream_connectinet_dgram_connectinet_dgram_connect
.socketpairsock_no_socketpairsock_no_socketpairsock_no_socketpair
.acceptinet_acceptsock_no_acceptsock_no_accept
.getnameinet_getnameinet_getnameinet_getname
.polltcp_polludp_polldatagram_poll
.ioctlinet_ioctlinet_ioctlinet_ioctl
.listeninet_listensock_no_listensock_no_listen
.shutdowninet_shutdowninet_shutdowninet_shutdown
.setsockoptsock_common_setsockoptsock_common_setsockoptsock_common_setsockopt
.getsockoptsock_common_getsockoptsock_common_getsockoptsock_common_getsockopt
.sendmsgtcp_sendmsginet_sendmsginet_sendmsg
.recvmsgsock_common_recvmsgsock_common_recvmsgsock_common_recvmsg
.mmapsock_no_mmapsock_no_mmapsock_no_mmap
.sendpagetcp_sendpageinet_sendpageinet_sendpage
.splice_readtcp_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函数会被调用,这个函数会调用socketops->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

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_protocolsk_type等字段与BSD socket中的相同
  • sk_socket对应着BSD套接字
  • sk_receive_queue是这个套接字接收到的skb队列
  • sk_write_queue是这个套接字要发送的skb链表
  • sk_error_queue错误队列

sock提供了三个队列:sk_receive_queuesk_write_queuesk_error_queue,分别用来处理接收、发送的skb以及出错信息。skb_queue_tail用于skb的入栈操作,skb_dequeue用于skb的出栈操作。

inet_sock

作为协议在进行报文发送过程中所使用到的唯一用来保存协议及报文相关数据及状态的数据结构,不同的协议会根据其具体协议特性来添加新的字段。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_socksock是不同的数据类型,单由于inet_socksock作为了其第一个数据成员,使得inet_sock类型的变量也可以强制转换为sock进行使用。

udp_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;另一个是以daddrdport,即目的地址和端口,为键值,记为Hash2

在进行查找时,它会先从Hash1中进行查找,当查找到的sk数量大于10的时候再从Hash2中查找,从而加快查找的速度。通过这种方式查找,会获得一个sk的链表,通过计算链表中的sk与skb的匹配程度来选取一个最合适的sk来处理skb。当skb的sportsaddrdportdaddr与sk一样时,认为他们完全匹配,此时直接返回sk。

从上面的分析我们可以看出,当接收到skb时,与其sportsaddrdportdaddr完全一致的sk会获得该skb的处理权。当找不到这样的sk时,daddrINADDR_ANYdport与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 networkingLinux内核分析 - 网络[十二]:UDP模块 - socket

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值