當我們談論在 Linux 中每秒更新一個時鐘時,我認為類似于以下代碼的東西是我想到的。
while :; do date %T; sleep 1; done
這段代碼總是困擾著我,因為有一個無限回圈每秒運行兩個命令,這意味著背景關系切換會導致處理器使用率出現輕微峰值。
考慮到這一點,我想知道:這真的是最好的方法嗎?有沒有更聰明的方法來做到這一點?例如,如果我想用像 C 這樣的低級語言重現這個,那么唯一的方法是否仍然是一個無限回圈,一個printf顯示時鐘和一個一秒sleep?也就是說,有沒有辦法避免這種背景關系切換并以更智能的方式使用CPU?
uj5u.com熱心網友回復:
你不想完全避免背景關系切換,你想讓內核在 99% 的秒內運行其他東西,它沒有運行/usr/bin/date以將時間格式化為字串并將write(2)其格式化為標準輸出。(或者讓這個 CPU 內核進入睡眠狀態,以節省電力。但這實際上不算作背景關系切換,因為軟體從不更改頁表或保存/恢復 FP 暫存器。即使是系統呼叫也完全進入內核保存/恢復整數暫存器,并且在沒有硬體修復的 Intel CPU 上啟用軟體 Meltdown 緩解實際上會更改頁表,但是。Spectre 緩解清除分支預測歷史的成本更高。)
(如果您沒有在 Linux 文本控制臺上運行它,如 ctrl alt F2,則需要背景關系切換到您的終端模擬器或 sshd 或任何控制偽終端主端的任何東西。僅在后一種情況將寫入視頻 RAM 實際上發生在由 進行的write(0, buf, len)系統呼叫中date,即在該行程的背景關系中。)
如果您想最小化背景關系切換(以及一般的系統呼叫),您需要在單個行程中進行睡眠和寫入。但這在 bash 中是不可能的;它沒有內置睡眠。(Bash確實必須printf '%(%T)T\n' $EPOCHSECONDS列印當前時間,但忙于等待會很糟糕)。你想用 C 撰寫一個程式,它只做睡眠和時間列印。
使用固定 1 秒延遲的回圈將累積錯誤,因為它在date啟動和退出之后才開始下一秒,并且 shell 已經分叉/執行/usr/bin/sleep下一次迭代(加上sleep可執行檔案中的啟動開銷)。
無需撰寫自己的 C 程式,您可以通過 using 將watch -p -t --exec其降低到每秒一次 fork/exec(以及一堆其他系統呼叫),它以間隔直接使用 fork/exec 而不是/bin/sh -c.
-t告訴它不要列印標題(包括時間)-p(精確)查詢當前時間clock_gettime并使用nanosleep,避免誤差累積,每次都瞄準同一目標時間。(默認設定是在命令運行之間的固定時間間隔內休眠,無論花費多長時間。)
我們可以跟蹤它的系統呼叫,看看它做了什么。(我使用了更短的睡眠間隔,所以我不必讓它坐那么久。)請注意,clock_gettime它沒有出現,strace因為它沒有進入內核;glibc 包裝器呼叫 vDSO 實作。內核匯出的代碼(映射到每個用戶空間行程)讀取內核匯出的資料:由內核的定時器中斷更新的粗略時間,以及用于rdtsc從當前粗略時間插入偏移量的比例因子/偏移量,因為現代 x86-64 系統具有可從用戶空間訪問的精確恒定頻率計數器。
(watch實際上列印在“替代”螢屏上,所以當它退出時輸出從你的終端消失;輸出的那部分是為了示例目的而偽造的。其余部分是從終端模擬器復制/粘貼的,并添加了## 注釋。 )
# use strace -f ... to trace into child processes, and see all the syscalls from date
$ strace -o foo.tr watch -p -t -n 0.5 --exec date %T
22:31:54
control-C
$ less foo.tr
... startup stuff from watch, including some terminal-size ioctl
pipe([3, 4]) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f0f744fba10) = 3832377
# Linux implements fork() in terms of clone(2)
close(4) = 0
fcntl(3, F_GETFL) = 0 (flags O_RDONLY)
newfstatat(3, "", {st_mode=S_IFIFO|0600, st_size=0, ...}, AT_EMPTY_PATH) = 0
# (IDK why it's doing an fstat on the pipe FD)
read(3, "22:16:45\n", 4096) = 9
read(3, "", 4096) = 0
# reads from the pipe until EOF
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3832377, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
close(3) = 0
# then closes it
wait4(3832377, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 3832377
rt_sigaction(SIGTSTP, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f0f7453ada0}, {sa_handler=0x7f0
f746f4790, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f0f7453ada0}, 8) = 0
# and waits for the child PID
write(1, "\33[?1049h\33[22;0;0t\33[1;42r\33(B\33[m\33["..., 46) = 46
# clears the screen and moves cursor to the top left
write(1, "22:16:45\33[42;134H", 17) = 17
# and copies what it read from the pipe earlier.
rt_sigaction(SIGTSTP, {sa_handler=0x7f0f746f4790, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f0f7453ada0}, NULL, 8) =
0
## There's a clock_gettime() somewhere, probably here,
## but the vDSO implementation avoids entering the kernel so strace doesn't see it.
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=498451000}, NULL) = 0
# After calculating the exact time until the next event
# tell the kernel we're done until then
# Then the cycle starts over again when it wakes
pipe([3, 4]) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f0f744fba10) = 3832378
close(4) = 0
fcntl(3, F_GETFL) = 0 (flags O_RDONLY)
...
watchwithout-t將列印當前時間作為其標題的一部分。所以如果這是你想要的,你就不再需要date了。
不過,這并不有一個選項,不運行任何程式。如果當前時區發生變化,它每次都會統計 /etc/localtime 。
您可以使用/bin/true,但這仍然需要分叉/執行并運行其動態聯結器啟動開銷。或者您可以使用watch --exec /non-existant并讓它execve每次都列印錯誤。但即便如此,它仍然會在嘗試執行之前分叉,創建一個新的 PID 并對其進行背景關系切換。
uj5u.com熱心網友回復:
我懷疑有沒有辦法避免背景關系切換——或者如果有辦法,它會比涉及sleep.
以您為代表的技術的真正問題
while :; do date %T; sleep 1; done
是他們失去了時間。例如,如果我運行這個修改,合并我自己的dateexpr程式,除其他外,它具有使用亞秒的能力:
while :; do dateexpr %H:%M:%.2S now; sleep 1; done
,這是我看到的:
10:13:48.40
10:13:49.41
10:13:50.43
10:13:51.44
10:13:52.46
10:13:53.47
10:13:54.49
10:13:55.50
所以看起來“背景關系切換”——啟動每個sleep和/date或dateexpr行程的開銷——需要 10-20 毫秒。
我寫了一個程式(用 C 語言)來解決這個問題。它持續監視時間,并計算出一個略小于一秒的睡眠時間值,這樣它就可以每秒準確地呼叫一次子命令。它看起來像這樣:
$ synchro dateexpr %H:%M:%.2S now
10:17:11.01
10:17:12.01
10:17:13.01
10:17:14.01
10:17:15.01
10:17:16.01
10:17:17.01
在啟動被呼叫的行程時仍然有 10 毫秒的錯誤,但至少它不會累積。
但是為了完成它的作業,我的synchro程式不得不進行更多的系統呼叫,所以實際上有更多的背景關系切換,而不是更少。
但是,當然,一般來說sleep,當您想暫停一段時間時,呼叫類似的方法是正確的做法,因為您明確地放棄了控制權,并且作業系統知道它根本不必安排您的行程運行,因此,您在睡覺時對系統的其余部分施加的負載最小。是的,涉及到幾個背景關系切換,但它們似乎很小,付出的代價很小,而且正如我所說,我認為您無法繞過它們。
我想知道是否有一種方法可以完全在用戶空間中運行時鐘或計時器,也許這也是您要問的。但我懷疑有沒有辦法,因為沒有什么 [腳注 1] 你可以在用戶空間中得到任何關于時間或時鐘的資訊——這些資訊都在內核中,這意味著它需要一個系統打電話給它。
(當然,我在這里專門考慮在傳統的多任務作業系統下運行的行程。如果您正在為帶有 RTC 的微處理器撰寫嵌入式代碼,那么毫無疑問您可以完全按照自己的意愿去做,而無需在全部。)
有一種可能性很小,至少在某些(也許現在大多數?)Linux 版本下,有一種稱為vDSO的機制,它使某些系統呼叫能夠在用戶空間中執行,而無需背景關系切換。接受這種特殊處理的系統呼叫的首要候選物件是gettimeofday和 相關的。因此,在使用 vDSO 的系統上,您可以撰寫一個帶有忙等待回圈的程式,重復呼叫gettimeofday(time或clock_gettime,如果那些也使用 vDSO)直到到達所需的時間,并且由于 vDSO,您將在沒有背景關系切換的情況下執行此操作。但當然,忙著等待是一個幾乎無可救藥的可怕想法,所以我不是認真推薦這個。(這就是我在本答案開頭說的“如果有辦法,它會比涉及 的技術更浪費sleep。”的意思。)
Footnote 1. I said there's "nothing you can get your hands on in user space that gives you any information about time or clocks", but that's not quite true. As the comments from Peter Cordes remind us, Intel processors, at least, give us the "Time Stamp Counter" and the rdtsc instruction to read it. This is a potentially vital — but also hugely problematic! — tool for writing certain high-precision timing applications, but I've never used it so I won't try to explain it or its caveats.
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/315040.html
