Page 1 of 1

How do OS's do things like sleep

Posted: Tue Jul 05, 2022 12:41 pm
by RayanMargham
Alright so I've been very confused with this, I want to do things like sleep() and stuff and well I'm not sure how, I get there are things like the local apic timer and stuff but i'm confused on how they do things like sleep, how to setup them up etc.

What I've heard is the most accurate way of using a timer is calibrating the local apic timer with the HPET or something

I'm still confused on this and there aren't many resources on this anyone able to help?

Re: How do OS's do things like sleep

Posted: Wed Jul 06, 2022 8:57 am
by nullplan
There is quite a lot to unpack here, but I'll try to be brief.

First, you need a timer API. One that is independent of the actual timer in use, so you can abstract that away. My approach was something like this:

Code: Select all

struct timer {
  uint64_t deadline;
  void (*cb)(void*);
  void *arg;
  size_t idx;
};

int register_timer(struct timer*, const struct timespec*);
int deregister_timer(struct timer*);
Pointers to the timers get registered in a priority queue. register_timer() fills out the deadline and idx elements, the priority queue contains pointers to struct timer, and is ordered as min-heap by deadline.

The timer driver itself will then always look at the smallest deadline, figure out how long that is away from now, and tell the timer hardware to generate an interrupt at that time. This is done each time a timer is registered, then the CPU goes to sleep (with a "hlt" instruction) and then the interrupt will wake it again. Then all timers whose deadlines have passed will be executed from the interrupt handler. So far so simple.

Now, if some task wants to sleep for a time, all you have to do is mark the thread as sleeping for the scheduler, then register a timer that will mark the thread as runnable again when it is up.

Code: Select all

static void wake_task(void* p)
{
  mark_task_runnable(p);
}

int sys_sleep(int seconds) {
  struct timer t = {0, wake_task, current_task()};
  int r = register_timer(&t, &(struct timespec){seconds, 0});
  if (r == 0) {
    mark_task_sleeping(current_task());
    schedule();
  }
  deregister_timer(&t);
  if (r)
    mark_task_runnable(current_task());
  if (r == 0 && (current_task()->flags & TIF_SIGPENDING))
    r = -EINTR;
  return r;
}
So we register the timer, then go to sleep, then deregister it, because it might be that we were awoken by a signal. If that is the case, the syscall exit code will notice and do the right thing.

In some OS textbooks you will find the notion of a "system tick". It used to be pretty common to regularly interrupt the program flow to rotate the active task, update the system time, &c. I no longer do that. I will order a timer interrupt if I need one, but otherwise am happy to just let tasks run uninterrupted if there are fewer runnable tasks than available CPUs. Why should I interrupt them? There is nothing else to do! System time can be kept just as well by hardware, in particular the TSC.

As for the actual timer driver, yes, that is when you need to look up how HPET, LAPIC timer or the PIT work. I have a framework like this:

Code: Select all

struct hardware_timer {
  uint64_t (*deadline_from_ts)(const struct timespec*);
  uint64_t (*current_time)(void);
  int (*request_interrupt)(uint64_t);
  int (*abort_interrupt)(void);
};
And these can all be implemented for all the different timers a PC can have.