自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從Linux源碼看Socket(TCP)的Accept

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

[[404913]]

前言

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

一個(gè)最簡(jiǎn)單的Server端例子

眾所周知,一個(gè)Server端Socket的建立,需要socket、bind、listen、accept四個(gè)步驟。

今天,筆者就聚焦于accept。

代碼如下:

  1. void start_server(){ 
  2.     // server fd 
  3.     int sockfd_server; 
  4.     // accept fd  
  5.     int sockfd; 
  6.     int call_err; 
  7.     struct sockaddr_in sock_addr; 
  8.      ...... 
  9.     call_err=bind(sockfd_server,(struct sockaddr*)(&sock_addr),sizeof(sock_addr)); 
  10.       ...... 
  11.     call_err=listen(sockfd_server,MAX_BACK_LOG); 
  12.      ...... 
  13.     while(1){ 
  14.         struct sockaddr_in* s_addr_client = mem_alloc(sizeof(struct sockaddr_in)); 
  15.               int client_length = sizeof(*s_addr_client); 
  16.          // 這邊就是我們今天的聚焦點(diǎn)accept 
  17.         sockfd = accept(sockfd_server,(struct sockaddr_ *)(s_addr_client),(socklen_t *)&(client_length)); 
  18.         if(sockfd == -1){ 
  19.             printf("Accept error!\n"); 
  20.             continue
  21.         } 
  22.         process_connection(sockfd,(struct sockaddr_in*)(&s_addr_client)); 
  23.     } 

首先我們通過socket系統(tǒng)調(diào)用創(chuàng)建了一個(gè)Socket,其中指定了SOCK_STREAM,而且最后一個(gè)參數(shù)為0,也就是建立了一個(gè)通常所有的TCP Socket。在這里,我們直接給出TCP Socket所對(duì)應(yīng)的ops也就是操作函數(shù)。

accept系統(tǒng)調(diào)用

好了,我們直接進(jìn)入accept系統(tǒng)調(diào)用吧。

  1. #include <sys/socket.h> 
  2. // 成功,返回代表新連接的描述符,錯(cuò)誤返回-1,同時(shí)錯(cuò)誤碼設(shè)置在errno 
  3. int accept(int sockfd,struct sockaddr* addr,socklen_t *addrlen); 
  4. // 注意,實(shí)際上Linux還有個(gè)accept擴(kuò)展accept4: 
  5. // 額外添加的flags參數(shù)可以為新連接描述符設(shè)置O_NONBLOCK|O_CLOEXEC(執(zhí)行exec后關(guān)閉)這兩個(gè)標(biāo)記 
  6. int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags); 

注意,這邊的accept調(diào)用是被glibc用SYSCALL_CANCEL包了一層,其將返回值修正為只有0和-1這兩個(gè)選擇,同時(shí)將錯(cuò)誤碼的絕對(duì)值設(shè)置在errno內(nèi)。由于glibc對(duì)于系統(tǒng)調(diào)用的封裝過于復(fù)雜,就不在這里細(xì)講了。如果要尋找具體的邏輯,用

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

在整個(gè)glibc代碼中搜索即可。

理解accept的關(guān)鍵點(diǎn)是,它會(huì)創(chuàng)建一個(gè)新的Socket,這個(gè)新的Socket來(lái)與對(duì)端運(yùn)行connect()的對(duì)等Socket進(jìn)行連接,如下圖所示:

