注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
1、函数原型
int listen(int sockfd, int backlog);
参数说明:
sockfd:套接字的文件描述符,即socket()系统调用返回的fd
backlog:保存客户端请求的队列长度
listen()系统调用是比较简单的,但是涉及了backlog,这个参数是比较复杂的,影响到半连接队列和全连接队列。具体分析可参看《TCP 的backlog详解》。
2、内核实现
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
//根据fd获取对应的sock结构体,分析bind()系统调用时已经讨论过
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);//调用对应协议的listen函数
fput_light(sock->file, fput_needed);
}
return err;
}
和bind()函数一样,既然是通过fd来操作,第一步肯定是要通过fd获取对应的socket结构体,才能操作网络相关的变量和结构体。由之前系统调用的分析,我们知道sock->ops指向的是inet_stream_ops结构体,因此sock->ops->listen指向的便是inet_listen()。
/*
* Move a socket into listening state.
*/
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err;
lock_sock(sk);
err = -EINVAL;
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
old_state = sk->sk_state;
//调用listen只能是处于close或者listen状态(只能修改backlog参数)的连接
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) {
//快速开启选项,暂不分析
if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) != 0 &&
inet_csk(sk)->icsk_accept_queue.fastopenq == NULL) {
if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) != 0)
err = fastopen_init_queue(sk, backlog);
else if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT2) != 0)
err = fastopen_init_queue(sk, ((uint)sysctl_tcp_fastopen) >> 16);
else
err = 0;
if (err)
goto out;
tcp_fastopen_init_key_once(true);
}
//初始化socket,包括半连接队列等
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;
}
inet_csk_listen_start()主要是两件事:一个是分配半连接队列,另一个是将socket加入listen哈希表中。
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);
//根据nr_table_entries(和用户设置的backlog参数相关)分配半连接队列
int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
if (rc != 0)
return rc;
sk->sk_max_ack_backlog = 0;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
sk->sk_state = TCP_LISTEN;//宣告socket进入listen状态
/* 其实在调用listen()之前调用bind()系统调用时已经调用过sk->sk_prot->get_port一次,
* 这里再次调用是因为bind与listen之间并不是原子操作,用户可能在此修改过连接的一些属性
* 比如,sk->reuse,sk->sk_reuseport等,所以再次检查端口保证该端口可用。*/
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
inet->inet_sport = htons(inet->inet_num);//设置源端口
sk_dst_reset(sk);
sk->sk_prot->hash(sk);//将处于listen状态的连接根据端口号放入listen哈希表中
return 0;
}
sk->sk_state = TCP_CLOSE;//失败了,socket状态置为close初始值
__reqsk_queue_destroy(&icsk->icsk_accept_queue);
return -EADDRINUSE;
}
最后便是加入listen哈希表。
void inet_hash(struct sock *sk)
{
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->h.hashinfo;
struct inet_listen_hashbucket *ilb;
if (sk->sk_state != TCP_LISTEN) {
__inet_hash_nolisten(sk, NULL);
return;
}
WARN_ON(!sk_unhashed(sk));
//根据端口号,获取对应的哈希表
ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
spin_lock(&ilb->lock);
__sk_nulls_add_node_rcu(sk, &ilb->head);//将socket放入listen链表中
sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);
spin_unlock(&ilb->lock);
}
所以listen()就两件事,根据用户设置的backlog计算真正的请求队列长度,然后分配半连接队列。还有就是再次确认端口可用,然后将该连接加入listen哈希表。
部分结构体关系如下图: