參考:

目前創(chuàng)新互聯(lián)已為上1000家的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬空間、網(wǎng)站托管維護、企業(yè)網(wǎng)站設(shè)計、淶源網(wǎng)站維護等服務(wù),公司將堅持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
Goroutine并發(fā)調(diào)度模型深度解析手擼一個協(xié)程池
Golang 的 goroutine 是如何實現(xiàn)的?
Golang - 調(diào)度剖析【第二部分】
OS線程初始棧為2MB。Go語言中,每個goroutine采用動態(tài)擴容方式,初始2KB,按需增長,最大1G。此外GC會收縮棧空間。
BTW,增長擴容都是有代價的,需要copy數(shù)據(jù)到新的stack,所以初始2KB可能有些性能問題。
更多關(guān)于stack的內(nèi)容,可以參見大佬的文章。 聊一聊goroutine stack
用戶線程的調(diào)度以及生命周期管理都是用戶層面,Go語言自己實現(xiàn)的,不借助OS系統(tǒng)調(diào)用,減少系統(tǒng)資源消耗。
Go語言采用兩級線程模型,即用戶線程與內(nèi)核線程KSE(kernel scheduling entity)是M:N的。最終goroutine還是會交給OS線程執(zhí)行,但是需要一個中介,提供上下文。這就是G-M-P模型
Go調(diào)度器有兩個不同的運行隊列:
go1.10\src\runtime\runtime2.go
Go調(diào)度器根據(jù)事件進行上下文切換。
調(diào)度的目的就是防止M堵塞,空閑,系統(tǒng)進程切換。
詳見 Golang - 調(diào)度剖析【第二部分】
Linux可以通過epoll實現(xiàn)網(wǎng)絡(luò)調(diào)用,統(tǒng)稱網(wǎng)絡(luò)輪詢器N(Net Poller)。
文件IO操作
上面都是防止M堵塞,任務(wù)竊取是防止M空閑
每個M都有一個特殊的G,g0。用于執(zhí)行調(diào)度,gc,棧管理等任務(wù),所以g0的棧稱為調(diào)度棧。g0的棧不會自動增長,不會被gc,來自os線程的棧。
go1.10\src\runtime\proc.go
G沒辦法自己運行,必須通過M運行
M通過通過調(diào)度,執(zhí)行G
從M掛載P的runq中找到G,執(zhí)行G
首先你的理解是錯的,不管用戶態(tài)的API(syscall)是否是同步還是異步,在kernel層面都是異步的。
其實實現(xiàn)原理很簡單,就是利用C(嵌入?yún)R編)語言可以直接修改寄存器(setcontext/setjmp/longjmp均是類似原理,修改程序指針eip實現(xiàn)跳轉(zhuǎn),棧指針實現(xiàn)上線文切換)來實現(xiàn)從func_a調(diào)進去,從func_b返回出來這種行為。對于golang來說,func_a/func_b屬于不同的goroutine,從而就實現(xiàn)了goroutine的調(diào)度切換。
另外對于所有可能阻塞的syscall,golang對其進行了封裝,底層實際是epoll方式做的,注冊回調(diào)后切換到另一個runnable的goroutine。
epoll是linux中IO多路復(fù)用的一種機制,I/O多路復(fù)用就是通過一種機制,一個進程可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應(yīng)的讀寫操作。當然linux中IO多路復(fù)用不僅僅是epoll,其他多路復(fù)用機制還有select、poll,但是接下來介紹epoll的內(nèi)核實現(xiàn)。
events可以是以下幾個宏的集合:
epoll相比select/poll的優(yōu)勢 :
epoll相關(guān)的內(nèi)核代碼在fs/eventpoll.c文件中,下面分別分析epoll_create、epoll_ctl和epoll_wait三個函數(shù)在內(nèi)核中的實現(xiàn),分析所用linux內(nèi)核源碼為4.1.2版本。
epoll_create用于創(chuàng)建一個epoll的句柄,其在內(nèi)核的系統(tǒng)實現(xiàn)如下:
sys_epoll_create:
可見,我們在調(diào)用epoll_create時,傳入的size參數(shù),僅僅是用來判斷是否小于等于0,之后再也沒有其他用處。
整個函數(shù)就3行代碼,真正的工作還是放在sys_epoll_create1函數(shù)中。
sys_epoll_create - sys_epoll_create1:
sys_epoll_create1 函數(shù)流程如下:
sys_epoll_create - sys_epoll_create1 - ep_alloc:
sys_epoll_create - sys_epoll_create1 - ep_alloc - get_unused_fd_flags:
linux內(nèi)核中,current是個宏,返回的是一個task_struct結(jié)構(gòu)(我們稱之為進程描述符)的變量,表示的是當前進程,進程打開的文件資源保存在進程描述符的files成員里面,所以current-files返回的當前進程打開的文件資源。rlimit(RLIMIT_NOFILE) 函數(shù)獲取的是當前進程可以打開的最大文件描述符數(shù),這個值可以設(shè)置,默認是1024。
相關(guān)視頻推薦:
支撐億級io的底層基石 epoll實戰(zhàn)揭秘
網(wǎng)絡(luò)原理tcp/udp,網(wǎng)絡(luò)編程epoll/reactor,面試中正經(jīng)“八股文”
學習地址:C/C++Linux服務(wù)器開發(fā)/后臺架構(gòu)師【零聲教育】-學習視頻教程-騰訊課堂
需要更多C/C++ Linux服務(wù)器架構(gòu)師學習資料加群 812855908 獲取(資料包括C/C++,Linux,golang技術(shù),Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協(xié)程,DPDK,ffmpeg等),免費分享
__alloc_fd的工作是為進程在[start,end)之間(備注:這里start為0, end為進程可以打開的最大文件描述符數(shù))分配一個可用的文件描述符,這里就不繼續(xù)深入下去了,代碼如下:
sys_epoll_create - sys_epoll_create1 - ep_alloc - get_unused_fd_flags - __alloc_fd:
然后,epoll_create1會調(diào)用anon_inode_getfile,創(chuàng)建一個file結(jié)構(gòu),如下:
sys_epoll_create - sys_epoll_create1 - anon_inode_getfile:
anon_inode_getfile函數(shù)中首先會alloc一個file結(jié)構(gòu)和一個dentry結(jié)構(gòu),然后將該file結(jié)構(gòu)與一個匿名inode節(jié)點anon_inode_inode掛鉤在一起,這里要注意的是,在調(diào)用anon_inode_getfile函數(shù)申請file結(jié)構(gòu)時,傳入了前面申請的eventpoll結(jié)構(gòu)的ep變量,申請的file-private_data會指向這個ep變量,同時,在anon_inode_getfile函數(shù)返回來后,ep-file會指向該函數(shù)申請的file結(jié)構(gòu)變量。
簡要說一下file/dentry/inode,當進程打開一個文件時,內(nèi)核就會為該進程分配一個file結(jié)構(gòu),表示打開的文件在進程的上下文,然后應(yīng)用程序會通過一個int類型的文件描述符來訪問這個結(jié)構(gòu),實際上內(nèi)核的進程里面維護一個file結(jié)構(gòu)的數(shù)組,而文件描述符就是相應(yīng)的file結(jié)構(gòu)在數(shù)組中的下標。
dentry結(jié)構(gòu)(稱之為“目錄項”)記錄著文件的各種屬性,比如文件名、訪問權(quán)限等,每個文件都只有一個dentry結(jié)構(gòu),然后一個進程可以多次打開一個文件,多個進程也可以打開同一個文件,這些情況,內(nèi)核都會申請多個file結(jié)構(gòu),建立多個文件上下文。但是,對同一個文件來說,無論打開多少次,內(nèi)核只會為該文件分配一個dentry。所以,file結(jié)構(gòu)與dentry結(jié)構(gòu)的關(guān)系是多對一的。
同時,每個文件除了有一個dentry目錄項結(jié)構(gòu)外,還有一個索引節(jié)點inode結(jié)構(gòu),里面記錄文件在存儲介質(zhì)上的位置和分布等信息,每個文件在內(nèi)核中只分配一個inode。 dentry與inode描述的目標是不同的,一個文件可能會有好幾個文件名(比如鏈接文件),通過不同文件名訪問同一個文件的權(quán)限也可能不同。dentry文件所代表的是邏輯意義上的文件,記錄的是其邏輯上的屬性,而inode結(jié)構(gòu)所代表的是其物理意義上的文件,記錄的是其物理上的屬性。dentry與inode結(jié)構(gòu)的關(guān)系是多對一的關(guān)系。
sys_epoll_create - sys_epoll_create1 - fd_install:
總結(jié)epoll_create函數(shù)所做的事:調(diào)用epoll_create后,在內(nèi)核中分配一個eventpoll結(jié)構(gòu)和代表epoll文件的file結(jié)構(gòu),并且將這兩個結(jié)構(gòu)關(guān)聯(lián)在一塊,同時,返回一個也與file結(jié)構(gòu)相關(guān)聯(lián)的epoll文件描述符fd。當應(yīng)用程序操作epoll時,需要傳入一個epoll文件描述符fd,內(nèi)核根據(jù)這個fd,找到epoll的file結(jié)構(gòu),然后通過file,獲取之前epoll_create申請eventpoll結(jié)構(gòu)變量,epoll相關(guān)的重要信息都存儲在這個結(jié)構(gòu)里面。接下來,所有epoll接口函數(shù)的操作,都是在eventpoll結(jié)構(gòu)變量上進行的。
所以,epoll_create的作用就是為進程在內(nèi)核中建立一個從epoll文件描述符到eventpoll結(jié)構(gòu)變量的通道。
epoll_ctl接口的作用是添加/修改/刪除文件的監(jiān)聽事件,內(nèi)核代碼如下:
sys_epoll_ctl:
根據(jù)前面對epoll_ctl接口的介紹,op是對epoll操作的動作(添加/修改/刪除事件),ep_op_has_event(op)判斷是否不是刪除操作,如果op != EPOLL_CTL_DEL為true,則需要調(diào)用copy_from_user函數(shù)將用戶空間傳過來的event事件拷貝到內(nèi)核的epds變量中。因為,只有刪除操作,內(nèi)核不需要使用進程傳入的event事件。
接著連續(xù)調(diào)用兩次fdget分別獲取epoll文件和被監(jiān)聽文件(以下稱為目標文件)的file結(jié)構(gòu)變量(備注:該函數(shù)返回fd結(jié)構(gòu)變量,fd結(jié)構(gòu)包含file結(jié)構(gòu))。
接下來就是對參數(shù)的一些檢查,出現(xiàn)如下情況,就可以認為傳入的參數(shù)有問題,直接返回出錯:
當然下面還有一些關(guān)于操作動作如果是添加操作的判斷,這里不做解釋,比較簡單,自行閱讀。
在ep里面,維護著一個紅黑樹,每次添加注冊事件時,都會申請一個epitem結(jié)構(gòu)的變量表示事件的監(jiān)聽項,然后插入ep的紅黑樹里面。在epoll_ctl里面,會調(diào)用ep_find函數(shù)從ep的紅黑樹里面查找目標文件表示的監(jiān)聽項,返回的監(jiān)聽項可能為空。
接下來switch這塊區(qū)域的代碼就是整個epoll_ctl函數(shù)的核心,對op進行switch出來的有添加(EPOLL_CTL_ADD)、刪除(EPOLL_CTL_DEL)和修改(EPOLL_CTL_MOD)三種情況,這里我以添加為例講解,其他兩種情況類似,知道了如何添加監(jiān)聽事件,其他刪除和修改監(jiān)聽事件都可以舉一反三。
為目標文件添加監(jiān)控事件時,首先要保證當前ep里面還沒有對該目標文件進行監(jiān)聽,如果存在(epi不為空),就返回-EEXIST錯誤。否則說明參數(shù)正常,然后先默認設(shè)置對目標文件的POLLERR和POLLHUP監(jiān)聽事件,然后調(diào)用ep_insert函數(shù),將對目標文件的監(jiān)聽事件插入到ep維護的紅黑樹里面:
sys_epoll_ctl - ep_insert:
前面說過,對目標文件的監(jiān)聽是由一個epitem結(jié)構(gòu)的監(jiān)聽項變量維護的,所以在ep_insert函數(shù)里面,首先調(diào)用kmem_cache_alloc函數(shù),從slab分配器里面分配一個epitem結(jié)構(gòu)監(jiān)聽項,然后對該結(jié)構(gòu)進行初始化,這里也沒有什么好說的。我們接下來看ep_item_poll這個函數(shù)調(diào)用:
sys_epoll_ctl - ep_insert - ep_item_poll:
ep_item_poll函數(shù)里面,調(diào)用目標文件的poll函數(shù),這個函數(shù)針對不同的目標文件而指向不同的函數(shù),如果目標文件為套接字的話,這個poll就指向sock_poll,而如果目標文件為tcp套接字來說,這個poll就是tcp_poll函數(shù)。雖然poll指向的函數(shù)可能會不同,但是其作用都是一樣的,就是獲取目標文件當前產(chǎn)生的事件位,并且將監(jiān)聽項綁定到目標文件的poll鉤子里面(最重要的是注冊ep_ptable_queue_proc這個poll callback回調(diào)函數(shù)),這步操作完成后,以后目標文件產(chǎn)生事件就會調(diào)用ep_ptable_queue_proc回調(diào)函數(shù)。
接下來,調(diào)用list_add_tail_rcu將當前監(jiān)聽項添加到目標文件的f_ep_links鏈表里面,該鏈表是目標文件的epoll鉤子鏈表,所有對該目標文件進行監(jiān)聽的監(jiān)聽項都會加入到該鏈表里面。
然后就是調(diào)用ep_rbtree_insert,將epi監(jiān)聽項添加到ep維護的紅黑樹里面,這里不做解釋,代碼如下:
sys_epoll_ctl - ep_insert - ep_rbtree_insert:
前面提到,ep_insert有調(diào)用ep_item_poll去獲取目標文件產(chǎn)生的事件位,在調(diào)用epoll_ctl前這段時間,可能會產(chǎn)生相關(guān)進程需要監(jiān)聽的事件,如果有監(jiān)聽的事件產(chǎn)生,(revents event-events 為 true),并且目標文件相關(guān)的監(jiān)聽項沒有鏈接到ep的準備鏈表rdlist里面的話,就將該監(jiān)聽項添加到ep的rdlist準備鏈表里面,rdlist鏈接的是該epoll描述符監(jiān)聽的所有已經(jīng)就緒的目標文件的監(jiān)聽項。并且,如果有任務(wù)在等待產(chǎn)生事件時,就調(diào)用wake_up_locked函數(shù)喚醒所有正在等待的任務(wù),處理相應(yīng)的事件。當進程調(diào)用epoll_wait時,該進程就出現(xiàn)在ep的wq等待隊列里面。接下來講解epoll_wait函數(shù)。
總結(jié)epoll_ctl函數(shù):該函數(shù)根據(jù)監(jiān)聽的事件,為目標文件申請一個監(jiān)聽項,并將該監(jiān)聽項掛人到eventpoll結(jié)構(gòu)的紅黑樹里面。
epoll_wait等待事件的產(chǎn)生,內(nèi)核代碼如下:
sys_epoll_wait:
首先是對進程傳進來的一些參數(shù)的檢查:
參數(shù)全部檢查合格后,接下來就調(diào)用ep_poll函數(shù)進行真正的處理:
sys_epoll_wait - ep_poll:
ep_poll中首先是對等待時間的處理,timeout超時時間以ms為單位,timeout大于0,說明等待timeout時間后超時,如果timeout等于0,函數(shù)不阻塞,直接返回,小于0的情況,是永久阻塞,直到有事件產(chǎn)生才返回。
當沒有事件產(chǎn)生時((!ep_events_available(ep))為true),調(diào)用__add_wait_queue_exclusive函數(shù)將當前進程加入到ep-wq等待隊列里面,然后在一個無限for循環(huán)里面,首先調(diào)用set_current_state(TASK_INTERRUPTIBLE),將當前進程設(shè)置為可中斷的睡眠狀態(tài),然后當前進程就讓出cpu,進入睡眠,直到有其他進程調(diào)用wake_up或者有中斷信號進來喚醒本進程,它才會去執(zhí)行接下來的代碼。
如果進程被喚醒后,首先檢查是否有事件產(chǎn)生,或者是否出現(xiàn)超時還是被其他信號喚醒的。如果出現(xiàn)這些情況,就跳出循環(huán),將當前進程從ep-wp的等待隊列里面移除,并且將當前進程設(shè)置為TASK_RUNNING就緒狀態(tài)。
如果真的有事件產(chǎn)生,就調(diào)用ep_send_events函數(shù),將events事件轉(zhuǎn)移到用戶空間里面。
sys_epoll_wait - ep_poll - ep_send_events:
ep_send_events沒有什么工作,真正的工作是在ep_scan_ready_list函數(shù)里面:
sys_epoll_wait - ep_poll - ep_send_events - ep_scan_ready_list:
ep_scan_ready_list首先將ep就緒鏈表里面的數(shù)據(jù)鏈接到一個全局的txlist里面,然后清空ep的就緒鏈表,同時還將ep的ovflist鏈表設(shè)置為NULL,ovflist是用單鏈表,是一個接受就緒事件的備份鏈表,當內(nèi)核進程將事件從內(nèi)核拷貝到用戶空間時,這段時間目標文件可能會產(chǎn)生新的事件,這個時候,就需要將新的時間鏈入到ovlist里面。
僅接著,調(diào)用sproc回調(diào)函數(shù)(這里將調(diào)用ep_send_events_proc函數(shù))將事件數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
sys_epoll_wait - ep_poll - ep_send_events - ep_scan_ready_list - ep_send_events_proc:
ep_send_events_proc回調(diào)函數(shù)循環(huán)獲取監(jiān)聽項的事件數(shù)據(jù),對每個監(jiān)聽項,調(diào)用ep_item_poll獲取監(jiān)聽到的目標文件的事件,如果獲取到事件,就調(diào)用__put_user函數(shù)將數(shù)據(jù)拷貝到用戶空間。
回到ep_scan_ready_list函數(shù),上面說到,在sproc回調(diào)函數(shù)執(zhí)行期間,目標文件可能會產(chǎn)生新的事件鏈入ovlist鏈表里面,所以,在回調(diào)結(jié)束后,需要重新將ovlist鏈表里面的事件添加到rdllist就緒事件鏈表里面。
同時在最后,如果rdlist不為空(表示是否有就緒事件),并且由進程等待該事件,就調(diào)用wake_up_locked再一次喚醒內(nèi)核進程處理事件的到達(流程跟前面一樣,也就是將事件拷貝到用戶空間)。
到這,epoll_wait的流程是結(jié)束了,但是有一個問題,就是前面提到的進程調(diào)用epoll_wait后會睡眠,但是這個進程什么時候被喚醒呢?在調(diào)用epoll_ctl為目標文件注冊監(jiān)聽項時,對目標文件的監(jiān)聽項注冊一個ep_ptable_queue_proc回調(diào)函數(shù),ep_ptable_queue_proc回調(diào)函數(shù)將進程添加到目標文件的wakeup鏈表里面,并且注冊ep_poll_callbak回調(diào),當目標文件產(chǎn)生事件時,ep_poll_callbak回調(diào)就去喚醒等待隊列里面的進程。
總結(jié)一下epoll該函數(shù): epoll_wait函數(shù)會使調(diào)用它的進程進入睡眠(timeout為0時除外),如果有監(jiān)聽的事件產(chǎn)生,該進程就被喚醒,同時將事件從內(nèi)核里面拷貝到用戶空間返回給該進程。
標題名稱:go語言epoll go語言入門
當前網(wǎng)址:http://chinadenli.net/article26/doeohcg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供標簽優(yōu)化、自適應(yīng)網(wǎng)站、網(wǎng)站策劃、網(wǎng)站制作、微信公眾號、做網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)