Linux 內核角度分析服務器 Listen 細節

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/somaxconn
128

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,
 ......
};

故 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 (!((<< 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 (!((<< 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。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ianxON2by3lodtTWK4TsJw