一文讲解从Linux内核角度分析服务器Listen细节

Listen功能简述

编写服务器程序时,在Linux中需要调用Listen系统调用,如下所示,Listen系统调用的主要功能就是根据传入的backlog参数创建连接队列,并将套接字的状态迁移至LISTEN状态,最后将监听sock注册到TCP全局的监听套接字哈希表。

int listen(int sockfd, int backlog);

Listen系统调用-函数执行流程

系统调用调用的函数执行如下所示:

SYSCALL_DEFINE2(listen, int, fd, int, backlog){ struct socket *sock; int err, fput_needed; int somaxconn; 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);  fput_light(sock->file, fput_needed); } return err;}

其中sockfd_lookup_light函数根据fd描述符得到struct socket结构体,并找到当前系统设定的最大可监听连接数somaxconnPROC文件系统中somaxconn默认为128,意味着单个套接口队列的长度,可最大监听128个连接 ,如下所示:

# cat /proc/sys/net/core/somaxconn128

net_defaults_init_net函数初始化此值为宏SOMAXCONN(128):

static int __net_init net_defaults_init_net(struct net *net){ net->core.sysctl_somaxconn = SOMAXCONN; return 0;}

somaxconn与Listen系统调用传入的参数backlog进行比较,若当前传入的参数backlog大于somaxconn则使用somaxconn,即backlog最大值不能超过somaxconn。该系统调用核心是执行:sock->ops->listen(sock,backlog) ;也就是说找到服务器的socket后,通过它的协议操作表结构struct proto_ops执行其listen钩子函数,proto_ops协议操作表结构的挂入是在socket创建过程根据协议类型进行设置的,TCP实际挂入的是inet_stream_ops操作表结构,listen在inet_stream_ops表中的赋值如下所示:

const struct proto_ops inet_stream_ops = { ...... .listen     = inet_listen, ......};


更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂


故sock->ops->listen针对于TCP而言,继续调用inet_listen函数:

int inet_listen(struct socket *sock, int backlog){ struct sock *sk = sock->sk; unsigned char old_state; int err, tcp_fastopen; lock_sock(sk); err = -EINVAL; if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)  goto out; old_state = sk->sk_state;  //状态检查 if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))  goto out;   tcp_fastopen = sock_net(sk)->ipv4.sysctl_tcp_fastopen;  if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&      (tcp_fastopen & TFO_SERVER_ENABLE) &&      !inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {   fastopen_queue_tune(sk, backlog);   tcp_fastopen_init_key_once(sock_net(sk));  }  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_listen函数首先是对套接字类型、状态进行检查,类型必须是流式套接字且状态必须是close或者listen状态:

 if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)  goto out; old_state = sk->sk_state; if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))  goto out; 

inet_listen函数核心的继续调用inet_csk_start_listen函数:

