linux网络子系统分析(三)—— INET连接建立API分析之bind & listen

目录

一、概述

二、bind

2.1TCP bind

2.1.2 UDP bind

2.1.3 bind tips

2.2 listen


一、概述

前面分析了socket流程,这里继续分析inet连接建立的其他socket API

二、bind

bind函数声明如下:

  •  int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind就是将addr和创建的socket进行绑定,对inet(man 7 ip)来说,一个进程想要接收报文就要和本地接口地址(local interface address)进行绑定,就是一个pair (address, port),如果 address指定为INADDR_ANY,则绑定本地所有接口。当然如果没有调用bind函数,在connect或者listen的时候都会以(INADDR_ANY, random port)自动绑定。如果是其他address family,bind的方式有所不同,具体用法参见man,下面来看一下bind如何解决不同address family对应的addr的统一,其对sockaddr的定义如下:

struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

struct sockaddr_in {
  __kernel_sa_family_t	sin_family;	
  __be16		sin_port;	
  struct in_addr	sin_addr;	

  /* Pad to size of `struct sockaddr'. */
  unsigned char		__pad[__SOCK_SIZE__ - sizeof(short int) -
			sizeof(unsigned short int) - sizeof(struct in_addr)];
};

从上面可以看到,对于inet, 使用sockaddr_in{}对应sockaddr{}, 并以inet需要的地址(address, port) pair替代了sockaddr{}的sa_date域,这样再使用时强转一下就可以了。

接下来开始分析bind的实现,socket建立后,bind系统调用到inet_bind(sock->ops->bind)

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
	struct sock *sk = sock->sk;
	int err;

	/* If the socket has its own bind function then use it. (RAW) */
	if (sk->sk_prot->bind) {
		return sk->sk_prot->bind(sk, uaddr, addr_len);
	}

	return __inet_bind(sk, uaddr, addr_len, false, true);
}

sk->sk_prot->bind在tcp, udp协议下都是空,直接看__inet_bind(关键部分):

int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len,
		bool force_bind_address_no_port, bool with_lock)
{
    snum = ntohs(addr->sin_port);
    
    inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
	
    /* Make sure we are allowed to bind here. */
	if (snum || !(inet->bind_address_no_port ||
		      force_bind_address_no_port)) {
		if (sk->sk_prot->get_port(sk, snum)) {
			inet->inet_saddr = inet->inet_rcv_saddr = 0;
			err = -EADDRINUSE;
			goto out_release_sock;
		}
		err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
		if (err) {
			inet->inet_saddr = inet->inet_rcv_saddr = 0;
			goto out_release_sock;
		}
	}

        inet->inet_sport = htons(inet->inet_num);  
	inet->inet_daddr = 0;  
	inet->inet_dport = 0;  
}

这段函数主要将源IP和源port与对应的sk进行绑定,根据注释:

  • inet->inet_rcv_saddr       rcv_saddr is the one  used by hash lookups
  • inet->inet_saddr              saddr is used for transmit

绑定时要确定(address, port) pair是否可用,具体调用sk->sk_prot->get_port(sk, snum),检测是否冲突,该接口在tcp和udp中是不同的,下面会分析

最终,该函数确定了四元组中的两个:inet_saddr, inet_sport

2.1 TCP bind

在tcp中,get_port对应的具体实现是inet_csk_get_port.

inet_csk_get_port是为指定的sock指定一个本地port,如果bind时没有指定,需要系统分配——奇数端口,偶数留给connect,bind使用hash表记录不同的二元组,hash结构的位置在proto{}中,

union {
    struct inet_hashinfo *hashinfo;
    struct udp_table	*udp_table;
    struct raw_hashinfo	*raw_hash;
} h;

TCP使用inet_hashinfo{}

struct inet_hashinfo {
	struct inet_ehash_bucket	*ehash;
	spinlock_t			*ehash_locks;
	unsigned int			ehash_mask;
	unsigned int			ehash_locks_mask;

	struct inet_bind_hashbucket	*bhash;

	unsigned int			bhash_size;

	struct kmem_cache		*bind_bucket_cachep;

	struct inet_listen_hashbucket	listening_hash[INET_LHTABLE_SIZE]
					____cacheline_aligned_in_smp;

	atomic_t			bsockets;
};

可以看到TCP使用三个hash表,分别是ehash,bhash,listening_hash,bind时用到bhash,其他用到在说,如下图:

