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.

Was it helpful?

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.

Licensed under: CC-BY-SA with attribution
scroll top