diff --git a/rts/Capability.c b/rts/Capability.c
index a9fe393a08db207bab3a60b1c14dda5ac1ad7fd2..ad6965d93f45f0949919f7937dcf1fc4dad50c97 100644
--- a/rts/Capability.c
+++ b/rts/Capability.c
@@ -1322,7 +1322,7 @@ markCapability (evac_fn evac, void *user, Capability *cap,
     }
 #endif
 
-    markCapabilityIOManager(evac, user, cap->iomgr);
+    markCapabilityIOManager(evac, user, cap);
 
     // Free STM structures for this Capability
     stmPreGCHook(cap);
diff --git a/rts/Capability.h b/rts/Capability.h
index 463e03cf133d14ef843327706bbbcacba8088813..f54eaf97ba9520889deaae18787acd51f5ff8687 100644
--- a/rts/Capability.h
+++ b/rts/Capability.h
@@ -24,7 +24,6 @@
 #include "Task.h"
 #include "Sparks.h"
 #include "sm/NonMovingMark.h" // for MarkQueue
-#include "IOManager.h" // for CapIOManager
 
 #include "BeginPrivate.h"
 
@@ -36,6 +35,15 @@
 #define CAPABILITY_ALIGNMENT 64
 #endif
 
+/* A forward declaration of the per-capability data structures belonging to
+ * the I/O manager. It is opaque and only passed by pointer, so the full
+ * structure definition is not needed. The full definition can be found in
+ * IOManagerInternals.h, which is only used by IOManager.c and the individual
+ * I/O manager implementations.
+ */
+struct _CapIOManager;
+typedef struct _CapIOManager CapIOManager;
+
 /* N.B. This must be consistent with CapabilityPublic in RtsAPI.h */
 struct Capability_ {
     // State required by the STG virtual machine when running Haskell
diff --git a/rts/IOManager.c b/rts/IOManager.c
index e0e4ef04ac2e207446e00926b0a18fe7c6f40182..894a12cf4276d9cae47f8ac16a7733cc5d6652e0 100644
--- a/rts/IOManager.c
+++ b/rts/IOManager.c
@@ -22,6 +22,9 @@
 #include "Schedule.h"
 #include "RtsFlags.h"
 #include "RtsUtils.h"
+#include "sm/Evac.h"
+
+#include "IOManagerInternals.h"
 
 #if defined(IOMGR_ENABLED_SELECT)
 #include "Threads.h"
@@ -475,30 +478,32 @@ void wakeupIOManager(void)
     }
 }
 
-void markCapabilityIOManager(evac_fn       evac,
-                             void         *user,
-                             CapIOManager *iomgr)
+void markCapabilityIOManager(evac_fn evac, void *user, Capability *cap)
 {
-
     switch (iomgr_type) {
 #if defined(IOMGR_ENABLED_SELECT)
         case IO_MANAGER_SELECT:
+        {
+            CapIOManager *iomgr = cap->iomgr;
             evac(user, (StgClosure **)(void *)&iomgr->blocked_queue_hd);
             evac(user, (StgClosure **)(void *)&iomgr->blocked_queue_tl);
             evac(user, (StgClosure **)(void *)&iomgr->sleeping_queue);
             break;
+        }
 #endif
 
 #if defined(IOMGR_ENABLED_WIN32_LEGACY)
         case IO_MANAGER_WIN32_LEGACY:
+        {
+            CapIOManager *iomgr = cap->iomgr;
             evac(user, (StgClosure **)(void *)&iomgr->blocked_queue_hd);
             evac(user, (StgClosure **)(void *)&iomgr->blocked_queue_tl);
             break;
+        }
 #endif
         default:
             break;
     }
-
 }
 
 