bind时根据snum进行hash,冲突时挂入inet_bind_bucket链表,这里考虑同一个port复用的情况——相同port放在同一个inet_bind_bucket中,如果可复用就加入到链表中(owners)。

下面开始分析代码,先来看传入的sport是0的情况:

[net/ipv4/inet_connection_sock.c]

	if (!port) {
		head = inet_csk_find_open_port(sk, &tb, &port);
		if (!head)
			return ret;
		if (!tb)
			goto tb_not_found;
		goto success;
	}

其核心函数inet_csk_find_open_port:

attempt_half = (sk->sk_reuse == SK_CAN_REUSE) ? 1 : 0;
other_half_scan:
	inet_get_local_port_range(net, &low, &high);
	high++; /* [32768, 60999] -> [32768, 61000[ */
	if (high - low < 4)
		attempt_half = 0;
	if (attempt_half) {
		int half = low + (((high - low) >> 2) << 1);

		if (attempt_half == 1)
			high = half;
		else
			low = half;
	}
	remaining = high - low;
	if (likely(remaining > 1))
		remaining &= ~1U;

	offset = prandom_u32() % remaining;
	/* __inet_hash_connect() favors ports having @low parity
	 * We do the opposite to not pollute connect() users.
	 */
	offset |= 1U;

初始化阶段inet_get_local_port_range先获取系统配置的端口范围,[low, high + 1),这里先不考虑端口reuse的情况.接下来计算在端口范围内查找的起始offset,可以看到是在remain范围内随机产生的,最后要保证offset是奇数,因为偶数给connect用

other_parity_scan:
	port = low + offset;
	for (i = 0; i < remaining; i += 2, port += 2) {
		if (unlikely(port >= high))
			port -= remaining;
		if (inet_is_local_reserved_port(net, port))
			continue;
		head = &hinfo->bhash[inet_bhashfn(net, port,
						  hinfo->bhash_size)];
		spin_lock_bh(&head->lock);
		inet_bind_bucket_for_each(tb, &head->chain)
			if (net_eq(ib_net(tb), net) && tb->port == port) {
				if (!inet_csk_bind_conflict(sk, tb, false, false))
					goto success;
				goto next_port;
			}
		tb = NULL;
		goto success;
next_port:
		spin_unlock_bh(&head->lock);
		cond_resched();
	}

	offset--;
	if (!(offset & 1))
		goto other_parity_scan;

	if (attempt_half == 1) {
		/* OK we now try the upper half of the range */
		attempt_half = 2;
		goto other_half_scan;
	}
	return NULL;

从随机选取的port = low + offset位置进行查找,保证遍历所有的奇数端口(),端口选择,根据随机的port hash选定一个slot,遍历slot上的链,这里的原则是,如果和port有冲突就重新选择port (next_port)那接下来看一下inet_csk_bind_conflict:

sk_for_each_bound(sk2, &tb->owners) {
	if (sk != sk2 &&
		(!sk->sk_bound_dev_if ||
		!sk2->sk_bound_dev_if ||
		sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
			if ((!reuse || !sk2->sk_reuse ||
			    sk2->sk_state == TCP_LISTEN) &&
			    (!reuseport || !sk2->sk_reuseport ||
			     rcu_access_pointer(sk->sk_reuseport_cb) ||
			     (sk2->sk_state != TCP_TIME_WAIT &&
			     !uid_eq(uid, sock_i_uid(sk2))))) {
				if (inet_rcv_saddr_equal(sk, sk2, true))
					break;
			}
			if (!relax && reuse && sk2->sk_reuse &&   //这个relax条件注意一下
			    sk2->sk_state != TCP_LISTEN) {
				if (inet_rcv_saddr_equal(sk, sk2, true))
					break;
			}
		}
	}

这里遍历tb->owners,检测冲突的条件。我们在bind时要限制(address, port)二元组的唯一性,bhash按照port进行hash,那么在port相同的条件下,检测冲突必然落在address的唯一性检查上,如下,如果sk和sk2相比较,只要满足下列任一条件,就会不产生冲突:

  • sk是同一个                                                                                                     sk是同一个了就无所谓冲突了
  • sk和sk2对应的sk_bound_dev_if 不是同一个                                                 不同的端口自然对应不同的地址
  • sk和sk2都指定sk_reuse,且此时sk2的状态不是TCP_LISTEN
  • sk和sk2都指定sk_reuseport且此时sk2->sk_state状态是TCP_WAIT
  • sk和sk2 IP地址不同                                                                                       同一个端口上不同的地址

当然了,用户也可以指定reuse控制复用。

而下面的代码是说即使sk和sk2都指定sk_reuse,且此时sk2的状态不是TCP_LISTEN,在 !relax情况下,只要IP地址相等还是认为是冲突

			if (!relax && reuse && sk2->sk_reuse &&   //这个relax条件注意一下
			    sk2->sk_state != TCP_LISTEN) {
				if (inet_rcv_saddr_equal(sk, sk2, true))
					break;
			}

接下分析当成功分配了一个sport后的情形,有两种情况:

1. hash未冲突(head!=0, tb=NULL)这时候要为sport新建一个tb,走下面的流程

tb_not_found:
	tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
				     net, head, port);
	if (!tb)
		goto fail_unlock;
