Java+Linux,深入內核源碼講解多線程之進程
之前寫了兩篇文章,都是針對Linux這個系統的,為什么?我為什么這么喜歡寫這個系統的知識,可能就是為了今天的內容多線程系列,現在多線程不是一個面試重點 啊,那如果你能深入系統內核回答這個知識點,面試官會怎么想?你會不會占據面試的主動權(我不會說今天被一個面試者驚艷到了的)今天,我就開始一個系列的內容,多線程--高并發,深入的給大家講解,我就不信講不明白這么個小東西,有問題的地方希望大家能夠指出,謝謝,大家一起成長
今天我們將第一個知識點:進程
Linux 內核如何描述一個進程?
1. Linux 的進程
進程的術語是 process,是 Linux 最基礎的抽象,另一個基礎抽象是文件。
最簡單的理解,進程就是執行中 (executing, 不等于running) 的程序。
更準確一點的理解,進程包括執行中的程序以及相關的資源 (包括cpu狀態、打開的文件、掛起的信號、tty、內存地址空間等)。
一種簡潔的說法:進程 = n*執行流 + 資源,n>=1。
Linux 進程的特點:
- 通過系統調用 fork() 創建進程,fork() 會復制現有進程來創建一個全新的進程。
- 內核里,并不嚴格區分進程和線程。
- 從內核的角度看,調度單位是線程 (即執行流)。可以把線程看做是進程里的一條執行流,1個進程里可以有1個或者多個線程。
- 內核里,常把進程稱為 task 或者 thread,這樣描述更準確,因為許多進程就只有1條執行流。
- 內核通過輕量級進程 (lightweight process) 來支持多線程。1個輕量級進程就對應1個線程,輕量級進程之間可以共享打開的文件、地址空間等資源。
2. Linux 的進程描述符
2.1 task_struct
內核里,通過 task_struct 結構體來描述一個進程,稱為進程描述符 (process descriptor),它保存著支撐一個進程正常運行的所有信息。
每一個進程,即便是輕量級進程(即線程),都有1個 task_struct。
- sched.h (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: 信號處理相關。
其他字段,等到有需要的時候再回過頭來學習。
2.2 當發生系統調用或者進程切換時,內核如何找到 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 架構中的定義如下:
- // thread_info.h (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:
- // thread_info.h (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/init.h>
- #include <linux/module.h>
- #include <linux/sched.h>
- 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進程
1.Java進程的創建
Java提供了兩種方法用來啟動進程或其它程序:
- 使用Runtime的exec()方法
- 使用ProcessBuilder的start()方法
1.1 ProcessBuilder
ProcessBuilder類是J2SE 1.5在java.lang中新添加的一個新類,此類用于創建操作系統進程,它提供一種啟動和管理進程(也就是應用程序)的方法。在J2SE 1.5之前,都是由Process類處來實現進程的控制管理。
每個 ProcessBuilder 實例管理一個進程屬性集。start() 方法利用這些屬性創建一個新的 Process 實例。start() 方法可以從同一實例重復調用,以利用相同的或相關的屬性創建新的子進程。
每個進程生成器管理這些進程屬性:
- 命令 是一個字符串列表,它表示要調用的外部程序文件及其參數(如果有)。在此,表示有效的操作系統命令的字符串列表是依賴于系統的。例如,每一個總體變量,通常都要成為此列表中的元素,但有一些操作系統,希望程序能自己標記命令行字符串——在這種系統中,Java 實現可能需要命令確切地包含這兩個元素。
- 環境 是從變量 到值 的依賴于系統的映射。初始值是當前進程環境的一個副本(請參閱 System.getenv())。
- 工作目錄。默認值是當前進程的當前工作目錄,通常根據系統屬性 user.dir 來命名。
- redirectErrorStream 屬性。最初,此屬性為 false,意思是子進程的標準輸出和錯誤輸出被發送給兩個獨立的流,這些流可以通過 Process.getInputStream() 和 Process.getErrorStream() 方法來訪問。如果將值設置為 true,標準錯誤將與標準輸出合并。這使得關聯錯誤消息和相應的輸出變得更容易。在此情況下,合并的數據可從 Process.getInputStream() 返回的流讀取,而從 Process.getErrorStream() 返回的流讀取將直接到達文件尾。
修改進程構建器的屬性將影響后續由該對象的 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 1.1 開始,將本地編碼字節流轉換為 Unicode 字符流的首選方法是使用 InputStreamReader 和 BufferedReader 類。
- OutputStream getLocalizedOutputStream(OutputStream out)
- 已過時。 從 JDK 1.1 開始,將 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錯誤。下面讓我們來看一個簡單的例子。
- // Test1.java文件
- import java.io.*;
- public class Test
- {
- public static void main(String[] args)
- {
- FileOutputStream fOut = new FileOutputStream("c:\\Test1.txt");
- fOut.close();
- System.out.println("被調用成功!");
- }
- }
- // Test_Exec.java
- public class Test_Exec
- {
- public static void main(String[] args)
- {
- Runtime run = Runtime.getRuntime();
- Process p = run.exec("java test1");
- }
- }
通過java Test_Exec運行程序后,發現在C盤多了個Test1.txt文件,但在控制臺中并未出現"被調用成功!"的輸出信息。因此可以斷定,Test已經被執行成功,但因為某種原因,Test的輸出信息未在Test_Exec的控制臺中輸出。這個原因也很簡單,因為使用exec建立的是Test_Exec的子進程,這個子進程并沒有自己的控制臺,因此,它并不會輸出任何信息。
如果要輸出子進程的輸出信息,可以通過Process中的getInputStream得到子進程的輸出流(在子進程中輸出,在父進程中就是輸入),然后將子進程中的輸出流從父進程的控制臺輸出。具體的實現代碼如下如示:
- // Test_Exec_Out.java
- import java.io.*;
- public class Test_Exec_Out
- {
- public static void main(String[] args)
- {
- Runtime run = Runtime.getRuntime();
- Process p = run.exec("java test1");
- BufferedInputStream in = new BufferedInputStream(p.getInputStream());
- BufferedReader br = new BufferedReader(new InputStreamReader(in));
- String s;
- while ((s = br.readLine()) != null)
- System.out.println(s);
- }
- }
從上面的代碼可以看出,在Test_Exec_Out.java中通過按行讀取子進程的輸出信息,然后在Test_Exec_Out中按每行進行輸出。 上面討論的是如何得到子進程的輸出信息。那么,除了輸出信息,還有輸入信息。既然子進程沒有自己的控制臺,那么輸入信息也得由父進程提供。我們可以通過Process的getOutputStream方法來為子進程提供輸入信息(即由父進程向子進程輸入信息,而不是由控制臺輸入信息)。我們可以看看如下的代碼:
- // Test2.java文件
- import java.io.*;
- public class Test
- {
- public static void main(String[] args)
- {
- BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
- System.out.println("由父進程輸入的信息:" + br.readLine());
- }
- }
- // Test_Exec_In.java
- import java.io.*;
- public class Test_Exec_In
- {
- public static void main(String[] args)
- {
- Runtime run = Runtime.getRuntime();
- Process p = run.exec("java test2");
- BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()));
- bw.write("向子進程輸出信息");
- bw.flush();
- bw.close(); // 必須得關閉流,否則無法向子進程中輸入信息
- // System.in.read();
- }
- }
從以上代碼可以看出,Test1得到由Test_Exec_In發過來的信息,并將其輸出。當你不加bw.flash()和bw.close()時,信息將無法到達子進程,也就是說子進程進入阻塞狀態,但由于父進程已經退出了,因此,子進程也跟著退出了。如果要證明這一點,可以在最后加上System.in.read(),然后通過任務管理器(在windows下)查看java進程,你會發現如果加上bw.flush()和bw.close(),只有一個java進程存在,如果去掉它們,就有兩個java進程存在。這是因為,如果將信息傳給Test2,在得到信息后,Test2就退出了。在這里有一點需要說明一下,exec的執行是異步的,并不會因為執行的某個程序阻塞而停止執行下面的代碼。因此,可以在運行test2后,仍可以執行下面的代碼。
exec方法經過了多次的重載。上面使用的只是它的一種重載。它還可以將命令和參數分開,如exec("java.test2")可以寫成exec("java", "test2")。exec還可以通過指定的環境變量運行不同配置的java虛擬機。
除了使用Runtime的exec方法建立子進程外,還可以通過ProcessBuilder建立子進程。ProcessBuilder的使用方法如下:
- // Test_Exec_Out.java
- import java.io.*;
- public class Test_Exec_Out
- {
- public static void main(String[] args)
- {
- ProcessBuilder pb = new ProcessBuilder("java", "test1");
- Process p = pb.start();
- … …
- }
- }
在建立子進程上,ProcessBuilder和Runtime類似,不同的ProcessBuilder使用start()方法啟動子進程,而Runtime使用exec方法啟動子進程。得到Process后,它們的操作就完全一樣的。
ProcessBuilder和Runtime一樣,也可設置可執行文件的環境信息、工作目錄等。下面的例子描述了如何使用ProcessBuilder設置這些信息。
- ProcessBuilder pb = new ProcessBuilder("Command", "arg2", "arg2", ''');
- // 設置環境變量
- Map<String, String> env = pb.environment();
- env.put("key1", "value1");
- env.remove("key2");
- env.put("key2", env.get("key1") + "_test");
- pb.directory("..\abcd"); // 設置工作目錄
- Process p = pb.start(); // 建立子進程




















