You are working on an Operating System, it has io-wait (epoll, poll, select) primitive, but will only allow you to wait on one type of stream/file at a time. However it does have threads. Using threads could work but having lots of threads will lead to the normal problems of concurrency. There is no other value of having threads, as you have only one CPU core. You wish you had io-wait, and could implement the code as an event queue.
Therefore,
Put threads at IO boundaries, have these threads wait on one channel each. Communicate with the main thread using pipes. The boundary threads do nothing more that convert IO into piped data. The main thread can then just wait on pipes. All the application logic goes in the main thread.
Each boundary thread, should be input or output, and for one device. It should have no business logic. It should just forward from a pipe to an output, or from an input to a pipe.