TCP之系统调用bind()

这篇笔记记录了TCP协议对bind()系统调用的实现。

1. 概述

应用程序可以通过bind()系统调用将套接字和本地地址绑定,这里的地址包括L3的IP地址和L4的端口,应用程序可以只指定其中一个,另外一个由内核自动选择。

这里我们不关注IP地址的绑定过程,因为这没什么好看的,就是校验地址合法性,然后保存到内核相关数据结构中即可;这里重点看L4端口的绑定过程。

2. 端口信息的管理

一般来讲,一个端口不能同时分配给两个套接字使用(这里不考虑端口复用的情形,这是另外一个复杂的话题),这样可能会破坏五元组的唯一性,导致数据收发的混乱。所以TCP有必要将那些已分配的端口维护起来,这样在绑定过程中,才能快速的识别是否能够绑定成功。

2.1 端口信息

内核定义了struct inet_bind_bucket来表示一个已经被绑定了的端口,每一个已绑定端口对应一个该结构,该结构定义如下:

struct inet_bind_bucket {
	struct net		*ib_net;
	//端口号,主机字节序
	unsigned short		port;
	//端口复用相关
	signed short		fastreuse;
	//用于将inet_bind_bucket结构组织成哈希列表
	struct hlist_node	node;
	//端口被分配给了哪个套接字。由于端口可能被多个套接字复用,所以这里使用哈希链表
	//该链表的元素为struct tcp_sock
	struct hlist_head	owners;
};

可见,数据结构的定义相对来说是直接的,包含了端口号和对应的TCB(传输控制块)。

2.2 已绑定端口信息哈希表

整个TCP层面使用哈希表为来组织已绑定端口信息,即上面的struct inet_bind_bucket。哈希表是一个全局结构,其占用内存在TCP协议初始化函数tcp_init()执行过程中分配。

path: net/ipv4/tcp_ipv4.c

struct inet_hashinfo __cacheline_aligned tcp_hashinfo;
//inet_hashinfo是TCP层面的多个哈希表的集合,下面只列出了和端口管理相关的字段
struct inet_hashinfo {
	...
	//指向已绑定端口哈希表,哈希表占用内存在tcp_init()中分配
	struct inet_bind_hashbucket	*bhash;
	//bhash哈希表的桶大小,必要时会扩大哈希表的容量以提升效率
	unsigned int			bhash_size;

	//保护对该结构成员的互斥访问
	rwlock_t			lhash_lock ____cacheline_aligned;
	//对该结构的引用计数
	atomic_t			lhash_users;
	//指向一个用于分配struct inet_bind_bucket的高速缓存,该缓存同样在tcp_init()中创建
	struct kmem_cache			*bind_bucket_cachep;
};

从上面可以看到,哈希桶中表头元素并不是struct inet_bind_bucket,而是struct inet_bind_hashbucket,该结构定义如下,表头元素定义了一个自旋锁,这样可以降低锁的粒度,提升该哈希表的效率。

struct inet_bind_hashbucket {
	spinlock_t		lock;
	struct hlist_head	chain;
};

最后,已分配端口信息的哈希表组织结构如下图所示:
在这里插入图片描述

3. bind()系统调用实现

我们直接略过文件系统和通用套接字层的处理,从协议族的处理开始看起。

3.1 inet_bind()

