2

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.

  • 2
    "The Kernel class cannot actually see the CPU class directly, ..." - this is the opposite of how you'd expect an OS to actually work. The kernel is responsible for telling each CPU what thread (userspace or otherwise) to run, and for handling interrupts. You're missing the whole scheduler layer. – Useless May 27 '18 at 10:13
  • 2
    @useless I think CPU is a very misleading term to use here. From what i understood it's more an execution environment of a (byte code interpreting) virtual machine. The "CPU" needs the kernel to perform important operations here. – Daniel Jour May 27 '18 at 10:46
  • @DanielJour you're right, CPU is kind of misleading. I believe Interpreter might be a better name. – tay10r May 27 '18 at 12:21
  • @useless are you saying I shouldn't use the whole process, thread, CPU hierarchy? – tay10r May 27 '18 at 12:22
  • 2
    And yet, using a scheduler the way a real OS does will break the problematic dependency chain up nicely. The kernel tells a "CPU" which "thread" to run, instead of asking a thread to execute itself on a CPU and also tunnel interrupt state. – Useless May 27 '18 at 17:51
0

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.

0

CPU/Interpreters shouldn't belong to Threads/Processes but to Kernel. They are resources that get allocated by the kernel at thread request.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.