tb_found:
	if (!hlist_empty(&tb->owners)) {
		if (sk->sk_reuse == SK_FORCE_REUSE)
			goto success;

		if ((tb->fastreuse > 0 && reuse) ||
		    sk_reuseport_match(tb, sk))
			goto success;
		if (inet_csk_bind_conflict(sk, tb, true, true))
			goto fail_unlock;
	}

可以看到首先新建了一个tb,此时tb一定是空的,不会执行tb_found流程,跳过去直接执行success流程

success:
	if (!hlist_empty(&tb->owners)) {
		tb->fastreuse = reuse;
		if (sk->sk_reuseport) {
			tb->fastreuseport = FASTREUSEPORT_ANY;
			tb->fastuid = uid;
			tb->fast_rcv_saddr = sk->sk_rcv_saddr;
			tb->fast_ipv6_only = ipv6_only_sock(sk);
		} else {
			tb->fastreuseport = 0;
		}
	} else {
		if (!reuse)
			tb->fastreuse = 0;
		if (sk->sk_reuseport) {
			if (!sk_reuseport_match(tb, sk)) {
				tb->fastreuseport = FASTREUSEPORT_STRICT;
				tb->fastuid = uid;
				tb->fast_rcv_saddr = sk->sk_rcv_saddr;
				tb->fast_ipv6_only = ipv6_only_sock(sk);
			}
		} else {
			tb->fastreuseport = 0;
		}
	}
	if (!inet_csk(sk)->icsk_bind_hash)
		inet_bind_hash(sk, tb, port);

此时,tb下的链表tb->owner一定是空的,走else流程,这里关注tb上的两个字段:tb->fastreuse和tb->fastreuseport,前面说过满足下面任意一个条件一定不会产生冲突

  • sk和sk2都指定sk_reuse,且此时sk2的状态不是TCP_LISTEN
  • sk和sk2都指定sk_reuseport且此时sk2->sk_state状态是TCP_WAIT

这两个标记的意思是如果标记置位,那么意味着tb的链中所有的sk都满足不产生冲突的条件,就不用再去调用很重的inet_csk_bind_conflict操作了,从而简化了判断过程。最后将sk挂入tb->owner进行管理,在这里源端口inet_num赋值,并记录tb

void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
		    const unsigned short snum)
{
	inet_sk(sk)->inet_num = snum;
	sk_add_bind_node(sk, &tb->owners);
	inet_csk(sk)->icsk_bind_hash = tb;
}

2.  hash冲突(head!=NULL,tb!=NULL)但是sport没有冲突,这时候省去了分配tb,执行tb_found流程,此时tb->owners一定不为空,为了方便看,再贴一下:

tb_found:
	if (!hlist_empty(&tb->owners)) {
		if (sk->sk_reuse == SK_FORCE_REUSE)
			goto success;

		if ((tb->fastreuse > 0 && reuse) ||
		    sk_reuseport_match(tb, sk))
			goto success;
		if (inet_csk_bind_conflict(sk, tb, true, true))
			goto fail_unlock;
	}

这里先进行的上面说的快速检索tb->fastreuse > 0 && reuse,sk_reuseport_match道理相同,但是其条件多一些,就封装成函数了。如果快速检索没有匹配,会再次调用inet_csk_bind_conflict检查冲突,注意这里和自动搜寻sport时参数是不一致的(false,false->true,true),上面是没有检查reuseport条件的,而且认为下面的代码是冲突行为:

if (!relax && reuse && sk2->sk_reuse &&
    sk2->sk_state != TCP_LISTEN) {
		if (inet_rcv_saddr_equal(sk, sk2, true))
			break;
}

