1. 程式人生 > 其它 >linux 休眠定時喚醒_定時器導致CPU爆滿的原因

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主要被其中一個執行緒消耗。

5142ca59e2adf34012074375eb0861c7.png

其他平臺的測試驗證

由於rust程式碼是跨平臺的SDK,因此,也需要驗證下其他平臺是否有同樣的問題。

iOS

使用Instruments抓了iOS與Windows類似場景,消耗CPU最多的也是這個執行緒。

dac3a457b099b73cf67a436c68635854.png

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