您好,登錄后才能下訂單哦!
本篇文章為大家展示了如何用從Linux源碼看Socket的accept,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
筆者一直覺得如果能知道從應用到框架再到操作系統的每一處代碼,是一件Exciting的事情。 今天筆者就從Linux源碼的角度看下Server端的Socket在進行Accept的時候到底做了哪些事情(基于Linux 3.10內核)。
眾所周知,一個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系統調用吧。
#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 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模式即是如此。但是,如果一瞬間有大量連接涌進來,單線程處理還是有瓶頸的,無法充分利用多核的優勢,在海量短連接場景下就顯得稍顯無力了。這也是有解決方式的!
前面講過,由于我們是在同一個fd上多線程去運行epoll_wait才會有此問題,那么其實我們多開幾個fd就解決了。首先想到的方案是,多開幾個端口號,人為分開監聽fd,但這個明顯帶來了額外的復雜性。為了解決這一問題,Linux提供了so_reuseport這個參數,其原理如下圖所示:
多個fd監聽同一個端口號,在內核中做負載均衡(Sharding),將accept的任務分散到不同的線程的不同Socket上(Sharding),毫無疑問可以利用多核能力,大幅提升連接成功后的Socket分發能力。那么我們的線程模型也可以改為用多線程accept了,如下圖所示:
在前面的討論中,accept_queue是accept系統調用中的核心成員,那么這個accept_queue是怎么被填充(add)的呢?如下圖所示: 圖中展示了client和server在三次交互中,accept_queue(全連接隊列)和syn_table半連接hash表的變遷情況。在accept_queue被填充后,由用戶線程通過accept系統調用從隊列中獲取對應的fd 值得注意的是,當用戶線程來不及處理的時候,內核會drop掉三次握手成功的連接,導致一些詭異的現象。
https://my.oschina.net/alchemystar/blog/3098219
另外,對于accept_queue具體的填充機制以及源碼,可以見筆者另一篇博客的詳細分析 《從Linux源碼看Socket(TCP)的listen及連接隊列》:
https://my.oschina.net/alchemystar/blog/4672630
上述內容就是如何用從Linux源碼看Socket的accept,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。