Task Manager for Linux: Phase 2

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

对代码的结构进行了一些小调整:

Task Manager for Linux: Phase 2

其中额外添加了四个文件,Item/movingobj.hRuntime/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)

Task Manager for Linux: Phase 2

上一篇:捋一捋Python的文件处理(上)


下一篇:linux开放指定端口