Skip to content

Commit 1309cc0

Browse files
committed
fix(ide): full-polling mode for IDE/ATAPI with scheduler-pump and IRQ hardening
Convert the IDE subsystem from interrupt-driven to full polling: - IDEPacketCommand: poll BSY/DRQ/phase in setup() with transferIn/transferOut and bounded iteration (MAX_PHASE_ITERATIONS = 1<<20) - IDEIdCommand: poll for completion and read data in setup() - IDERWSectorsCommand.pollWait(): unchanged (already polling) - All handleIRQ() methods become no-ops; CTR_NIEN disables device IRQs IDEBus hardening: - resetChannel(): softwareReset() + CTR_NIEN + clear currentCommand (prevents channel running with interrupts enabled after timeout recovery) - executeAndWait(): sleep-poll loop instead of waitUntilFinished() to drive reschedule and dispatch pending interrupts - process(): clear currentCommand on successful setup completion - handleInterrupt(): deassert INTRQ for stray IRQs and no-op handlers - probe(): keep CTR_NIEN after ATAPI identify fallback vm-ints.asm: send EOI at interrupt level for legacy IDE lines (IRQ14/15) to prevent PIC line latching from resume-overrun; other lines keep deferred EOI required by level-triggered PCI devices. VmIsolate: add scheduler-pump thread (user-mode, MIN_PRIORITY, yields continuously) started from VmIsolate.run() covering both shell and installer paths; self-heals if previous pump's isolate exited. IDEConstants: remove unused CTR_IEN, add CTR_NIEN with Javadoc; move IR_CD/IR_IO from IDEPacketCommand to IDEConstants; collapse duplicate section headers and orphaned Javadoc. Tests: add IDEPacketCommandTest (6 tests) with FakeIDEIO double; fix FakeIDEIO.Phase.dataIn() intReason from 0x00 to IR_IO (was causing test hangs by taking the write path). Ref: #613 (diagnosis: kernel-mode IRQ resume-overrun drops completions; polling avoids the dependency on yieldpoints in kernel context but does not fix the assembly-level defects)
1 parent b7d6a19 commit 1309cc0

10 files changed

Lines changed: 766 additions & 129 deletions

File tree

core/src/core/org/jnode/vm/isolate/VmIsolate.java

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
* by the Free Software Foundation; either version 2.1 of the License, or
99
* (at your option) any later version.
1010
*
11-
* This library is distributed in the hope that it will be useful, but
11+
* This library is distributed in the hope that it will be useful, but
1212
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13-
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
13+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1414
* License for more details.
1515
*
1616
* You should have received a copy of the GNU Lesser General Public License
17-
* along with this library; If not, write to the Free Software Foundation, Inc.,
17+
* along with this library; If not, write to the Free Software Foundation, Inc.,
1818
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
1919
*/
20-
20+
2121
package org.jnode.vm.isolate;
2222

2323
import java.io.IOException;
@@ -460,7 +460,7 @@ public final void systemHalt(Isolate isolate, int status) {
460460
private void stopAllThreads() {
461461
// TODO - investigate it
462462
// TODO - this is probably unsafe because any of the threads being killed could
463-
// be in the middle of updating a critical system data structure. I'm also
463+
// be in the middle of updating a critical system data structure. I'm also
464464
// unsure of the order in which we are killing the threads here. It might be
465465
// better to kill the isolate's main thread first to give it the chance to
466466
// do a graceful shutdown. (Stephen Crawley - 2008-11-08)
@@ -484,7 +484,7 @@ private void stopAllThreads() {
484484
doExit();
485485
}
486486
} else {
487-
// TODO - analyze this case
487+
// TODO - analyze this case
488488
doExit();
489489
}
490490
}
@@ -941,11 +941,67 @@ public void run() {
941941
}
942942
}
943943

