wordpress刪除垃圾評論東莞網(wǎng)站seo技術(shù)
文章目錄
- 1.前言
- 2.Linux內(nèi)核中連接的組織形式
- 2.1套接字和文件描述符
- 2.2創(chuàng)建連接 & 獲取連接
- 3.全連接隊列
- 3.1為什么有全連接隊列?
- 3.2全連接隊列的長度
1.前言
TCP是面向連接的,TCP的各種可靠性機(jī)制實際都不是從主機(jī)到主機(jī)的,而是基于連接的。
比如一臺服務(wù)器啟動后可能有多個客戶端前來訪問,如果TCP不是基于連接的,也就意味著服務(wù)器端只有一個接收緩沖區(qū),此時各個客戶端發(fā)來的數(shù)據(jù)都會拷貝到這個接收緩沖區(qū)當(dāng)中,此時這些數(shù)據(jù)就可能會互相干擾。
而我們在進(jìn)行TCP通信之前需要先建立連接,就是因為TCP的各種可靠性保證都是基于連接的,要保證傳輸數(shù)據(jù)的可靠性的前提就是先建立好連接。
而一臺機(jī)器上可能會存在大量的連接,此時操作系統(tǒng)就不得不對這些連接進(jìn)行管理。
- 操作系統(tǒng)在管理這些連接時需要“先描述,再組織”,在操作系統(tǒng)中一定有一個描述連接的結(jié)構(gòu)體,該結(jié)構(gòu)體當(dāng)中包含了連接的各種屬性字段,所有定義出來的連接結(jié)構(gòu)體最終都會以某種數(shù)據(jù)結(jié)構(gòu)組織起來,此時操作系統(tǒng)對連接的管理就變成了對該數(shù)據(jù)結(jié)構(gòu)的增刪查改。
- 建立連接,實際就是在操作系統(tǒng)中用該結(jié)構(gòu)體定義一個結(jié)構(gòu)體變量,然后填充連接的各種屬性字段,最后將其插入到管理連接的數(shù)據(jù)結(jié)構(gòu)當(dāng)中即可。
- 斷開連接,實際就是將某個連接從管理連接的數(shù)據(jù)結(jié)構(gòu)當(dāng)中刪除,釋放該連接曾經(jīng)占用的各種資源。
以上都是理論層次的理解,那我們今天就具體地探究Linux源碼中連接是如何組織起來的:
2.Linux內(nèi)核中連接的組織形式
2.1套接字和文件描述符
網(wǎng)絡(luò)通信本質(zhì)上也是IO的過程,而且之前我們也說過調(diào)用send、recv等函數(shù)本質(zhì)上是向Tcp維護(hù)的發(fā)送緩沖區(qū)、接收緩沖區(qū)寫入和讀出數(shù)據(jù),既然是IO操作,所以一個套接字的本質(zhì)其實就是一個文件描述符對應(yīng)的文件。Linux下一切皆文件。
一個服務(wù)器本質(zhì)上就是一個進(jìn)程,而進(jìn)程到文件的關(guān)系我們早已經(jīng)在系統(tǒng)部分學(xué)習(xí)過:
當(dāng)我們創(chuàng)建套接字時,Linux系統(tǒng)還會為我們創(chuàng)建一個新的結(jié)構(gòu)體對象struct socket:

我們觀察到這個結(jié)構(gòu)體內(nèi)部包含了一個struct file類型的指針,該指針指向的就是該套接字對應(yīng)的文件對象,所以我們此時可以通過該套接字找到對應(yīng)的文件了,但是更重要的是我們需要通過文件描述符找到對應(yīng)的套接字呢呀,現(xiàn)在只有一個單向的指針,即我現(xiàn)在需要從文件找到對應(yīng)的套接字對象。
所以在struct file結(jié)構(gòu)體中還包含一個指針:

