如何做一個線程鏡像?

一個線程會包含若干個棧幀,如果在執行到某個方法的某行代碼處斷開,我們可以把當前的線程上下文信息(pc計數器、frame信息等)保存到本地並釋放相關資源,在後面的某個時間點再載入線程鏡像,繼續執行斷開點之後的邏輯,這個實現有什麼思路嗎?


題主所想的是在同一進程內保存/恢複線程,還是可能跨進程(在上一個進程里保存了線程,然後再下一個進程里恢復出來)?

Java線程這玩兒是有辦法保存其狀態的。

有很多Java層面的coroutine其實就要做這種事情,主要是通過bytecode instrumentation來實現「提取Java線程的狀態」以及「恢復Java線程的狀態」的。但這種層面實現通常都無法支持在線程棧上有native函數調用的情況。

然後有一個在JVM層面實現的Java coroutine,就是把整個棧都暫時存堆里了:Coroutines for Java

而要跨越進程來做的話就會更加困難很多。我見過的唯一一個這方面的研究項目是基於Maxine VM做的Sovereign項目:Sovereign: Migrating Java Threads to Improve Availability of Web Applications


這個東西坑的深度比你想的大三個數量級,別說線程,就是單個進程,到現在沒有完美的實現。

根據題主描述,需求是要把一個運行中的線程保存到本地一個鏡像(文件)中,釋放其所擁有的各系統資源,後續要從上述鏡像恢復這個線程並繼續其被中斷的運行。

工業上,這個做法叫CR,Checkpoint-Restart,首先來看簡單一個數量級的如何保存再恢復一個進程的問題,狀態究竟是什麼?

1. 打開的tcp/udp socket怎麼處理?你所持有的socket對面,在和你通訊的進程、線程呢?

2. 打開的文件句柄怎麼處理?比如你的程序讀了一個已鎖文件的前50%,然後被你鏡像保存了下來,過一會兒再恢復運行,文件內容變了,程序邏輯怎麼管控?

3. memory mappings,timer,pipes,等等,怎麼處理?

4. 各block/char型設備?ioctl()? 噩夢?

5. /proc/$pid/status,socket id,/proc/$pid/mountinfo?

各類別的外部依賴下面再深入。

也許JVM可以幫你?別逗哈,那是忽悠你。配合BLCR那種kernel module的進程級別方案可以從內核得到所有需要的信息,JVM這種純用戶態的,怎麼死也不知道。

看資料的話,求思路,自己搜索:

Condor

BLCR

libckpt

CRIU

上面這些是一部分保存再重啟一個進程的坑,到了線程上,這些坑一個不少,同時進程內存空間都變了,重啟起來的線程還能一致的運行嗎?

然後不管是進程還是線程,一旦是多進程、多線程交換消息的通訊,且涉及到在通訊中的進程/線程需要分別保存各自重啟,且針對的是通用的程序,而非某特定簡單網路應用,那麼恭喜你,難度再大一個數量級。考慮一個簡單場景,物理時間序步驟如下:

1. 進程/線程X發送一個消息給進程/線程Y

2. 進程/線程X保存自己的狀態

3. 進程/線程Y在收到這個消息前保存自己的狀態,然後立刻收到消息

此時,你有X和Y兩個保存下來的checkpoint image,你可以選擇恢復,可恢復以後X認為消息已發送,而Y永遠看不到這個消息。這是一個簡單場景,實際工業上的坑更多更深。Leslie Lamport為首一代科學家努力,才解決這個問題和它的優化。

上面說的是針對一種通用的方案,不修改應用,輪子直接幹活。如果是針對某個應用,寫一個應用專用的checkpoint-restart方法或做別的應用修改,則容易的很,cs本科學生都應該會。個人理解,做這類checkpoint-restart,線程比進程難,上面提到了,進程比容器難,容器比vmware那樣的vm難。直觀概念:vm保存恢復無數年前就成熟了,容器也有,但90年代condor就有process級別支持,到今天還沒有一個真正完美方案。

同時要做到真正通用,這個坑很深。要做一個能基本跑起來支持一些應用的不難,然後就是噩夢般的填坑,無數的坑,去慢慢讓更多通用應用能用。

當然,如果是google,oracle類似公司的有這個財力去填坑造輪子,做這樣的研究,那不應該來這裡問。

利益相關,這東西是咱吃飯傢伙的一部分。


可以參考下協程的實現,一般都要做相似的這些的,比如騰訊的libco庫


假設不考慮跨進程恢複線程,也就是還在原來的進程裡面恢復。

那其實就是做一個 n:m 模型的協程庫。

可能的思路:

1. makecontext 一個協程上下文ucontext_t ctx ,啟動線程 T1 ,然後T1裡面 setcontext 切換過去。

2. 然後做切換,確保T沒有在執行,比如在 sleep 什麼的,然後在管理線程裡面,把 ctx 保存一下,把 ctx 指向的 stack 內存塊也保存一下(當然你可以保存到文件里,共享內存里,甚至遠程mysql 里等等)。然後讓 T1 線程退出(比如喚醒 T1,讓 T1 調用 pthread_exit() )。

3. 然後把 stack 那塊內存 先 munmap 再 mmap 佔住(要不然待會要恢復的時候,不一定能在這個地址上 mmap 到內存,那就恢復不了了),這個mmap 可以不佔用 實際內存,比如你可以設置成 PROT_NONE 許可權。

3. 要恢復的時候,再啟動一個新線程 T2 ,把保存的 ctx 和 stack 取出來,把 stack memcpy 到mmap 佔住的原來那塊地址上,

可能遇到的問題:

1. 如果 stack 裡面保存了某些全局變數的指針(或者類似指針的數組下標),網路 socket fd,文件fd,持有了鎖 mutex ,持有了 timer id 之類的東西,

那你要自己確保恢復了以後,這些東西沒有失效。(實際業務代碼里是可以做到的)

這也不是什麼新鮮想法了,很多現成的輪子可以直接用。

比如:

uThreads: Concurrent User Threads in C++(and C)

可以參考 :

a Coroutine Library for C and Unix

facebook/folly

yyzybb537/libgo


推薦閱讀:

C 語言中函數 fopen 所打開的文件指針指向的文件到底是什麼?
Android系統可以高度定製化,但為什麼那些專業遊戲機(例如3DS,PS4,PS Vita,Wii U)不用此作為主機操作系統?
為什麼微軟這樣的大公司也分不清 KB 和 KiB?
計算機專業本科生、鍵盤黨,覬覦linux的高效性,想棄win投Linux,求合適的發行版本?

TAG:操作系統 | Java虛擬機JVM |