- Rewrite kernel timer implementation to use Windows 2003's hash-based table timer...
[reactos.git] / reactos / ntoskrnl / ke / dpc.c
1 /*
2 * PROJECT: ReactOS Kernel
3 * LICENSE: GPL - See COPYING in the top level directory
4 * FILE: ntoskrnl/ke/dpc.c
5 * PURPOSE: Deferred Procedure Call (DPC) Support
6 * PROGRAMMERS: Alex Ionescu (alex.ionescu@reactos.org)
7 * Philip Susi (phreak@iag.net)
8 * Eric Kohl (ekohl@abo.rhein-zeitung.de)
9 */
10
11 /* INCLUDES ******************************************************************/
12
13 #include <ntoskrnl.h>
14 #define NDEBUG
15 #include <debug.h>
16
17 /* GLOBALS *******************************************************************/
18
19 ULONG KiMaximumDpcQueueDepth = 4;
20 ULONG KiMinimumDpcRate = 3;
21 ULONG KiAdjustDpcThreshold = 20;
22 ULONG KiIdealDpcRate = 20;
23 BOOLEAN KeThreadDpcEnable;
24 FAST_MUTEX KiGenericCallDpcMutex;
25
26 /* PRIVATE FUNCTIONS *********************************************************/
27
28 VOID
29 NTAPI
30 KiTimerExpiration(IN PKDPC Dpc,
31 IN PVOID DeferredContext,
32 IN PVOID SystemArgument1,
33 IN PVOID SystemArgument2)
34 {
35 LARGE_INTEGER SystemTime, InterruptTime, Interval;
36 LONG Limit, Index, i;
37 ULONG Timers, ActiveTimers, DpcCalls;
38 PLIST_ENTRY ListHead, NextEntry;
39 PKTIMER_TABLE_ENTRY TimerEntry;
40 KIRQL OldIrql;
41 PKTIMER Timer;
42 PKDPC TimerDpc;
43 ULONG Period;
44 DPC_QUEUE_ENTRY DpcEntry[MAX_TIMER_DPCS];
45 PKSPIN_LOCK_QUEUE LockQueue;
46
47 /* Disable interrupts */
48 _disable();
49
50 /* Query system and interrupt time */
51 KeQuerySystemTime(&SystemTime);
52 InterruptTime.QuadPart = KeQueryInterruptTime();
53 Limit = KeTickCount.LowPart;
54
55 /* Bring interrupts back */
56 _enable();
57
58 /* Get the index of the timer and normalize it */
59 Index = PtrToLong(SystemArgument1);
60 if ((Limit - Index) >= TIMER_TABLE_SIZE)
61 {
62 /* Normalize it */
63 Limit = Index + TIMER_TABLE_SIZE - 1;
64 }
65
66 /* Setup index and actual limit */
67 Index--;
68 Limit &= (TIMER_TABLE_SIZE - 1);
69
70 /* Setup accounting data */
71 DpcCalls = 0;
72 Timers = 24;
73 ActiveTimers = 4;
74
75 /* Lock the Database and Raise IRQL */
76 OldIrql = KiAcquireDispatcherLock();
77
78 /* Start expiration loop */
79 do
80 {
81 /* Get the current index */
82 Index = (Index + 1) & (TIMER_TABLE_SIZE - 1);
83
84 /* Get list pointers and loop the list */
85 ListHead = &KiTimerTableListHead[Index].Entry;
86 while (ListHead != ListHead->Flink)
87 {
88 /* Lock the timer and go to the next entry */
89 LockQueue = KiAcquireTimerLock(Index);
90 NextEntry = ListHead->Flink;
91
92 /* Get the current timer and check its due time */
93 Timers--;
94 Timer = CONTAINING_RECORD(NextEntry, KTIMER, TimerListEntry);
95 if ((NextEntry != ListHead) &&
96 (Timer->DueTime.QuadPart <= InterruptTime.QuadPart))
97 {
98 /* It's expired, remove it */
99 ActiveTimers--;
100 if (RemoveEntryList(&Timer->TimerListEntry))
101 {
102 /* Get the entry and check if it's empty */
103 TimerEntry = &KiTimerTableListHead[Timer->Header.Hand];
104 if (IsListEmpty(&TimerEntry->Entry))
105 {
106 /* Clear the time then */
107 TimerEntry->Time.HighPart = 0xFFFFFFFF;
108 }
109 }
110
111 /* Make it non-inserted, unlock it, and signal it */
112 Timer->Header.Inserted = FALSE;
113 KiReleaseTimerLock(LockQueue);
114 Timer->Header.SignalState = 1;
115
116 /* Get the DPC and period */
117 TimerDpc = Timer->Dpc;
118 Period = Timer->Period;
119
120 /* Check if there's any waiters */
121 if (!IsListEmpty(&Timer->Header.WaitListHead))
122 {
123 /* Check the type of event */
124 if (Timer->Header.Type == TimerNotificationObject)
125 {
126 /* Unwait the thread */
127 KxUnwaitThread(&Timer->Header, IO_NO_INCREMENT);
128 }
129 else
130 {
131 /* Otherwise unwait the thread and signal the timer */
132 KxUnwaitThreadForEvent((PKEVENT)Timer, IO_NO_INCREMENT);
133 }
134 }
135
136 /* Check if we have a period */
137 if (Period)
138 {
139 /* Calculate the interval and insert the timer */
140 Interval.QuadPart = Int32x32To64(Period, -10000);
141 while (!KiInsertTreeTimer(Timer, Interval));
142 }
143
144 /* Check if we have a DPC */
145 if (TimerDpc)
146 {
147 /* Setup the DPC Entry */
148 DpcEntry[DpcCalls].Dpc = TimerDpc;
149 DpcEntry[DpcCalls].Routine = TimerDpc->DeferredRoutine;
150 DpcEntry[DpcCalls].Context = TimerDpc->DeferredContext;
151 DpcCalls++;
152 }
153
154 /* Check if we're done processing */
155 if (!(ActiveTimers) || !(Timers))
156 {
157 /* Release the dispatcher while doing DPCs */
158 KiReleaseDispatcherLock(DISPATCH_LEVEL);
159
160 /* Start looping all DPC Entries */
161 for (i = 0; DpcCalls; DpcCalls--, i++)
162 {
163 /* Call the DPC */
164 DpcEntry[i].Routine(DpcEntry[i].Dpc,
165 DpcEntry[i].Context,
166 UlongToPtr(SystemTime.LowPart),
167 UlongToPtr(SystemTime.HighPart));
168 }
169
170 /* Reset accounting */
171 Timers = 24;
172 ActiveTimers = 4;
173
174 /* Lock the dispatcher database */
175 KiAcquireDispatcherLock();
176 }
177 }
178 else
179 {
180 /* Check if the timer list is empty */
181 if (NextEntry != ListHead)
182 {
183 /* Sanity check */
184 ASSERT(KiTimerTableListHead[Index].Time.QuadPart <=
185 Timer->DueTime.QuadPart);
186
187 /* Update the time */
188 _disable();
189 KiTimerTableListHead[Index].Time.QuadPart =
190 Timer->DueTime.QuadPart;
191 _enable();
192 }
193
194 /* Release the lock */
195 KiReleaseTimerLock(LockQueue);
196
197 /* Check if we've scanned all the timers we could */
198 if (!Timers)
199 {
200 /* Release the dispatcher while doing DPCs */
201 KiReleaseDispatcherLock(DISPATCH_LEVEL);
202
203 /* Start looping all DPC Entries */
204 for (i = 0; DpcCalls; DpcCalls--, i++)
205 {
206 /* Call the DPC */
207 DpcEntry[i].Routine(DpcEntry[i].Dpc,
208 DpcEntry[i].Context,
209 UlongToPtr(SystemTime.LowPart),
210 UlongToPtr(SystemTime.HighPart));
211 }
212
213 /* Reset accounting */
214 Timers = 24;
215 ActiveTimers = 4;
216
217 /* Lock the dispatcher database */
218 KiAcquireDispatcherLock();
219 }
220
221 /* Done looping */
222 break;
223 }
224 }
225 } while (Index != Limit);
226
227 /* Check if we still have DPC entries */
228 if (DpcCalls)
229 {
230 /* Release the dispatcher while doing DPCs */
231 KiReleaseDispatcherLock(DISPATCH_LEVEL);
232
233 /* Start looping all DPC Entries */
234 for (i = 0; DpcCalls; DpcCalls--, i++)
235 {
236 /* Call the DPC */
237 DpcEntry[i].Routine(DpcEntry[i].Dpc,
238 DpcEntry[i].Context,
239 UlongToPtr(SystemTime.LowPart),
240 UlongToPtr(SystemTime.HighPart));
241 }
242
243 /* Lower IRQL if we need to */
244 if (OldIrql != DISPATCH_LEVEL) KeLowerIrql(OldIrql);
245 }
246 else
247 {
248 /* Unlock the dispatcher */
249 KiReleaseDispatcherLock(OldIrql);
250 }
251 }
252
253 VOID
254 NTAPI
255 KiQuantumEnd(VOID)
256 {
257 PKPRCB Prcb = KeGetCurrentPrcb();
258 PKTHREAD NextThread, Thread = Prcb->CurrentThread;
259
260 /* Check if a DPC Event was requested to be signaled */
261 if (InterlockedExchange(&Prcb->DpcSetEventRequest, 0))
262 {
263 /* Signal it */
264 KeSetEvent(&Prcb->DpcEvent, 0, 0);
265 }
266
267 /* Raise to synchronization level and lock the PRCB and thread */
268 KeRaiseIrqlToSynchLevel();
269 KiAcquireThreadLock(Thread);
270 KiAcquirePrcbLock(Prcb);
271
272 /* Check if Quantum expired */
273 if (Thread->Quantum <= 0)
274 {
275 /* Make sure that we're not real-time or without a quantum */
276 if ((Thread->Priority < LOW_REALTIME_PRIORITY) &&
277 !(Thread->ApcState.Process->DisableQuantum))
278 {
279 /* Reset the new Quantum */
280 Thread->Quantum = Thread->QuantumReset;
281
282 /* Calculate new priority */
283 Thread->Priority = KiComputeNewPriority(Thread, 1);
284
285 /* Check if a new thread is scheduled */
286 if (!Prcb->NextThread)
287 {
288 #ifdef NEW_SCHEDULER
289 /* Get a new ready thread */
290 NextThread = KiSelectReadyThread(Thread->Priority, Prcb);
291 if (NextThread)
292 {
293 /* Found one, set it on standby */
294 NextThread->State = Standby;
295 Prcb->NextThread = NextThread;
296 }
297 #else
298 /* Just leave now */
299 KiReleasePrcbLock(Prcb);
300 KeLowerIrql(DISPATCH_LEVEL);
301 KiDispatchThread(Ready);
302 return;
303 #endif
304 }
305 else
306 {
307 /* Otherwise, make sure that this thread doesn't get preempted */
308 Thread->Preempted = FALSE;
309 }
310 }
311 else
312 {
313 /* Otherwise, set maximum quantum */
314 Thread->Quantum = MAX_QUANTUM;
315 }
316 }
317
318 /* Release the thread lock */
319 KiReleaseThreadLock(Thread);
320
321 /* Check if there's no thread scheduled */
322 if (!Prcb->NextThread)
323 {
324 /* Just leave now */
325 KiReleasePrcbLock(Prcb);
326 KeLowerIrql(DISPATCH_LEVEL);
327 return;
328 }
329
330 /* Get the next thread now */
331 NextThread = Prcb->NextThread;
332
333 /* Set current thread's swap busy to true */
334 KiSetThreadSwapBusy(Thread);
335
336 /* Switch threads in PRCB */
337 Prcb->NextThread = NULL;
338 Prcb->CurrentThread = NextThread;
339
340 /* Set thread to running and the switch reason to Quantum End */
341 NextThread->State = Running;
342 Thread->WaitReason = WrQuantumEnd;
343
344 /* Queue it on the ready lists */
345 KxQueueReadyThread(Thread, Prcb);
346
347 /* Set wait IRQL to APC_LEVEL */
348 Thread->WaitIrql = APC_LEVEL;
349
350 /* Swap threads */
351 KiSwapContext(Thread, NextThread);
352
353 /* Lower IRQL back to DISPATCH_LEVEL */
354 KeLowerIrql(DISPATCH_LEVEL);
355 }
356
357 VOID
358 FASTCALL
359 KiRetireDpcList(IN PKPRCB Prcb)
360 {
361 PKDPC_DATA DpcData = Prcb->DpcData;
362 PLIST_ENTRY DpcEntry;
363 PKDPC Dpc;
364 PKDEFERRED_ROUTINE DeferredRoutine;
365 PVOID DeferredContext, SystemArgument1, SystemArgument2;
366 ULONG_PTR TimerHand;
367
368 /* Main outer loop */
369 do
370 {
371 /* Set us as active */
372 Prcb->DpcRoutineActive = TRUE;
373
374 /* Check if this is a timer expiration request */
375 if (Prcb->TimerRequest)
376 {
377 TimerHand = Prcb->TimerHand;
378 Prcb->TimerRequest = 0;
379 _enable();
380 KiTimerExpiration(NULL, NULL, (PVOID) TimerHand, NULL);
381 _disable();
382 }
383
384 /* Loop while we have entries in the queue */
385 while (DpcData->DpcQueueDepth)
386 {
387 /* Lock the DPC data */
388 KefAcquireSpinLockAtDpcLevel(&DpcData->DpcLock);
389
390 /* Make sure we have an entry */
391 if (!IsListEmpty(&DpcData->DpcListHead))
392 {
393 /* Remove the DPC from the list */
394 DpcEntry = RemoveHeadList(&DpcData->DpcListHead);
395 Dpc = CONTAINING_RECORD(DpcEntry, KDPC, DpcListEntry);
396
397 /* Clear its DPC data and save its parameters */
398 Dpc->DpcData = NULL;
399 DeferredRoutine = Dpc->DeferredRoutine;
400 DeferredContext = Dpc->DeferredContext;
401 SystemArgument1 = Dpc->SystemArgument1;
402 SystemArgument2 = Dpc->SystemArgument2;
403
404 /* Decrease the queue depth */
405 DpcData->DpcQueueDepth--;
406
407 /* Clear DPC Time */
408 Prcb->DebugDpcTime = 0;
409
410 /* Release the lock */
411 KefReleaseSpinLockFromDpcLevel(&DpcData->DpcLock);
412
413 /* Re-enable interrupts */
414 _enable();
415
416 /* Call the DPC */
417 DeferredRoutine(Dpc,
418 DeferredContext,
419 SystemArgument1,
420 SystemArgument2);
421 ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
422
423 /* Disable interrupts and keep looping */
424 _disable();
425 }
426 else
427 {
428 /* The queue should be flushed now */
429 ASSERT(DpcData->DpcQueueDepth == 0);
430
431 /* Release DPC Lock */
432 KefReleaseSpinLockFromDpcLevel(&DpcData->DpcLock);
433 break;
434 }
435 }
436
437 /* Clear DPC Flags */
438 Prcb->DpcRoutineActive = FALSE;
439 Prcb->DpcInterruptRequested = FALSE;
440
441 /* Check if we have deferred threads */
442 if (Prcb->DeferredReadyListHead.Next)
443 {
444 /* FIXME: 2K3-style scheduling not implemeted */
445 ASSERT(FALSE);
446 }
447 } while (DpcData->DpcQueueDepth);
448 }
449
450 VOID
451 NTAPI
452 KiInitializeDpc(IN PKDPC Dpc,
453 IN PKDEFERRED_ROUTINE DeferredRoutine,
454 IN PVOID DeferredContext,
455 IN KOBJECTS Type)
456 {
457 /* Setup the DPC Object */
458 Dpc->Type = Type;
459 Dpc->Number= 0;
460 Dpc->Importance= MediumImportance;
461 Dpc->DeferredRoutine = DeferredRoutine;
462 Dpc->DeferredContext = DeferredContext;
463 Dpc->DpcData = NULL;
464 }
465
466 /* PUBLIC FUNCTIONS **********************************************************/
467
468 /*
469 * @implemented
470 */
471 VOID
472 NTAPI
473 KeInitializeThreadedDpc(IN PKDPC Dpc,
474 IN PKDEFERRED_ROUTINE DeferredRoutine,
475 IN PVOID DeferredContext)
476 {
477 /* Call the internal routine */
478 KiInitializeDpc(Dpc, DeferredRoutine, DeferredContext, ThreadedDpcObject);
479 }
480
481 /*
482 * @implemented
483 */
484 VOID
485 NTAPI
486 KeInitializeDpc(IN PKDPC Dpc,
487 IN PKDEFERRED_ROUTINE DeferredRoutine,
488 IN PVOID DeferredContext)
489 {
490 /* Call the internal routine */
491 KiInitializeDpc(Dpc, DeferredRoutine, DeferredContext, DpcObject);
492 }
493
494 /*
495 * @implemented
496 */
497 BOOLEAN
498 NTAPI
499 KeInsertQueueDpc(IN PKDPC Dpc,
500 IN PVOID SystemArgument1,
501 IN PVOID SystemArgument2)
502 {
503 KIRQL OldIrql;
504 PKPRCB Prcb, CurrentPrcb = KeGetCurrentPrcb();
505 ULONG Cpu;
506 PKDPC_DATA DpcData;
507 BOOLEAN DpcConfigured = FALSE, DpcInserted = FALSE;
508 ASSERT_DPC(Dpc);
509
510 /* Check IRQL and Raise it to HIGH_LEVEL */
511 KeRaiseIrql(HIGH_LEVEL, &OldIrql);
512
513 /* Check if the DPC has more then the maximum number of CPUs */
514 if (Dpc->Number >= MAXIMUM_PROCESSORS)
515 {
516 /* Then substract the maximum and get that PRCB. */
517 Cpu = Dpc->Number - MAXIMUM_PROCESSORS;
518 Prcb = KiProcessorBlock[Cpu];
519 }
520 else
521 {
522 /* Use the current one */
523 Prcb = CurrentPrcb;
524 Cpu = Prcb->Number;
525 }
526
527 /* Check if this is a threaded DPC and threaded DPCs are enabled */
528 if ((Dpc->Type == ThreadedDpcObject) && (Prcb->ThreadDpcEnable))
529 {
530 /* Then use the threaded data */
531 DpcData = &Prcb->DpcData[DPC_THREADED];
532 }
533 else
534 {
535 /* Otherwise, use the regular data */
536 DpcData = &Prcb->DpcData[DPC_NORMAL];
537 }
538
539 /* Acquire the DPC lock */
540 KiAcquireSpinLock(&DpcData->DpcLock);
541
542 /* Get the DPC Data */
543 if (!InterlockedCompareExchangePointer(&Dpc->DpcData, DpcData, NULL))
544 {
545 /* Now we can play with the DPC safely */
546 Dpc->SystemArgument1 = SystemArgument1;
547 Dpc->SystemArgument2 = SystemArgument2;
548 DpcData->DpcQueueDepth++;
549 DpcData->DpcCount++;
550 DpcConfigured = TRUE;
551
552 /* Check if this is a high importance DPC */
553 if (Dpc->Importance == HighImportance)
554 {
555 /* Pre-empty other DPCs */
556 InsertHeadList(&DpcData->DpcListHead, &Dpc->DpcListEntry);
557 }
558 else
559 {
560 /* Add it at the end */
561 InsertTailList(&DpcData->DpcListHead, &Dpc->DpcListEntry);
562 }
563
564 /* Check if this is the DPC on the threaded list */
565 if (&Prcb->DpcData[DPC_THREADED].DpcListHead == &DpcData->DpcListHead)
566 {
567 /* Make sure a threaded DPC isn't already active */
568 if (!(Prcb->DpcThreadActive) && !(Prcb->DpcThreadRequested))
569 {
570 /* FIXME: Setup Threaded DPC */
571 ASSERT(FALSE);
572 }
573 }
574 else
575 {
576 /* Make sure a DPC isn't executing already */
577 if (!(Prcb->DpcRoutineActive) && !(Prcb->DpcInterruptRequested))
578 {
579 /* Check if this is the same CPU */
580 if (Prcb != CurrentPrcb)
581 {
582 /*
583 * Check if the DPC is of high importance or above the
584 * maximum depth. If it is, then make sure that the CPU
585 * isn't idle, or that it's sleeping.
586 */
587 if (((Dpc->Importance == HighImportance) ||
588 (DpcData->DpcQueueDepth >=
589 Prcb->MaximumDpcQueueDepth)) &&
590 (!(AFFINITY_MASK(Cpu) & KiIdleSummary) ||
591 (Prcb->Sleeping)))
592 {
593 /* Set interrupt requested */
594 Prcb->DpcInterruptRequested = TRUE;
595
596 /* Set DPC inserted */
597 DpcInserted = TRUE;
598 }
599 }
600 else
601 {
602 /* Check if the DPC is of anything but low importance */
603 if ((Dpc->Importance != LowImportance) ||
604 (DpcData->DpcQueueDepth >=
605 Prcb->MaximumDpcQueueDepth) ||
606 (Prcb->DpcRequestRate < Prcb->MinimumDpcRate))
607 {
608 /* Set interrupt requested */
609 Prcb->DpcInterruptRequested = TRUE;
610
611 /* Set DPC inserted */
612 DpcInserted = TRUE;
613 }
614 }
615 }
616 }
617 }
618
619 /* Release the lock */
620 KiReleaseSpinLock(&DpcData->DpcLock);
621
622 /* Check if the DPC was inserted */
623 if (DpcInserted)
624 {
625 /* Check if this was SMP */
626 if (Prcb != CurrentPrcb)
627 {
628 /* It was, request and IPI */
629 KiIpiSendRequest(AFFINITY_MASK(Cpu), IPI_DPC);
630 }
631 else
632 {
633 /* It wasn't, request an interrupt from HAL */
634 HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
635 }
636 }
637
638 /* Lower IRQL */
639 KeLowerIrql(OldIrql);
640 return DpcConfigured;
641 }
642
643 /*
644 * @implemented
645 */
646 BOOLEAN
647 NTAPI
648 KeRemoveQueueDpc(IN PKDPC Dpc)
649 {
650 PKDPC_DATA DpcData;
651 UCHAR DpcType;
652 ASSERT_DPC(Dpc);
653
654 /* Disable interrupts */
655 _disable();
656
657 /* Get DPC data and type */
658 DpcType = Dpc->Type;
659 DpcData = Dpc->DpcData;
660 if (DpcData)
661 {
662 /* Acquire the DPC lock */
663 KiAcquireSpinLock(&DpcData->DpcLock);
664
665 /* Make sure that the data didn't change */
666 if (DpcData == Dpc->DpcData)
667 {
668 /* Remove the DPC */
669 DpcData->DpcQueueDepth--;
670 RemoveEntryList(&Dpc->DpcListEntry);
671 Dpc->DpcData = NULL;
672 }
673
674 /* Release the lock */
675 KiReleaseSpinLock(&DpcData->DpcLock);
676 }
677
678 /* Re-enable interrupts */
679 _enable();
680
681 /* Return if the DPC was in the queue or not */
682 return DpcData ? TRUE : FALSE;
683 }
684
685 /*
686 * @implemented
687 */
688 VOID
689 NTAPI
690 KeFlushQueuedDpcs(VOID)
691 {
692 PAGED_CODE();
693
694 /* Check if this is an UP machine */
695 if (KeActiveProcessors == 1)
696 {
697 /* Check if there are DPCs on either queues */
698 if ((KeGetCurrentPrcb()->DpcData[DPC_NORMAL].DpcQueueDepth) ||
699 (KeGetCurrentPrcb()->DpcData[DPC_THREADED].DpcQueueDepth))
700 {
701 /* Request an interrupt */
702 HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
703 }
704 }
705 else
706 {
707 /* FIXME: SMP support required */
708 ASSERT(FALSE);
709 }
710 }
711
712 /*
713 * @implemented
714 */
715 BOOLEAN
716 NTAPI
717 KeIsExecutingDpc(VOID)
718 {
719 /* Return if the Dpc Routine is active */
720 return KeGetCurrentPrcb()->DpcRoutineActive;
721 }
722
723 /*
724 * @implemented
725 */
726 VOID
727 NTAPI
728 KeSetImportanceDpc (IN PKDPC Dpc,
729 IN KDPC_IMPORTANCE Importance)
730 {
731 /* Set the DPC Importance */
732 ASSERT_DPC(Dpc);
733 Dpc->Importance = Importance;
734 }
735
736 /*
737 * @implemented
738 */
739 VOID
740 NTAPI
741 KeSetTargetProcessorDpc(IN PKDPC Dpc,
742 IN CCHAR Number)
743 {
744 /* Set a target CPU */
745 ASSERT_DPC(Dpc);
746 Dpc->Number = Number + MAXIMUM_PROCESSORS;
747 }
748
749 /* EOF */