[프로젝트] raspi-monitor | 프로젝트 생성

dev2820·2021년 10월 9일
0

이제 /proc 디렉토리로부터 정보를 읽어올 프로그램을 만들어야합니다. 생성된 프로그램은 우분투 위에 올려 사용할 예정이니, 사실 C나 C++로 프로그램을 짜는게 유리할 것 같습니다만, 저는 nodejs 프로그램을 만들고 돌리겠습니다. 그 편이 데이터베이스 연동이나, 문자열 처리가 더 좋을 것 같고, 자잘한 파일을 여럿 읽기에 좋을 것 같아요.

nodejs 설치

아직 우분투에 nodejs가 설치되어 있지 않다면 다음과 같이 설치합니다. 아래 링크도 참고해주세요.

https://github.com/nodesource/distributions/blob/master/README.md#debinstall

# Using Ubuntu
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs

위 코드는 16 이상 버전의 nodejs를 설치합니다.

프로젝트 생성

nodejs이니 npm을 통해 프로젝트를 생성합시다.

npm init

그리고 app.js로 실행하고 종료하도록 하죠. test command로 nodemon을 쓰도록 해놨는데, 워낙 가벼운 파일이라 nodemon을 쓸 것까지도 없을 것 같아요.

dotenv 설치

실행에 필요한 각종 설정들(argv)은 실행 인자로 전달할수도 있지만, 더 폭넓고 자유로운 설정을 위해 dotenv 모듈을 이용하겠습니다.

npm install dotenv

.env 파일에는 추후 데이터베이스 이름,비밀번호 등을 작성할 것이기 때문에 github에 올리면 안됩니다..gitignore를 생성해줍시다.

이제 필요한 설정값들은 .env 파일에 써주면 됩니다.

간단한 코드 작성

app.js 파일을 생성하고 파일을 읽어오는 코드를 작성해봅시다.
몇몇 파일을 읽거나, 리눅스 명령을 실행해야하는데, 일단 다음 파일을 읽어오는 코드를 작성해봅시다.

  • /proc/uptime: uptime 정보가 저장되어 있다.
  • /sys/class/hwmon/hwmon0/temp1_input: cpu 온도 정보가 저장되어 있다.
  • /proc/loadavg: loadavg 1분 5분 15분 정보가 저장되어 있다.
  • /proc/stat: cpu 사용률 정보가 저장되어 있다.
  • /proc/meminfo: 메모리 정보가 저장되어 있다.
  • /proc/diskstats: 디스크 정보가 저장되어 있다.
  • /proc/net/dev: 네트워크 정보가 저장되어 있다.

app.js를 다음과 같이 작성하고 실행해봅시다.

'use strict';

const fs = require('fs/promises')

const filePathList = [
        "/proc/uptime",                            // uptime 정보
        "/sys/class/hwmon/hwmon0/temp1_input",      // cpu 온도 정보
        "/proc/loadavg",                           // loadavg 1분 5분 15분 정보
        "/proc/stat",                              // cpu 사용률 정보
        "/proc/meminfo",                           // 메모리 정보
        "/proc/diskstats",                         // 디스크 정보
        "/proc/net/dev"                            // 네트워크 정보
    ];

const filePromiseList = filePathList.map(path=>{
    return fs.readFile(path,{ encoding:'utf-8'})
})

Promise.all(filePromiseList).then(contents=>{
    console.log(contents);
})

우분투 환경에서 코드를 실행해봅시다.

node app.js

붸에에엑
뭔가 잘 출력된 것 같군요. 코드에 문제는 없는 것 같습니다. 다만 정규표현식을 통해 좀 다듬을 필요는 있어보입니다.

값 다듬기

값을 저장하는 객체를 선언하는 부분도 여기에 작성하고 싶었지만, 코드가 너무 기니, 코드 전문이 궁금하시면
github을 참고해주세요.

여기선 간단히 읽어온 파일 정보를 어떤 방식으로 사용하는지만 다루겠습니다.

uptime 정보 다듬기

/proc/uptime 파일에 저장된 값은 다음과 같습니다.

309592.89 1236625.72

첫 번째 값은 시스템 부팅 후 지난 시간(초) 이고, 두 번째 값은 시스템 부팅 후 아무것도 안한 시간(idle) 입니다. 두 번째 값이 더 커서 이상해보일 수 있는데, 두 번째 값은 모든 코어의 idle 시간을 합한것이기 때문에 라즈베리파이의 경우 코어가 4개이므로 4로 나누어줘야 각 코어의 idle 시간이 나옵니다.

두 번째 값은 필요 없으니 첫 번째 값만 저장합시다.

실수를 읽는 정규표현식은 다음과 같습니다.

