TCP之系统调用listen()

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

1. 概述

服务器端程序需要调用listen()系统调用将socket状态由TCP_CLOSE迁移到TCP_LISTEN,这样该套接字才能处理来自客户端的SYN请求。

2. 监听套接字的管理

为了查询方便,TCP协议将所有的监听套接字用全局的哈希表管理起来,哈希表信息如下:

path: net/ipv4/tcp_ipv4.c
struct inet_hashinfo __cacheline_aligned tcp_hashinfo;

/* This is for listening sockets, thus all sockets which possess wildcards. */
#define INET_LHTABLE_SIZE	32	/* Yes, really, this is all you need. */

//inet_hashinfo是TCP层面的多个哈希表的集合,下面只列出了和监听套接字管理相关的字段
struct inet_hashinfo {
	...
	/* All sockets in TCP_LISTEN state will be in here.  This is the only
	 * table where wildcard'd TCP sockets can exist.  Hash function here
	 * is just local port number.
	 */
	struct hlist_head		listening_hash[INET_LHTABLE_SIZE];

	//保护对该结构成员的互斥访问
	rwlock_t			lhash_lock ____cacheline_aligned;
	//对该结构的引用计数
	atomic_t			lhash_users;
};

TCP对监听套接字的组织可以用下图表示:
在这里插入图片描述

3. listen()系统调用实现

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

3.1 inet_listen()

inet_listen()主要是进行一些条件检查,以判断当前套接字是否允许进行侦听处理,代码如下:

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

	lock_sock(sk);

	//套接口层socket的状态应该是未连接的、类型必须是SOCK_STREAM,
	//从这里可以看到,UDP套接字是不可以调用listen()的
	err = -EINVAL;
	if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
		goto out;

	//只能是CLOSE或LISTEN状态的套接口才能调用listen()
	old_state = sk->sk_state;
	if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
		goto out;

	/* Really, if the socket is already in listen state
	 * we can only allow the backlog to be adjusted.
	 */
	if (old_state != TCP_LISTEN) {
		//创建连接请求队列;并将TCB状态迁移到TCP_LISTEN
		err = inet_csk_listen_start(sk, backlog);
		if (err)
			goto out;
	}
	//将用户指定的backlog更新到套接字的sk_max_ack_backlog中,该变量就是
	//accept连接队列所允许的最大值,即如果服务器端程序迟迟不调用accept,那
	//么一旦已连接套接字超过该限定值,那么三次握手将无法完成,客户端会出现连
	//接失败的问题
	sk->sk_max_ack_backlog = backlog;
	err = 0;

out:
	release_sock(sk);
	return err;
}

从上面的代码逻辑来看,应用程序是可以通过多次调用listen()修改sk_max_ack_backlog参数的。

3.2 inet_csk_listen_start()

//nr_table_entries就是listen()调用是用户程序传入的backlog参数
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
	struct inet_sock *inet = inet_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);

	//根据用户指定的backlog值分配SYN请求队列,该函数分析见下文
	int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
	if (rc != 0)
		return rc;

	//初始化该监听套接字TCB中的计数变量
	sk->sk_max_ack_backlog = 0;
	sk->sk_ack_backlog = 0;
	//清零延迟ACK相关数据成员
	inet_csk_delack_init(sk);

	/* There is race window here: we announce ourselves listening,
	 * but this transition is still not validated by get_port().
	 * It is OK, because this socket enters to hash table only
	 * after validation is complete.
	 */
	//将TCB的状态迁移到TCP_LISTEN
	sk->sk_state = TCP_LISTEN;
	//如果listen()之前已经bind()过,那么该函数会直接返回0;如果之前未bind()过,
	//那么该函数会为该监听套接字绑定一个端口,即实现自动绑定。关于端口绑定,可以参
	//考《TCP之系统调用bind()》
	if (!sk->sk_prot->get_port(sk, inet->num)) {
		//端口绑定成功
		inet->sport = htons(inet->num);
		//路由相关操作
		sk_dst_reset(sk);
		//监听状态的套接字需要注册到TCP的监听套接字散列表中,即(tcphashinfo->listening_hash)
		//对于TCP,该回调函数实际上是inet_hash(),见下文
		sk->sk_prot->hash(sk);

		return 0;
	}
	//绑定端口出错时,设置TCB状态为TCP_CLOSE,并且销毁已经分配的accept连接队列和SYN请求队列
	sk->sk_state = TCP_CLOSE;
	__reqsk_queue_destroy(&icsk->icsk_accept_queue);
	return -EADDRINUSE;
}

