Browse Source

Implement event loop timers.

GObjects can now register a timer with the GEventLoop. This will eventually
cause GTimerEvents to be dispatched to the GObject.

This needed a few supporting changes in the kernel:

- The PIT now ticks 1000 times/sec.
- select() now supports an arbitrary timeout.
- gettimeofday() now returns something in the tv_usec field.

With these changes, the clock window in guitest2 finally ticks on its own.
Andreas Kling 6 years ago
parent
commit
95c3442d59
10 changed files with 140 additions and 7 deletions
  1. 5 0
      AK/HashMap.h
  2. 7 3
      Kernel/Process.cpp
  3. 2 0
      Kernel/Process.h
  4. 11 1
      Kernel/Scheduler.cpp
  5. 10 0
      Kernel/i8253.cpp
  6. 4 1
      Kernel/i8253.h
  7. 6 1
      LibGUI/GEvent.h
  8. 70 1
      LibGUI/GEventLoop.cpp
  9. 21 0
      LibGUI/GEventLoop.h
  10. 4 0
      LibGUI/GObject.cpp

+ 5 - 0
AK/HashMap.h

@@ -83,6 +83,11 @@ public:
         return find(key) != end();
     }
 
+    void remove(IteratorType it)
+    {
+        m_table.remove(it);
+    }
+
 private:
     HashTable<Entry, EntryTraits> m_table;
 };

+ 7 - 3
Kernel/Process.cpp

@@ -1442,7 +1442,7 @@ int Process::sys$gettimeofday(timeval* tv)
     InterruptDisabler disabler;
     auto now = RTC::now();
     tv->tv_sec = now;
-    tv->tv_usec = 0;
+    tv->tv_usec = PIT::ticks_since_boot() % 1000;
     return 0;
 }
 
@@ -1976,8 +1976,12 @@ int Process::sys$select(const Syscall::SC_select_params* params)
     // FIXME: Implement exceptfds support.
     ASSERT(!exceptfds);
 
-    // FIXME: Implement timeout support.
-    ASSERT(!timeout || (!timeout->tv_sec && !timeout->tv_usec));
+    if (timeout) {
+        m_select_timeout = *timeout;
+        m_select_has_timeout = true;
+    } else {
+        m_select_has_timeout = false;
+    }
 
     if (nfds < 0)
         return -EINVAL;

+ 2 - 0
Kernel/Process.h

@@ -331,6 +331,8 @@ private:
     int m_blocked_fd { -1 };
     Vector<int> m_select_read_fds;
     Vector<int> m_select_write_fds;
+    timeval m_select_timeout;
+    bool m_select_has_timeout { false };
     size_t m_max_open_file_descriptors { 16 };
     SignalActionData m_signal_action_data[32];
     dword m_pending_signals { 0 };

+ 11 - 1
Kernel/Scheduler.cpp

@@ -1,6 +1,8 @@
 #include "Scheduler.h"
 #include "Process.h"
 #include "system.h"
+#include "RTC.h"
+#include "i8253.h"
 
 //#define LOG_EVERY_CONTEXT_SWITCH
 //#define SCHEDULER_DEBUG
@@ -29,7 +31,7 @@ bool Scheduler::pick_next()
     }
 
     // Check and unblock processes whose wait conditions have been met.