[0-9]*[.]?[0-9]+
const reg = /[0-9]*[.]?[0-9]+/g
const parseResult = content.match(reg); // [0]:uptime, [1]:idle time

const uptime = parseInt(parseResult[0]);

uptime은 저 값을 그대로 저장할 생각이니 가공할 필요가 없습니다.parseInt로 정수로 만들고 저장해줍니다. idle time은 안써요.

cpu 온도정보 다듬기

temp1_input 파일엔
35502 같이 숫자만 덩그러니 있습니다.

35502면 35.502도겠죠.

this.thermal = parseFloat(content)/100.0;

loadavg 정보 다듬기

/proc/loadavg의 값은 다음과 같습니다.

0.00 0.00 0.00 1/279 49189

처음 나오는 저 세 실수가 각각 loadavg1m,loadavg5m,loadavg15m (1분평균,5분평균,15분평균)이므로 똑같이 실수를 읽어 저장해줍시다.

const reg = /[0-9]*[.]?[0-9]+/g
const parseResult = content.match(reg); // [0]: loadavg1m, [1]: loadavg5m time [2]: loadavg15m

const loadavg1m = parseFloat(parseResult[0]);
const loadavg5m = parseFloat(parseResult[1]);
const loadavg15m = parseFloat(parseResult[2]);

cpu 정보 다듬기

cpu 609073 6793 411414 173602192 489585 0 39047 0 0 0
cpu0 109615 2167 98044 43441475 108226 0 19254 0 0 0
cpu1 186932 1500 97830 43397936 101704 0 5524 0 0 0
cpu2 168992 1167 98112 43410221 107949 0 6412 0 0 0
cpu3 143532 1958 117427 43352559 171705 0 7856 0 0 0
...
/proc/stat 내용 일부

각 라인은 앞에서부터 total,ni,sy,id,wa,hi,si,st 입니다.

total = ni+sy+id+wa+hi+si+st

각 숫자가 무슨 의미인지 궁금하다면

여기를 클릭 us, user : time running un-niced user processes
sy, system : time running kernel processes
ni, nice : time running niced user processes
id, idle : time spent in the kernel idle handler
wa, IO-wait : time waiting for I/O completion
hi : time spent servicing hardware interrupts
si : time spent servicing software interrupts
st : time stolen from this vm by the hypervisor

linux top manual에서 발췌한 내용입니다. 쉘에서 `man top` 을 입력해도 같은 내용을 찾을 수 있을 겁니다.

아마 이런 궁금증이 생기신 분들이 있을거같아요.

"쉘에서 top 명령으로 출력한 값과 일치하지 않는데?"

아마 이런 식을 적용했을거 같아요.
us 사용률(%) = us 값 / total 값 * 100

하지만 위와 같은 식으로 us 사용률을 계산하면 top과 다른 결과가 나옵니다.
이유는 /proc/stat 에 저장되는 값들이 부팅 이후 누적된 값 이라서 그렇습니다. 따라서 제대로된 us 사용률을 구하려면 아래와 같은 식을 적용해야 합니다.

us 사용률 = (현재 값 - 이전 값) / 시간 * 100

us 뿐만 아니라 sy,id 등의 모든 값에서도 마찬가지입니다.

예를 들어 3초 간격으로 id 사용률을 계산한다면

id 사용률 = (현재 id 값 - 3초 전 id 값) / 3 * 100

이 됩니다.

뭐... 이와같이 ni,sy 등등의 필요한 값들을 구해주고, cpu_usage를 구해야겠죠. cpu 사용률은 (100-id사용률) 입니다. id가 cpu가 쉬는 비율을 의미하거든요.


const lines = content.split('\n'); // content에는 /proc/stat 파일의 내용이 들어있어요.
const regInt = /[0-9]+/g; // 정수를 읽는 정규표현식
const parseCpu = lines[0].match(regInt);

const cpuUs =  (parseInt(parseCpu[0]) - this.beforeCpuUs)/diffTotal*100.0;
...

위와 같은 방식으로 각 값을 구해주면 됩니다.

memory 정보 다듬기

MemTotal: 3884328 kB
MemFree: 1781180 kB
MemAvailable: 3405948 kB
Buffers: 153136 kB
Cached: 1364320 kB
SwapCached: 0 kB
Active: 1005736 kB
Inactive: 768284 kB
Active(anon): 265016 kB
Inactive(anon): 552 kB
Active(file): 740720 kB
Inactive(file): 767732 kB
Unevictable: 17260 kB
Mlocked: 17260 kB
SwapTotal: 0 kB
SwapFree: 0 kB
...
/proc/meminfo 내용 일부

다음으로 저장할 값은 메모리 정보입니다. 메모리는 다행히 잘 정리되어있고, 누적값도 아닙니다. 숫자만 읽어내면 되겠네요.

