If you're implementing select
, then presumably you (or your colleagues) are implementing a significant part of the OS interface. So, it depends how you're implementing the I/O subsystem, and the scheduler.
If you're not implementing the scheduler, then you need some kind of "control centre" which uses synchronisation primitives (probably condition variables) to track changes in the states of the file descriptors you're interested in.
After initially checking the current state of the file descriptors listed, fundamentally, how select
works is that it doesn't return until the timeout expires, or one of the descriptors it cares about changes state (in a way that it cares about). This could be handled entirely in the scheduler, so that the thread is unavailable for scheduling until the required conditions occur, or the thread could block until some change occurs that it might be interested in, check whether it really is interested, and then either return if it is, or block again if it isn't. At the extreme of this approach, a completely dumb (and inefficient) implementation could have a single condition variable that every caller to select
waits on, and which is broadcast by the I/O system every time a state change occurs (as Potatoswatter points out, you also have to record the state).
If you're not implementing the I/O subsystem, then the only way to implement select
is on top of some other, similar primitive which your I/O subsystem does support. Alternatively, take on the task of implementing I/O: wrap everything in your own layer, and make select
available only through that layer. The implementation of this layer depends on the underlying API, but if all else fails you can create one thread per file descriptor, do blocking I/O ops in that thread, and then notify your "control centre" when the fd is readable, writable, or has errors.
Get the design right first, which probably means studying how other OSes do it. Otherwise you will spend the next 2 years fixing race conditions.