日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

linux内核线程socket,从Linux源码看Socket(TCP)的accept

發布時間:2023/12/4 linux 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 linux内核线程socket,从Linux源码看Socket(TCP)的accept 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

從Linux源碼看Socket(TCP)的accept

前言

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

一個最簡單的Server端例子

眾所周知,一個Server端Socket的創建,須要socket、bind、listen、accept四個步驟。

今天,筆者就聚焦于accept。

代碼以下:react

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也就是操做函數。

linux

accept系統調用

好了,咱們直接進入accept系統調用吧。多線程

#include

// 成功,返回表明新鏈接的描述符,錯誤返回-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,再次跟蹤下調用棧:socket

sock->ops->accept

|->inet_steam_ops->accept(inet_accept)

/* 由一開始的sock圖可知sk_prot=tcp_prot

|->sk1->sk_prot->accept

|->inet_csk_accept

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

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://www.cnblogs.com/alchemystar/p/13161781.html

在這里描述一下緣由,核心就是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://www.cnblogs.com/alchemystar/p/13473999.html

另外,對于accept_queue具體的填充機制以及源碼,能夠見筆者另外一篇博客的詳細分析

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

https://www.cnblogs.com/alchemystar/p/13845081.html

總結

Linux內核源碼博大精深,每次扎進去探索時候都會廢寢忘食,其間能夠看到各類優雅的設計,在此分享出來,但愿對讀者有所幫助。歡迎你們關注我公眾號,里面有各類干貨,還有大禮包相送哦!

總結

以上是生活随笔為你收集整理的linux内核线程socket,从Linux源码看Socket(TCP)的accept的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。