linux多线程_Java+Linux,深入内核源码讲解多线程之进程
之前寫了兩篇文章,都是針對Linux這個系統的,為什么?我為什么這么喜歡寫這個系統的知識,可能就是為了今天的內容多線程系列,現在多線程不是一個面試重點 啊,那如果你能深入系統內核回答這個知識點,面試官會怎么想?你會不會占據面試的主動權(我不會說今天被一個面試者驚艷到了的)今天,我就開始一個系列的內容,多線程--高并發,深入的給大家講解,我就不信講不明白這么個小東西,有問題的地方希望大家能夠指出,謝謝,大家一起成長
今天我們將第一個知識點:進程
Linux 內核如何描述一個進程?
1. Linux 的進程
進程的術語是 process,是 Linux 最基礎的抽象,另一個基礎抽象是文件。
最簡單的理解,進程就是執行中 (executing, 不等于running) 的程序。
更準確一點的理解,進程包括執行中的程序以及相關的資源 (包括cpu狀態、打開的文件、掛起的信號、tty、內存地址空間等)。
一種簡潔的說法:進程 = n*執行流 + 資源,n>=1。
Linux 進程的特點:
2. Linux 的進程描述符
2.1 task_struct
內核里,通過 task_struct 結構體來描述一個進程,稱為進程描述符 (process descriptor),它保存著支撐一個進程正常運行的所有信息。
每一個進程,即便是輕量級進程(即線程),都有1個 task_struct。
?(include\linux)??struct?task_struct?{??struct?thread_info?thread_info;??volatile?long?state;??void?*stack;??[...]??struct?mm_struct?*mm;??[...]??pid_t?pid;??[...]??struct?task_struct?*parent;??[...]??char?comm[TASK_COMM_LEN];??[...]??struct?files_struct?*files;??[...]??struct?signal_struct?*signal;??}?這是一個龐大的結構體,不僅有許多進程相關的基礎字段,還有許多指向其他數據結構的指針。
它包含的字段能完整地描述一個正在執行的程序,包括 cpu 狀態、打開的文件、地址空間、掛起的信號、進程狀態等。
作為初學者,先簡單地了解部分字段就好::
- struct thread_info thread_info: 進程底層信息,平臺相關,下面會詳細描述。
- long state: 進程當前的狀態,下面是幾個比較重要的進程狀態以及它們之間的轉換流程。
- void *stack: 指向進程內核棧,下面會解釋。
- struct mm_struct *mm: 與進程地址空間相關的信息都保存在一個叫內存描述符 (memory descriptor) 的結構體 (mm_struct) 中。
pid_t pid: 進程標識符,本質就是一個數字,是用戶空間引用進程的唯一標識。
struct?task_struct?*parent:?父進程的?task_struct。??char?comm[TASK_COMM_LEN]:?進程的名稱。??struct?files_struct *files:?打開的文件表。??struct?signal_struct?*signal:?信號處理相關。?其他字段,等到有需要的時候再回過頭來學習。
當發生系統調用或者進程切換時,內核如何找到 task_struct ?
對于 ARM 架構,答案是:通過內核棧 (kernel mode stack)。
為什么要有內核棧?
因為內核是可重入的,在內核中會有多條與不同進程相關聯的執行路徑。因此不同的進程處于內核態時,都需要有自己私有的進程內核棧 (process kernel stack)。
當進程從用戶態切換到內核態時,所使用的棧會從用戶棧切換到內核棧。
至于是如何切換的,關鍵詞是系統調用,這不是本文關注的重點,先放一邊,學習內核要懂得恰當的時候忽略細節。
當發生進程切換時,也會切換到目標進程的內核棧。
同上,關鍵詞是硬件上下文切換 (hardware context switch),忽略具體實現。
無論何時,只要進程處于內核態,就會有內核棧可以使用,否則系統就離崩潰不遠了。
ARM 架構的內核棧和 task_struct 的關系如下:
內核棧的長度是 THREAD_SIZE,對于 ARM 架構,一般是 2 個頁框的大小,即 8KB。
內核將一個較小的數據結構 thread_info 放在內核棧的底部,它負責將內核棧和 task_struct 串聯起來。thread_info 是平臺相關的,在 ARM 架構中的定義如下:
//??(arch\arm\include\asm)??struct?thread_info?{??unsigned?long?flags;?/*?low?level?flags?*/??int?preempt_count;?/*?0?=>?preemptable,?<0?=>?bug?*/??mm_segment_t?addr_limit;?/*?address?limit?*/??struct?task_struct?*task;?/*?main?task?structure?*/??[...]??struct?cpu_context_save?cpu_context;?/*?cpu?context?*/??[...]??};?thread_info 保存了一個進程能被調度執行的最底層信息(low level task data),例如struct cpu_context_save cpu_context 會在進程切換時用來保存/恢復寄存器上下文。
內核通過內核棧的棧指針可以快速地拿到 thread_info:
//??(include\linux)??static?inline?struct?thread_info?*current_thread_info(void)??{??//?current_stack_pointer?是當前進程內核棧的棧指針??return?(struct?thread_info?*)??(current_stack_pointer?&?~(THREAD_SIZE?-?1));??}??然后通過?thread_info?找到?task_struct:??//?current.h?(include\asm-generic)??#define?current?(current_thread_info()->task)?內核里通過 current 宏可以獲得當前進程的 task_struct。
2.3 task_struct 的分配和初始化
當上層應用使用 fork() 創建進程時,內核會新建一個 task_struct。
進程的創建是個復雜的工作,可以延伸出無數的細節。這里我們只是簡單地了解一下 task_struct 的分配和部分初始化的流程。
fork() 在內核里的核心流程:
dup_task_struct() 做了什么?
至于設置內核棧里做了什么,涉及到了進程的創建與切換,不在本文的關注范圍內,以后再研究了。
3. 實驗:打印 task_struct / thread_info / kernel mode stack
實驗目的:
- 梳理 task_struct / thread_info / kernel mode stack 的關系。
實驗代碼:
實驗代碼:?#include?<linux/?#include?<linux/?#include?<linux/>??static?void?print_task_info(struct?task_struct?*task)?{?????printk(KERN_NOTICE?"%10s?%5d?task_struct?(%p)?/?stack(%p~%p)?/?thread_info->task?(%p)",?????????task->comm,?????????task->pid,?????????task,?????????task->stack,?????????((unsigned?long?*)task->stack)?+?THREAD_SIZE,?????????task_thread_info(task)->task);?}??static?int?__init?task_init(void)?{?????struct?task_struct?*task?=?current;??????printk(KERN_INFO?"task?module?init\n");??????print_task_info(task);?????do?{?????????task?=?task->parent;?????????print_task_info(task);?????}?while?(task->pid?!=?0);??????return?0;?}?module_init(task_init);??static?void?__exit?task_exit(void)?{?????printk(KERN_INFO?"task?module?exit\n?");?}?module_exit(task_exit);?運行效果:
task?module?init?????insmod??3123?task_struct?(edb42580)?/?stack(ed46c000~ed474000)?/?thread_info->task?(edb42580)???????bash??2393?task_struct?(eda13e80)?/?stack(c9dda000~c9de2000)?/?thread_info->task?(eda13e80)???????sshd??2255?task_struct?(ee5c9f40)?/?stack(c9d2e000~c9d36000)?/?thread_info->task?(ee5c9f40)???????sshd???543?task_struct?(ef15f080)?/?stack(ee554000~ee55c000)?/?thread_info->task?(ef15f080)????systemd?????1?task_struct?(ef058000)?/?stack(ef04c000~ef054000)?/?thread_info->task?(ef058000)?在程序里,我們通過 task_struct 找到 stack,然后通過 stack 找到 thread_info,最后又通過 thread_info->task 找到 task_struct。
到這里,不知道你對進程的概念是不是有了一個清晰的理解
但是上面是通過Linux進行了線程的展示,在日常的工作中,代碼的實現和編寫我們還是以Java為主,那我們來看一下Java進程
進程的創建
Java提供了兩種方法用來啟動進程或其它程序:
- 使用Runtime的exec()方法
- 使用ProcessBuilder的start()方法
ProcessBuilder
ProcessBuilder類是J2SE 1.5在中新添加的一個新類,此類用于創建操作系統進程,它提供一種啟動和管理進程(也就是應用程序)的方法。在J2SE 1.5之前,都是由Process類處來實現進程的控制管理。
每個 ProcessBuilder 實例管理一個進程屬性集。start() 方法利用這些屬性創建一個新的 Process 實例。start() 方法可以從同一實例重復調用,以利用相同的或相關的屬性創建新的子進程。
每個進程生成器管理這些進程屬性:
- 命令 是一個字符串列表,它表示要調用的外部程序文件及其參數(如果有)。在此,表示有效的操作系統命令的字符串列表是依賴于系統的。例如,每一個總體變量,通常都要成為此列表中的元素,但有一些操作系統,希望程序能自己標記命令行字符串——在這種系統中,Java 實現可能需要命令確切地包含這兩個元素。
- 環境 是從變量 到值 的依賴于系統的映射。初始值是當前進程環境的一個副本(請參閱 ())。
- 工作目錄。默認值是當前進程的當前工作目錄,通常根據系統屬性 來命名。
- redirectErrorStream 屬性。最初,此屬性為 false,意思是子進程的標準輸出和錯誤輸出被發送給兩個獨立的流,這些流可以通過 () 和 () 方法來訪問。如果將值設置為 true,標準錯誤將與標準輸出合并。這使得關聯錯誤消息和相應的輸出變得更容易。在此情況下,合并的數據可從 () 返回的流讀取,而從 () 返回的流讀取將直接到達文件尾。
修改進程構建器的屬性將影響后續由該對象的 start() 方法啟動的進程,但從不會影響以前啟動的進程或 Java 自身的進程。大多數錯誤檢查由 start() 方法執行。可以修改對象的狀態,但這樣 start() 將會失敗。例如,將命令屬性設置為一個空列表將不會拋出異常,除非包含了 start()。
注意,此類不是同步的。如果多個線程同時訪問一個 ProcessBuilder,而其中至少一個線程從結構上修改了其中一個屬性,它必須 保持外部同步。
構造方法摘要
ProcessBuilder(List?command)??利用指定的操作系統程序和參數構造一個進程生成器。??ProcessBuilder(String...?command)??利用指定的操作系統程序和參數構造一個進程生成器。?方法摘要
List?command()??返回此進程生成器的操作系統程序和參數。??ProcessBuilder?command(List?command)??設置此進程生成器的操作系統程序和參數。??ProcessBuilder?command(String...?command)??設置此進程生成器的操作系統程序和參數。??File?directory()??返回此進程生成器的工作目錄。??ProcessBuilder?directory(File?directory)??設置此進程生成器的工作目錄。??Map?environment()??返回此進程生成器環境的字符串映射視圖。??boolean?redirectErrorStream()??通知進程生成器是否合并標準錯誤和標準輸出。??ProcessBuilder?redirectErrorStream(boolean?redirectErrorStream)??設置此進程生成器的?redirectErrorStream?屬性。??Process?start()??使用此進程生成器的屬性啟動一個新進程。?1.2 Runtime
每個 Java 應用程序都有一個 Runtime 類實例,使應用程序能夠與其運行的環境相連接。可以通過 getRuntime 方法獲取當前運行時。
應用程序不能創建自己的 Runtime 類實例。但可以通過 getRuntime 方法獲取當前Runtime運行時對象的引用。一旦得到了一個當前的Runtime對象的引用,就可以調用Runtime對象的方法去控制Java虛擬機的狀態和行為。
Java代碼 收藏代碼
void?addShutdownHook(Thread?hook)??注冊新的虛擬機來關閉掛鉤。??int?availableProcessors()??向?Java?虛擬機返回可用處理器的數目。??Process?exec(String?command)??在單獨的進程中執行指定的字符串命令。??Process?exec(String[]?cmdarray)??在單獨的進程中執行指定命令和變量。??Process?exec(String[]?cmdarray,?String[]?envp)??在指定環境的獨立進程中執行指定命令和變量。??Process?exec(String[]?cmdarray,?String[]?envp,?File?dir)??在指定環境和工作目錄的獨立進程中執行指定的命令和變量。??Process?exec(String?command,?String[]?envp)??在指定環境的單獨進程中執行指定的字符串命令。??Process?exec(String?command,?String[]?envp,?File?dir)??在有指定環境和工作目錄的獨立進程中執行指定的字符串命令。??void?exit(int?status)??通過啟動虛擬機的關閉序列,終止當前正在運行的?Java?虛擬機。??long?freeMemory()??返回?Java?虛擬機中的空閑內存量。??void?gc()??運行垃圾回收器。??InputStream?getLocalizedInputStream(InputStream?in)??已過時。?從?JDK??開始,將本地編碼字節流轉換為?Unicode?字符流的首選方法是使用?InputStreamReader?和?BufferedReader?類。??OutputStream?getLocalizedOutputStream(OutputStream?out)??已過時。?從?JDK??開始,將?Unicode?字符流轉換為本地編碼字節流的首選方法是使用?OutputStreamWriter、BufferedWriter?和?PrintWriter?類。??static?Runtime?getRuntime()??返回與當前?Java?應用程序相關的運行時對象。??void?halt(int?status)??強行終止目前正在運行的?Java?虛擬機。??void?load(String?filename)??加載作為動態庫的指定文件名。??void?loadLibrary(String?libname)??加載具有指定庫名的動態庫。??long?maxMemory()??返回?Java?虛擬機試圖使用的最大內存量。??boolean?removeShutdownHook(Thread?hook)??取消注冊某個先前已注冊的虛擬機關閉掛鉤。??void?runFinalization()??運行掛起?finalization?的所有對象的終止方法。??static?void?runFinalizersOnExit(boolean?value)??已過時。?此方法本身具有不安全性。它可能對正在使用的對象調用終結方法,而其他線程正在操作這些對象,從而導致不正確的行為或死鎖。??long?totalMemory()??返回?Java?虛擬機中的內存總量。??void?traceInstructions(boolean?on)??啟用/禁用指令跟蹤。??void?traceMethodCalls(boolean?on)??啟用/禁用方法調用跟蹤。?1.3 Process
不管通過哪種方法啟動進程后,都會返回一個Process類的實例代表啟動的進程,該實例可用來控制進程并獲得相關信息。Process 類提供了執行從進程輸入、執行輸出到進程、等待進程完成、檢查進程的退出狀態以及銷毀(殺掉)進程的方法:
void?destroy()??殺掉子進程。??一般情況下,該方法并不能殺掉已經啟動的進程,不用為好。??int?exitValue()??返回子進程的出口值。??只有啟動的進程執行完成、或者由于異常退出后,exitValue()方法才會有正常的返回值,否則拋出異常。??InputStream?getErrorStream()??獲取子進程的錯誤流。??如果錯誤輸出被重定向,則不能從該流中讀取錯誤輸出。??InputStream?getInputStream()??獲取子進程的輸入流。??可以從該流中讀取進程的標準輸出。??OutputStream?getOutputStream()??獲取子進程的輸出流。??寫入到該流中的數據作為進程的標準輸入。??int?waitFor()??導致當前線程等待,如有必要,一直要等到由該?Process?對象表示的進程已經終止。?2.多進程編程實例
一般我們在java中運行其它類中的方法時,無論是靜態調用,還是動態調用,都是在當前的進程中執行的,也就是說,只有一個java虛擬機實例在運行。而有的時候,我們需要通過java代碼啟動多個java子進程。這樣做雖然占用了一些系統資源,但會使程序更加穩定,因為新啟動的程序是在不同的虛擬機進程中運行的,如果有一個進程發生異常,并不影響其它的子進程。
在Java中我們可以使用兩種方法來實現這種要求。最簡單的方法就是通過Runtime中的exec方法執行java classname。如果執行成功,這個方法返回一個Process對象,如果執行失敗,將拋出一個IOException錯誤。下面讓我們來看一個簡單的例子。
//?文件?import?.*;?public?class?Test?{? public?static?void?main(String[]?args)? {?FileOutputStream?fOut?=?new?FileOutputStream("c:\\");?fOut.close();?System.out.println("被調用成功!");? }?}???//??public?class?Test_Exec?{? public?static?void?main(String[]?args)? {?Runtime?run?=?();?Process?p?=?run.exec("java?test1");? }?}?通過java Test_Exec運行程序后,發現在C盤多了個文件,但在控制臺中并未出現"被調用成功!"的輸出信息。因此可以斷定,Test已經被執行成功,但因為某種原因,Test的輸出信息未在Test_Exec的控制臺中輸出。這個原因也很簡單,因為使用exec建立的是Test_Exec的子進程,這個子進程并沒有自己的控制臺,因此,它并不會輸出任何信息。
如果要輸出子進程的輸出信息,可以通過Process中的getInputStream得到子進程的輸出流(在子進程中輸出,在父進程中就是輸入),然后將子進程中的輸出流從父進程的控制臺輸出。具體的實現代碼如下如示:
//??import?.*;?public?class?Test_Exec_Out?{? public?static?void?main(String[]?args)? {?Runtime?run?=?();?Process?p?=?run.exec("java?test1");?BufferedInputStream?in?=?new?BufferedInputStream(());?BufferedReader?br?=?new?BufferedReader(new?InputStreamReader(in));?String?s;?while?((s?=?())?!=?null)? System.out.println(s);? }?}?從上面的代碼可以看出,在中通過按行讀取子進程的輸出信息,然后在Test_Exec_Out中按每行進行輸出。 上面討論的是如何得到子進程的輸出信息。那么,除了輸出信息,還有輸入信息。既然子進程沒有自己的控制臺,那么輸入信息也得由父進程提供。我們可以通過Process的getOutputStream方法來為子進程提供輸入信息(即由父進程向子進程輸入信息,而不是由控制臺輸入信息)。我們可以看看如下的代碼:
//?文件?import?.*;?public?class?Test?{? public?static?void?main(String[]?args)? {?BufferedReader?br?=?new?BufferedReader(new?InputStreamReader(System.in));?System.out.println("由父進程輸入的信息:"?+?());? }?}???//??import?.*;?public?class?Test_Exec_In?{? public?static?void?main(String[]?args)? {?Runtime?run?=?();?Process?p?=?run.exec("java?test2");?BufferedWriter?bw?=?new?BufferedWriter(new?OutputStreamWriter(()));?("向子進程輸出信息");?();?bw.close();?//?必須得關閉流,否則無法向子進程中輸入信息?//?System.in.read();? }?}?從以上代碼可以看出,Test1得到由Test_Exec_In發過來的信息,并將其輸出。當你不加()和()時,信息將無法到達子進程,也就是說子進程進入阻塞狀態,但由于父進程已經退出了,因此,子進程也跟著退出了。如果要證明這一點,可以在最后加上.read(),然后通過任務管理器(在windows下)查看java進程,你會發現如果加上()和(),只有一個java進程存在,如果去掉它們,就有兩個java進程存在。這是因為,如果將信息傳給Test2,在得到信息后,Test2就退出了。在這里有一點需要說明一下,exec的執行是異步的,并不會因為執行的某個程序阻塞而停止執行下面的代碼。因此,可以在運行test2后,仍可以執行下面的代碼。
exec方法經過了多次的重載。上面使用的只是它的一種重載。它還可以將命令和參數分開,如exec("")可以寫成exec("java", "test2")。exec還可以通過指定的環境變量運行不同配置的java虛擬機。
除了使用Runtime的exec方法建立子進程外,還可以通過ProcessBuilder建立子進程。ProcessBuilder的使用方法如下:
//??import?.*;?public?class?Test_Exec_Out?{? public?static?void?main(String[]?args)? {?ProcessBuilder?pb?=?new?ProcessBuilder("java",?"test1");?Process?p?=?();?…?…? }?}?在建立子進程上,ProcessBuilder和Runtime類似,不同的ProcessBuilder使用start()方法啟動子進程,而Runtime使用exec方法啟動子進程。得到Process后,它們的操作就完全一樣的。
ProcessBuilder和Runtime一樣,也可設置可執行文件的環境信息、工作目錄等。下面的例子描述了如何使用ProcessBuilder設置這些信息。
ProcessBuilder?pb?=?new?ProcessBuilder("Command",?"arg2",?"arg2",?''');?//?設置環境變量?Map<String,?String>?env?=?();?("key1",?"value1");?("key2");?("key2",?("key1")?+?"_test");?("..\abcd");?//?設置工作目錄?Process?p?=?();?//?建立子進程?【編輯推薦】
【責任編輯:
未麗燕TEL:(010)68476606】
點贊 0
總結
以上是生活随笔為你收集整理的linux多线程_Java+Linux,深入内核源码讲解多线程之进程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 做男性不育检需到焦作哪家医院好
- 下一篇: ipad连接电脑_这些应用让iPad生产