Designing a Way for Process to Communicate with Kernel
https://softwareengineering.stackexchange.com/questions/371630
-
06-02-2021 - |
Question
I've got a project that is sort of a virtual operating system.
In this project, the Kernel
class is responsible for creating Process
classes. The process class consists of Thread
classes and the Thread
class will consist of a CPU
class. The CPU
class is actually a CPU
emulator and the Thread
is therefore an "emulated" thread. So when the CPU
class encounters an interrupt instruction, it needs to be handled by the kernel because it is usually a system call. The Kernel
class cannot actually see the CPU
class directly, it is embedded in the Thread
class, which is embedded in the Process
class.
The approach I'm using now uses a InterruptHandler
class, which handles the system calls and breakpoints. Here's what the code looks like.
class CPU final {
std::shared_ptr<MemoryBus> memoryBus;
std::shared_ptr<InterruptHandler> interruptHandler;
std::uint32_t regs[32];
public:
void Run(unsigned int steps) {
for (decltype(steps) i = 0; i < steps; i++)
RunInst();
}
protected:
// This function will call the interrupt
// handler if it encounters an interrupt instruction.
void RunInst();
};
class Thread final {
std::shared_ptr<CPU> cpu;
std::shared_ptr<InterruptHandler> interruptHandler;
public:
Thread() : cpu(new CPU) {
}
void Run(unsigned int steps) {
cpu->Run(steps);
}
};
class Process final {
std::vector<std::shared_ptr<Threads>> threads;
std::shared_ptr<MemoryMap> memoryMap;
std::shared_ptr<InterruptHandler> interruptHandler;
public:
void Run(unsigned int steps) {
for (auto &t : threads)
t->Run(steps);
}
};
class InterruptHandler {
std::shared_ptr<FS> fs;
public:
void HandleInterrupt(CPU &cpu, int interrupt_type);
void HandleSyscall(CPU &cpu, int syscall_type);
void HandleWrite(CPU &cpu, int fd, const void *buf, unsigned int len);
// More system calls follow
};
class Kernel final {
std::vector<std::shared_ptr<Process>> processes;
std::vector<InterruptHandler> interruptHandler;
public:
void Run(unsigned int steps) {
for (auto &p : processes)
p->Run(steps);
}
void AddProcess(const std::string &path) {
// opens process, assigns interrupt handler
processes.emplace_back(process);
}
};
The problem here is that the interrupt handler class has to be passed to just about every sub class of the Process
class, including the Process
class. Also, the InterruptClass
seems like it's going to end up containing a lot of the same data as the Kernel
class. I don't like this because I feel like it violates the principle of "don't repeat yourself."
I know there's a better way of designing this but I haven't really figured it out yet. Is the Proxy design pattern applicable to this scenario? What about using signals and slots, similar to Qt and also described here?
I've left out the URL of this project, because I don't want to come off as self promoting. If the code I provided is too incomplete, I'll reference files in the project for more info.
Solution
It is quite obvious that what I'm trying to do here is a design error.
With the dependency graph between the kernel and the CPU/Interpreter class, it is quite obvious that they shouldn't be interacting the way I'm trying to get them to interact. There are two solutions, I've realized, to handle software interrupts from the CPU.
Solution One:
Break up the dependency between the kernel and the CPU/Interpreter. I could have declared the CPU/Interpreter classes as the topmost class in the design. The CPU/Interpreter emulates the system and the kernel is written in the byte code that the CPU/Interpreter emulates. This is similar to the way systems run normally.
Solution Two:
Instead of using CPU interrupt instructions to implement system calls (the way they are usually done), I could use memory mapped input/output. I've already implemented a MemoryMap
class made up of abstract MemorySection
class instances. The only current implementation of the MemorySection
is just a flat memory section. I could derive the MemorySection
class to a memory mapped file system interface, memory allocation interface, etc..
I ended up going with solution two, because it doesn't require hardly any rewriting of the project.
OTHER TIPS
CPU/Interpreters shouldn't belong to Threads/Processes but to Kernel. They are resources that get allocated by the kernel at thread request.