Kernel:作業系統核心VFS:虛拟檔案系統,樹結構,為什麼是虛拟檔案系統?因為VFS隻是一個目錄樹,映射到實體位址,挂載
用df指令可以看出,我的根目錄 / 是挂在/dev/mapper/centos_bogon-root(實體位址)目錄下的,而/boot是挂在/dev/sda1目錄下的,是以VFS隻是一個虛拟的目錄,這個目錄的結構是趨于穩定的,和真實檔案的位置不對等,這就是虛拟記憶體的概念。FD:檔案描述符,Linux系統在兩個程式通路同一個檔案的時候,,檔案隻會在記憶體加載一次,兩個程式共享同一份,OS會為兩個程式各自維護一個檔案描述符,這個檔案描述符指向檔案的具體位置,每個檔案描述符中都有一個seek指針,這個指針代表偏移量。
Seek偏移量,你每讀取一個位元組,seek加一,0t0 ->0t1,
每一個檔案都有0:标準輸入,1:标準輸出,2:報錯輸出。
Dirty:髒,什麼是髒?我們知道存放在記憶體(RAM)中的資料關機時是不會儲存的,是以要把記憶體中的資料刷寫到磁盤(ROM),這個還沒有刷寫的狀态就是髒,這個刷寫過程肯定不是實時的,它采用的是LRU算法,他可以根據規定的時間進行刷寫或者目前資料在記憶體中所占的比例,超過這個比例也會刷寫,那麼當程式加載一個檔案到記憶體并修改,這個時候斷電,記憶體中的資料是不會刷寫的,就會丢失資料,這是系統核心的刷寫方法,每個程式都可以定義自己的刷寫方法,例如Redis中的刷寫政策。
ln連接配接指令硬連接配接:兩個虛拟位址同時指向一個實體位址,一個檔案在硬連結狀态,删除其中一個引用,對檔案沒有影響。軟連接配接:一個虛拟位址指向另一個虛拟位址,另一個虛拟位址指向實體位址,删除被指向的位址,指向的位址報錯。重定向:把你将要進行的輸入輸出操作重定向到其他操作,和管道不同,管道是傳遞資料。
輸入:<
輸出:>管道:|
當我們在Linux系統中使用管道時,如果管道兩端的指令優先級低于管道的優先級,程序先執行管道指令,會在管道的兩端各啟動一個子程序,而子程序和父程序之間有程序隔離,是以下面的給a指派的操作會在子程序中執行,子程序執行完後關閉,在父程序中是看不到的,是以才需要配置環境變量,配置環境變量後父子程序都會看到這個檔案。
PageCache:緩存頁,
為什麼使用PageCache?
減少硬體IO調用,讓程式優先使用記憶體
一個程式處于運作狀态,如果它想要讀取一段資料,會先去記憶體的PageCache中讀取,如果沒有,就會觸發缺頁中斷,OS保護現場,CPU中的緩存資料重新寫入記憶體,從使用者态切換到核心态,由協處理器從磁盤上擷取資料,這個時候程式處于挂起狀态,不受作業系統排程,當資料擷取完成又會觸發一次中斷,程式修改為活躍狀态。
程序配置設定記憶體:注意:這裡說的是虛拟空間。系統給程序配置設定記憶體會配置設定代碼區記憶體、資料區記憶體、堆、棧,這裡的堆是程序的堆,程序會根據你自己配置的Xmx來開辟JVM堆,堆内記憶體是JVM堆的堆内記憶體,堆外記憶體是JVM堆外,如果系統通路這個程序中的堆記憶體,通路堆外記憶體的速度比通路堆内記憶體速度快,為什麼?因為通路堆内記憶體時,JVM會把堆内記憶體位址拷貝到堆外記憶體中,而堆外記憶體位址是可以直接通路的。
檔案IO
普通IO和bufferedIO誰快,為什麼?
BufferedIO快,因為使用BufferedIO時,JVM虛拟機會維護一塊8kb的數組,一次寫8kb,8kb滿了觸發中斷,切換核心态,而普通IO是按照位元組進行寫操作,寫完觸發中斷,寫入磁盤,然後繼續進行寫操作,BufferedIO節省的時間是使用者态和核心态切換的時間。NIO
普通IO是通過系統調用,需要進行使用者态和核心态的切換,而NIO不需要經過系統調用,當他調用FileChannel中的map方法(注意:隻有檔案可以調用map方法),會把檔案通過位元組流直接映射到一個程序和記憶體共享的空間,不需要進行使用者态和核心态的切換,但是他還是會受到PageCache的限制,會丢資料。可以使用c程式員寫的jni擴充庫,使用Linux核心的Direct IO,他不會使用Linux系統的PageCache體系,允許你自己建立自己的PageCache,把PageCache私有化,其他程序通過Linux排程不會通路到你自己建立的PageCache,但是PageCache所有的毛病(丢資料/髒)他都有,不過需要你自己去處理。mmap記憶體映射:
mmap不是系統調用,不需要進行使用者态和核心态的切換,換言之他不接受系統排程,直接映射到記憶體,但是他還是要受到PageCache限制,還是會丢資料。
網絡IO
服務端啟動時會啟動一個監聽服務,這個監聽會監聽你配置連接配接的端口号,三次握手是通過這個監聽服務進行的,當用戶端啟動時,會随機配置設定一個端口号去連接配接服務端,為什麼說TCP協定是面向連接配接的,因為在用戶端與服務端三次握手成功後,作業系統會在用戶端和服務端同時開辟資源,同時建立socket檔案,這個檔案就代表了連接配接。什麼是socket?
Socket是一個四元組,裡面包括:用戶端IP_用戶端端口号+服務端IP_服務端端口号
如果我們寫一段代碼,把伺服器啟動後,讓他等待鍵盤輸入,這個時候服務端進入IO阻塞狀态,用戶端啟動請求建立連接配接,這個時候是可以進行三次握手的,而且也可以進行資料的傳輸,傳輸完成後存放到緩沖區,緩沖區滿了就會丢資料,
那麼為什麼服務端都已經阻塞住了,用戶端還能向服務端建立連接配接發送資料呢?這是因為資料的傳輸是在核心态的,他是在記憶體中開辟資源,由作業系統進行排程,而IO阻塞等待鍵盤輸入,是在使用者态的,
它是屬于程序自己管理的,是以傳輸資料沒有影響,但是這個時候用戶端通路的服務端記憶體中的檔案還沒有被配置設定檔案描述符,隻是建立了一個連接配接,并緩存了一些資料,但是程式在阻塞中,還沒有主動去調用這個檔案,隻有程式用到這個檔案,這個檔案才會被配置設定檔案描述符。
TCP擁塞:TCP協定三次握手的時候會協商一個隊列大小,發送資料的時候,會把多個資料包打包發送,如果接受的一方隊列滿了,就産生資料擁塞,如果不處理,就會丢包,TCP協定會給另一方發資料告訴他隊列滿了,另一方暫停發送,等待他處理資料包,然後會再發一個資料包告訴他可以繼續發送,這就是擁塞控制。
BIO模型(同步阻塞IO)
BIO 服務端啟動後,主線程調用accept(系統調用)阻塞,等待用戶端連接配接,當用戶端連接配接時,三次握手完後,主線程再次進行系統調用(clone()),克隆一個子線程,用戶端和服務端資料包傳輸通過子線程進行,傳輸完成後,子線程關閉,主線程再次進入阻塞狀态,這個過程中經過了兩次使用者态和核心态的切換。循環等待,在用戶端讀取的時候,等待服務端傳輸資料也會進行使用者态和核心态的切換并阻塞,想要提高效率,可以使用線程池。
BIO使用線程池,預先建立一定數量的線程放到線程池中,不需要頻繁的進行系統調用建立線程,但是多個線程傳輸資料,線程切換還是會進行線程等待,頻繁進行系統調用,線程多了之後,消耗CPU資源,是以就有了NIO。NIO模型(同步非阻塞IO)
服務端啟動後,如果沒有用戶端連接配接,他不會阻塞的去等待用戶端的連接配接,核心會傳回一個-1告訴程序目前沒有用戶端連接配接,可以去幹别的事情,當用戶端連接配接進來,核心在傳回一個用戶端的FD,目前線程就處理請求,不會建立新的線程,而BIO在accept方法中阻塞,沒有傳回值,是以不能繼續執行,而在用戶端讀取的時候雖然不需要進行阻塞,但是還是會進行系統調用,占用CPU資源。
NIO問題:如果你利用線程池一次啟動10000個線程,但是你需要發送的資料隻用到了1000個線程,在NIO模型中,這一千個線程傳回用戶端的檔案描述符,而剩餘的線程會傳回-1,他們都會進行系統調用,是以說你隻用到了1000個線程,他卻調用了10000個,還都是系統調用,切換核心态,消耗CPU資源,是以就有了多路複用器。
多路複用
用戶端的程式使用一個多路複用器到服務端擷取多個IO的狀态,如果某個IO中存在資料包,那麼多路複用器就會告訴程式這個IO的FD,然後由程式自己通過FD擷取資料包。NIO多路複用(Select(在所有系統通用)、Poll兩種)
NIO模型擷取IO狀态是一個線程擷取一個,他們之間是隔離的,而多路複用會在中間加一個多路複用器,他的作用就是統一擷取IO狀态,意思就是之前是10000個IO擷取10000個線程去擷取10000個資料,現在是使用一個多路複用器通過一個線程擷取10000個資料。弊端
每次擷取狀态都要往服務端發送一個fds數組,通過檔案描述符擷取檔案狀态,而且核心都要進行周遊擷取狀态,進階EPOLL。
EPOLL也是利用多路複用器原理,使用一個線程擷取狀态。EPOLL流程
伺服器啟動進入listen狀态,然後調用epoll_cerate(這個方法隻會調用一次)建立一個紅黑樹檔案,調用epoll_ctl來存放檔案描述符到紅黑樹,用戶端擷取狀态時,觸發IO中斷,進行系統調用,會把紅黑樹的資料拷貝到List集合中,程式調用epoll_wait擷取這個集合。
EPOLL在IO和資料處理上做了解耦,因為如果不做解耦的話,IO在讀取資料時,讀取一段處理一段,在處理階段可能會發生阻塞,那麼可不可以異步的進行處理,可以建立一個線程池專門處理發過來的資料包,IO拿到資料之後放到記憶體,交給其他線程處理,但是這種方法進行R/W操作時會産生多餘的系統調用,也會浪費CPU的資源,是以Netty中處理Epoll多路複用時,會産生多個線程,這些線程會線性的分塊處理整個系統的檔案描述符,最好的配置設定的線程數是:CPU核數或者CPU核數的2倍,這裡的負責多路複用器的線程被放在boss組,而正常R/W的線程放在worker組中,由主線程進行配置設定。IO中斷
首先明确,中斷時一定會調用callback,是以可以在callback中加一些操作,EPOLL就是這樣做的,在callback時,不僅會把檔案描述符放到緩存,還會調用EPOLL的方法,把FD的狀态放到一個List集合中,當用戶端與服務端第一次建立連接配接時,通過網卡觸發IO中斷,執行回調函數,傳回FD和連接配接狀态,FD會有一個緩沖區存儲這些狀态,是以EPOLL之前的IO模型都需要周遊FD擷取狀态資訊。Linux系統預設使用EPOLL四次分手
完整的四次分手,用戶端關閉,發送一個FIN包表示想要關閉,服務端接收并向用戶端發送FIN_ACK确認收到,然後服務端再向用戶端發送FIN包表示我也想要關閉,用戶端響應一個ACK确認收到,這個時候4次分手結束。四次分手狀态:
在服務關閉的時候需要調用連接配接另一端的close方法,雙方建立連接配接之後,一端關閉如果沒有調用另一端的close,這個時候就是用戶端(假設這裡是用戶端先發起的關閉)會向服務端發送一個FIN包表示想要關閉,服務端就會進入CLOSE_WAIT狀态并向用戶端發送一個FIN_ACK确認自己收到這個包,這時候用戶端進入FIN_WAIT2狀态等待服務端發送FIN包,但是因為沒有調用服務端的close是以服務端不會給用戶端發送FIN表示想要關閉。
如果調用了close方法,四次分手完成,這個時候發起關閉的一端會進入TIME_WAIT狀态,另一端直接關閉,為什麼四次握手成功會有一個TIME_WAIT狀态,這是因為發起關閉的一端傳回一個ACK确認包之後無法确定另一端有沒有收到這個包,如果另一端因為網絡等原因沒有收到ACK确認包,那麼會再次發送FIN包,這個時候如果用戶端的資源已經清理了,那這個包就會被直接丢棄,服務端就會一直處于等待狀态。TIME_WAIT時間一般時封包活躍時間的兩倍。在TIME_WAIT狀态沒有結束前,會一直占用SOCKET四元組資源,同一組四元組不能建立新的連接配接,在這裡第二次和第三次分手可以合并。
**AIO(異步非阻塞) **
Linux系統目前還沒有成熟的AIO模型