接下来走success流程的tb非空还是在设置快速检索的方法,不再赘述。

继续分析bind指定了port的流程,流程上和前面差不多:

head = &hinfo->bhash[inet_bhashfn(net, port,
					  hinfo->bhash_size)];
spin_lock_bh(&head->lock);
inet_bind_bucket_for_each(tb, &head->chain)
	if (net_eq(ib_net(tb), net) && tb->port == port)
		goto tb_found;
tb_not_found:
...
tb_found:
...
success:
...

前面都分析过了,只是省去了搜寻过程而已。

2.2 UDP bind

UDP获取端口的函数是udp_v4_get_port

[net/ipv4/udp.c]

int udp_v4_get_port(struct sock *sk, unsigned short snum)
{
	unsigned int hash2_nulladdr =
		ipv4_portaddr_hash(sock_net(sk), htonl(INADDR_ANY), snum);
	unsigned int hash2_partial =
		ipv4_portaddr_hash(sock_net(sk), inet_sk(sk)->inet_rcv_saddr, 0);

	/* precompute partial secondary hash */
	udp_sk(sk)->udp_portaddr_hash = hash2_partial;
	return udp_lib_get_port(sk, snum, hash2_nulladdr);
}

接下来看udp_lib_get_port,还是先看bind未指定源端口的情况:

	if (!snum) {
		int low, high, remaining;
		unsigned int rand;
		unsigned short first, last;
		DECLARE_BITMAP(bitmap, PORTS_PER_CHAIN);

		inet_get_local_port_range(net, &low, &high);
		remaining = (high - low) + 1;

		rand = prandom_u32();
		first = reciprocal_scale(rand, remaining) + low;
		/*
		 * force rand to be an odd multiple of UDP_HTABLE_SIZE
		 */
		rand = (rand | 1) * (udptable->mask + 1);
		last = first + udptable->mask + 1;
        ...
}

(address, port)之间的关系仍然是通过hash实现的,对应下面的udp部分:

union {
	struct inet_hashinfo	*hashinfo;
	struct udp_table	*udp_table;
	struct raw_hashinfo	*raw_hash;
	struct smc_hashinfo	*smc_hash;
} h;

struct udp_table {
	struct udp_hslot	*hash;
	struct udp_hslot	*hash2;
	unsigned int		mask;
	unsigned int		log;
};

2.3 bind tips

  • 同一个socket多次绑定相同或者不同的端口

不允许重复bind或者绑定多个port,来自inet_bind,如果第一次绑定成功,对应的inet_sock的inet_num已经赋值,不允许继续执行了。

​
	/* Check these errors (active socket, double bind). */
	err = -EINVAL;
	if (sk->sk_state != TCP_CLOSE || inet->inet_num)
		goto out_release_sock;
  • 不同的socket绑定相同的端口(同一个协议,udp)

三、 listen

只有tcp需要listen,其函数声明:

  • int listen(int sockfd, int backlog);

listen将socket标记为passive,表明已经准备好接受新的连接,其实就是将状态机的状态从TCP_CLOSE变成TCP_LISTEN,即执行一个被动打开操作。

[net/ipv4/af_inet.c]

int inet_listen(struct socket *sock, int backlog)
{
	struct sock *sk = sock->sk;
	unsigned char old_state;
	int err, tcp_fastopen;

	lock_sock(sk);

	err = -EINVAL;
	if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
		goto out;

	old_state = sk->sk_state;
	if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
		goto out;

	if (old_state != TCP_LISTEN) {
		tcp_fastopen = sock_net(sk)->ipv4.sysctl_tcp_fastopen;
		if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
		    (tcp_fastopen & TFO_SERVER_ENABLE) &&
		    !inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
			fastopen_queue_tune(sk, backlog);
			tcp_fastopen_init_key_once(sock_net(sk));
		}

		err = inet_csk_listen_start(sk, backlog);
		if (err)
			goto out;
	}
	sk->sk_max_ack_backlog = backlog;
	err = 0;

out:
	release_sock(sk);
	return err;
}
EXPORT_SYMBOL(inet_listen);
  • 参数检查
  • sk状态,处于TCP_CLOSE状态可以进入TCP_LISTEN状态,处于TCP_LISTEN状态根据后面的逻辑只允许设置sk_max_ack_backlog

接下来看核心部分,有一个TCP_FASTOPEN的概念,这是一个优化点,目前暂时不分析了。直接看inet_csk_listen_start