這個指針指向的就是套接字socket結(jié)構(gòu),所以我們現(xiàn)在就可以通過該文件描述符完成對套接字的操作了(讀取數(shù)據(jù)、獲取連接等)。
2.2創(chuàng)建連接 & 獲取連接
我們上面提到過連接本質(zhì)上是內(nèi)核中的一種數(shù)據(jù)結(jié)構(gòu),在Linux中實際就是struct tcp_sock
結(jié)構(gòu)體,該結(jié)構(gòu)體專門用于TCP協(xié)議。
它包含了TCP協(xié)議特有的字段和方法,如TCP頭部長度(tcp_header_len)、滑動窗口(rcv_wnd、snd_wnd)、擁塞控制算法相關(guān)字段(如srtt_us、mdev_us等)以及發(fā)送和接收隊列等。
這個結(jié)構(gòu)體是TCP連接在內(nèi)核中的完整表示,包含了TCP協(xié)議運(yùn)行所需的所有狀態(tài)信息和控制邏輯。
struct tcp_sock
結(jié)構(gòu)體的第一個字段是struct inet_connection_sock
結(jié)構(gòu)體,該結(jié)構(gòu)體增加了與連接管理相關(guān)的字段,如連接狀態(tài)(icsk_state)、重傳機(jī)制等。這個結(jié)構(gòu)體為TCP連接提供了必要的狀態(tài)管理和控制機(jī)制。
struct inet_connection_sock
結(jié)構(gòu)體的第一個字段是struct inet_sock
結(jié)構(gòu)體,該結(jié)構(gòu)體增加了與IP層相關(guān)的字段和方法,如IP地址(sin_addr或sin6_addr)、端口號(sin_port或sin6_port)等。這個結(jié)構(gòu)體為TCP和UDP等基于IP的協(xié)議提供了更具體的支持。
struct inet_sock
結(jié)構(gòu)體的第一個字段是struct sock
結(jié)構(gòu)體,該結(jié)構(gòu)體包含了如套接字狀態(tài)(state)、接收和發(fā)送緩沖區(qū)(sk_buff)隊列、定時器(timer)等通用字段。
更重要的是你會發(fā)現(xiàn)與文件描述符相關(guān)的struct socket結(jié)構(gòu)體中有一個字段就是struct sock
類型的指針 sk:

