这篇笔记记录了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);
}