從 Linux 源碼看 Socket-TCP- 的 accept

前言

筆者一直覺得如果能知道從應用到框架再到操作系統的每一處代碼,是一件 Exciting 的事情。今天筆者就從 Linux 源碼的角度看下 Server 端的 Socket 在進行 Accept 的時候到底做了哪些事情 (基於 Linux 3.10 內核)。

一個最簡單的 Server 端例子

衆所周知,一個 Server 端 Socket 的建立,需要 socket、bind、listen、accept 四個步驟。
今天,筆者就聚焦於 accept。

代碼如下:

void start_server(){
    // server fd
    int sockfd_server;
    // accept fd 
    int sockfd;
    int call_err;
    struct sockaddr_in sock_addr;
     ......
    call_err=bind(sockfd_server,(struct sockaddr*)(&sock_addr),sizeof(sock_addr));
      ......
    call_err=listen(sockfd_server,MAX_BACK_LOG);
     ......
    while(1){
        struct sockaddr_in* s_addr_client = mem_alloc(sizeof(struct sockaddr_in));
              int client_length = sizeof(*s_addr_client);
         // 這邊就是我們今天的聚焦點accept
        sockfd = accept(sockfd_server,(struct sockaddr_ *)(s_addr_client),(socklen_t *)&(client_length));
        if(sockfd == -1){
            printf("Accept error!\n");
            continue;
        }
        process_connection(sockfd,(struct sockaddr_in*)(&s_addr_client));
    }
}

首先我們通過 socket 系統調用創建了一個 Socket,其中指定了 SOCK_STREAM, 而且最後一個參數爲 0,也就是建立了一個通常所有的 TCP Socket。在這裏,我們直接給出 TCP Socket 所對應的 ops 也就是操作函數。

accept 系統調用

好了,我們直接進入 accept 系統調用吧。

#include <sys/socket.h>
// 成功,返回代表新連接的描述符,錯誤返回-1,同時錯誤碼設置在errno
int accept(int sockfd,struct sockaddr* addr,socklen_t *addrlen);
// 注意,實際上Linux還有個accept擴展accept4:
// 額外添加的flags參數可以爲新連接描述符設置O_NONBLOCK|O_CLOEXEC(執行exec後關閉)這兩個標記
int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);

注意,這邊的 accept 調用是被 glibc 用 SYSCALL_CANCEL 包了一層,其將返回值修正爲只有 0 和 - 1 這兩個選擇,同時將錯誤碼的絕對值設置在 errno 內。由於 glibc 對於系統調用的封裝過於複雜,就不在這裏細講了。如果要尋找具體的邏輯,用

// 注意accept和(之間要有空格,不然搜索不到
accept (int

在整個 glibc 代碼中搜索即可。
理解 accept 的關鍵點是,它會創建一個新的 Socket, 這個新的 Socket 來與對端運行 connect() 的對等 Socket 進行連接,如下圖所示:

接下來,我們就進入 Linux 內核源碼棧吧

accept
 |->SYSCALL_CANCEL(accept......)
   ......
    |->SYSCALL_DEFINE3(accept
     // 最終調用了sys_accept4
     |->sys_accept4    
      /* 檢測監聽描述符fd是否存在,不存在,返回-BADF
      |->sockfd_lookup_light
       |->sock_alloc /*新建Socket*/
         |->get_unused_fd_flags /*獲取一個未用的fd*/
          |->sock->ops->accept(sock...) /*調用核心*/

上述流程如下面所示:

由此得知,核心函數在 sock->ops->accept 上,由於我們關注的是 TCP, 那麼其實現即爲
inet_stream_ops->accept 也即 inet_accept,再次跟蹤下調用棧:

    sock->ops->accept
        |->inet_steam_ops->accept(inet_accept)
            /* 由一開始的sock圖可知sk_prot=tcp_prot
            |->sk1->sk_prot->accept
                |->inet_csk_accept

好了,穿過了層層包裝,終於到具體邏輯部分了。上代碼:

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    /* 獲取當前監聽sock的accept隊列*/
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    ......
    /* 如果監聽Socket狀態非TCP_LISEN,返回錯誤 */
    if (sk->sk_state != TCP_LISTEN)
        goto out_err
    /* 如果當前accept隊列爲空 */
    if (reqsk_queue_empty(queue)) {
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
        /* 如果是非阻塞模式,直接返回-EAGAIN */
        error = -EAGAIN;
        if (!timeo)
            goto out_err;
        /* 如果是阻塞模式,切超時時間不爲0,則等待新連接進入隊列 */
        error = inet_csk_wait_for_connect(sk, timeo);
        if (error)
            goto out_err;
    }    
    /* 到這裏accept queue不爲空,從queue中獲取一個連接 */
    req = reqsk_queue_remove(queue);
    newsk = req->sk;
    /* fastopen 判斷邏輯 */
    ......
    /* 返回新的sock,也就是accept派生出的和client端對等的那個sock */
    return newsk
}

上面流程如下圖所示:

我們關注下 inet_csk_wait_for_connect, 即 accept 的超時邏輯:

static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
    for (;;) {
        /* 通過增加EXCLUSIVE標誌使得在BIO中調用accept中不會產生驚羣效應 */
        prepare_to_wait_exclusive(sk_sleep(sk), &wait,
                      TASK_INTERRUPTIBLE);
        if (reqsk_queue_empty(&icsk->icsk_accept_queue))
            timeo = schedule_timeout(timeo);
        .......
        err = -EAGAIN;
        /* 這邊accept超時,返回的是-EAGAIN */
        if (!timeo)
            break;
    }
    finish_wait(sk_sleep(sk), &wait);
    return err;                        
}