所以socket套接字可以通過這個 sk指針 獲取tcp_sock
結(jié)構(gòu)體中的所有字段內(nèi)容(通過類型轉(zhuǎn)換)。
比如想要獲取
tcp_sock
結(jié)構(gòu)體中inet_connection_sock
結(jié)構(gòu)體中的字段內(nèi)容,就可以將sk指針轉(zhuǎn)換成inet_connection_sock
類型獲取。這種通過一個指針獲取不同結(jié)構(gòu)體中屬性的方式被稱為“C風(fēng)格的多態(tài)”。
以上是Tcp連接,如果是Udp連接呢?我們說Udp是無連接的通信協(xié)議,所以對于Udp來說沒有struct inet_connection_sock
結(jié)構(gòu)體,因為該結(jié)構(gòu)體內(nèi)部維護(hù)的是與連接管理的相關(guān)字段,但是同樣的根據(jù)socket
結(jié)構(gòu)體中的 sk指針 指向udp_sock
結(jié)構(gòu)體來獲取udp連接的各種屬性內(nèi)容。所以 該socket結(jié)構(gòu)體 被稱為 “BSD socket ”— 通用socket接口。
既然socket結(jié)構(gòu)體既可以指向Tcp套接字又可以指向Udp套接字,那是如何區(qū)分不同套接字類型的呢?
int socket(int domain, int type, int protocol);
參數(shù)type對應(yīng)著socket結(jié)構(gòu)體中的type字段:
所以創(chuàng)建一個listen套接字的流程就是申請文件描述符獲得文件結(jié)構(gòu)體,創(chuàng)建套接字socket和連接tcp_sock,然后將他們關(guān)聯(lián)起來。
那么調(diào)用accept()函數(shù)是從listen套接字監(jiān)聽的套接字中獲取普通連接并返回,這個過程又是怎樣的呢?
實際上在struct inet_connection_sock
結(jié)構(gòu)體中維護(hù)一個全連接隊列,當(dāng)經(jīng)歷過三次握手后,系統(tǒng)會自動創(chuàng)建一個連接tcp_sock,然后將該連接加入到全連接隊列中,當(dāng)調(diào)用accept()函數(shù)時,操作系統(tǒng)會申請新的文件描述符和套接字socket,然后從全連接隊列中取出一個連接tcp_sock,之后普通套接字socket中的 sk指針 指向該連接tcp_sock,就完成了獲取連接的操作。
3.全連接隊列
實際TCP在進(jìn)行連接管理時會用到兩個連接隊列:
- 全連接隊列(accept隊列)。全連接隊列用于保存處于ESTABLISHED狀態(tài),但沒有被上層調(diào)用accept取走的連接。
- 半連接隊列。半連接隊列用于保存處于SYN_SENT和SYN_RCVD狀態(tài)的連接,也就是還未完成三次握手的連接,維護(hù)時間比較短。
而全連接隊列的長度實際會受到listen第二個參數(shù)的影響,一般TCP全連接隊列的長度就等于listen第二個參數(shù)backlog
的值加一。
int listen(int sockfd, int backlog);
如果將listen的第二個參數(shù)值設(shè)置為3,此時服務(wù)器端最多就允許存在4個處于ESTABLISHED狀態(tài)的連接。
在服務(wù)器端已經(jīng)有4個ESTABLISHED狀態(tài)的連接的情況下,再有客戶端發(fā)來建立連接請求,此時服務(wù)器端就會新增狀態(tài)為SYN_RCVD的連接,該連接實際就是放在半連接隊列當(dāng)中的。
3.1為什么有全連接隊列?
一般當(dāng)服務(wù)器壓力較大時連接隊列的作用才會體現(xiàn)出來,如果服務(wù)器壓力本身就不大,那么一旦底層有連接建立成功,上層就會立馬將該連接讀走并進(jìn)行處理。
服務(wù)器端啟動時一般會預(yù)先創(chuàng)建多個服務(wù)線程為客戶端提供服務(wù),主線程從底層accept上來連接后就可以將其交給這些服務(wù)線程進(jìn)行處理。
如果向服務(wù)器發(fā)起連接請求的客戶端很少,那么連接一旦在底層建立好就被主線程立馬accept上來并交給服務(wù)線程處理了。
但如果向服務(wù)器發(fā)起連接請求的客戶端非常多并且業(yè)務(wù)處理非常繁忙,即當(dāng)每個服務(wù)線程都在為某個連接提供服務(wù)時,底層再建立好連接主線程就不能獲取上來了,此時底層這些已經(jīng)建立好的連接就會被放到連接隊列當(dāng)中,只有等某個服務(wù)線程空閑時,主線程就會從這個連接隊列當(dāng)中獲取建立好的連接。
如果沒有這個連接隊列,那么當(dāng)服務(wù)器端的服務(wù)線程都在提供服務(wù)時,其他客戶端發(fā)來的連接請求就會直接被拒絕。
但有可能正當(dāng)這個連接請求被拒絕時,某個服務(wù)線程提供服務(wù)完畢,此時這個服務(wù)線程就無法立馬得到一個連接為之提供服務(wù),所以一定有一段時間內(nèi)這個服務(wù)線程是處于閑置狀態(tài)的,直到再有客戶端發(fā)來連接請求。
而如果設(shè)置了連接隊列,當(dāng)某個服務(wù)線程提供完服務(wù)后,如果連接隊列當(dāng)中有建立好的連接,那么主線程就可以立馬從連接隊列當(dāng)中獲取一個連接交給該服務(wù)線程進(jìn)行處理,此時就可以保證服務(wù)器幾乎是滿載工作的,降低了服務(wù)器的閑置率。
3.2全連接隊列的長度
雖然維護(hù)連接隊列能讓服務(wù)器處于幾乎滿載工作的狀態(tài),但連接隊列也不能設(shè)置得太長。
- 如果隊列太長,也就意味著在隊列較尾部的連接需要等待較長時間才能得到服務(wù),此時客戶端的請求也就遲遲得不到響應(yīng)。
- 此外,服務(wù)器維護(hù)連接也是需要成本的,連接隊列設(shè)置的越長,系統(tǒng)就要花費(fèi)越多的成本去維護(hù)這個隊列。
- 但與其與其維護(hù)一個長連接,造成客戶端等待過久,并且占用大量暫時用不到的資源,還不如將部分資源節(jié)省出來給服務(wù)器使用,讓服務(wù)器更快的為客戶端提供服務(wù)。
所以全連接隊列要取一個合適的長度,系統(tǒng)一般設(shè)置為5。
全連接隊列的長度=min(backlog,net.core.somaxconn)+1:
- 用戶層調(diào)用listen時傳入的第二個參數(shù)backlog。
- 系統(tǒng)變量net.core.somaxconn,在 Linux 系統(tǒng)中,這個值默認(rèn)可能因不同的發(fā)行版和內(nèi)核版本而異,但常見的默認(rèn)值可能是 128。然而,對于高負(fù)載的服務(wù)器,特別是在處理大量并發(fā)連接時,這個默認(rèn)值可能太低,導(dǎo)致新的連接被拒絕(因為監(jiān)聽隊列已滿)。
通過以下命令可以查看系統(tǒng)變量net.core.somaxconn的值。
sudo sysctl -a | grep net.core.somaxconn
Stay hungry, Stay foolish. —史蒂夫-喬布斯