@@ -545,18 +550,24 @@ setIOManagerControlFd(uint32_t cap_no, int fd) {
 #endif
 
 
-bool anyPendingTimeoutsOrIO(CapIOManager *iomgr)
+bool anyPendingTimeoutsOrIO(Capability *cap)
 {
     switch (iomgr_type) {
 #if defined(IOMGR_ENABLED_SELECT)
         case IO_MANAGER_SELECT:
-          return (iomgr->blocked_queue_hd != END_TSO_QUEUE)
-              || (iomgr->sleeping_queue   != END_TSO_QUEUE);
+        {
+            CapIOManager *iomgr = cap->iomgr;
+            return (iomgr->blocked_queue_hd != END_TSO_QUEUE)
+                || (iomgr->sleeping_queue   != END_TSO_QUEUE);
+        }
 #endif
 
 #if defined(IOMGR_ENABLED_WIN32_LEGACY)
         case IO_MANAGER_WIN32_LEGACY:
-          return (iomgr->blocked_queue_hd != END_TSO_QUEUE);
+        {
+            CapIOManager *iomgr = cap->iomgr;
+            return (iomgr->blocked_queue_hd != END_TSO_QUEUE);
+        }
 #endif
 
     /* For the purpose of the scheduler, the threaded I/O managers never have
diff --git a/rts/IOManager.h b/rts/IOManager.h
index b54dbd0974d1b8df15841a6c5909657b488744e3..d56f9a94e7448379c80f54ed41e0b4b3cf9a351f 100644
--- a/rts/IOManager.h
+++ b/rts/IOManager.h
@@ -19,10 +19,9 @@
 
 #pragma once
 
-#include "BeginPrivate.h"
-
 #include "sm/GC.h" // for evac_fn
-#include "posix/Select.h" // for LowResTime TODO: switch to normal Time
+
+#include "BeginPrivate.h"
 
 /* The ./configure gives us a set of CPP flags, one for each named I/O manager:
  * IOMGR_BUILD_<name>                : which ones should be built (some)
@@ -156,6 +155,20 @@ extern bool rts_IOManagerIsWin32Native;
 #endif
 
 
+/* The CapIOManager is the per-capability data structure belonging to the I/O
+ * manager. It is defined in full in IOManagerInternals.h. The opaque forward
+ * declaration for it lives in Capability.h, and looks like:
+ *
+ * struct _CapIOManager;
+ * typedef struct _CapIOManager CapIOManager;
+ *
+ * It can be accessed as cap->iomgr.
+ *
+ * The content of the structure is defined conditionally so it is different for
+ * each I/O manager implementation.
+ */
+
+
 /* Parse the I/O manager flag value, returning if is available, unavailable or
  * unrecognised.
  *
@@ -177,42 +190,6 @@ parseIOManagerFlag(const char *iomgrstr, IO_MANAGER_FLAG *flag);
  */
 bool is_io_mng_native_p (void);
 
-/* The per-capability data structures belonging to the I/O manager.
- *
- * It can be accessed as cap->iomgr.
- *
- * The content of the structure is defined conditionally so it is different for
- * each I/O manager implementation.
- *
- * TODO: once the content of this struct is genuinely private, and not shared
- * with other parts of the RTS, then it can be made opaque, so the content is
- * known only to the I/O manager and not the rest of the RTS.
- */
-typedef struct {
-
-#if defined(IOMGR_ENABLED_SELECT)
-    /* Thread queue for threads blocked on I/O completion. */
-    StgTSO *blocked_queue_hd;
-    StgTSO *blocked_queue_tl;
-
-    /* Thread queue for threads blocked on timeouts. */
-    StgTSO *sleeping_queue;
-#endif
-
-#if defined(IOMGR_ENABLED_WIN32_LEGACY)
-    /* Thread queue for threads blocked on I/O completion. */
-    StgTSO *blocked_queue_hd;
-    StgTSO *blocked_queue_tl;
-#endif
-
-#if defined(IOMGR_ENABLED_MIO_POSIX)
-    /* Control FD for the (posix) MIO manager for this capability,
-     */
-    int control_fd;
-#endif
-
-} CapIOManager;
-
 
 /* Init hook: called from hs_init_ghc, early in the startup after the RTS flags
  * have been processed.
@@ -283,7 +260,7 @@ void wakeupIOManager(void);
 
 /* GC hook: mark any per-capability GC roots the I/O manager uses.
  */
-void markCapabilityIOManager(evac_fn evac, void *user, CapIOManager *iomgr);
+void markCapabilityIOManager(evac_fn evac, void *user, Capability *cap);
 
 
 /* GC hook: scavenge I/O related tso->block_info. Used by scavengeTSO.
@@ -324,7 +301,7 @@ void appendToIOBlockedQueue(Capability *cap, StgTSO *tso);
  * This is used by the scheduler as part of deadlock-detection, and the
  * "context switch as often as possible" test.
  */
-bool anyPendingTimeoutsOrIO(CapIOManager *iomgr);
+bool anyPendingTimeoutsOrIO(Capability *cap);
 
 /* If there are any completed I/O operations or expired timers, process the
  * completions as appropriate (which will typically unblock some waiting
diff --git a/rts/IOManagerInternals.h b/rts/IOManagerInternals.h
new file mode 100644
index 0000000000000000000000000000000000000000..a294f28aa4909cc2fada597b4152e7ff45ecf2c3
--- /dev/null
+++ b/rts/IOManagerInternals.h
@@ -0,0 +1,54 @@
+/* -----------------------------------------------------------------------------
+ *
+ * (c) The GHC Team 1998-2020
+ *
+ * Internal type definitions for use within the I/O manager implementations,
+ * but not exposed to the rest of the RTS that calls into the I/O managers.
+ *
+ * In particular this defines the representation of CapIOManager, which is
+ * known only to IOManager.c and each individual I/O manager implementation.
+ *
+ * -------------------------------------------------------------------------*/
+
+#pragma once
+
+#include "IOManager.h"
+
+#include "BeginPrivate.h"
+
+/* The per-capability data structures belonging to the I/O manager.
+ *
+ * It can be accessed as cap->iomgr.
+ *
+ * The content of the structure is defined conditionally so it is different for
+ * each I/O manager implementation.
+ *
+ * Here is where we actually define the representation.
+ */
+struct _CapIOManager {
+
+#if defined(IOMGR_ENABLED_SELECT)
+    /* Thread queue for threads blocked on I/O completion. */
+    StgTSO *blocked_queue_hd;
+    StgTSO *blocked_queue_tl;
+
+    /* Thread queue for threads blocked on timeouts. */
+    StgTSO *sleeping_queue;
+#endif
+
+#if defined(IOMGR_ENABLED_WIN32_LEGACY)
+    /* Thread queue for threads blocked on I/O completion. */
+    StgTSO *blocked_queue_hd;
+    StgTSO *blocked_queue_tl;
+#endif
+
+#if defined(IOMGR_ENABLED_MIO_POSIX)
+    /* Control FD for the (posix) MIO manager for this capability,
+     */
+    int control_fd;
+#endif
+
+};
+
+#include "EndPrivate.h"
+
diff --git a/rts/RtsFlags.c b/rts/RtsFlags.c
index b0c6b13beb1ba85e55c17f81484d4d9b4a1e5497..ed88015e23646e2f60db504286e0ff6c8203f206 100644
--- a/rts/RtsFlags.c
+++ b/rts/RtsFlags.c
@@ -16,6 +16,7 @@
 #include "sm/OSMem.h"
 #include "hooks/Hooks.h"
 #include "Capability.h"
+#include "IOManager.h"
 
 #if defined(HAVE_CTYPE_H)
 #include <ctype.h>
diff --git a/rts/Schedule.c b/rts/Schedule.c
index 1dd903e27a99eed2baadc7ed00c09cdefbc79dce..0f3c737d0c28cd662cb07c954797d58c16a48d4f 100644
--- a/rts/Schedule.c
+++ b/rts/Schedule.c
@@ -405,7 +405,7 @@ schedule (Capability *initialCapability, Task *task)
      */
     if (RtsFlags.ConcFlags.ctxtSwitchTicks == 0 &&
         (!emptyRunQueue(cap) ||
-          anyPendingTimeoutsOrIO(cap->iomgr))) {
+          anyPendingTimeoutsOrIO(cap))) {
         RELAXED_STORE(&cap->context_switch, 1);
     }
 
@@ -932,7 +932,7 @@ scheduleCheckBlockedThreads(Capability *cap USED_IF_NOT_THREADS)
      * awaitCompletedTimeoutsOrIO below for the case of !defined(THREADED_RTS)
      * && defined(mingw32_HOST_OS).
      */
-    if (anyPendingTimeoutsOrIO(cap->iomgr))
+    if (anyPendingTimeoutsOrIO(cap))
     {
         if (emptyRunQueue(cap)) {
             // block and wait
@@ -959,7 +959,7 @@ scheduleDetectDeadlock (Capability **pcap, Task *task)
      * other tasks are waiting for work, we must have a deadlock of
      * some description.
      */
-    if ( emptyRunQueue(cap) && !anyPendingTimeoutsOrIO(cap->iomgr) )
+    if ( emptyRunQueue(cap) && !anyPendingTimeoutsOrIO(cap) )
     {
 #if defined(THREADED_RTS)
         /*
diff --git a/rts/posix/Select.c b/rts/posix/Select.c
index 26da034f0253e3be7b6c4e409c8a853d348cf71f..92c7948e55d6361b5ad084e2612dc4ef2f155b06 100644
--- a/rts/posix/Select.c
+++ b/rts/posix/Select.c
@@ -19,7 +19,7 @@
 #include "RtsUtils.h"
 #include "Capability.h"
 #include "Select.h"
-#include "IOManager.h"
+#include "IOManagerInternals.h"
 #include "Stats.h"
 #include "GetTime.h"
 
diff --git a/rts/posix/Signals.c b/rts/posix/Signals.c
index c21a5c07e599fa3533b500be56e517f8437cc717..b6bfe77f3527f2e22ef01e61a7254df6ea668f05 100644
--- a/rts/posix/Signals.c
+++ b/rts/posix/Signals.c
@@ -19,6 +19,12 @@
 #include "ThreadLabels.h"
 #include "Libdw.h"
 
+/* TODO: eliminate this include. This file should be about signals, not be
+ * part of an I/O manager implementation. The code here that are really part
+ * of an I/O manager should be moved into an appropriate I/O manager impl.
+ */
+#include "IOManagerInternals.h"
+
 #if defined(alpha_HOST_ARCH)
 # if defined(linux_HOST_OS)
 #  include <asm/fpu.h>
diff --git a/rts/win32/AsyncMIO.c b/rts/win32/AsyncMIO.c
index a581c555abe9cc77f833725aba8e7f8ab7764fbb..964914961f086246349e68f3a79e21fc2e854afe 100644
--- a/rts/win32/AsyncMIO.c
+++ b/rts/win32/AsyncMIO.c
@@ -16,6 +16,7 @@
 #include <stdio.h>
 #include "Schedule.h"
 #include "Capability.h"
+#include "IOManagerInternals.h"
 #include "win32/AsyncMIO.h"
 #include "win32/MIOManager.h"