inet_bind()是AF_INET协议族提供的处理bind()系统调用的接口。

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
	struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
	struct sock *sk = sock->sk;
	struct inet_sock *inet = inet_sk(sk);
	unsigned short snum;
	int chk_addr_ret;
	int err;

	//如果传输层提供了bind()接口,则直接使用传输层的接口完成绑定;
	//IPv4协议族中只有RAW套接字实现了该接口
	if (sk->sk_prot->bind) {
		err = sk->sk_prot->bind(sk, uaddr, addr_len);
		goto out;
	}
	//校验地址信息结构是否是AF_INET协议族的地址结构
	err = -EINVAL;
	if (addr_len < sizeof(struct sockaddr_in))
		goto out;

	//识别应用程序指定的IP地址类型
	chk_addr_ret = inet_addr_type(&init_net, addr->sin_addr.s_addr);
	//这里涉及较多的新概念,不过其大体意思是判定应用程序是否可以绑定到某些特别的IP地址上面
	/* Not specified by any standard per-se, however it breaks too
	 * many applications when removed.  It is unfortunate since
	 * allowing applications to make a non-local bind solves
	 * several problems with systems using dynamic addressing.
	 * (ie. your servers still start up even if your ISDN link
	 *  is temporarily down)
	 */
	err = -EADDRNOTAVAIL;
	if (!sysctl_ip_nonlocal_bind &&
	    !inet->freebind &&
	    addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
	    chk_addr_ret != RTN_LOCAL &&
	    chk_addr_ret != RTN_MULTICAST &&
	    chk_addr_ret != RTN_BROADCAST)
		goto out;

	//系统调用参数指定要绑定的端口,0表示有内核自动绑定一个端口
	snum = ntohs(addr->sin_port);
	err = -EACCES;
	//如果应用程序指定了想要绑定的端口(不为0),并且指定的端口号小于1024,
	//那么需要判端调用者是否有权限绑定这些保留端口,如果没有绑定则绑定失败
	if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
		goto out;

	/*      We keep a pair of addresses. rcv_saddr is the one
	 *      used by hash lookups, and saddr is used for transmit.
	 *
	 *      In the BSD API these are the same except where it
	 *      would be illegal to use them (multicast/broadcast) in
	 *      which case the sending device address is used.
	 */
	lock_sock(sk);

	/* Check these errors (active socket, double bind). */
	err = -EINVAL;
	//如果TCB的状态不是CLOSE或者该TCB已经绑定过了(绑定后的源端口信息会被保存
	//到inet->num中,见下文),那么绑定失败,可以看出内核不允许重复调用bind()
	if (sk->sk_state != TCP_CLOSE || inet->num)
		goto out_release_sock;

	//将应用程序指定要绑定的地址保存到TCB中。关于这两个地址的区别,待研究
	inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;
	if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
		inet->saddr = 0;  /* Use device */

	//调用传输层协议提供的接口执行具体的端口绑定:
	//TCP为inet_csk_get_port();UDP为udp_v4_get_port(),
	if (sk->sk_prot->get_port(sk, snum)) {
		//返回非0值,绑定失败,返回地址被使用错误
		inet->saddr = inet->rcv_saddr = 0;
		err = -EADDRINUSE;
		goto out_release_sock;
	}

	//设置地址和端口绑定标记到TCB中
	if (inet->rcv_saddr)
		sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
	if (snum)
		sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
	//已绑定端口的网络字节序表示保存到inet->sport中
	inet->sport = htons(inet->num);
	inet->daddr = 0;
	inet->dport = 0;
	//复位路由信息
	sk_dst_reset(sk);
	err = 0;
out_release_sock:
	release_sock(sk);
out:
	return err;
}

注:inet_bind()属于AF_INET协议族层面的绑定处理,所以UDP的绑定也会执行该函数。

3.2 inet_csk_get_port()

TCP协议的端口绑定过程由函数inet_csk_get_port()完成,在看该函数实现之前,先要理清楚该函数要完成的工作:

  1. 如果应用程序没有指明要绑定的端口,那么首先分配一个可用的端口;
  2. 拿到了可用端口后,如果是尚未分配的,那么需要创建对应的端口信息,即struct inet_bind_bucket,并初始化其中的各个字段;如果是已经分配了的端口,那么发生了端口复用,更新已有的struct inet_bind_bucket字段即可;
  3. 工作完成后,将struct inet_bind_bucket结构加入到TCP的bhash中。
//snum就是应用调用bind()时指定的端口号
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
	struct inet_hashinfo *hashinfo = sk->sk_prot->hashinfo;
	struct inet_bind_hashbucket *head;
	struct hlist_node *node;
	struct inet_bind_bucket *tb;
	int ret;
	struct net *net = sk->sk_net;

	local_bh_disable();
	if (!snum) {
		//应用没有指明要绑定哪个端口,需要由内核自动选择一个
		int remaining, rover, low, high;
		//获取可用于动态绑定的端口区间[low,high],这是由两个系统参数指定的值
		inet_get_local_port_range(&low, &high);
		
		//下面循环的核心目的就是找一个可用的端口号,而且尽可能的保证寻找过程具有一定的随机性;
		//这样可以保证动态分配的端口号能够尽可能均匀的分布在bhash中
		
		//初始化remaining为端口区间的长度,下面会尝试在[low,high]之间
		//找一个可用端口,所以remaining代表的就是最大循环次数
		remaining = (high - low) + 1;
		//随机选取一个循环起点
		rover = net_random() % remaining + low;

		do {
			//获取端口号对应哈系表表头,哈希算法就是“端口号%哈希表长度”
			head = &hashinfo->bhash[inet_bhashfn(rover, hashinfo->bhash_size)];
			spin_lock(&head->lock);
			//遍历该哈希表,一旦该列表中有相同的端口号(已经被绑定了)则继续轮询下一个
			//端口号(跳到netx标签处),由此可见,动态绑定是永远都不会复用已绑定端口的
			inet_bind_bucket_for_each(tb, node, &head->chain)
				if (tb->ib_net == net && tb->port == rover)
					goto next;
			//到这里,说明rover就是一个可用的空闲端口,结束查找过程
			break;
		next:
			spin_unlock(&head->lock);
			//轮询到达动态端口区间上界,则从下界开始继续轮询
			if (++rover > high)
				rover = low;
		} while (--remaining > 0);

		/* Exhausted local port range during search?  It is not
		 * possible for us to be holding one of the bind hash
		 * locks if this test triggers, because if 'remaining'
		 * drops to zero, we broke out of the do/while loop at
		 * the top level, not from the 'break;' statement.
		 */
		ret = 1;
		//remaining小于等于0,说明上面没有找到空闲端口
		if (remaining <= 0)
			goto fail;

		//将找到的空闲端口号记录到snum中
		/* OK, here is the one we will use.  HEAD is
		 * non-NULL and we hold it's mutex.
		 */
		snum = rover;
	} else {
		//应用程序指明了要绑定的端口号,直接找到对应的哈希列表
		head = &hashinfo->bhash[inet_bhashfn(snum, hashinfo->bhash_size)];
		spin_lock(&head->lock);
		//遍历该哈希列表:
		//1. 如果能够找到该端口号,说明该端口号已经被其它套接字绑定过了,这时需要跳转到
		//		tb_found标签处继续判断该端口是否允许复用.
		//2. 如果该循环没有找到该端口号,那么说明应用程序指定的端口号还没有被绑定过
		inet_bind_bucket_for_each(tb, node, &head->chain)
			if (tb->ib_net == net && tb->port == snum)
				goto tb_found;
	}
	//到这里有两种情况:
	//1. 动态绑定场景:找到了一个可用的空闲端口号
	//2. 应用程序指定了端口号场景:该端口号尚未被任何套接字绑定过
	//这两种场景都需要跳转到tb_not_found处创建端口信息结构struct inet_bind_bucket,
	//并将其加入到TCP的bhash表中
	tb = NULL;
	goto tb_not_found;
