xref: /reactos/ntoskrnl/cc/lazywrite.c (revision 6924b8ff)
1 /*
2  * COPYRIGHT:       See COPYING in the top level directory
3  * PROJECT:         ReactOS kernel
4  * FILE:            ntoskrnl/cc/lazywrite.c
5  * PURPOSE:         Cache manager
6  *
7  * PROGRAMMERS:     Pierre Schweitzer (pierre@reactos.org)
8  */
9 
10 /* INCLUDES *****************************************************************/
11 
12 #include <ntoskrnl.h>
13 #define NDEBUG
14 #include <debug.h>
15 
16 /* Counters:
17  * - Amount of pages flushed by lazy writer
18  * - Number of times lazy writer ran
19  */
20 ULONG CcLazyWritePages = 0;
21 ULONG CcLazyWriteIos = 0;
22 
23 /* Internal vars (MS):
24  * - Lazy writer status structure
25  * - Lookaside list where to allocate work items
26  * - Queue for high priority work items (read ahead)
27  * - Queue for regular work items
28  * - Available worker threads
29  * - Queue for stuff to be queued after lazy writer is done
30  * - Marker for throttling queues
31  * - Number of ongoing workers
32  * - Three seconds delay for lazy writer
33  * - One second delay for lazy writer
34  * - Zero delay for lazy writer
35  * - Number of worker threads
36  */
37 LAZY_WRITER LazyWriter;
38 NPAGED_LOOKASIDE_LIST CcTwilightLookasideList;
39 LIST_ENTRY CcExpressWorkQueue;
40 LIST_ENTRY CcRegularWorkQueue;
41 LIST_ENTRY CcIdleWorkerThreadList;
42 LIST_ENTRY CcPostTickWorkQueue;
43 BOOLEAN CcQueueThrottle = FALSE;
44 ULONG CcNumberActiveWorkerThreads = 0;
45 LARGE_INTEGER CcFirstDelay = RTL_CONSTANT_LARGE_INTEGER((LONGLONG)-1*3000*1000*10);
46 LARGE_INTEGER CcIdleDelay = RTL_CONSTANT_LARGE_INTEGER((LONGLONG)-1*1000*1000*10);
47 LARGE_INTEGER CcNoDelay = RTL_CONSTANT_LARGE_INTEGER((LONGLONG)0);
48 ULONG CcNumberWorkerThreads;
49 
50 /* FUNCTIONS *****************************************************************/
51 
52 VOID
53 CcPostWorkQueue(
54     IN PWORK_QUEUE_ENTRY WorkItem,
55     IN PLIST_ENTRY WorkQueue)
56 {
57     KIRQL OldIrql;
58     PWORK_QUEUE_ITEM ThreadToSpawn;
59 
60     /* First of all, insert the item in the queue */
61     OldIrql = KeAcquireQueuedSpinLock(LockQueueWorkQueueLock);
62     InsertTailList(WorkQueue, &WorkItem->WorkQueueLinks);
63 
64     /* Now, define whether we have to spawn a new work thread
65      * We will spawn a new one if:
66      * - There's no throttle in action
67      * - There's still at least one idle thread
68      */
69     ThreadToSpawn = NULL;
70     if (!CcQueueThrottle && !IsListEmpty(&CcIdleWorkerThreadList))
71     {
72         PLIST_ENTRY ListEntry;
73 
74         /* Get the idle thread */
75         ListEntry = RemoveHeadList(&CcIdleWorkerThreadList);
76         ThreadToSpawn = CONTAINING_RECORD(ListEntry, WORK_QUEUE_ITEM, List);
77 
78         /* We're going to have one more! */
79         CcNumberActiveWorkerThreads += 1;
80     }
81 
82     KeReleaseQueuedSpinLock(LockQueueWorkQueueLock, OldIrql);
83 
84     /* If we have a thread to spawn, do it! */
85     if (ThreadToSpawn != NULL)
86     {
87         /* We NULLify it to be consistent with initialization */
88         ThreadToSpawn->List.Flink = NULL;
89         ExQueueWorkItem(ThreadToSpawn, CriticalWorkQueue);
90     }
91 }
92 
93 VOID
94 NTAPI
95 CcScanDpc(
96     IN PKDPC Dpc,
97     IN PVOID DeferredContext,
98     IN PVOID SystemArgument1,
99     IN PVOID SystemArgument2)
100 {
101     PWORK_QUEUE_ENTRY WorkItem;
102 
103     /* Allocate a work item */
104     WorkItem = ExAllocateFromNPagedLookasideList(&CcTwilightLookasideList);
105     if (WorkItem == NULL)
106     {
107         LazyWriter.ScanActive = FALSE;
108         return;
109     }
110 
111     /* And post it, it will be for lazy write */
112     WorkItem->Function = LazyScan;
113     CcPostWorkQueue(WorkItem, &CcRegularWorkQueue);
114 }
115 
116 VOID
117 CcWriteBehind(VOID)
118 {
119     ULONG Target, Count;
120 
121     Target = CcTotalDirtyPages / 8;
122     if (Target != 0)
123     {
124         /* Flush! */
125         DPRINT("Lazy writer starting (%d)\n", Target);
126         CcRosFlushDirtyPages(Target, &Count, FALSE, TRUE);
127 
128         /* And update stats */
129         CcLazyWritePages += Count;
130         ++CcLazyWriteIos;
131         DPRINT("Lazy writer done (%d)\n", Count);
132     }
133 }
134 
135 VOID
136 CcLazyWriteScan(VOID)
137 {
138     ULONG Target;
139     KIRQL OldIrql;
140     PLIST_ENTRY ListEntry;
141     LIST_ENTRY ToPost;
142     PWORK_QUEUE_ENTRY WorkItem;
143 
144     /* Do we have entries to queue after we're done? */
145     InitializeListHead(&ToPost);
146     OldIrql = KeAcquireQueuedSpinLock(LockQueueMasterLock);
147     if (LazyWriter.OtherWork)
148     {
149         while (!IsListEmpty(&CcPostTickWorkQueue))
150         {
151             ListEntry = RemoveHeadList(&CcPostTickWorkQueue);
152             WorkItem = CONTAINING_RECORD(ListEntry, WORK_QUEUE_ENTRY, WorkQueueLinks);
153             InsertTailList(&ToPost, &WorkItem->WorkQueueLinks);
154         }
155         LazyWriter.OtherWork = FALSE;
156     }
157     KeReleaseQueuedSpinLock(LockQueueMasterLock, OldIrql);
158 
159     /* Our target is one-eighth of the dirty pages */
160     Target = CcTotalDirtyPages / 8;
161     if (Target != 0)
162     {
163         /* There is stuff to flush, schedule a write-behind operation */
164 
165         /* Allocate a work item */
166         WorkItem = ExAllocateFromNPagedLookasideList(&CcTwilightLookasideList);
167         if (WorkItem != NULL)
168         {
169             WorkItem->Function = WriteBehind;
170             CcPostWorkQueue(WorkItem, &CcRegularWorkQueue);
171         }
172     }
173 
174     /* Post items that were due for end of run */
175     while (!IsListEmpty(&ToPost))
176     {
177         ListEntry = RemoveHeadList(&ToPost);
178         WorkItem = CONTAINING_RECORD(ListEntry, WORK_QUEUE_ENTRY, WorkQueueLinks);
179         CcPostWorkQueue(WorkItem, &CcRegularWorkQueue);
180     }
181 
182     /* If we have deferred writes, try them now! */
183     if (!IsListEmpty(&CcDeferredWrites))
184     {
185         CcPostDeferredWrites();
186         /* Reschedule immediately a lazy writer run
187          * Keep us active to have short idle delay
188          */
189         CcScheduleLazyWriteScan(FALSE);
190     }
191     else
192     {
193         /* We're no longer active */
194         OldIrql = KeAcquireQueuedSpinLock(LockQueueMasterLock);
195         LazyWriter.ScanActive = FALSE;
196         KeReleaseQueuedSpinLock(LockQueueMasterLock, OldIrql);
197     }
198 }
199 
200 VOID CcScheduleLazyWriteScan(
201     IN BOOLEAN NoDelay)
202 {
203     /* If no delay, immediately start lazy writer,
204      * no matter it was already started
205      */
206     if (NoDelay)
207     {
208         LazyWriter.ScanActive = TRUE;
209         KeSetTimer(&LazyWriter.ScanTimer, CcNoDelay, &LazyWriter.ScanDpc);
210     }
211     /* Otherwise, if it's not running, just wait three seconds to start it */
212     else if (!LazyWriter.ScanActive)
213     {
214         LazyWriter.ScanActive = TRUE;
215         KeSetTimer(&LazyWriter.ScanTimer, CcFirstDelay, &LazyWriter.ScanDpc);
216     }
217     /* Finally, already running, so queue for the next second */
218     else
219     {
220         KeSetTimer(&LazyWriter.ScanTimer, CcIdleDelay, &LazyWriter.ScanDpc);
221     }
222 }
223 
224 VOID
225 NTAPI
226 CcWorkerThread(
227     IN PVOID Parameter)
228 {
229     KIRQL OldIrql;
230     BOOLEAN DropThrottle, WritePerformed;
231     PWORK_QUEUE_ITEM Item;
232 #if DBG
233     PIRP TopLevel;
234 #endif
235 
236     /* Get back our thread item */
237     Item = Parameter;
238     /* And by default, don't touch throttle */
239     DropThrottle = FALSE;
240     /* No write performed */
241     WritePerformed =  FALSE;
242 
243 #if DBG
244     /* Top level IRP should be clean when started
245      * Save it to catch buggy drivers (or bugs!)
246      */
247     TopLevel = IoGetTopLevelIrp();
248     if (TopLevel != NULL)
249     {
250         DPRINT1("(%p) TopLevel IRP for this thread: %p\n", PsGetCurrentThread(), TopLevel);
251     }
252 #endif
253 
254     /* Loop till we have jobs */
255     while (TRUE)
256     {
257         PWORK_QUEUE_ENTRY WorkItem;
258 
259         /* Lock queues */
260         OldIrql = KeAcquireQueuedSpinLock(LockQueueWorkQueueLock);
261 
262         /* If we have to touch throttle, reset it now! */
263         if (DropThrottle)
264         {
265             CcQueueThrottle = FALSE;
266             DropThrottle = FALSE;
267         }
268 
269         /* Check first if we have read ahead to do */
270         if (IsListEmpty(&CcExpressWorkQueue))
271         {
272             /* If not, check regular queue */
273             if (IsListEmpty(&CcRegularWorkQueue))
274             {
275                 break;
276             }
277             else
278             {
279                 WorkItem = CONTAINING_RECORD(CcRegularWorkQueue.Flink, WORK_QUEUE_ENTRY, WorkQueueLinks);
280             }
281         }
282         else
283         {
284             WorkItem = CONTAINING_RECORD(CcExpressWorkQueue.Flink, WORK_QUEUE_ENTRY, WorkQueueLinks);
285         }
286 
287         /* Get our work item, if someone is waiting for us to finish
288          * and we're not the only thread in queue
289          * then, quit running to let the others do
290          * and throttle so that noone starts till current activity is over
291          */
292         if (WorkItem->Function == SetDone && CcNumberActiveWorkerThreads > 1)
293         {
294             CcQueueThrottle = TRUE;
295             break;
296         }
297 
298         /* Otherwise, remove current entry */
299         RemoveEntryList(&WorkItem->WorkQueueLinks);
300         KeReleaseQueuedSpinLock(LockQueueWorkQueueLock, OldIrql);
301 
302         /* And handle it */
303         switch (WorkItem->Function)
304         {
305             case ReadAhead:
306                 CcPerformReadAhead(WorkItem->Parameters.Read.FileObject);
307                 break;
308 
309             case WriteBehind:
310                 PsGetCurrentThread()->MemoryMaker = 1;
311                 CcWriteBehind();
312                 PsGetCurrentThread()->MemoryMaker = 0;
313                 WritePerformed = TRUE;
314                 break;
315 
316             case LazyScan:
317                 CcLazyWriteScan();
318                 break;
319 
320             case SetDone:
321                 KeSetEvent(WorkItem->Parameters.Event.Event, IO_NO_INCREMENT, FALSE);
322                 DropThrottle = TRUE;
323                 break;
324 
325             default:
326                 DPRINT1("Ignored item: %p (%d)\n", WorkItem, WorkItem->Function);
327                 break;
328         }
329 
330         /* And release the item */
331         ExFreeToNPagedLookasideList(&CcTwilightLookasideList, WorkItem);
332     }
333 
334     /* Our thread is available again */
335     InsertTailList(&CcIdleWorkerThreadList, &Item->List);
336     /* One less worker */
337     --CcNumberActiveWorkerThreads;
338     KeReleaseQueuedSpinLock(LockQueueWorkQueueLock, OldIrql);
339 
340     /* If there are pending write openations and we have at least 20 dirty pages */
341     if (!IsListEmpty(&CcDeferredWrites) && CcTotalDirtyPages >= 20)
342     {
343         /* And if we performed a write operation previously, then
344          * stress the system a bit and reschedule a scan to find
345          * stuff to write
346          */
347         if (WritePerformed)
348         {
349             CcLazyWriteScan();
350         }
351     }
352 
353 #if DBG
354     /* Top level shouldn't have changed */
355     if (TopLevel != IoGetTopLevelIrp())
356     {
357         DPRINT1("(%p) Mismatching TopLevel: %p, %p\n", PsGetCurrentThread(), TopLevel, IoGetTopLevelIrp());
358     }
359 #endif
360 }
361 
362 /*
363  * @implemented
364  */
365 NTSTATUS
366 NTAPI
367 CcWaitForCurrentLazyWriterActivity (
368     VOID)
369 {
370     KIRQL OldIrql;
371     KEVENT WaitEvent;
372     PWORK_QUEUE_ENTRY WorkItem;
373 
374     /* Allocate a work item */
375     WorkItem = ExAllocateFromNPagedLookasideList(&CcTwilightLookasideList);
376     if (WorkItem == NULL)
377     {
378         return STATUS_INSUFFICIENT_RESOURCES;
379     }
380 
381     /* We want lazy writer to set our event */
382     WorkItem->Function = SetDone;
383     KeInitializeEvent(&WaitEvent, NotificationEvent, FALSE);
384     WorkItem->Parameters.Event.Event = &WaitEvent;
385 
386     /* Use the post tick queue */
387     OldIrql = KeAcquireQueuedSpinLock(LockQueueMasterLock);
388     InsertTailList(&CcPostTickWorkQueue, &WorkItem->WorkQueueLinks);
389 
390     /* Inform the lazy writer it will have to handle the post tick queue */
391     LazyWriter.OtherWork = TRUE;
392     /* And if it's not running, queue a lazy writer run
393      * And start it NOW, we want the response now
394      */
395     if (!LazyWriter.ScanActive)
396     {
397         CcScheduleLazyWriteScan(TRUE);
398     }
399 
400     KeReleaseQueuedSpinLock(LockQueueMasterLock, OldIrql);
401 
402     /* And now, wait until lazy writer replies */
403     return KeWaitForSingleObject(&WaitEvent, Executive, KernelMode, FALSE, NULL);
404 }
405