2021-05-12 14:32:11
快速記住Linux核心的基本概念
打算給我們部門弄個內部分享。發現大家對一些底層知識的認知停留在一句一句的,比如聽說JVM使用-XX:-UseBiasedLocking取消偏向鎖可以提高效能,因為它只適用於非多執行緒高並行應用。使用數位物件的快取-XX:AutoBoxCacheMax=20000比預設快取-128~127要提高效能。對於JVM和Linux核心,作業系統沒有系統的概念,遇到實際問題往往沒有思路。所以我的內部分享,主要分為Linux部分,jvm部分和redis部分。這篇是Linux篇。學習思路為主,知識為輔。我也是菜鳥一枚~~不過是個鑽石心的菜鳥,不怕別人知道我有多菜。
先說為什麼我要去學習Linux核心。我在上家公司負責整個公司的搜尋引擎。有一次很熟練的在一台虛擬機器上新搭建了一套,壓測到8000,額,報了一個NIO異常,說是:too many open files。當時查了一下,那台機器太破,和很多服務公用,記憶體快滿了。所以換了台好點的機器就沒有這個問題了。但是控制代碼超限到底是個什麼東西呢?先來看看Linux核心的一些基本概念。
大局觀嘛,先來看看unix的體系結構。
簡單解釋一下:任何計算機系統都包含一個基本的程式集合,它控制計算機硬體資源,提供程式執行環境。稱為作業系統。在這個集合裡,最重要的程式被稱為核心,在系統啟動時被裝載。因為它相對較小,而且位於環境的核心。核心的介面被稱為系統呼叫(system call)。公用函數庫構建在系統呼叫介面之上,也可使用系統呼叫。shell是一個特殊的應用程式,為執行其他應用程式提供一個介面。
一些作業系統允許所有的使用者程式直接與硬體部分進行互動,如MS-DOS。但是類Unix作業系統在胡勇應用程式前把與計算機物理組織相關的所有底層細節隱藏了。當程式想使用硬體資源時,必須向作業系統發出一個請求,核心對這個請求進行評估,如果允許使用這個資源,核心代表應用程式與相關的硬體部分進行互動。為了實施這種機制,現代作業系統依靠特殊的硬體特性來禁止使用者程式直接與底層硬體部分打交道,或者直接存取任意的實體地址。硬體為CPU引入了至少兩種不同的執行模式:使用者程式的非特權模式和核心的特權模式。Unix把他們分別稱為使用者態(User Mode)和核心態(Kernel Model)。
我們平時敲的一些Linux命令,實際上都是對應的核心的C語言函數。比如cat xxx | grep 'x'。這裡面兩個命令用|連線起來,這個叫做“管道”。先用男孩紙慣用的職業一點的語言介紹一下:管道是一個廣泛應用的進程間通訊手段。其作用是在具有親緣關係的進程之間傳遞訊息,所謂有親緣關係,是指有同一個祖先。可以是父子,兄弟或者祖孫等等。反正只要共同的祖先呼叫了pipe函數,開啟的管道檔案會在fork之後,被各個後代所共用。其本質是核心維護了一塊緩衝區與管道檔案相關聯,對管道檔案的操作,被核心轉換成對這塊緩衝區記憶體的操作。分為匿名管道和命名管道。
這裡面包含了一些概念。進程的概念大家都應該很清楚:程式的執行範例被稱為進程。UNIX系統確保每個進程都有一個唯一的數位表示符,稱為進程ID(process ID),它是一個非負數。Linux很多命令都會將其顯示出來。有3個用於進程控制的主要函數:fork,exec和waitpid。其中fork函數用來建立一個新進程,此進程是呼叫進程的一個副本,稱為子進程。fork對父進程返回新的子進程的進程ID(一個非負整數),對子進程則返回0。因為fork建立一個新進程,所以說它被呼叫一次,但返回兩次。
一個進程內的所有執行緒共用同一地址空間,檔案描述符,棧以及進程相關的屬性。因為它們能存取同一儲存區,所以各執行緒在存取共用資料時需要採取同步措施以避免不一致性。說到這裡大家都應該多少有些概念了:為什麼進程開銷大,執行緒涉及鎖。
匿名管道是一個未命名的,單向管道,通過父進程和一個子進程之間傳輸資料。只能實現本地機器上兩個進程之間的通訊,而不能實現跨網路的通訊。常用的比如Linux命令。
命名管道是進程間單向或雙向管道,建立時指定一個名字,任何進程都可以通過該名字開啟管道的另一端,可跨網路通訊。
這是一個jvisualvm偵錯的截圖,藍框部分就相當於一個命名管道。
好,現在來回答一個問題:使用者進程間通訊主要哪幾種方式?
剛才說的匿名管道和命名管道都算一種。除此之外,還有:信號,訊息佇列,共用記憶體,號誌和通訊端。不用頭疼,看到最後你很可能會有豁然開朗的感覺,學的東西終於可以串在一起了。
信號(signal):其實是軟中斷信號的簡稱。用來通知進程發生了非同步事件。在軟體層次上是對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求是一樣的。信號是進程間通訊機制中唯一的非同步通訊機制,一個進程不必通過任何操作來等待信號的到達。
收到信號的進程對各種信號有不同的處理方法,主要是三類:
1>類似中斷的處理程式,對於需要處理的信號,進程可以指定處理常式,由該函數來處理。
2>忽略某個信號,對該信號不做任何處理。
3>對該信號的處理保留系統的預設值,這種預設操作,對大部分的信號的預設操作是讓進程終止。進程通過系統呼叫signal來指定進程對某個信號的處理行為。
下面是window的信號列表
Linux也是用kill -l命令:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1
36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
64) SIGRTMAX
我在用gdb命令執行偵錯C語言程式的時候經常可以看到這些號誌。
再來看訊息佇列。訊息佇列提供了一種從一個進程向另一個進程傳送一個資料塊的方法。每個資料塊都被認為含有一個型別,接收進程可以獨立的接收含有不同型別的資料結構。可以通過傳送訊息來避免命名管道的同步和阻塞問題。但是訊息佇列和命名管道一樣,每個資料塊都有一個最大長度的限制。
共用記憶體就是允許兩個不相關的進程存取同一個邏輯記憶體。共用記憶體是在兩個正在執行的進程之間共用和傳遞資料的一種非常有效的方式。不同進程之間共用的記憶體通常安排為同一段實體記憶體。進程可以將同一段共用記憶體連線到他們自己的地址空間中,所有進程都可以存取共用記憶體中的地址。
號誌:為了防止出現因多個程式同時存取一個共用資源而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行執行緒存取程式碼的臨界區域。臨界區域是指執行資料更新的程式碼需要獨占式的執行。而號誌就可以提供這樣的一種存取機制。讓一個臨界區同一時間只有一個執行緒在存取它,也就是說號誌是用來協調對共用資源存取的。
通訊端:這種通訊機制使得用戶端/伺服器的開發工作既可以在本地單機上進行,也可以跨網路進行。它的特性有三個屬性確定:域(domain),型別(type)和協定(protocol)。簡單的說:源IP地址和目的IP地址以及源埠號和目的埠號的組合成為通訊端。
下面介紹一下通訊過程,裡面涉及一些C語言的函數,不用怕,眼熟即可。如果你學習過nio,你會發現這些是很常接觸的。
要想使不同主機的進程通訊,就必須使用通訊端,通訊端是用socket()函數建立,如果需要C/S模式,則需要把server的通訊端與地址和埠系結起來,使用bind(),當上述操作完成後,便可使用listen()來監聽這個埠,如果有其他程式來connect,那麼server將會呼叫accept()來接受這個申請並為其服務。client是呼叫connect()來建立與server之間的連線,這時會使用三次握手來建立一條資料連結。當連線被建立後,server與client便可以通訊了,通訊可以使用read()/write(),send()/recv(),sendto()/recvfrom()等函數來實現,但是不同的函數作用和使用位置是不同的。當資料傳送完後,可以呼叫close()來關閉server與client之間的連結。
到此,本篇文章的主要內容就沒有了,基本就在介紹一個東西:linux核心的進程通訊。這是學習任何高階程式語言nio部分的基礎。下面引入一些輔助理解的概念。
檔案控制代碼:在檔案I/O中,要從一個檔案讀取資料,應用程式首先要呼叫作業系統函數並傳送檔名,並選一個到該檔案的路徑來開啟檔案。該函數取回一個順序號,即檔案控制代碼(file handle),該檔案控制代碼對於開啟的檔案是唯一的識別依據。一個控制代碼就是你給一個檔案,裝置,通訊端(socket)或者管道的一個名字,以便幫助你記住你證處理的名字,並隱藏某些快取等的複雜性。說白了就是檔案指標啦。
檔案描述符:核心利用檔案描述符來存取檔案。開啟現存檔案或新建檔案時,核心會返回一個檔案描述符。讀寫檔案也需要使用檔案描???符來指定待讀寫的檔案。檔案描述符形式上是非負整數,實際上它是一個索引值,指向核心為每一個進程所維護的該進程開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向進程返回一個檔案描述符。在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。但是檔案描述符往往值適用於unix,linux這樣的作業系統。習慣上,標準輸入的檔案描述符是0,標準輸出是1,標準錯誤是2.
`/letv/apps/jdk/bin/java -DappPort=4 $JAVA_OPTS -cp $PHOME/conf:$PHOME/lib/* com.letv.mms.transmission.http.VideoFullServerBootstrap $1 $3 > /dev/null 2>&1 &`
自己部署過java後台程式的話,對上面的shell命令應該都能理解。 /dev/null 2>&1 這裡面的2就是檔案描述符,這個是將錯誤輸出到檔案。
這兩個概念比較繞,不用過多區分,可以當成一回事來理解。開啟檔案(open files)包括檔案控制代碼但不僅限於檔案控制代碼,由於lnux所有的事務都以檔案的形式存在,要使用諸如共用記憶體,號誌,訊息佇列,記憶體對映等都會開啟檔案,但這些不會佔用檔案控制代碼。檢視進程允許開啟的最大檔案控制代碼數的linux命令:ulimit -n
好了,今天的概念都介紹完了,回到最初的問題:too many open files。 當時的機器破,記憶體快滿了。所以搜尋引擎走的是索引檔案,有很多的IO操作,共用記憶體和記憶體對映這塊的檔案肯定是供不上的,報錯了。縈繞在心頭兩年的問題稍微有點認知了。
本文永久更新連結地址:http://www.linuxidc.com/Linux/2017-08/146342.htm
相關文章