int inet_csk_listen_start(struct sock *sk, int backlog)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct inet_sock *inet = inet_sk(sk);
	int err = -EADDRINUSE;

	reqsk_queue_alloc(&icsk->icsk_accept_queue);

	sk->sk_max_ack_backlog = backlog;
	sk->sk_ack_backlog = 0;
	inet_csk_delack_init(sk);

	inet_sk_state_store(sk, TCP_LISTEN);
	if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
		inet->inet_sport = htons(inet->inet_num);

		sk_dst_reset(sk);
		err = sk->sk_prot->hash(sk);

		if (likely(!err))
			return 0;
	}

	inet_sk_set_state(sk, TCP_CLOSE);
	return err;
}

首先,通过分配

void reqsk_queue_alloc(struct request_sock_queue *queue)
{
	spin_lock_init(&queue->rskq_lock);

	spin_lock_init(&queue->fastopenq.lock);
	queue->fastopenq.rskq_rst_head = NULL;
	queue->fastopenq.rskq_rst_tail = NULL;
	queue->fastopenq.qlen = 0;

	queue->rskq_accept_head = NULL;
}

我们知道bind可以通过某种方式复用(address, port),这主要是针对客户端,由于客户端可以自行指定远端主机地址,因此绑定多个本地地址是没有问题的。但是对于服务端来说,只能确定一个(address, port),因为服务端是被动连接,它不能区分client的连接由那个app处理。实现该唯一绑定的途径就是listen

注意到,listen时再次调用了分配端口号函数,这是因为bind和listen调用之间有一个race window。从上面我们知道对于bind,即使是同一个(address, port)也能绑定成功(如指定了sk->reuse), 但是这两者不能同时listen成功,即不能有多个app同时监听同一个连接。那么当一个进程listen时,需要检测当前sk是不是还是可用的(因为可能在这个race windows中其他的sk已经将状态变成TCP_LISTEN, 或者清除了sk->reuse),总之,再次调用get_port就是要保证TCP_LISTEN状态的sk对端口的独占。

我们看到在调用get_port函数时,先将TCP状态设置为TCP_LISTEN,这是没有问题的,虽然有race window的存在,但是结果对我们并没有影响——get_port要么有一个成功,要么都不成功。

最终将sk加入到listening_hash中

int inet_hash(struct sock *sk)
{
	int err = 0;
	if (sk->sk_state != TCP_CLOSE) {
		local_bh_disable();
		err = __inet_hash(sk, NULL);
		local_bh_enable();
	}
	return err;
}
int __inet_hash(struct sock *sk, struct sock *osk)
{
	struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
	struct inet_listen_hashbucket *ilb;
	int err = 0;

	if (sk->sk_state != TCP_LISTEN) {
		inet_ehash_nolisten(sk, osk);
		return 0;
	}
	WARN_ON(!sk_unhashed(sk));
	ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];

	spin_lock(&ilb->lock);
	if (sk->sk_reuseport) {
		err = inet_reuseport_add_sock(sk, ilb);
		if (err)
			goto unlock;
	}
	if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&
		sk->sk_family == AF_INET6)
		hlist_add_tail_rcu(&sk->sk_node, &ilb->head);
	else
		hlist_add_head_rcu(&sk->sk_node, &ilb->head);
	inet_hash2(hashinfo, sk);
	ilb->count++;
	sock_set_flag(sk, SOCK_RCU_FREE);
	sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);
unlock:
	spin_unlock(&ilb->lock);

	return err;
}

这里先不分析reuseport的情况了,只给出一张bind的示意图:

注意到inet_hash2(hashinfo, sk),对应hashinfo->lhash2,结构和上面一样,当lhash被初始化的时候,使用(address, port)作为key

四、小结

bind的作用是将(address, port)二元组和socket绑定,Linux实现中使用bhash这个以sport为key的hash函数来保证唯一性。所以当涉及到二元组的冲突检测时,一个条件是如果bhash冲突,那么优先考虑address是否是不同的:这体现在同一个网口的不同address或者不同网口不同address。当二元组完全相同的时候,还可以根据是否指定reuse,reuseport来进一步决定是否能复用。

虽然允许bind多个地址,但想要套接字进入监听状态,就一定要保证进程对socket是独占的,即不再允许复用,如果允许多个socket进入监听状态,那么用户连接来了不能确定那个进程。使用listen来完成这一功能。而listen的也对全连接队列的设置有影响,即指定完成三次握手sock最多的个数。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值