Task Manager for Linux: Phase 2
Process Info
在 Phase1 中,我是通过调用
system(("ps auxc > " + LogFile).c_str());
的方式获取进程信息。
在助教老师的建议下,修改为了手动读取 /proc
进行相应计算的方式:
// process.cpp
int CalcuProcTime(const Tokens& tok) {
return StrToInt(tok.GetArgs(13)) + // user mode jiffies
StrToInt(tok.GetArgs(14)); // kernel mode jiffies
}
void Process::CalculateRatio(int DurTotal, int FullSiz) {
Tokens line;
std::ifstream ProcInput(InputFile);
ProcInput >> line;
int CurProcTime = CalcuProcTime(line);
RatioCPU = std::floor(1000.0 * (CurProcTime - PreProcTime) / DurTotal) / 10.0;
RatioMEM = std::floor(1000.0 * StrToInt(line.GetArgs(22)) / 8192 / FullSiz) / 10.0;
PreProcTime = CurProcTime;
}
Process::Process(int var) {
Pid = var;
std::ostringstream oss;
oss << "/proc/" << Pid << "/stat";
InputFile = oss.str();
Tokens line;
std::ifstream ProcInput(oss.str());
ProcInput >> line;
CommandStr = line.GetArgs(1);
PreProcTime = CalcuProcTime(line);
}
// process_list.cpp
void ProcessList::UpdateList(bool LogFlag) {
/* using a few functions provided by the kernel
to read the dirent */
struct dirent* ent;
DIR* ProcDir = opendir("/proc");
std::vector<int> PidList;
if (ProcDir == NULL) {
std::cerr << red << "Error opening /proc" << std::endl;
return;
}
while (ent = readdir(ProcDir)) {
if (isdigit(*ent->d_name)) {
PidList.push_back(strtol(ent->d_name, NULL, 10));
}
}
closedir(ProcDir);
for (auto pid: PidList) {
if (!FindProcess(pid, false)) {
lst.push_back(Process(pid));
}
}
for (auto iteproc = lst.begin(); iteproc != lst.end(); ) {
auto itepid = std::lower_bound(
PidList.begin(), PidList.end(), iteproc->GetPid());
if (*itepid != iteproc->GetPid()) {
if (iteproc + 1 != lst.end()) {
*iteproc = lst.back();
}
lst.pop_back();
} else iteproc++;
}
std::this_thread::sleep_for(
std::chrono::microseconds(20000));
int CurTotalTime = ReadCPUTotal(),
DurTotalTime = CurTotalTime - PreTotalTime;
for (auto&& proc: lst) {
proc.CalculateRatio(DurTotalTime, MemTotal);
}
PreTotalTime = CurTotalTime;
SortCol(DefaultCol, Defaultinc);
if (!LogFlag) return;
auto CurTime = time(nullptr);
std::ostringstream oss;
oss << "Logs/" << std::put_time(gmtime(&CurTime), "%F-%T") << ".log";
std::ofstream LogOut(oss.str());
LogOut << "PID" << tab << "%CPU" << tab
<< "%MEM" << tab << "COMMAND" << std::endl;
for (auto proc: lst) {
LogOut << proc << std::endl;
}
}
其中,计算进程的内存占用则读取相应信息计算比例即可。
而对于进程的 CPU 占用时,我是通过考察一个时间段内每个进程占用的机时、总机时,计算比例得到的。
可以发现,通过这种方式进行进程 CPU 占用计算,时间段相对较长时,结果就会相对准确。
具体地,这里选取了 Time_lastupdate
\(\sim\) Time_thisupdate + std::chrono::micorseconds(20000)
进行计算,在运行结果上比较接近于 ps
。实现上,这里利用了之前实现的 Tokens
类来管理 /proc/[pid]/stat
的相应信息。解析 stat
文件中的信息时,我参考了内核的文档。
Structure
对代码的结构进行了一些小调整:
其中额外添加了四个文件,Item/movingobj.h
与 Runtime/birdgame.h
在 movingobj.h
中,主要管理游戏中的各种对象实体。birdgame.h
中实现游戏运行时的相应功能。
Class: MovingObj
// movingobj.h
#pragma once
#include <set>
#include <string>
#include <utility>
#include <functional>
typedef std::pair<int, int> Node;
class MovingObj {
protected:
std::set<Node> Obj;
void ObjModify(std::function<Node(Node)> func);
public:
bool FindCoo(Node x, bool DeleteFlag = false);
bool CheckBoardCrash(int H, int W, bool FlagAll = 0);
bool CheckObjCrash(MovingObj& rhs, bool DeleteFlag = false);
// virtual void AutoMove() = 0;
virtual void PrintUnit() = 0;
Node GetCorner(bool FindXMax);
void AddNode(Node x);
};
class Bird: public MovingObj {
public:
Bird() {}
Bird(Node x);
void AutoMove();
void PrintUnit();
void ControlMove(int delta = 2);
};
class Bullet: public Bird {
public:
Bullet(Node x);
void PrintUnit();
void AutoMove();
};
class Pipe: public MovingObj {
protected:
const int Width = 5;
bool PipeType;
public:
Pipe() {};
Pipe(int x, int y, int h, bool type);
void PrintUnit();
void AutoMove(int H);
void BackMove();
bool GetType();
};
class FallPipe: public Pipe {
public:
FallPipe(Pipe& pip, Node x);
void FixDown(Pipe& pip);
void AutoMove();
};
// movingobj.cpp
#include <map>
#include <utility>
#include <iostream>
#include "movingobj.h"
#include "../Utils/iostaff.h"
void MovingObj::AddNode(Node x) {
Obj.insert(x);
}
void MovingObj::ObjModify(std::function<Node(Node)> func) {
std::set<Node> NewObj;
for (auto itm: Obj) {
NewObj.insert(func(itm));
}
Obj = NewObj;
}
// set DeleteFlag = true to delete what have found
bool MovingObj::FindCoo(Node x, bool DeleteFlag) {
if (DeleteFlag) {
if (Obj.count(x)) {
return Obj.erase(x), true;
}
return false;
}
return Obj.count(x);
}
// FlagAll=1 check forall, FlagAll=0 check exsiting
bool MovingObj::CheckBoardCrash(int H, int W, bool FlagAll) {
for (auto itm: Obj) {
if (0 <= itm.first && itm.first <= W && 0 <= itm.second && itm.second <= H) {
if (FlagAll) return false;
} else {
if (!FlagAll) return true;
}
}
return FlagAll;
}
/* FindXMax=true to find the Node with maximum x-coo, minimum y-coo
FindXMax=false to find the Node with minimum x-coo, minimum y-coo
*/
Node MovingObj::GetCorner(bool FindXMax) {
auto func = [&](Node lhs, Node rhs) {
if (lhs.first == rhs.first) {
return (lhs.second < rhs.second)? lhs: rhs;
}
return ((lhs.first < rhs.first) ^ FindXMax)? lhs: rhs;
};
Node res = *Obj.begin();
for (auto itm: Obj) {
res = func(res, itm);
}
return res;
}
/* DeleteFlag=true delete the crashed Node when checking and return true when hit through
DeleteFlag=false return true when crashing checked.
*/
bool MovingObj::CheckObjCrash(MovingObj& rhs, bool DeleteFlag) {
if (DeleteFlag) {
bool ret = false;
for (auto itm: Obj) {
if (rhs.Obj.count(itm)) {
rhs.Obj.erase(itm);
ret |= !(rhs.Obj.count(std::make_pair(itm.first + 1, itm.second)));
}
}
return ret;
}
for (auto itm: Obj) {
if (rhs.Obj.count(itm)) {
return true;
}
}
return false;
}
Bird::Bird(Node x) {
AddNode(x);
AddNode(std::make_pair(x.first + 1, x.second));
}
void Bird::AutoMove() {
ObjModify([](Node x) {
return Node(x.first, x.second - 1);
});
}
void Bird::PrintUnit() {
std::cout << "??";
}
void Bullet::PrintUnit() {
std::cout << "??";
}
void Pipe::PrintUnit() {
std::cout << backg << " " << endo;
}
void Bird::ControlMove(int delta) {
ObjModify([&](Node x) {
return Node(x.first, x.second + delta);
});
}
Bullet::Bullet(Node x): Bird::Bird(x) {}
void Bullet::AutoMove() {
ObjModify([](Node x) {
return Node(x.first + 1, x.second);
});
}
void Pipe::AutoMove(int H) {
const static int tx[] = {1, 0, -1, 0},
ty[] = {0, 1, 0, -1};
int ColorCnt = 0, SpecifyCode = 0;
std::map<Node, int> HashCheck;
std::function<void(Node)> DfsConnected = [&](Node cur) {
if (cur.second < 0 || cur.second > 23) {
SpecifyCode = ColorCnt;
}
HashCheck[cur] = ColorCnt;
for (int i = 0; i != 4; ++i) {
Node nex = std::make_pair(cur.first + tx[i], cur.second + ty[i]);
if (HashCheck[nex]) continue;
if (FindCoo(nex, false)) {
DfsConnected(nex);
}
}
};
for (auto unit: Obj) {
if (HashCheck[unit]) continue;
ColorCnt++;
DfsConnected(unit);
}
ObjModify([&](Node x) {
return Node(x.first - 1,
x.second - (HashCheck[x] != SpecifyCode));
});
}
void FallPipe::AutoMove() {
ObjModify([](Node x) {
return Node(x.first - 1, x.second - 2);
});
}
Pipe::Pipe(int x, int y, int h, bool type) {
PipeType = type;
for (int i = 0; i != h; ++i) {
for (int j = 0; j != Width; ++j) {
AddNode(std::make_pair(x + j, y + i));
}
}
}
bool Pipe::GetType() {
return PipeType;
}
// To cut a part of [Pipe& pip] from the last hitted [Node x]
FallPipe::FallPipe(Pipe& pip, Node Corner) {
const int delta = pip.GetType()? -1: +1;
int CooY = Corner.second + delta;
while (true) {
bool FoundNull = true;
for (int i = -1; i <= Width; ++i) {
Node CurCoo = std::make_pair(Corner.first - i, CooY);
if (pip.FindCoo(CurCoo, true)) {
AddNode(CurCoo);
FoundNull = false;
}
}
if (FoundNull) break;
CooY += delta;
}
}
void FallPipe::FixDown(Pipe& pip) {
for (auto itm: Obj) {
pip.AddNode(itm);
}
}
每个对象都使用了一个 std::set<std::pair<int,int> >
维护它所包含的点的坐标信息。
小鸟与子弹的形象使用了 unicode 的字符画,管道的形象使用了一个绿色的空格(基于已经实现的 Form
类),使用虚函数 PrintUnit
管理。
每个对象都有一个同名的 AutoMove()
方法,在运行时刻不断变换坐标位置。为了便于实现,使用了一个高阶函数抽象 ObjModify
,可以按照传入的变化函数更改整个 set
的值。
其中 Pipe
类的 AutoMove()
比较复杂。我使用了一个线性复杂度的搜索算法寻找 Pipe
类的联通块,并使孤立的联通快下落。这一功能是为了弥补在多个子弹击中同一个 FallPipe
时导致的 bug 。稍后我们将在 Demo 中看见。
Class: Board
游戏要求实现一个实时按键反馈的运行机制,因此这里我额外实现了一个 Keyboard
类,实现类似与 Windows 下 kbhit()
与 getch()
的功能。
// iostaff.h
#include <unistd.h>
class Keyboard {
public:
Keyboard();
~Keyboard();
bool kbhit();
char getch();
private:
struct termios initial_settings, new_settings;
int peek_character;
};
// iostaff.cpp
Keyboard::Keyboard() {
tcgetattr(0, &initial_settings);
new_settings = initial_settings;
new_settings.c_lflag &= ~ICANON;
new_settings.c_lflag &= ~ECHO;
new_settings.c_lflag &= ~ISIG;
new_settings.c_cc[VMIN] = 1;
new_settings.c_cc[VTIME] = 0;
tcsetattr(0, TCSANOW, &new_settings);
peek_character = -1;
}
Keyboard::~Keyboard() {
tcsetattr(0, TCSANOW, &initial_settings);
}
bool Keyboard::kbhit() {
unsigned char ch;
int nread;
if (peek_character != -1) return true;
new_settings.c_cc[VMIN] = 0;
tcsetattr(0, TCSANOW, &new_settings);
nread = read(0, &ch, 1);
new_settings.c_cc[VMIN] = 1;
tcsetattr(0, TCSANOW, &new_settings);
if (nread == 1) {
peek_character = ch;
return true;
}
return false;
}
char Keyboard::getch(){
char ch;
if (peek_character != -1) {
ch = peek_character;
peek_character = -1;
} else {
read(0, &ch, 1);
}
return ch;
}
这里也是使用了一些内核提供的现有函数实现的。
接下来考虑实现 Board
类:
// birdgame.h
#pragma once
#include <list>
#include "../Item/movingobj.h"
#include "../Item/process_list.h"
class Board {
private:
const int BoardHeight, BoardWidth, PipeNum;
bool GameOverFlag;
int GameScore, Magazine;
ProcessList& pst;
std::list<Pipe*> PipeList;
std::list<FallPipe*> FallList;
std::list<Bullet*> BulletList;
Bird* brd;
int RatiobaseRand(double ratio);
void PrintGameBoard();
void UpdatePosition();
void CheckCrash();
void UpdatePipe();
public:
Board(ProcessList& prl);
~Board();
void GameLoop();
};
// birdgame.cpp
#include <thread>
#include <cstdlib>
#include <condition_variable>
#include "birdgame.h"
#include "../Item/process.h"
#include "../Utils/iostaff.h"
#include "../Item/process_list.h"
Board::Board(ProcessList& prl):
BoardHeight(23), BoardWidth(100), PipeNum(18), pst(prl) {
Magazine = 5;
GameScore = GameOverFlag = false;
brd = new Bird(std::make_pair(2, 10));
UpdatePipe();
}
Board::~Board() {
for (auto blt: BulletList) delete blt;
for (auto pip: PipeList) delete pip;
for (auto pip: FallList) delete pip;
delete brd;
}
int Board::RatiobaseRand(double ratio) {
static int res = 0;
int Modu = BoardHeight * 0.65;
(res += ratio * 100) %= Modu;
for (int i = 0; i != 20; ++i) {
(res = res * 277 + 13) %= Modu;
}
return int(res + BoardHeight * 0.05);
}
void Board::UpdatePipe() {
while (PipeList.size() < PipeNum) {
pst.UpdateList(false);
auto tmprocess = pst.GetProcess(0);
int randHeight = RatiobaseRand(tmprocess.GetRatioCPU()),
UpperHeight = BoardHeight * 0.3 + randHeight,
currx = (PipeList.empty())? 30: (PipeList.back()->GetCorner(true)).first + 14;
/* set -1 here to remain a root underground
in case the bullet hit through CooY=0, the problem caused by the falling pipe
same for BoardHeight - UpperHeight + 2 here */
PipeList.push_back(new Pipe(currx, -1, randHeight + 1, false));
PipeList.push_back(new Pipe(currx, UpperHeight, BoardHeight - UpperHeight + 2, true));
}
while (PipeList.front()->CheckBoardCrash(BoardHeight, BoardWidth, true)) {
delete PipeList.front();
PipeList.pop_front();
Magazine += ((++GameScore) % 10 == 0);
}
}
void Board::UpdatePosition() {
brd->AutoMove();
for (auto pip: PipeList) pip->AutoMove(BoardHeight);
for (auto pip: FallList) pip->AutoMove();
for (auto blt: BulletList) blt->AutoMove();
}
void Board::CheckCrash() {
if (brd->CheckBoardCrash(BoardHeight, BoardWidth)) {
GameOverFlag = true;
return;
}
auto PipeCheck = [&](Pipe* pip, bool FlagFalling) {
if (GameOverFlag) return;
if (brd->CheckObjCrash(*pip)) {
GameOverFlag = true;
return;
}
if (!FlagFalling) {
for (auto ite = FallList.begin(); ite != FallList.end(); ) {
auto TmpCoo = (*ite)->GetCorner(true);
if (pip->FindCoo(std::make_pair(TmpCoo.first, TmpCoo.second - 1))) {
(*ite)->FixDown(*pip);
delete (*ite);
ite = FallList.erase(ite);
} else ite++;
}
}
for (auto blt: BulletList) {
if (blt->CheckObjCrash(*pip, true)) {
FallList.push_back(new FallPipe(*pip, blt->GetCorner(true)));
}
}
};
for (auto pip: PipeList) PipeCheck(pip, false);
for (auto fall_pip: FallList) PipeCheck(fall_pip, true);
for (auto ite = BulletList.begin(); ite != BulletList.end(); ) {
if ((*ite)->CheckBoardCrash(BoardHeight, BoardWidth)) {
delete (*ite);
ite = BulletList.erase(ite);
} else ite++;
}
}
void Board::PrintGameBoard() {
system("clear");
PrintTableline(BoardWidth);
for (int y = BoardHeight - 1; y >= 0; --y) {
for (int x = 0; x < BoardWidth; ++x) {
bool FoundFlag = false;
auto CurCoo = std::make_pair(x, y);
auto PrintObj = [&](MovingObj* itm, bool FlagBird = false) {
if (FoundFlag) return;
if (itm->FindCoo(CurCoo)) {
itm->PrintUnit();
FoundFlag = true;
if (FlagBird) ++x;
}
};
PrintObj(brd, true);
for (auto pip: PipeList) PrintObj(pip);
for (auto pip: FallList) PrintObj(pip);
for (auto blt: BulletList) PrintObj(blt, true);
if (!FoundFlag) std::cout << " ";
}
std::cout << std::endl;
}
PrintTableline(BoardWidth);
std::cout << green << bold << "Score" << endo << ": " << GameScore / 2 << tab
<< green << bold << "Magazine" << endo << ": " << Magazine << std::endl;
PrintTableline(BoardWidth);
}
void Board::GameLoop() {
/* create an auto-refreshing thread to got a flat display interface
using a condition_variable to
avoid the interruption of auto-refreshing thread,
(caused by user input)
which can result in garbled print
*/
std::condition_variable cv;
std::mutex ThreadMute;
bool ReadyInput = true;
auto InputThread = std::thread([&] {
Keyboard kbd;
while (true) {
if (GameOverFlag) break;
std::unique_lock<std::mutex> LockThread(ThreadMute);
cv.wait(LockThread, [&]{
return ReadyInput;
});
if (kbd.kbhit()) {
char cht = toupper(kbd.getch());
if (cht == ‘W‘) brd->ControlMove(2);
else if (cht == ‘E‘) brd->ControlMove(4);
else if (cht == ‘Q‘) GameOverFlag = true;
else if (cht == ‘L‘ && Magazine > 0) {
BulletList.push_back(new Bullet(brd->GetCorner(true)));
BulletList.back()->AutoMove();
Magazine--;
}
}
LockThread.unlock();
}
kbd.~Keyboard();
});
while (true) {
ReadyInput = false;
if (GameOverFlag) {
cv.notify_one();
break;
}
UpdatePipe();
UpdatePosition();
CheckCrash();
PrintGameBoard();
ReadyInput = true;
cv.notify_one();
std::this_thread::sleep_for(
std::chrono::milliseconds(100));
}
InputThread.join();
}
这里也是用到了一个多线程的技术,分别实现用户输入与界面打印的功能。特别地还用到了 condition_variable
,避免了用户输入中断界面打印时出现的乱码情况。213 行的 std::this_thread::sleep_for(std::chrono::milliseconds(100));
则保证了游戏运行时能有一个非常稳定的帧率。
在打印游戏界面时,用了一个多态抽象,调用各对象动态绑定的 PrintUnit()
方法来简化实现。
此外,考虑到在刚刚实现的计算 CPU 占用率方法。在游戏运行时刻,两次更新时间间隔较长。这导致运算结果会相对稳定,影响游戏体验。因此使用了线性迭代的方法提高了随机结果的方差,即 RatiobaseRand()
的内容。
Demo
![2021-04-30 16-29-01 的屏幕截图](/home/znr/Pictures/2021-04-30 16-29-02 的屏幕截图.png)
![2021-04-30 16-29-01 的屏幕截图](/home/znr/Pictures/2021-04-30 16-29-01 的屏幕截图.png)