YouCompleteMe如何获得未使用的端口

intro

由于每次vim都启动一个ycmd服务进程,并且端口地址是由vim客户端指定的(因为ycmd启动之后vim客户端需要连接过去),所以vim在指定端口的时候就需要给出一个当前没有使用中的端口。

那么如何获得一个未使用的端口呢?

tsecer@harry: ps aux | fgrep ycmd
tsecer+ 3022084  0.9  2.5 1372356 840644 ?      Ssl  *   0:45 /usr/bin/python3.11 /home/tsecer/.vim/bundle/YouCompleteMe/python/ycm/../../third_party/ycmd/ycmd --port=52721 --options_file=/tmp/tmp32kne20x --log=info --idle_suicide_seconds=1800 --stdout=/tmp/ycmd_52721_stdout_g2udc2_0.log --stderr=/tmp/ycmd_52721_stderr_u7831z99.log
tsecer+ 3101254  0.0  0.1 2359252 33960 pts/8   Sl+ * 0:00 vim ../third_party/ycmd/ycmd/utils.py
tsecer+ 3101255  0.0  0.2 626420 73136 ?        Ssl  *   0:00 /usr/bin/python3.11 /home/tsecer/.vim/bundle/YouCompleteMe/python/ycm/../../third_party/ycmd/ycmd --port=56359 --options_file=/tmp/tmpxbiz25f7 --log=info --idle_suicide_seconds=1800 --stdout=/tmp/ycmd_56359_stdout_epz8p7zc.log --stderr=/tmp/ycmd_56359_stderr_3_4qdtui.log
tsecer+ 3164762  0.0  0.0  13212  1088 pts/2    S+   *   0:00 grep -F --color=auto ycmd
tsecer@harry: 

Linux系统中有一些系统调用可以让操作系统自动分配一个可用的端口。例如sendto

Call sendto without calling bind first, the socket will be bound automatically (to a free port).

或者通过bind一个0端口。

Another option is to specify port 0 to bind(). That will allow you to bind to a specific IP address (in case you have multiple installed) while still binding to a random port. If you need to know which port was picked, you can use getsockname() after the binding has been performed.

这两种方法都有一个进程安全的问题:执行系统调用之后需要保持socket一直存在,否则理论上这个socket关闭之后可能会分配给下一个进程(尽管和pid一样,port应该不会立马回收,而是尽量先单调递增)。

ycm

可以看到,ycm是通过YouCompleteMe类的_SetUpServer函数来设置server信息,并且端口是通过utils.GetUnusedLocalhostPort函数获得。

# ~/.vim/bundle/YouCompleteMe/python/ycm/youcompleteme.py
class YouCompleteMe:
  def __init__( self, default_options = {} ):
    self._logger = logging.getLogger( 'ycm' )
    self._client_logfile = None
    self._server_stdout = None
    self._server_stderr = None
    self._server_popen = None
    self._default_options = default_options
    self._ycmd_keepalive = YcmdKeepalive()
    self._SetUpLogging()
    self._SetUpServer()
    self._ycmd_keepalive.Start()


  def _SetUpServer( self ):
    self._available_completers = {}
    self._user_notified_about_crash = False
    self._filetypes_with_keywords_loaded = set()
    self._server_is_ready_with_cache = False
    self._message_poll_requests = {}
    ###...
        # The temp options file is deleted by ycmd during startup.
    with NamedTemporaryFile( delete = False, mode = 'w+' ) as options_file:
      json.dump( options_dict, options_file )

    server_port = utils.GetUnusedLocalhostPort()

    BaseRequest.server_location = 'http://127.0.0.1:' + str( server_port )
    BaseRequest.hmac_secret = hmac_secret

函数实现的确是通过bind获得之后马上关闭socket。

### ~/.vim/bundle/YouCompleteMe/third_party/ycmd/ycmd/utils.py
def GetUnusedLocalhostPort():
  sock = socket.socket()
  # This tells the OS to give us any free port in the range [1024 - 65535]
  sock.bind( ( '', 0 ) ) 
  port = sock.getsockname()[ 1 ] 
  sock.close()
  return port

kernel

看起来是随机未使用端口,而不是连续的。

///@file:linux\net\ipv4\inet_connection_sock.c
/*
 * Find an open port number for the socket.  Returns with the
 * inet_bind_hashbucket locks held if successful.
 */
static struct inet_bind_hashbucket *
inet_csk_find_open_port(const struct sock *sk, struct inet_bind_bucket **tb_ret,
			struct inet_bind2_bucket **tb2_ret,
			struct inet_bind_hashbucket **head2_ret, int *port_ret)
{
	struct inet_hashinfo *hinfo = tcp_or_dccp_get_hashinfo(sk);
	int i, low, high, attempt_half, port, l3mdev;
	struct inet_bind_hashbucket *head, *head2;
	struct net *net = sock_net(sk);
	struct inet_bind2_bucket *tb2;
	struct inet_bind_bucket *tb;
	u32 remaining, offset;
	bool relax = false;

	l3mdev = inet_sk_bound_l3mdev(sk);
ports_exhausted:
	attempt_half = (sk->sk_reuse == SK_CAN_REUSE) ? 1 : 0;
other_half_scan:
	inet_sk_get_local_port_range(sk, &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 = get_random_u32_below(remaining);
	/* __inet_hash_connect() favors ports having @low parity
	 * We do the opposite to not pollute connect() users.
	 */
	offset |= 1U;

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);
		if (inet_use_bhash2_on_bind(sk)) {
			if (inet_bhash2_addr_any_conflict(sk, port, l3mdev, relax, false))
				goto next_port;
		}

		head2 = inet_bhashfn_portaddr(hinfo, sk, net, port);
		spin_lock(&head2->lock);
		tb2 = inet_bind2_bucket_find(head2, net, port, l3mdev, sk);
		inet_bind_bucket_for_each(tb, &head->chain)
			if (inet_bind_bucket_match(tb, net, port, l3mdev)) {
				if (!inet_csk_bind_conflict(sk, tb, tb2,
							    relax, false))
					goto success;
				spin_unlock(&head2->lock);
				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;
	}

	if (READ_ONCE(net->ipv4.sysctl_ip_autobind_reuse) && !relax) {
		/* We still have a chance to connect to different destinations */
		relax = true;
		goto ports_exhausted;
	}
	return NULL;
success:
	*port_ret = port;
	*tb_ret = tb;
	*tb2_ret = tb2;
	*head2_ret = head2;
	return head;
}

outro

ycm使用bind系统调用,传入参数为0的端口地址(INADDR_ANY ),由操作系统分配端口之后再销毁socket。尽管这种操作理论上不是进程安全的,但是考虑到内核端口的分配策略和vim的使用场景(启动频率极低),所以在实践中应该不会出现并发导致的问题。

posted on 2024-05-16 18:15  tsecer  阅读(9)  评论(0编辑  收藏  举报

导航