int inet_csk_listen_start(struct sock *sk, int backlog){ struct inet_connection_sock *icsk = inet_csk(sk); struct inet_sock *inet = inet_sk(sk); int err = -EADDRINUSE;  //分配以及初始化accept队列 reqsk_queue_alloc(&icsk->icsk_accept_queue);  //设置accept队列的最大长度 sk->sk_max_ack_backlog = backlog;  //初始化当前的sk_ack_backlog,即当前队列的计数 sk->sk_ack_backlog = 0; inet_csk_delack_init(sk);   sk_state_store(sk, TCP_LISTEN); if (!sk->sk_prot->get_port(sk, inet->inet_num)) {  inet->inet_sport = htons(inet->inet_num);  sk_dst_reset(sk);、    //注册全局监听哈希表中  err = sk->sk_prot->hash(sk);  if (likely(!err))   return 0; } sk->sk_state = TCP_CLOSE; return err;}

inet_csk_listen_start函数通过reqsk_queue_alloc创建连接队列,队列结构体如下,队列的最大长度是sk_max_ack_backlog,也就是用户传入的backlog参数值,队列的长度计数是sk_ack_backlog。

struct request_sock_queue { spinlock_t  rskq_lock; u8   rskq_defer_accept; u32   synflood_warned; atomic_t  qlen; atomic_t  young; struct request_sock *rskq_accept_head; struct request_sock *rskq_accept_tail; struct fastopen_queue fastopenq;  /* Check max_qlen != 0 to determine          * if TFO is enabled.          */};

其中request_sock结构体是请求队列的节点如下所示,*dl_next将所有的accept请求串起来。

struct request_sock { struct sock_common  __req_common;#define rsk_refcnt   __req_common.skc_refcnt#define rsk_hash   __req_common.skc_hash#define rsk_listener   __req_common.skc_listener#define rsk_window_clamp  __req_common.skc_window_clamp#define rsk_rcv_wnd   __req_common.skc_rcv_wnd struct request_sock  *dl_next; u16    mss; u8    num_retrans; /* number of retransmits */ u8    cookie_ts:1; /* syncookie: encode tcpopts in timestamp */ u8    num_timeout:7; /* number of timeouts */ u32    ts_recent; struct timer_list  rsk_timer; const struct request_sock_ops *rsk_ops; struct sock   *sk; u32    *saved_syn; u32    secid; u32    peer_secid;};

struct request_sock_queue和struct request_sock的关系如下:

inet_csk_listen_start调用的分配并初始化连接队列的函数reqsk_queue_alloc如下所示,其中可以看到queue->rskq_accept_head初始化为NULL

void reqsk_queue_alloc(struct request_sock_queue *queue){ spin_lock_init(&queue->rskq_lock); spin_lock_init(&queue->fastopenq.lock); queue->fastopenq.rskq_rst_head = NULL; queue->fastopenq.rskq_rst_tail = NULL; queue->fastopenq.qlen = 0; queue->rskq_accept_head = NULL;}

inet_csk_listen_start函数中另一个核心内容就是调用哈希函数:

sk->sk_prot->hash(sk)将监听sock注册到TCP全局的监听套接字哈希表,对于TCP对应的协议栈,hash函数是inet_hash:

int inet_hash(struct sock *sk){ int err = 0; if (sk->sk_state != TCP_CLOSE) {  local_bh_disable();  err = __inet_hash(sk, NULL);  local_bh_enable(); } return err;}

继续调用__inet_hash:

int __inet_hash(struct sock *sk, struct sock *osk){ struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; struct inet_listen_hashbucket *ilb; int err = 0; if (sk->sk_state != TCP_LISTEN) {  inet_ehash_nolisten(sk, osk);  return 0; } WARN_ON(!sk_unhashed(sk)); ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)]; spin_lock(&ilb->lock); if (sk->sk_reuseport) {  err = inet_reuseport_add_sock(sk, ilb);  if (err)   goto unlock; } if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&  sk->sk_family == AF_INET6)  hlist_add_tail_rcu(&sk->sk_node, &ilb->head); else  hlist_add_head_rcu(&sk->sk_node, &ilb->head); sock_set_flag(sk, SOCK_RCU_FREE); sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);unlock: spin_unlock(&ilb->lock); return err;}

关于:

struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;

通过图解,如下所示:

最终得到struct inet_hashinfo:

struct inet_hashinfo { struct inet_ehash_bucket *ehash; spinlock_t   *ehash_locks; unsigned int   ehash_mask; unsigned int   ehash_locks_mask; struct inet_bind_hashbucket *bhash; unsigned int   bhash_size;  struct kmem_cache  *bind_bucket_cachep; struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE]     ____cacheline_aligned_in_smp;};

内核将监听队列分为32个哈希桶bucket:listening_hash[INET_LHTABLE_SIZE],保存处于监听状态的TCP套接口哈希链表,每个哈希桶由独立的保护锁和链表,此结构通过减小锁的粒度,增加并行处理的可能,每个哈希桶如下所示:

struct inet_listen_hashbucket { spinlock_t  lock; struct hlist_head head;};

哈希桶的选择由函数inet_sk_listen_hashfn的返回值决定

struct inet_listen_hashbucket *ilb;ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];

inet_sk_listen_hash函数使用数据包的目的端口号(本地监听端口号)计算的hash值为索引得到具体的哈希桶,如下所示:

static inline int inet_sk_listen_hashfn(const struct sock *sk){ return inet_lhashfn(sock_net(sk), inet_sk(sk)->inet_num);}
static inline u32 inet_lhashfn(const struct net *net, const unsigned short num){ return (num + net_hash_mix(net)) & (INET_LHTABLE_SIZE - 1);}

内核为处于LISTEN状态的socket分配了大小为32的哈希桶,监听的端口号经过哈希算法运算打散到这些哈希桶中(如果开启了IPV6,并且启用了端口重用,将此套接口添加在监听套接口桶的链表末尾;否则,添加到链表头部,如下代码所示)

if (sk->sk_reuseport) {  err = inet_reuseport_add_sock(sk, ilb);  if (err)   goto unlock; } if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&  sk->sk_family == AF_INET6)  hlist_add_tail_rcu(&sk->sk_node, &ilb->head); else  hlist_add_head_rcu(&sk->sk_node, &ilb->head);

如下图所示,哈希链表的组织方式:

当收到客户端的 SYN 握手报文以后,会根据目标端口号的哈希值计算出哈希冲突链表,然后遍历这条哈希链表得到对应的socket。

原文地址:Linux内核角度分析服务器Listen细节 - Linux内核 - 我爱内核网 - 构建全国最权威的内核技术交流分享论坛

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章