以前書いたPythonでラズパイのCPU温度を測定するにあるtemp.py
を実行すると、whileループで発火するごとにCPUを数パーセント使用していました。
モニタリング用のスクリプトですから極力軽くなくてはいかんということで改良したいと思います。
ちなみに改良後はこんな感じの出力になりました。
左からCPU温度、周波数、全体の使用率、コア毎の使用率です。
#原因
何が重かったかといいますと、LinuxのOSコマンドを実行するために用いたsubprocess
というモジュールです。
こちらについてはやはりpythonのsubprocessでcatするのはまちがっている。で詳しく触れていますので是非ご覧ください。
簡単に言ってしまえば、テキスト取得のためだけにわざわざ子プロセスを立ち上げるので遅いということです。
open()
というファイル操作用組み込み関数を使えばPythonだけで完結しますので凡そ50倍以上速くなります。
#改善点
改善したのは主に四点です。
-
subprocess
をopen()
に置き換え -
while
のループは遅いということなのでfor
に置き換え -
print
内の文字列操作をformatで書き換え - 動作電圧表示を廃止し、使用率を表示
一つめが今回の目玉です。
以前はsubprocess
を用いてvcgencmd
というRaspberry Pi OS謹製コマンドを実行していました。
これだとRaspberry Pi OSでしか実行できませんし、そもそもvcgencmd
はシェルで実行するように設計されていますのでPythonから打つには向いていません。
Pythonで監視とは名ばかりのサブプロセス爆弾はもうやめよう、ということでvcgencmd
を使わずにCPU情報を取得する点も今回のサブ目玉です。
二つめは微々たるものですが気分的に書き換えておきました。
三つめは主に可読性アップが目的です。
四つめは動作電圧を確認するメリットをあまり見いだせなかったのと、動作電圧を記録したファイルが見当たらなかったためです。
代わりにCPU使用率を表示するようにしました。
#コード
こちらが改良後のコードです。
#!/usr/bin/python3.7
import time
import sys
#CPU使用時間取得関数
def get_data():
with open('/proc/stat') as r: #CPU統計ファイル読み込み
stats = r.readlines() #行ごとにリスト化
stat = [line.strip().split() for line in stats if 'cpu' in line] #cpuが含まれている行をピックアップして改行文字を削除、空白区切りで二重リスト化
rs = [] #使用時間が入るリストを宣言
for data in stat: #統計からCPU全体と論理コアそれぞれのデータを取り出す
busy = int(data[1]) + int(data[2]) + int(data[3]) #Busy状態の時間を求める
all = busy + int(data[4]) #全体の時間を求める
rs.append([all, busy]) #リストに追加
return rs #値を返す
pre_data = get_data() #初回のCPU使用時間取得
ave = 0 #平均計算用の変数宣言
time.sleep(1) #一秒待機
try: #以下のexcept KeyboardInterruptでCtrl+Cをキャッチするための正常時動作部分
for i in range(60): #60回繰り返し
with open('/sys/class/thermal/thermal_zone0/temp') as t: #CPU温度読み込み
temp = int(t.read()) / 1000 #型変換して単位を合わせる
with open('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq') as f: #CPU周波数読み込み
freq = int(f.read()) #型変換
now_data = get_data() #現在のCPU使用時間取得
rates = [] #CPU使用率が入るリストを宣言
for j in range(5): #CPU全体+論理コア数(4)回繰り返し
now = now_data[j] #現在の使用時間から求めるCPUのデータを取り出す
pre = pre_data[j] #1秒前の使用時間から求めるCPUのデータを取り出す
rate = (now[1] - pre[1]) / (now[0] - pre[0]) * 100 #(Busy状態 / 全体) * 100でCPU使用率を求める
rates.append(rate) #CPU使用率をリストに追加
#formatメソッドを用いて整形し書き出し
print("Temp:{0:>6.2f}'C, Freq: {1:.2f}GHz, CPU:{2:>5.1f}% [{3:>3.0f},{4:>3.0f},{5:>3.0f},{6:>3.0f}] ({7:>2})".format(temp, freq / 1000000, rates[0], rates[1], rates[2], rates[3], rates[4], i + 1))
ave += temp #平均温度用に現在の温度を加算
pre_data = now_data #次の一秒前データ用に現在のデータを保存
time.sleep(1) #一秒待機
print("Average: {:.2f}'C (60s)".format(ave / 60)) #ループ終了後に平均を書き出し
except KeyboardInterrupt: #Ctrl+Cをキャッチ
sec = i + 1 #終了時の経過時間取得
print(" Aborted.\nAverage: {0:.2f}'C ({1}s)".format(ave / sec, sec)) #平均温度を書き出し
sys.exit() #正常終了
##CPU使用率
解説に先立ってCPU使用率の求め方を別に記したいと思います。
こちらを参考にさせて頂きました。
CPU使用率とは要するに全時間に対するCPUがビジー状態だった時間の割合ですから、Ubuntuが用意してくれているCPU統計からCPUの使用時間を取得してやれば、以下のような原理で求められます。
累積CPU稼働時間(s) | ... | 904 | 905 | 906 | 907 | 908 | ... |
---|---|---|---|---|---|---|---|
累積Busy状態時間(s) | ... | 302 | 302.5 | 302.6 | 303 | 303 | ... |
差分CPU稼働時間(s) | ... | 1 | 1 | 1 | 1 | 1 | ... |
差分Busy状態時間(s) | ... | - | 0.5 | 0.1 | 0.4 | 0 | ... |
CPU使用率(%) | ... | - | 50% | 10% | 40% | 0% | ... |
コードの大まかな流れは「データ取得(pre)」→「1秒待機」→「データ取得(nowと次回のpre)」→「計算」→「データ取得(次回のnow)」→(繰り返し)です。
UbuntuのCPU統計は/proc/stat
に記されており、マニュアルから必要事項を抜粋すると以下の通りです。
/proc/stat
kernel/system statistics. Varies with architecture. Common entries include:
cpu 10132153 290696 3084719 46828483 16683 0 25195 0 175628 0
cpu0 1393280 32966 572056 13343292 6130 0 17875 0 23933 0
#上の文字列を整形して右のようにします[[cpu, 10132153, 290696, 3084719, 46828483, 16683, 0, 25195, 0, 175628, 0], [cpu0, 1393280, 32966, 572056, 13343292, 6130, 0, 17875, 0, 23933, 0]]
The amount of time, measured in units of USER_HZ (1/100ths of a second on most architectures, use sysconf(_SC_CLK_TCK) to obtain the right value), that the system ("cpu" line) or the specific CPU ("cpuN" line) spent in various states:
user (1) Time spent in user mode. #ユーザーによってビジー
nice (2) Time spent in user mode with low priority (nice). #ユーザーによる優先度の低いプロセスによってビジー
system (3) Time spent in system mode. #システムによってビジー
idle (4) Time spent in the idle task. This value should be USER_HZ times the second entry in the /proc/uptime pseudo-file. #アイドル
こちらを参考に必要なデータを取り出せるよう整形していきます。
subprocess
で取得するのでしたらgrep
という便利なものがりますが今回は使えません。
ですのでopen()
に含まれるreadlines()
というものを使います。
こちらの記事を参考に、grep
と同様の動作を再現し、さらに加工してCPU > 項目
という二重リストにします。(上記マニュアル内コメント参照)
この二重リストからfor
でCPU全体と論理コア毎のデータを取り出してBusy状態と全体の経過時間を計算し、CPU全体と論理コア > 経過時間
の二重リストにして返します。
ここではリストの2,3,4番目の項目の総和でBusy状態、そしてBusy状態とIdle状態(5番目の項目)の総和で全体の経過時間を取得しています。
以上が使用時間取得関数get_data()
の処理です。
以降は解説の中で触れていきたいと思います。
##解説
大体コメントで記しましたが、上から順に扱っていきたいと思います。
僕自身初心者ですので過剰気味に解説していきます。
#####import
まず必要なモジュールをimport
していきます。
今回は待機用にtime
モジュールと、終了用にsys
モジュールを読み込みました。
これに加えて改良前のコードにはsubprocess
が含まれていましたが削除しました。
#####def get_data()
CPU使用率に関しては既に別項で扱いましたが、def
で累積使用時間を取得する関数を宣言しています。
#####pre_data
とave
、time.sleep(1)
ループに入る前に初回計算用のデータを取得して、CPU平均温度を求めるための変数を宣言します。
そして一秒待機します。
待機しないで進むと/proc/stat
の更新間隔より早くnow_data
が取得されてしまうので、差分がゼロになってしまい0で割るなと怒られます。
#####try
とexcept
ループ全体をtry
で囲むことでCtrl + C
の入力をエラーとしてではなく例外としてキャッチできるようにしてあります。
Ctrl + C
が入力された時点でexcept KeyboardInterrupt
に飛んで平均温度を計算したのち正常終了とします。
#####for i in range(60)
iに0~59までの数字を代入しながら処理を繰り返してくれます。
10000000回繰り返すと0.04秒ほどwhile
で繰り返すより早いそうです。
今回は誤差の範囲にもなりませんがカウンタを用意しなくてよいのがスマートで気に入りました。
#####with open()
subprocess
に代わって導入したファイルを開く組み込み関数です。
前述の通り、詳しくはこちらを見てください。
Python内部で処理が完結するので軽量化に貢献してくれています。
#####now_data
とrates
現在の累積CPU使用時間を取得しています。
CPU使用率を代入するための空リストを宣言しています。
#####for j in range(5)
CPU全体とコア毎にデータを取り出して処理します。
現在の累積使用時間now_data
と一秒前の累積使用時間pre_data
の差分を求めてCPU使用率を計算し、結果をリストにして返します。
(i
じゃないインデックスはなんて名前にすればいいんでしょうね。)
#####print
formatメソッドを用いて見やすくなるように整えて書き出しています。
平均値の除算などの簡単な計算もformatの内部でできるのでとても便利ですね。
#####ave
とpre_data
、time.sleep(1)
現在の温度を、平均温度を取得するためにave
に加えています。
次回のCPU使用率の計算のためにpre_data
に先ほど取得した現在のデータを代入しておきます。
一秒待機したのち処理がループされます。
#CPU情報をvcgencmd
を使わずに取得
既にふれていますがPythonからvcgencmd
で情報を取得するのはとても非効率です。
/proc/stat
のようにUbuntuでデータをファイルにしてくれているのでそちらから取得するようにしました。
##CPU温度
こちらは結構有名な場所かと思います。
cat /sys/class/thermal/thermal_zone0/temp
#温度の1000倍が出力される
vcgencmd
だと単位やらがついてきますが、数字だけですのでスクリプトに埋め込むにはこちらから取得するのがよさそうです。
除算もプログラム内でしたら苦になりませんし、なぜか有効数字はこちらのほうが大きいといいことづくめです。
##CPU周波数
これは見つけるのに苦労しましたが、如何なる事にも偉人というのはいらっしゃるものです。ありがたや。
Ubuntu CPUfreq その3 - CPUのcpufreqフォルダー内のファイルについて
#/sys/devices/system/cpu/cpu*/cpufreqにcpu(コア)の情報がまとまっています
#cpuinfo_cur_freqはハードウェアから取得した値
#scaling_cur_freqはLinuxカーネルから取得した値
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq
#Permission denied
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
#cpu0のCPU周波数が表示されます
場所を見つけたのはいいのですが、二つ問題に遭遇しました。
一つめはハードウェアから取得した数値はroot
しか見れないことで、もう一つはcpuコア毎にファイルが違うことです。
できればハードウェアから取得した数値を参照したいですがroot
なしで実行したいですし、cpuコア毎に周波数が違えばその平均を取りたいですが処理が4倍になるので避けたいです。
これらの問題を解決するために力技で検証してみました。
8つの値がすべて一致すれば一つの値を代表サンプルとしても問題はないと思うので全4コアのcpuinfo_cur_freq
とscaling_cur_freq
をそれぞれ取得し比較することを10000回繰り返します。
結果、95%程の割合ですべて一致しました。
どうも周波数変更時にずれが発生するようで、検証間にスリープをはさんだりすると95%程、何もせずひたすら繰り返すと100%一致でした。
また、ずれが発生するのは主にハードウェアとカーネルの間のようで、コア間の一致は悪くても99%程でした。
そこまで厳密になる必要もないですし、1秒ごとのサンプリングでは影響は少ないだろうということで今回はcpu0
のscaling_cur_freq
を代表値として採用することにしました。
Linuxでは(Raspberry Piでは?)コア毎に周波数を変化させて省電力化するような制御は行っていないのですね。
このあたり詳しい方がいらっしゃいましたらお教え頂ければ幸いです。
##CPU電圧
こちらはファイルの場所を見つけることができませんでした。
本気で探せば見つかるかもしれませんが、元から無くても良いか的な値でしたのでこの際削ることとしました。
もしご存じの方がいらっしゃればお教えください。
#追記
上記スクリプトを実行していたところ、極稀にですがZeroDivisionError: division by zero
というエラーに遭遇しました。
エラーメッセージの通り「ゼロで割ってんじゃねぇよぉ!」(数学的にゼロで割るというのは定義できないのでご法度)という魂の訴えですので対処してあげましょう。
今回の場合はnow_data
が処理遅れなど何らかの理由でpre_data
と同値となってしまい、rate = (now[1] - pre[1]) / (now[0] - pre[0]) * 100
にて分母がゼロとなりエラーが発生しています。
ですのでnow_data
を取得した時点でpre_data
と比較をして、同値の場合には0.1秒待機して一致しなくなるまで再帰的に取得するようにしています。
取り敢えずこれでエラーとは遭遇していないので解決とします。
また、温度だけでなくCPU使用率なども平均を表示するようにしました。
コピー用としてコメントを記述していないものを貼っておきます。
#!/usr/bin/python3.7
import time
import sys
def get_data():
with open('/proc/stat') as r:
stats = r.readlines()
stat = [line.strip().split() for line in stats if 'cpu' in line]
rs = []
for data in stat:
busy = int(data[1]) + int(data[2]) + int(data[3])
all = busy + int(data[4])
rs.append([all, busy])
return rs
def assign():
now_d = get_data()
if now_d == pre_data:
time.sleep(0.1)
assign()
else:
return now_d
pre_data = get_data()
duration = 60
ave_temp = 0
ave_freq = 0
ave_cpu = 0
time.sleep(1)
try:
for i in range(duration):
with open('/sys/class/thermal/thermal_zone0/temp') as t:
temp = int(t.read()) / 1000
with open('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq') as f:
freq = int(f.read()) / 1000000
now_data = assign()
rates = []
for j in range(5):
now = now_data[j]
pre = pre_data[j]
rate = (now[1] - pre[1]) / (now[0] - pre[0]) * 100
rates.append(rate)
print("Temp:{0:>6.2f}'C, Freq: {1:.2f}GHz, CPU:{2:>5.1f}% [{3:>3.0f},{4:>3.0f},{5:>3.0f},{6:>3.0f}] ({7:>2})".format(temp, freq, rates[0], rates[1], rates[2], rates[3], rates[4], duration - i))
ave_temp += temp
ave_freq += freq
ave_cpu += rates[0]
pre_data = now_data
time.sleep(1)
print("Average: {0:.2f}'C, {1:.2f}GHz, {2:.1f}% ({3}s)".format(ave_temp / duration, ave_freq / duration, ave_cpu / duration, duration))
except KeyboardInterrupt:
sec = i + 1
print(" Aborted.\nAverage: {0:.2f}'C, {1:.2f}GHz, {2:.1f}% ({3}s)".format(ave_temp / sec, ave_freq / sec, ave_cpu / sec, sec))
sys.exit()
#まとめ
重かったスクリプトを軽量化しようとして取り組み始め、PythonでCPUをモニターするからにはPythonだけで完結させようという目的も加わり、結局大幅な軽量化に成功しました。
知らなかった超便利な関数も知ることができ、日々勉強を痛感しております。
また、この記事を書いている途中にもコードを書き直したりして少し洗練することができました。見直しは大切ですね。
長々とした記事になってしまいましたが最後まで読んでいただきありがとうございました。
ご指摘などありましたらビシバシとお寄せいただければ幸いです。