3.2.1 创建连接请求队列

listen()的第一个核心操作就是为监听套接字分配连接请求队列,即inet_csk_listen_start()中的reqsk_queue_alloc(),下面看其实现。

int reqsk_queue_alloc(struct request_sock_queue *queue,
		      unsigned int nr_table_entries)
{
	size_t lopt_size = sizeof(struct listen_sock);
	struct listen_sock *lopt;

	//从下面的逻辑可以看出,backlog是如何影响半连接队列的哈希桶大小的:
	//1. 如果用户指定的backlog超过了系统最大值(/proc/sys/net/ipv4/tcp_max_syn_backlog),
	//	  那么取系统允许的最大值
	//2. 确保哈系桶的大小不小于8
	//最后通过roundup_pow_of_two()向上调整,使得最终的nr_table_entries值为2的整数幂
	nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
	nr_table_entries = max_t(u32, nr_table_entries, 8);
	nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
    
	//为request_sock和其内部的散列表listen_opt分配内存空间
	lopt_size += nr_table_entries * sizeof(struct request_sock *);
	if (lopt_size > PAGE_SIZE)
		lopt = __vmalloc(lopt_size,
			GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO,
			PAGE_KERNEL);
	else
		lopt = kzalloc(lopt_size, GFP_KERNEL);
	if (lopt == NULL)
		return -ENOMEM;

	//初始化max_qlen_log为nr_table_entries以2为底的对数,即2^max_qlen_log=nr_table_entries
	for (lopt->max_qlen_log = 3;
	     (1 << lopt->max_qlen_log) < nr_table_entries;
	     lopt->max_qlen_log++);

	//生成一个随机数,该随机数用于访问listen_opt哈希表时计算哈希值
	get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
	//初始化锁
	rwlock_init(&queue->syn_wait_lock);
	//初始化accept连接队列为空
	queue->rskq_accept_head = NULL;
	//记录listen_opt哈希表的桶大小到nr_table_entries 
	lopt->nr_table_entries = nr_table_entries;

	//将listen_opt记录到监听套接字连接请求队列中,即
	//tcp_sock.inet_conn.icsk_accept_queue.listen_opt
	write_lock_bh(&queue->syn_wait_lock);
	queue->listen_opt = lopt;
	write_unlock_bh(&queue->syn_wait_lock);

	return 0;
}

可以看出,reqsk_queue_alloc()的核心操作就是创建半连接队列listen_opt。

3.2.2 注册监听套接字

inet_csk_listen_start()中第二个重要的操作就是将该监听套接字添加到TCP全局的监听套接字哈希表中,这是通过调用inet_hash()完成的。

void inet_hash(struct sock *sk)
{
	//非TCP_CLOSE状态(如上,listen()调用过程中,到这里状态应该是TCP_LISTEN),
	//调用__inet_hash()
	if (sk->sk_state != TCP_CLOSE) {
		local_bh_disable();
		__inet_hash(sk);
		local_bh_enable();
	}
}

static void __inet_hash(struct sock *sk)
{
	struct inet_hashinfo *hashinfo = sk->sk_prot->hashinfo;
	struct hlist_head *list;
	rwlock_t *lock;

	//对于TCP,并且是listen()调用流程,TCB状态一定是TCP_LISTEN,肯定不满足该条件
	if (sk->sk_state != TCP_LISTEN) {
		__inet_hash_nolisten(sk);
		return;
	}

	//将该监听套接字加入到TCP的全局哈希表listenning_hash中
	BUG_TRAP(sk_unhashed(sk));
	list = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
	lock = &hashinfo->lhash_lock;
	inet_listen_wlock(hashinfo);
	__sk_add_node(sk, list);
	//还会累加对协议结构的引用计数
	sock_prot_inuse_add(sk->sk_prot, 1);
	write_unlock(lock);
	wake_up(&hashinfo->lhash_wait);
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值