接下來(lái),我們就進(jìn)入Linux內(nèi)核源碼棧吧

  1. accept 
  2.  |->SYSCALL_CANCEL(accept......) 
  3.    ...... 
  4.     |->SYSCALL_DEFINE3(accept 
  5.      // 最終調(diào)用了sys_accept4 
  6.      |->sys_accept4     
  7.       /* 檢測(cè)監(jiān)聽描述符fd是否存在,不存在,返回-BADF 
  8.       |->sockfd_lookup_light 
  9.        |->sock_alloc /*新建Socket*/ 
  10.          |->get_unused_fd_flags /*獲取一個(gè)未用的fd*/ 
  11.           |->sock->ops->accept(sock...) /*調(diào)用核心*/ 

上述流程如下面所示:

由此得知,核心函數(shù)在sock->ops->accept上,由于我們關(guān)注的是TCP,那么其實(shí)現(xiàn)即為

inet_stream_ops->accept也即inet_accept,再次跟蹤下調(diào)用棧:

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

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

  1. struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) 
  2.     struct inet_connection_sock *icsk = inet_csk(sk); 
  3.     /* 獲取當(dāng)前監(jiān)聽sock的accept隊(duì)列*/ 
  4.     struct request_sock_queue *queue = &icsk->icsk_accept_queue; 
  5.     ...... 
  6.     /* 如果監(jiān)聽Socket狀態(tài)非TCP_LISEN,返回錯(cuò)誤 */ 
  7.     if (sk->sk_state != TCP_LISTEN) 
  8.         goto out_err 
  9.     /* 如果當(dāng)前accept隊(duì)列為空 */ 
  10.     if (reqsk_queue_empty(queue)) { 
  11.         long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); 
  12.         /* 如果是非阻塞模式,直接返回-EAGAIN */ 
  13.         error = -EAGAIN; 
  14.         if (!timeo) 
  15.             goto out_err; 
  16.         /* 如果是阻塞模式,切超時(shí)時(shí)間不為0,則等待新連接進(jìn)入隊(duì)列 */ 
  17.         error = inet_csk_wait_for_connect(sk, timeo); 
  18.         if (error) 
  19.             goto out_err; 
  20.     }     
  21.     /* 到這里accept queue不為空,從queue中獲取一個(gè)連接 */ 
  22.     req = reqsk_queue_remove(queue); 
  23.     newsk = req->sk; 
  24.     /* fastopen 判斷邏輯 */ 
  25.     ...... 
  26.     /* 返回新的sock,也就是accept派生出的和client端對(duì)等的那個(gè)sock */ 
  27.     return newsk 

上面流程如下圖所示:

我們關(guān)注下inet_csk_wait_for_connect,即accept的超時(shí)邏輯:

  1. static int inet_csk_wait_for_connect(struct sock *sk, long timeo) 
  2.     for (;;) { 
  3.         /* 通過增加EXCLUSIVE標(biāo)志使得在BIO中調(diào)用accept中不會(huì)產(chǎn)生驚群效應(yīng) */ 
  4.         prepare_to_wait_exclusive(sk_sleep(sk), &wait, 
  5.                       TASK_INTERRUPTIBLE); 
  6.         if (reqsk_queue_empty(&icsk->icsk_accept_queue)) 
  7.             timeo = schedule_timeout(timeo); 
  8.         ....... 
  9.         err = -EAGAIN; 
  10.         /* 這邊accept超時(shí),返回的是-EAGAIN */ 
  11.         if (!timeo) 
  12.             break; 
  13.     } 
  14.     finish_wait(sk_sleep(sk), &wait); 
  15.     return err;                         

通過exclusice標(biāo)志使得我們?cè)贐IO中調(diào)用accept(不用epoll/select等)時(shí),不會(huì)驚群。

由代碼得知在accept超時(shí)時(shí)候返回(errno)的是EAGAIN而不是ETIMEOUT。

EPOLL(在accept時(shí)候)”驚群”

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

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

在這里描述一下原因,核心就是epoll_wait在水平觸發(fā)下會(huì)在這個(gè)fd仍有未處理事件的時(shí)候重新塞回ready_list并在此喚醒另一個(gè)等待在epoll上的進(jìn)程!

所以我們看到,雖然epoll_wait的時(shí)候給自己加了exclusive不會(huì)在有中斷事件觸發(fā)的時(shí)候驚群,但是水平觸發(fā)這個(gè)機(jī)制確也造成了類似”驚群”的現(xiàn)象!

由上面的討論看出,fd1仍舊有事件是造成額外喚醒的原因,這個(gè)也很好理解,畢竟這個(gè)事件是另一個(gè)線程處理的,那個(gè)線程估摸著還沒來(lái)得及運(yùn)行,自然也來(lái)不及處理!

我們看下在accept事件中,怎么判定這個(gè)fd(listen sock的fd)還有未處理事件的。

  1. // 通過f_op->poll判定 
  2. epi->ffd.file->f_op->poll 
  3.     |->tcp_poll 
  4.         /* 如果sock是listen狀態(tài),則由下面函數(shù)負(fù)責(zé) */ 
  5.         |->inet_csk_listen_poll 
  6.  
  7. /* 通過accept_queue隊(duì)列是否為空判斷監(jiān)聽sock是否有未處理事件*/ 
  8. static inline unsigned int inet_csk_listen_poll(const struct sock *sk) 
  9.     return !reqsk_queue_empty(&inet_csk(sk)->icsk_accept_queue) ? 
  10.             (POLLIN | POLLRDNORM) : 0; 