-    Process::for_each([] (auto& process) {
+    Process::for_each([] (Process& process) {
         if (process.state() == Process::BlockedSleep) {
             if (process.wakeup_time() <= system.uptime)
                 process.unblock();
@@ -71,6 +73,14 @@ bool Scheduler::pick_next()
                 process.unblock();
                 return true;
             }
+            if (process.m_select_has_timeout) {
+                auto now_sec = RTC::now();
+                auto now_usec = PIT::ticks_since_boot() % 1000;
+                if (now_sec > process.m_select_timeout.tv_sec || (now_sec == process.m_select_timeout.tv_sec && now_usec >= process.m_select_timeout.tv_usec)) {
+                    process.unblock();
+                    return true;
+                }
+            }
             for (int fd : process.m_select_read_fds) {
                 if (process.m_fds[fd].descriptor->can_read(process)) {
                     process.unblock();

+ 10 - 0
Kernel/i8253.cpp

@@ -57,16 +57,26 @@ asm(
 
 #define BASE_FREQUENCY     1193182
 
+static dword s_ticks_since_boot;
+
 void timer_interrupt_handler(RegisterDump& regs)
 {
     IRQHandlerScope scope(IRQ_TIMER);
+    ++s_ticks_since_boot;
     Scheduler::timer_tick(regs);
 }
 
 namespace PIT {
 
+dword ticks_since_boot()
+{
+    return s_ticks_since_boot;
+}
+
 void initialize()
 {
+    s_ticks_since_boot = 0;
+
     word timer_reload;
 
     IO::out8(PIT_CTL, TIMER0_SELECT | WRITE_WORD | MODE_SQUARE_WAVE);

+ 4 - 1
Kernel/i8253.h

@@ -1,9 +1,12 @@
 #pragma once
 
-#define TICKS_PER_SECOND          600
+#include <AK/Types.h>
+
+#define TICKS_PER_SECOND          1000
 
 namespace PIT {
 
 void initialize();
+dword ticks_since_boot();
 
 }

+ 6 - 1
LibGUI/GEvent.h

@@ -131,7 +131,12 @@ private:
 
 class GTimerEvent final : public GEvent {
 public:
-    GTimerEvent() : GEvent(GEvent::Timer) { }
+    explicit GTimerEvent(int timer_id) : GEvent(GEvent::Timer), m_timer_id(timer_id) { }
     ~GTimerEvent() { }
+
+    int timer_id() const { return m_timer_id; }
+
+private:
+    int m_timer_id;
 };
 

+ 70 - 1
LibGUI/GEventLoop.cpp

@@ -6,6 +6,7 @@
 #include <LibC/stdio.h>
 #include <LibC/fcntl.h>
 #include <LibC/string.h>
+#include <LibC/time.h>
 #include <LibC/sys/select.h>
 #include <LibC/gui.h>
 
@@ -137,11 +138,29 @@ void GEventLoop::wait_for_event()
     FD_ZERO(&rfds);
     FD_SET(m_event_fd, &rfds);
     struct timeval timeout = { 0, 0 };
-    int rc = select(m_event_fd + 1, &rfds, nullptr, nullptr, m_queued_events.is_empty() ? nullptr : &timeout);
+    if (!m_timers.is_empty())
+        get_next_timer_expiration(timeout);
+    int rc = select(m_event_fd + 1, &rfds, nullptr, nullptr, (m_queued_events.is_empty() && m_timers.is_empty()) ? nullptr : &timeout);
     if (rc < 0) {
         ASSERT_NOT_REACHED();
     }
 
+    for (auto& it : m_timers) {
+        auto& timer = *it.value;
+        if (!timer.has_expired())
+            continue;
+#ifdef GEVENTLOOP_DEBUG
+        dbgprintf("GEventLoop: Timer %d has expired, sending GTimerEvent to %p\n", timer.timer_id, timer.owner);
+#endif
+        post_event(timer.owner, make<GTimerEvent>(timer.timer_id));
+        if (timer.should_reload) {
+            timer.reload();
+        } else {
+            // FIXME: Support removing expired timers that don't want to reload.
+            ASSERT_NOT_REACHED();
+        }
+    }
+
     if (!FD_ISSET(m_event_fd, &rfds))
         return;
 
@@ -180,3 +199,53 @@ void GEventLoop::wait_for_event()
         }
     }
 }
+
+bool GEventLoop::EventLoopTimer::has_expired() const
+{
+    timeval now;
+    gettimeofday(&now, nullptr);
+    return now.tv_sec > fire_time.tv_sec || (now.tv_sec == fire_time.tv_sec && now.tv_usec >= fire_time.tv_usec);
+}
+
+void GEventLoop::EventLoopTimer::reload()
+{
+    gettimeofday(&fire_time, nullptr);
+    fire_time.tv_sec += interval / 1000;
+    fire_time.tv_usec += interval % 1000;
+}
+
+void GEventLoop::get_next_timer_expiration(timeval& soonest)
+{
+    ASSERT(!m_timers.is_empty());
+    bool has_checked_any = false;
+    for (auto& it : m_timers) {
+        auto& fire_time = it.value->fire_time;
+        if (!has_checked_any || fire_time.tv_sec < soonest.tv_sec || (fire_time.tv_sec == soonest.tv_sec && fire_time.tv_usec < soonest.tv_usec))
+            soonest = fire_time;
+        has_checked_any = true;
+    }
+}
+
+int GEventLoop::register_timer(GObject& object, int milliseconds, bool should_reload)
+{
+    ASSERT(milliseconds >= 0);
+    auto timer = make<EventLoopTimer>();
+    timer->owner = &object;
+    timer->interval = milliseconds;
+    timer->reload();
+    timer->should_reload = should_reload;
+    int timer_id = ++m_next_timer_id;  // FIXME: This will eventually wrap around.
+    ASSERT(timer_id); // FIXME: Aforementioned wraparound.
+    timer->timer_id = timer_id;
+    m_timers.set(timer->timer_id, move(timer));
+    return timer_id;
+}
+
+bool GEventLoop::unregister_timer(int timer_id)
+{
+    auto it = m_timers.find(timer_id);
+    if (it == m_timers.end())
+        return false;
+    m_timers.remove(it);
+    return true;
+}

+ 21 - 0
LibGUI/GEventLoop.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include "GEvent.h"
+#include <AK/HashMap.h>
 #include <AK/OwnPtr.h>
 #include <AK/Vector.h>
 
@@ -23,6 +24,9 @@ public:
 
     bool running() const { return m_running; }
 
+    int register_timer(GObject&, int milliseconds, bool should_reload);
+    bool unregister_timer(int timer_id);
+
 private:
     void wait_for_event();
     void handle_paint_event(const GUI_Event&, GWindow&);
@@ -30,6 +34,8 @@ private:
     void handle_key_event(const GUI_Event&, GWindow&);
     void handle_window_activation_event(const GUI_Event&, GWindow&);
 
+    void get_next_timer_expiration(timeval&);
+
     struct QueuedEvent {
         GObject* receiver { nullptr };
         OwnPtr<GEvent> event;
@@ -38,4 +44,19 @@ private:
 
     int m_event_fd { -1 };
     bool m_running { false };
+
+    int m_next_timer_id { 1 };
+
+    struct EventLoopTimer {
+        int timer_id { 0 };
+        int interval { 0 };
+        timeval fire_time;
+        bool should_reload { false };
+        GObject* owner { nullptr };
+
+        void reload();
+        bool has_expired() const;
+    };
+
+    HashMap<int, OwnPtr<EventLoopTimer>> m_timers;
 };

+ 4 - 0
LibGUI/GObject.cpp

@@ -60,12 +60,16 @@ void GObject::start_timer(int ms)
         dbgprintf("GObject{%p} already has a timer!\n", this);
         ASSERT_NOT_REACHED();
     }
+
+    m_timer_id = GEventLoop::main().register_timer(*this, ms, true);
 }
 
 void GObject::stop_timer()
 {
     if (!m_timer_id)
         return;
+    bool success = GEventLoop::main().unregister_timer(m_timer_id);
+    ASSERT(success);
     m_timer_id = 0;
 }