But what about this scenario:
That scenario is broken ("issue_hlt();" should not be used here).
First, consider the simpler case of asynchronous/non-blocking IO. A task asks the kernel to transfer some data to/from disk, the request gets put on some kind of queue, and the kernel returns back to the task. Eventually (possibly after many other requests for other tasks are finished) the request makes its way to the head of the disk driver's queue and is started, then finished. When the request is finished you send some kind of notification back to the task that made the request. This is simpler because it doesn't have anything (directly) to do with task switches or the scheduler or any timer.
For synchronous/blocking IO, it's similar. A task asks the kernel to transfer some data to/from disk (same as non-blocking), the request gets put on some kind of queue (same as non-blocking), eventually the request makes its way to the head of the disk driver's queue and is started then finished (same as non-blocking). The only difference is that after making the request you ask the scheduler to make the task block (so it gets no CPU time), and when the request is completed you unblock the task (instead of sending some sort of notification back to the task).
Notice that nothing I mentioned above (for both non-blocking and blocking) involves any "issue_hlt();".
Now, assume that your scheduler has some kind of "block_task()" function. When this is called the scheduler tries to find some other task to give CPU time. If there are no other tasks that want the CPU, then the CPU is idle. This is where "issue_hlt();" would be used - to wait until any IRQ handler to unblock a task. It doesn't matter if the IRQ is a timer IRQ where the IRQ handler unblocks a task that called "sleep()", or if the IRQ is a disk controller IRQ where the IRQ handler unblocks a task that was waiting for disk IO to complete.
The scheduler itself might use a timer to determine when the currently running task has used all the time it was given. This has nothing to do with "issue_hlt();" because "issue_hlt();" is only ever used (by the scheduler) when there is no task running
Also note that "no task running" is important for power management, and the "issue_hlt();" is like a temporary placeholder. Eventually you'll want to use a simple loop for a brief amount of time (in the hope that a task unblocks very soon), then after being idle for a little while drop the CPU speed down and do "issue_hlt();" for a while, then after being idle for even longer drop the CPU speed down even more and do "issue_hlt();". Eventually, after CPU has been idle for long enough, you might put the CPU into a very deep sleep state (possibly including "effectively turned off" where you have to do the "INIT-SIPI-SIPI" sequence to get it back online). For power management it can also be nice to have some look-ahead - for example, check if a task that called "sleep()" will wake up soon before you decide to drop the CPU speed down (and if a task will wake up soon, leave the CPU speed how it is, or maybe increase CPU speed instead of reducing CPU speed).Note: Before power management mattered; some OSs (including mine) used an "idle task" that never blocks. This made it easier to write the scheduler because the "no task running" was impossible. However, it's extremely difficult to implement power management properly if you have an "idle task" (you end up with a huge mess of race conditions, etc) so I'd recommend not using an "idle task" (even if you have no intention of supporting power management yet).