那么我們就可以根據(jù)邏輯畫出時(shí)序圖了。

其實(shí)不僅僅是accept,要是多線程epoll_wait同一個(gè)fd的read/write也是同樣的驚群,只不過應(yīng)該不會(huì)有人這么做吧。

正是由于這種”驚群”效應(yīng)的存在,所以我們經(jīng)常采用單開一個(gè)線程去專門accept的形式,例如reactor模式即是如此。但是,如果一瞬間有大量連接涌進(jìn)來(lái),單線程處理還是有瓶頸的,無(wú)法充分利用多核的優(yōu)勢(shì),在海量短連接場(chǎng)景下就顯得稍顯無(wú)力了。這也是有解決方式的!

采用so_reuseport解決驚群

前面講過,由于我們是在同一個(gè)fd上多線程去運(yùn)行epoll_wait才會(huì)有此問題,那么其實(shí)我們多開幾個(gè)fd就解決了。首先想到的方案是,多開幾個(gè)端口號(hào),人為分開監(jiān)聽fd,但這個(gè)明顯帶來(lái)了額外的復(fù)雜性。為了解決這一問題,Linux提供了so_reuseport這個(gè)參數(shù),其原理如下圖所示:

多個(gè)fd監(jiān)聽同一個(gè)端口號(hào),在內(nèi)核中做負(fù)載均衡(Sharding),將accept的任務(wù)分散到不同的線程的不同Socket上(Sharding),毫無(wú)疑問可以利用多核能力,大幅提升連接成功后的Socket分發(fā)能力。那么我們的線程模型也可以改為用多線程accept了,如下圖所示:

accept_queue全連接隊(duì)列

在前面的討論中,accept_queue是accept系統(tǒng)調(diào)用中的核心成員,那么這個(gè)accept_queue是怎么被填充(add)的呢?如下圖所示:

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

值得注意的是,當(dāng)用戶線程來(lái)不及處理的時(shí)候,內(nèi)核會(huì)drop掉三次握手成功的連接,導(dǎo)致一些詭異的現(xiàn)象,具體可以看筆者另一篇博客《解Bug之路-dubbo流量上線時(shí)的非平滑問題》:

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

另外,對(duì)于accept_queue具體的填充機(jī)制以及源碼,可以見筆者另一篇博客的詳細(xì)分析

《從Linux源碼看Socket(TCP)的listen及連接隊(duì)列》:

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

總結(jié)

Linux內(nèi)核源碼博大精深,每次扎進(jìn)去探索時(shí)候都會(huì)廢寢忘食,其間可以看到各種優(yōu)雅的設(shè)計(jì),在此分享出來(lái),希望對(duì)讀者有所幫助。

本文轉(zhuǎn)載自微信公眾號(hào)「解Bug之路」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系解Bug之路公眾號(hào)。

 

責(zé)任編輯:武曉燕 來(lái)源: 解Bug之路
相關(guān)推薦

2020-10-10 07:00:16

LinuxSocketTCP

2020-10-14 14:31:37

LinuxTCP連接

2021-07-15 14:27:47

LinuxSocketClose

2021-07-14 09:48:15

Linux源碼Epoll

2010-01-21 11:19:44

TCP Socketlinux

2019-11-17 22:11:11

TCPSYN隊(duì)列Accept隊(duì)列

2021-03-10 08:20:54

設(shè)計(jì)模式OkHttp

2017-04-05 20:00:32

ChromeObjectJS代碼

2015-05-28 10:34:16

TCPsocket

2018-02-02 15:48:47

ChromeDNS解析

2012-03-19 11:41:30

JavaSocket

2009-08-28 14:15:19

SocketVisual C#.N

2010-06-18 09:51:51

Linux Accep

2017-02-09 15:15:54

Chrome瀏覽器

2020-09-23 12:32:18

網(wǎng)絡(luò)IOMySQL

2022-03-08 11:29:06

Linux進(jìn)程系統(tǒng)

2011-01-18 13:42:18

Linuxsocket性能

2020-09-07 14:30:37

JUC源碼CAS

2019-02-17 10:05:24

TCPSocket網(wǎng)絡(luò)編程

2021-05-06 14:46:18

LinuxIcmpudp
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)