944+
144 /**
945+
* The scheduler-pump thread (see {@link #startSchedulerPump()}), or
946+
* {@code null} until first started. Volatile; deliberately NOT
947+
* synchronized - the worst case of a racing check-then-act is two
948+
* pump threads briefly coexisting, which is harmless (both are
949+
* daemon threads at minimum priority doing nothing but yield()).
950+
*/
951+
private static volatile Thread schedulerPump;
952+
953+
/**
954+
* Start the scheduler-pump thread.
955+
*
956+
* This thread runs in USER mode and continuously yields: every
957+
* Thread.yield() executes a yield point, which is the only mechanism
958+
* that can run the scheduler's reschedule when every other thread is
959+
* blocked without a timeout. Without it, a fully blocked system (all
960+
* threads waiting on untimed monitors/queues) would never dispatch
961+
* pending device interrupts nor expire timed waits again, because
962+
* the kernel-mode idle thread cannot take yield points (a yield
963+
* point taken in kernel mode terminates the system via
964+
* yieldPointHandler_kernelCode).
965+
*
966+
* IMPORTANT: this must only ever be called from a user-mode (proclet)
967+
* context. VmIsolate.run() qualifies, as it is invoked from
968+
* IsolateThread, a normal java.lang.Thread. Code earlier in boot
969+
* (VmSystem.initialize(), systemReadyForThreadSwitch()) runs in
970+
* kernel context and MUST NOT start this thread.
971+
*
972+
* Historically this was started from CommandShell.run(), which left
973+
* the installer path (org.jnode.install.Main bypasses CommandShell)
974+
* running heavy IDE I/O without any scheduler pumping, deadlocking
975+
* on the first IDE command.
976+
*/
977+
private static void startSchedulerPump() {
978+
final Thread t = schedulerPump;
979+
if ((t != null) && t.isAlive()) {
980+
return;
981+
}
982+
final Thread pump = new Thread(new Runnable() {
983+
public void run() {
984+
while (true) {
985+
Thread.yield();
986+
}
987+
}
988+
}, "scheduler-pump");
989+
pump.setDaemon(true);
990+
pump.setPriority(Thread.MIN_PRIORITY);
991+
pump.start();
992+
schedulerPump = pump;
993+
}
994+
944995
/**
945996
* Run this isolate. This method is called from IsolateThread.
946997
*/
947998
@PrivilegedActionPragma
948999
final void run(IsolateThread thread) {
1000+
// Ensure the scheduler-pump exists. Started here rather than in
1001+
// CommandShell so that every user-mode entry path (shell AND
1002+
// installer) is covered; self-heals if the isolate that hosted
1003+
// the previous pump has exited.
1004+
startSchedulerPump();
9491005
try {
9501006
// Set current
9511007
IsolatedStaticData.current = VmIsolate.this;
@@ -954,7 +1010,7 @@ final void run(IsolateThread thread) {
9541010
VmSystem.setOut(thread.getStdout());
9551011
VmSystem.setErr(thread.getStderr());
9561012
VmSystem.setIn(thread.getStdin());
957-
1013+
9581014
// Set the isolate's properties to a copy of the initial properties passed
9591015
// when the isolate was created. (This needs to be done really early
9601016
// via the IOContext switch to avoid NPEs when the native compiler,

core/src/native/x86/vm-ints.asm

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -379,20 +379,57 @@ timesliceHandler:
379379
; -----------------------------------------------
380380
; Handle an IRQ interrupt
381381
; -----------------------------------------------
382-
def_irq_handler:
383-
cmp GET_OLD_CS,USER_CS
384-
jne def_irq_kernel
385-
; Increment the appropriate IRQ counter and set threadSwitch indicator.
382+
; Count the IRQ, set the threadSwitch indicator and acknowledge it on the
383+
; PIC(s). Shared by the user-mode and kernel-mode interrupt paths.
384+
; ABP must point to the interrupt frame ([ABP+INTNO] = irq number).
385+
; The Java-level handler (and the old EOI in IRQThread.doHandle) run at
386+
; some arbitrary later reschedule; leaving the level in-service until then
387+
; can block lower-priority IRQ lines indefinitely, so EOI is sent here.
388+
def_irq_count_eoi:
386389
mov AAX,[ABP+INTNO]
387390
mov ADI,IRQCOUNT
388391
inc dword [ADI+AAX*4+(VmArray_DATA_OFFSET*SLOT_SIZE)]
389392
; Set thread switch indicator
390393
or THREADSWITCHINDICATOR, VmProcessor_TSI_SWITCH_NEEDED
394+
;
395+
; Send the EOI right away ONLY for the edge-triggered legacy IDE
396+
; lines (IRQ14 primary / IRQ15 secondary): their completion events
397+
; were historically dropped by the resume-overrun logic, leaving
398+
; commands waiting forever. Every other line keeps the original
399+
; deferred EOI (done in IRQThread.doHandle), which is required for
400+
; level-triggered PCI lines (e.g. PCnet) whose interrupt condition
401+
; is only cleared by the device driver itself.
402+
cmp dword [ABP+INTNO],14
403+
je def_irq_eoi_ide
404+
cmp dword [ABP+INTNO],15
405+
je def_irq_eoi_ide
406+
ret
407+
def_irq_eoi_ide:
408+
mov ADX,[ABP+INTNO]
409+
and ADX,1 ; 14 -> 0 (master), 15 -> 1 (slave)
410+
add ADX,0x60
411+
mov al,dl
412+
cmp dword [ABP+INTNO],14
413+
jb def_irq_eoi_master
414+
out 0xA0,al ; specific EOI on the slave PIC
415+
mov al,0x62 ; EOI the cascade line (IRQ2) on the master
416+
def_irq_eoi_master:
417+
out 0x20,al
418+
ret
419+
420+
def_irq_handler:
421+
cmp GET_OLD_CS,USER_CS
422+
jne def_irq_kernel
423+
call def_irq_count_eoi
391424
; Done
392425
ret
393-
426+
394427
def_irq_kernel:
395-
PRINT_STR irq_kernel_msg
428+
; NOTE: the historical PRINT_STR irq_kernel_msg diagnostic was
429+
; removed deliberately - it writes to the VGA text buffer from
430+
; interrupt context and would flood during IRQ storms. The event
431+
; is still counted (and now EOI'd for IDE lines) below.
432+
call def_irq_count_eoi
396433
ret
397434
398435
; -----------------------------------------------

fs/build-tests.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ help Output these messages
3838
<classpath refid="cp-test"/>
3939
<formatter type="xml"/>
4040
<test name="org.jnode.test.driver.bus.ide.IDEDriveDescriptorTest" todir="${basedir}/build/reports/junit"/>
41+
<test name="org.jnode.driver.bus.ide.command.IDEPacketCommandTest" todir="${basedir}/build/reports/junit"/>
4142
<test name="org.jnode.test.fs.filesystem.FSTestSuite" todir="${basedir}/build/reports/junit"/>
4243
<test name="org.jnode.test.fs.driver.tests.BlockDeviceAPITest" todir="${basedir}/build/reports/junit"/>
4344
<test name="org.jnode.test.fs.command.SyncCommandTest" todir="${basedir}/build/reports/junit"/>

fs/src/driver/org/jnode/driver/bus/ide/IDEBus.java

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ protected IDEBus(Device parent, boolean primary)
109109
irqRes = rm.claimIRQ(parent, io.getIrq(), this, true);
110110
// Reset the controller
111111
softwareReset();
112+
// Run the IDE channels entirely interrupt-free: every command
113+
// polls for completion (see the command classes). This removes
114+
// the whole class of lost/misrouted completion interrupts.
115+
io.setControlReg(CTR_NIEN);
112116
// Create and start the queue processor
113117
queueProcessor = new QueueProcessorThread<IDECommand>(name, commandQueue, this);
114118
queueProcessor.start();
@@ -128,6 +132,13 @@ public void execute(IDECommand command) {
128132
* Add the given command to the queue of commands to be executed and wait
129133
* for the command to finish.
130134
*
135+
* The wait is implemented as a sleep-poll loop on purpose: every
136+
* Thread.sleep suspends this thread, which runs a scheduler reschedule,
137+
* which in turn dispatches pending device interrupts
138+
* (IRQManager.dispatchInterrupts). Relying on Object.wait-notify alone
139+
* can deadlock on a fully blocked system, where nothing else ever
140+
* triggers a reschedule to deliver the interrupt that would notify us.
141+
*
131142
* @param command
132143
* @param timeout Maximum time to wait
133144
* @throws InterruptedException
@@ -136,7 +147,13 @@ public void execute(IDECommand command) {
136147
public void executeAndWait(IDECommand command, long timeout)
137148
throws InterruptedException, TimeoutException {
138149
execute(command);
139-
command.waitUntilFinished(timeout);
150+
final long deadline = System.currentTimeMillis() + timeout;
151+
while (!command.isFinished()) {
152+
Thread.sleep(20);
153+
if (!command.isFinished() && System.currentTimeMillis() >= deadline) {
154+
throw new TimeoutException("timeout");
155+
}
156+
}
140157
}
141158

142159
/**
@@ -156,9 +173,21 @@ public void stop() {
156173
public void handleInterrupt(int irq) {
157174
final IDECommand cmd = currentCommand;
158175
//log.debug("IDE IRQ " + irq + " cmd=" + cmd);
159-
if (cmd != null) {
176+
if (cmd == null) {
177+
// Stray interrupt with no command in flight. Read the Status
178+
// register to de-assert INTRQ: leaving the level asserted makes
179+
// level-triggered lines (e.g. PCI IDE IRQ14/15) re-fire forever.
180+
io.getStatusReg();
181+
if (log.isDebugEnabled()) {
182+
log.debug("Unknown IDE IRQ " + irq + " status 0x" + NumberUtils.hex(io.getAltStatusReg(), 2));
183+
}
184+
} else {
160185
try {
161186
cmd.handleIRQ(this, io);
187+
// Acknowledge any interrupt level the handler did not
188+
// consume (e.g. no-op handlers for polling commands);
189+
// an unquenchable level would otherwise refire forever.
190+
io.getStatusReg();
162191
if (cmd.isFinished()) {
163192
this.currentCommand = null;
164193
}
@@ -167,8 +196,6 @@ public void handleInterrupt(int irq) {
167196
this.currentCommand = null;
168197
cmd.setError(ERR_ABORT);
169198
}
170-
} else if (log.isDebugEnabled()) {
171-
log.debug("Unknown IDE IRQ " + irq + " status 0x" + NumberUtils.hex(io.getAltStatusReg(), 2));
172199
}
173200
}
174201

@@ -189,8 +216,8 @@ public IDEDriveDescriptor probe(boolean master) throws InterruptedException {
189216
return null;
190217
}
191218

192-
// Interrupts enabled
193-
io.setControlReg(CTR_BLANK);
219+
// Interrupts stay disabled (full polling mode)
220+
io.setControlReg(CTR_NIEN);
194221

195222
// First try a normal IDE Identify command
196223
IDEIdCommand cmd = new IDEIdCommand(primary, master, false);
@@ -223,8 +250,8 @@ public IDEDriveDescriptor probe(boolean master) throws InterruptedException {
223250

224251
// Clear any interrupts
225252
io.getStatusReg();
226-
// Interrupts enabled
227-
io.setControlReg(CTR_BLANK);
253+
// Interrupts stay disabled (full polling mode)
254+
io.setControlReg(CTR_NIEN);
228255

229256
// IDE Identify failed, do an ATAPI Identify
230257
cmd = new IDEIdCommand(primary, master, true);
@@ -247,6 +274,27 @@ public IDEDriveDescriptor probe(boolean master) throws InterruptedException {
247274
}
248275
}
249276

277+
/**
278+
* Reset both devices on this channel and clear any pending state.
279+
* Used to recover from commands that timed out and may have left the
280+
* device (emulator) with a latched interrupt or pending data phase.
281+
*
282+
* NOTE: there is an inherent, narrow race against the queue processor
283+
* thread assigning {@link #currentCommand} in {@link #process()}.
284+
* Callers must only invoke this method after the command they waited
285+
* for has timed out AND must expect that a concurrently queued command
286+
* may be aborted by the software reset; the retry issued by the caller
287+
* re-serializes execution afterwards.
288+
*/
289+
public void resetChannel() {
290+
softwareReset();
291+
// Re-disable interrupts after the reset; softwareReset() leaves
292+
// the channel with CTR_BLANK (interrupts enabled) which is not
293+
// what polling-mode code expects.
294+
io.setControlReg(CTR_NIEN);
295+
this.currentCommand = null;
296+
}
297+
250298
protected void softwareReset() {
251299
// Set reset
252300
io.setControlReg(CTR_SRST);
@@ -381,6 +429,7 @@ public void process(IDECommand cmd) /*throws Exception*/ {
381429
try {
382430
io.getStatusReg(); // Flush any pending IRQ
383431
cmd.setup(IDEBus.this, io);
432+
this.currentCommand = null;
384433
} catch (TimeoutException ex) {
385434
log.error("Timeout in setup of " + cmd + ": " + ex.getMessage());
386435
if ((io.getAltStatusReg() & ST_ERROR) != 0) {

fs/src/driver/org/jnode/driver/bus/ide/IDEConstants.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,33 @@ public interface IDEConstants {
172172
// Control bits
173173

174174
/**
175-
* Default control value
175+
* Default control value (interrupts enabled, no reset)
176176
*/
177177
public static final int CTR_BLANK = 0x00;
178+
178179
/**
179-
* Interrupt enable (0==enabled, 1==disabled)
180+
* Device control register bit 1 (nIEN): when SET, interrupt requests
181+
* from the drives on this channel are disabled. Despite its name, the
182+
* historical constant CTR_IEN carried this same (inverted) value; it
183+
* was unused and has been removed in favour of this correctly named
184+
* constant.
180185
*/
181-
public static final int CTR_IEN = 0x02;
186+
public static final int CTR_NIEN = 0x02;
187+
182188
/**
183189
* Software reset (1==reset, 0==reset finished)
184190
*/
185191
public static final int CTR_SRST = 0x04;
186192

193+
// --------------------------------
194+
// Interrupt Reason bits (sector count register)
195+
196+
/** Interrupt Reason bit 0 (CoD): transfer is a command, not data. */
197+
public static final int IR_CD = 0x01;
198+
199+
/** Interrupt Reason bit 1 (IO): transfer direction is device-to-host. */
200+
public static final int IR_IO = 0x02;
201+
187202
// --------------------------------
188203
// Timeout
189204

@@ -204,16 +219,13 @@ public interface IDEConstants {
204219
*/
205220
public static final int MAX_SECTOR_COUNT_48 = 65536;
206221

207-
// --------------------------------
208-
// IDE sector maximum addresses
209-
210222
/**
211-
* Maximum sector for 28 bit addresses
223+
* Maximum sector number for 28 bit addresses
212224
*/
213225
public static final long MAX_SECTOR_28 = 0xfffffffL;
214226

215227
/**
216-
* Maximum sector for 48 bit addresses
228+
* Maximum sector number for 48 bit addresses
217229
*/
218230
public static final long MAX_SECTOR_48 = 0xfffffffffffffL;
219231

0 commit comments

Comments
 (0)