주의할 점은, memAvailable이 스와핑 없이 새 프로세스를 올리는데 사용할 수 있는 메모리의 추정치이고, memFree는 물리적으로 사용되지 않는 메모리입니다. 즉 memAvailable이 가용메모리(=사용되지 않는 메모리)의 뜻이 아닙니다.

메모리 사용률 식은 다음과 같습니다.

memory usage(KB) = memTotal - (memFree + Buffers + Cached)
memory usage(%) = (memTotal - ( memFree + Buffers + Cached ) ) / memTotal * 100

const lines = content.split('\n'); // content = meminfo 정보
const regInt = /[0-9]+/g;

this.totalMemory = parseInt(lines[0].match(regInt)[0]);
this.freeMemory = parseInt(lines[1].match(regInt)[0]);
...

위와 같은 방식으로 각 메모리 정보를 읽어와서 total 메모리 정보를 계산하면 됩니다.

io(disk) 정보 다듬기

7 252 loop252 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
7 253 loop253 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
7 254 loop254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
7 255 loop255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
179 0 mmcblk0 16375 6818 1488625 61979 4800545 1576140 225426908 48197067 0 13958672 40773892 245 0 52157121 1781
179 1 mmcblk0p1 490 2955 29444 914 290 4356 124403 69332 0 4436 69224 5 0 268729 45
179 2 mmcblk0p2 15813 3863 1455711 60836 4800255 1571784 225302505 48127734 0 13956840 40704516 240 0 51888392 1735
8 0 sda 269 80 15522 3484 12416 313 12179928 72222 0 43024 50964 0 0 0 0
8 1 sda1 222 80 13134 3439 12399 313 12179928 72159 0 42844 50888 0 0 0 0
...

/proc/diskstats 내용 일부

loop 정보가 주르륵 나오고 mmcblk0 와 sda가 보입니다. mmcblk는 SD카드 장치를 의미하고, sda는 sata로 연결된 장치를 의미합니다. 저의 경우 HDD를 의미하죠.

순서대로

메이저번호 | 마이너번호 | 장치이름 | 성공적으로 읽은 수 | read merge | 읽은 섹터 수 | 읽기에 사용한 시간(ms) | 성공적으로 쓴 수 | write merge | 쓴 섹터 수 | 쓰기에 사용한 시간 | ... 정보가 많으니 그만 읽읍시다. 우리가 필요한건 읽은 섹터 수쓴 섹터 수 입니다. 섹터를 통해 몇 bytes를 읽고 쓰는지 확인하겠습니다.

sudo fdisk -l

쉘에 위 명령을 입력하면 아래와 같은 정보를 볼 수 있습니다.

mmcblk0와 sda 모두 1섹터를 512bytes(0.5KB)로 정의하고 있군요. 따라서 읽은 KB는

읽은 KB = 읽은 섹터 수 * 0.5(KB)

로 얻을 수 있습니다. 또한 diskstats의 값들도 마찬가지로 누적값이라서 이전 값을 빼줘야 ㄱread KB/s, write KB/s을 구할 수 있습니다.

read KB/s = (현재 읽은 섹터 수 - 과거 읽은 섹터 수) * (1/2) / 시간 간격

net 정보 다듬기

Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
docker0: 0 0 0 0 0 0 0 0 370257 1412 0 0 0 0 0 0
vethb6a5bc6: 0 0 0 0 0 0 0 0 380853 1562 0 0 0 0 0 0
eth0: 116680097 423134 0 245245 0 0 0 3943 49649527 112584 0 0 0 0 0 0
lo: 599604496 6619083 0 0 0 0 0 0 599604496 6619083 0 0 0 0 0 0
wlan0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

/proc/net/dev 내용

뭐랄까... 정리가 돼있긴한데, 미묘하게 되어있습니다. 그래도 field 이름을 설명해주는게 어디에요.

우리는 저기서 Receive와 Transmit의 bytes와 errs 정도를 읽어오면 됩니다.
또한 bytes를 Kb(kilo bit)로 전환해줘야겠죠.

/net/dev의 값도 누적값이라 다음과 같이 Kbps를 구해줘야합니다.

receive kbps = 현재 (receiveBytes - 과거 receiveBytes)/8 /1024 /시간 간격
transmit kbps = 현재 (transmitBytes - 과거 transmitBytes)/8 /1024 /시간 간격

8로 나눠주는건 byte를 bit 단위로 변환한거고, 1024로 나눈 것은 bit를 kilo bit로 변환한 겁니다.

같은 방식으로 receive err kbps,transmit err kbps도 구할 수 있겠죠.

저의 경우 eth0(이더넷)의 네트워크 정보만 기록했습니다.


다음글은 읽은 정보를 db에 저장하는 과정이 되겠네요.

profile
공부,번역하고 정리하는 곳

0개의 댓글