tb_found:
	//到这里说明要绑定的端口已经被其它套接字绑定,这时需要判断端口是否允许被复用。
	//这里之所以还要判断owners链表不为空,是为了让该函数提供端口检查的功能:即判
	//断是否已经当前套接字是否已经绑定了指定端口,如果绑定了,那么直接返回成功,见
	//《TCP之系统调用listen()》中有关该功能的用法

	//下面实际上就是判断端口是否可以复用的逻辑,如果判断可以复用,那么绑定成功,否则绑定失败
	if (!hlist_empty(&tb->owners)) {
		if (sk->sk_reuse > 1)
			goto success;
		if (tb->fastreuse > 0 &&
		    sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
			goto success;
		} else {
			ret = 1;
			if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb))
				goto fail_unlock;
		}
	}
tb_not_found:
	ret = 1;
	//根据snum创建一个新的端口信息结构并将该结构加入到bhash中
	if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
					net, head, snum)) == NULL)
		goto fail_unlock;
	//设置struct inet_bind_bucket中的复用标记
	if (hlist_empty(&tb->owners)) {
		if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
			tb->fastreuse = 1;
		else
			tb->fastreuse = 0;
	}
	else if (tb->fastreuse &&  (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
		tb->fastreuse = 0;
success:
	//使得TCB的icsk_bind_hash成员指向端口信息结构,并将该TCB加入到端口信息的owner链表中,
	//即建立TCB和端口信息结构之间的相互关联关系
	if (!inet_csk(sk)->icsk_bind_hash)
		inet_bind_hash(sk, tb, snum);
	BUG_TRAP(inet_csk(sk)->icsk_bind_hash == tb);
	ret = 0;

fail_unlock:
	spin_unlock(&head->lock);
fail:
	local_bh_enable();
	return ret;
}

void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
		    const unsigned short snum)
{
	inet_sk(sk)->num = snum;
	//将TCB加入到端口信息接口的owner链表中
	sk_add_bind_node(sk, &tb->owners);
	inet_csk(sk)->icsk_bind_hash = tb;
}

3.2.1 动态端口范围

从上面可以看出,动态指定的端口是取自一个区间的,而且动态指定的端口一定是还没有被任何套接字绑定过的端口。可以通过/proc/sys/net/ipv4/ip_local_port_range设置这两个参数。

/*
 * This array holds the first and last local port number.
 */
int sysctl_local_port_range[2] = { 32768, 61000 };
DEFINE_SEQLOCK(sysctl_port_range_lock);

void inet_get_local_port_range(int *low, int *high)
{
	unsigned seq;
	do {
		seq = read_seqbegin(&sysctl_port_range_lock);
		//动态可分配的端口区间是由下面的系统参数确认的,代码默认范围为[32768, 61000]
		*low = sysctl_local_port_range[0];
		*high = sysctl_local_port_range[1];
	} while (read_seqretry(&sysctl_port_range_lock, seq));
}

4. 小结

这里并没有介绍判断端口复用的细节,因为要想理解这段代码,还有许多概念需要理解,所以这里暂时不深究,等需要时再来仔细研究。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值