通過 exclusice 標誌使得我們在 BIO 中調用 accept(不用 epoll/select 等) 時,不會驚羣。
由代碼得知在 accept 超時時候返回 (errno) 的是 EAGAIN 而不是 ETIMEOUT。

EPOLL(在 accept 時候)” 驚羣”

由於在 EPOLL LT(水平觸發模式下), 一次 accept 事件,可能會喚醒多個等待在此 listen fd 上的 (epoll_wait) 線程, 而最終可能只有一個能成功的獲取到新連接(newfd), 其它的都是 - EGAIN,也即有一些不必要的線程被喚醒了,做了無用功。關於 epoll 的原理可以看下筆者之前的博客《從 linux 源碼看 epoll》:

https://my.oschina.net/alchemystar/blog/3008840

在這裏描述一下原因, 核心就是 epoll_wait 在水平觸發下會在這個 fd 仍有未處理事件的時候重新塞回 ready_list 並在此喚醒另一個等待在 epoll 上的進程!

所以我們看到,雖然 epoll_wait 的時候給自己加了 exclusive 不會在有中斷事件觸發的時候驚羣,但是水平觸發這個機制確也造成了類似” 驚羣” 的現象!
由上面的討論看出,fd1 仍舊有事件是造成額外喚醒的原因,這個也很好理解,畢竟這個事件是另一個線程處理的,那個線程估摸着還沒來得及運行,自然也來不及處理!
我們看下在 accept 事件中,怎麼判定這個 fd(listen sock 的 fd) 還有未處理事件的。

// 通過f_op->poll判定
epi->ffd.file->f_op->poll
    |->tcp_poll
        /* 如果sock是listen狀態,則由下面函數負責 */
        |->inet_csk_listen_poll

/* 通過accept_queue隊列是否爲空判斷監聽sock是否有未處理事件*/
static inline unsigned int inet_csk_listen_poll(const struct sock *sk)
{
    return !reqsk_queue_empty(&inet_csk(sk)->icsk_accept_queue) ?
            (POLLIN | POLLRDNORM) : 0;
}

那麼我們就可以根據邏輯畫出時序圖了。

其實不僅僅是 accept, 要是多線程 epoll_wait 同一個 fd 的 read/write 也是同樣的驚羣,只不過應該不會有人這麼做吧。
正是由於這種” 驚羣” 效應的存在,所以我們經常採用單開一個線程去專門 accept 的形式,例如 reactor 模式即是如此。但是,如果一瞬間有大量連接湧進來,單線程處理還是有瓶頸的, 無法充分利用多核的優勢, 在海量短連接場景下就顯得稍顯無力了。這也是有解決方式的!

採用 so_reuseport 解決驚羣

前面講過,由於我們是在同一個 fd 上多線程去運行 epoll_wait 纔會有此問題,那麼其實我們多開幾個 fd 就解決了。首先想到的方案是,多開幾個端口號,人爲分開監聽 fd,但這個明顯帶來了額外的複雜性。爲了解決這一問題,Linux 提供了 so_reuseport 這個參數,其原理如下圖所示:

多個 fd 監聽同一個端口號,在內核中做負載均衡 (Sharding), 將 accept 的任務分散到不同的線程的不同 Socket 上 (Sharding),毫無疑問可以利用多核能力,大幅提升連接成功後的 Socket 分發能力。那麼我們的線程模型也可以改爲用多線程 accept 了,如下圖所示:

accept_queue 全連接隊列

在前面的討論中, accept_queue 是 accept 系統調用中的核心成員,那麼這個 accept_queue 是怎麼被填充 (add) 的呢? 如下圖所示:

圖中展示了 client 和 server 在三次交互中,accept_queue(全連接隊列) 和 syn_table 半連接 hash 表的變遷情況。在 accept_queue 被填充後,由用戶線程通過 accept 系統調用從隊列中獲取對應的 fd

值得注意的是,當用戶線程來不及處理的時候,內核會 drop 掉三次握手成功的連接,導致一些詭異的現象,具體可以看筆者另一篇博客《解 Bug 之路 - dubbo 流量上線時的非平滑問題》:

https://my.oschina.net/alchemystar/blog/3098219

另外,對於 accept_queue 具體的填充機制以及源碼,可以見筆者另一篇博客的詳細分析
《從 Linux 源碼看 Socket(TCP) 的 listen 及連接隊列》:

https://my.oschina.net/alchemystar/blog/4672630

總結

Linux 內核源碼博大精深,每次扎進去探索時候都會廢寢忘食,其間可以看到各種優雅的設計,在此分享出來,希望對讀者有所幫助。歡迎大家關注我公衆號,裏面有各種乾貨,還有大禮包相送哦!

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