linux 休眠定時喚醒_定時器導致CPU爆滿的原因
技術標籤:linux 休眠定時喚醒
使用rust tokio Interval作為定時器,可能會在系統休眠或程序掛起恢復後,CPU爆滿,甚至導致宕機,Windows、iOS、Android都有這個問題。
引言
雖然rust程式碼寫得不多,但使用C++的效能分析方法論和工具鏈,也能分析rust程式碼的效能。這次,就是通過WPT(Windows Performance Tools)和Instruments分析了rust的第三方庫tokio的Interval的一個性能問題。
問題的發現
在Win10系統(CPU 12核)休眠恢復之後,發現rust程序的CPU佔比超過6%,相當於1個CPU核的70%。於是,趕緊用WPT抓了一個現場。通過WPT分析,發現CPU主要被其中一個執行緒消耗。
其他平臺的測試驗證
由於rust程式碼是跨平臺的SDK,因此,也需要驗證下其他平臺是否有同樣的問題。
iOS
使用Instruments抓了iOS與Windows類似場景,消耗CPU最多的也是這個執行緒。
Mac
經過反覆測試,未發現Mac有這個問題。
Android
經過反覆測試,未發現Android有這個問題。(後來發現,是因為Android連上電腦是充電模式,程序不會自動掛起,沒有進入到可復現這個問題的路徑。)
tokio的Interval的Demo實驗
經過多平臺的測試驗證,發現Windows和iOS有這個問題,但是Mac沒有發現這個問題,這就感覺到很迷糊了。通過WPT和Instruments的分析資料,已定位到相關程式碼,使用tokio的Interval執行一個定時任務,每隔1S執行1次。其中,tokio版本0.1.13。排查了自有程式碼,沒有找到可能引起這個問題的程式碼。經過現象分析,推測可能跟tokio有關,於是,寫了一個簡化的Demo進行驗證。
實驗程式碼說明
建立一個迴圈定時器任務,每隔1S執行1次。在每次定時器任務觸發時,輸出以下除錯資訊:
- 當前任務的序號
- 任務觸發的絕對時間
- Instant::now()獲取的時間
- 當前任務的到期時間
pub fn test_interval() { let interval = Duration::from_millis(1000); let task = Interval::new_interval(interval) .for_each(move |deadline| { let count = { let mut count = TIME_COUNT.write().unwrap(); *count += 1; *count }; println!("{}, {}, now: {:?}, deadline: {:?}", count, Utc::now().format("%T"), Instant::now(), deadline); Ok(()) }) .then(|_| { Ok(()) }); MY_RUNTIME.executor().spawn(task); }
系統休眠實驗
由於Windows平臺發現的問題,就是通過系統休眠的方式來發現的,因此,對Windows、Mac、Linux都進行了系統休眠實驗。
實驗方法:先讓系統休眠一段時間(超過2分鐘),然後重新啟用系統,觀察啟用後的任務執行次數。如果系統重新啟用後1S內,任務執行次數只有1-3次,屬於沒有問題;如果有大量任務執行,則是有問題的。
Windows系統休眠實驗結果
- 系統開始休眠時間:14:08:33。
- 啟用系統時間:14:15:05。
- 系統啟用後1S內,執行任務394次,存在問題。
1, 14:08:18, now: Instant { t: 1287693.9087257s }, deadline: Instant { t: 1287693.9079829s }
2, 14:08:19, now: Instant { t: 1287694.9086322s }, deadline: Instant { t: 1287694.9079829s }
16, 14:08:33, now: Instant { t: 1287708.9085874s }, deadline: Instant { t: 1287708.9079829s }
17, 14:15:05, now: Instant { t: 1288101.05249s }, deadline: Instant { t: 1287709.9079829s }
18, 14:15:05, now: Instant { t: 1288101.0528517s }, deadline: Instant { t: 1287710.9079829s }
409, 14:15:06, now: Instant { t: 1288101.9091369s }, deadline: Instant { t: 1288101.9079829s }
410, 14:15:07, now: Instant { t: 1288102.9145323s }, deadline: Instant { t: 1288102.9079829s }
411, 14:15:08, now: Instant { t: 1288103.9128121s }, deadline: Instant { t: 1288103.9079829s }
Mac系統休眠實驗結果
- 系統開始休眠時間:14:10:19。
- 啟用系統時間:14:18:39。
- 結束休眠後1S內,執行任務1次,沒有問題。
1, 14:09:50, now: Instant { t: 306133377041330 }, deadline: Instant { t: 306133373882499 }
2, 14:09:51, now: Instant { t: 306134375199583 }, deadline: Instant { t: 306134373882499 }
30, 14:10:19, now: Instant { t: 306162380111871 }, deadline: Instant { t: 306162373882499 }
31, 14:18:39, now: Instant { t: 306164208046139 }, deadline: Instant { t: 306163373882499 }
32, 14:18:40, now: Instant { t: 306164378924112 }, deadline: Instant { t: 306164373882499 }
33, 14:18:41, now: Instant { t: 306165379182917 }, deadline: Instant { t: 306165373882499 }
34, 14:18:42, now: Instant { t: 306166375919484 }, deadline: Instant { t: 306166373882499 }
Linux系統休眠實驗結果
使用的測試系統為ubuntu 18.04。
- 程序開始休眠時間:11:10:11。
- 程序啟用時間:11:31:52。
- 程序啟用後1S內,執行任務2次,沒有問題。
1, 11:09:58, now: Instant { tv_sec: 3437, tv_nsec: 200974533 }, deadline: Instant { tv_sec: 3437, tv_nsec: 199134350 }
14, 11:10:11, now: Instant { tv_sec: 3450, tv_nsec: 200868087 }, deadline: Instant { tv_sec: 3450, tv_nsec: 199134350 }
15, 11:31:52, now: Instant { tv_sec: 3452, tv_nsec: 388860753 }, deadline: Instant { tv_sec: 3451, tv_nsec: 199134350 }
16, 11:31:52, now: Instant { tv_sec: 3452, tv_nsec: 389004878 }, deadline: Instant { tv_sec: 3452, tv_nsec: 199134350 }
17, 11:31:53, now: Instant { tv_sec: 3453, tv_nsec: 201303263 }, deadline: Instant { tv_sec: 3453, tv_nsec: 199134350 }
18, 11:31:54, now: Instant { tv_sec: 3454, tv_nsec: 201269682 }, deadline: Instant { tv_sec: 3454, tv_nsec: 199134350 }
程序掛起實驗
通過iOS之前的測試分析,iOS出現問題時,並不是系統休眠,而是程序掛起,於是,對移動端進行了程序掛起的實驗。
iOS程序掛起實驗結果
先執行程序,然後把程序切換到後臺,等待一段時間後,把程序切換到前臺,觀察執行結果。
- 程序開始休眠時間:09:40:07。
- 程序啟用時間:09:41:56。
- 程序啟用後1S內,運行了109次任務,存在問題。
1, 09:39:10, Instant { t: 8575538386338 }, Instant { t: 8575538224636 }
58, 09:40:07, Instant { t: 8576906237923 }, Instant { t: 8576906224636 }
59, 09:41:56, Instant { t: 8579507571088 }, Instant { t: 8576930224636 }
60, 09:41:56, Instant { t: 8579507573627 }, Instant { t: 8576954224636 }
167, 09:41:56, Instant { t: 8579522286057 }, Instant { t: 8579522224636 }
168, 09:41:57, Instant { t: 8579546357653 }, Instant { t: 8579546224636 }
169, 09:41:58, Instant { t: 8579570335229 }, Instant { t: 8579570224636 }
Android程序掛起實驗結果
在測試Android時,發現連上電腦後,手機處於充電模式,App切換到後臺之後,仍然會執行,因此,切到後臺的方式不能測試程序掛起。下面就通過打斷點的方式,測試程序掛起恢復後的情況。
- 程序開始掛起時間:08:51:01。
- 程序啟用時間:08:55:25。
- 程序啟用後1S內,執行任務264次,存在問題。
1, 08:50:48, now: Instant { tv_sec: 16422, tv_nsec: 449018405 }, deadline: Instant { tv_sec: 16422, tv_nsec: 446896405 }
14, 08:51:01, now: Instant { tv_sec: 16435, tv_nsec: 449331405 }, deadline: Instant { tv_sec: 16435, tv_nsec: 446896405 }
15, 08:55:25, now: Instant { tv_sec: 16698, tv_nsec: 727410405 }, deadline: Instant { tv_sec: 16436, tv_nsec: 446896405 }
278, 08:55:25, now: Instant { tv_sec: 16699, tv_nsec: 449296405 }, deadline: Instant { tv_sec: 16699, tv_nsec: 446896405 }
279, 08:55:26, now: Instant { tv_sec: 16700, tv_nsec: 450704405 }, deadline: Instant { tv_sec: 16700, tv_nsec: 446896405 }
280, 08:55:27, now: Instant { tv_sec: 16701, tv_nsec: 449870405 }, deadline: Instant { tv_sec: 16701, tv_nsec: 446896405 }
281, 08:55:28, now: Instant { tv_sec: 16702, tv_nsec: 449879405 }, deadline: Instant { tv_sec: 16702, tv_nsec: 446896405 }
小結
通過以上的實驗結果證明,測試程式碼能復現相同場景問題,因此,這個問題跟tokio的Interval有關。
- 系統休眠恢復後,Windows有此問題,Mac和Linux無此問題。
- 程序掛起恢復後,iOS和Android都有此問題。同時,經過推測,Windows、Mac、Linux的程序掛起,也會有此問題。
tokio原始碼分析
下面就研究tokio的相關原始碼來分析問題原因。
Interval的實現原理
Interval處理定時器到期的函式為poll,主要作用是設定下一次定時器到期的時間。這裡是通過本次到期時間加上定時器時間間隔,作為下一次到期的時間。這裡使用的是相對時間,因此,每一次迴圈都需要執行。
impl Stream for Interval {
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
let _ = try_ready!(self.delay.poll());
// 這裡使用的當前時間是上次定時器到期的時間
let now = self.delay.deadline();
// 然後用上次到期時間加上時間間隔,作為下一次觸發定時器的時間
self.delay.reset(now + self.duration);
Ok(Some(now).into())
}
}
定時器的等待時間和喚醒的處理,是由Timer類的park和unpark函式實現。park是處理定時器等待的函式,通過當前實時時間和下一次定時器到期時間對比,判斷定時器是否到期。如果定時器未到期,則會進入park_timeout進行剩餘時間的等待。
impl<T, N> Park for Timer<T, N>
where
T: Park,
N: Now,
{
type Unpark = T::Unpark;
type Error = T::Error;
fn unpark(&self) -> Self::Unpark {
self.park.unpark()
}
fn park(&mut self) -> Result<(), Self::Error> {
self.process_queue();
match self.wheel.poll_at() {
Some(when) => {
// 獲取當前的實時時間,呼叫的是Instant::now()
let now = self.now.now();
// 本次定時器到期的時間
let deadline = self.expiration_instant(when);
// 如果定時器未到期,會進行等待
if deadline > now {
self.park.park_timeout(deadline - now)?;
} else {
self.park.park_timeout(Duration::from_secs(0))?;
}
}
None => {
self.park.park()?;
}
}
self.process();
Ok(())
}
}
通過上面幾個函式的邏輯判斷,Timer的等待和喚醒的處理使用的是實時時間,而Interval的定時器到期處理使用了相對時間。系統休眠或者程序掛起會導致相對時間暫停,休眠恢復後,就可能導致休眠期間的任務累加到休眠恢復後執行,從而導致短期內CPU消耗高。這個邏輯推斷,正好驗證了前面在Windows平臺中發現的問題。但是,為什麼Mac平臺下沒有和Windows一樣的問題呢?這個跟Instant的實現原理有關。
Instant的實現原理
Timer的park函式獲取實時時間是呼叫的Instant::now()函式,這個函式是獲取了系統當前時間,通過檢視原始碼和測試發現,各平臺的Instant::now()的實現原理不一樣。
Instant::now()的Windows相關原始碼
Windows系統中,Instant::now()呼叫的是QueryPerformanceCounter函式。經過測試,這個函式的時間計算包括系統休眠時間。
impl Instant {
pub fn now() -> Instant {
perf_counter::PerformanceCounterInstant::now().into()
}
}
mod perf_counter {
impl PerformanceCounterInstant {
pub fn now() -> Self {
Self { ts: query() }
}
}
fn query() -> c::LARGE_INTEGER {
let mut qpc_value: c::LARGE_INTEGER = 0;
cvt(unsafe { c::QueryPerformanceCounter(&mut qpc_value) }).unwrap();
qpc_value
}
}
Instant::now()的Mac和iOS相關原始碼
Mac和iOS系統中,Instant::now()呼叫的是mach_absolute_time函式。檢視蘋果的開發者文件得知,mach_absolute_time的時間不包括系統休眠的時間。
impl Instant {
pub fn now() -> Instant {
extern "C" {
fn mach_absolute_time() -> u64;
}
Instant { t: unsafe { mach_absolute_time() } }
}
}
mach_absolute_time在蘋果開發者文件中的說明如下:
mach_absolute_time
Returns current value of a clock that increments monotonically in tick units (starting at an arbitrary point), this clock does not increment while the system is asleep.
Instant::now()的Android和Linux相關原始碼
Android和Linux系統中,Instant::now()呼叫的是clock_gettime函式。檢視相關資料得知,clock_gettime的CLOCK_MONOTONIC的時間不包括系統休眠的時間。
impl Instant {
pub fn now() -> Instant {
Instant { t: now(libc::CLOCK_MONOTONIC) }
}
}
fn now(clock: clock_t) -> Timespec {
let mut t = Timespec { t: libc::timespec { tv_sec: 0, tv_nsec: 0 } };
cvt(unsafe { libc::clock_gettime(clock, &mut t.t) }).unwrap();
t
}
在StackOverflow查到的關於clock_gettime的問題回答如下:
Note that on Linux, CLOCK_MONOTONIC does not measure time spent in suspend, although by the POSIX definition it should. You can use the Linux-specific CLOCK_BOOTTIME for a monotonic clock that keeps running during suspend. https:// stackoverflow.com/quest ions/3523442/difference-between-clock-realtime-and-clock-monotonic
小結
Windows的實時時間計算會包括系統休眠時間。Mac、iOS、Android、Linux的實時時間計算不包括系統休眠時間。
系統Instant呼叫函式是否包括系統休眠時間WindowsQueryPerformanceCounter是Mac、iOSmach_absolute_time否Android、Linuxclock_gettime (Monotonic Clock)否
問題處理
通過以上的分析,得出結論:
- Windows的系統休眠恢復後有問題,是因為Windows平臺的Instant的時間計算包括系統休眠時間。
- 所有系統的程序掛起都有問題,是因為Instant的時間計算包括程序掛起時間。
要想解決這個問題,要麼棄用tokio::timer::Interval,或者修改Interval的程式碼。
Interval的修改建議
要解決這個問題,重點是要使用系統時間來計算下一次定時器的到期時間。但是這種修復不一定符合tokio庫對Interval的設計,只適合修改之後自己使用。
impl Stream for Interval {
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
let _ = try_ready!(self.delay.poll());
// let now = self.delay.deadline();
let now = Instant::now();
self.delay.reset(now + self.duration);
Ok(Some(now).into())
}
}
本文使用的tokio版本為0.1.13,但看了目前最新的0.2.13,也是同樣的實現方式。
關於作者
微信公眾號:程式設計師bingo
Blog: https://bingoli.github.io/
GitHub: https://github.com/bingoli