Process Hacker
etwdisk.c
Go to the documentation of this file.
1 /*
2  * Process Hacker Extended Tools -
3  * ETW disk monitoring
4  *
5  * Copyright (C) 2011 wj32
6  *
7  * This file is part of Process Hacker.
8  *
9  * Process Hacker is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * Process Hacker is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with Process Hacker. If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 #include "exttools.h"
24 #include "etwmon.h"
25 
26 typedef struct _ETP_DISK_PACKET
27 {
28  SLIST_ENTRY ListEntry;
29  ET_ETW_DISK_EVENT Event;
30  PPH_STRING FileName;
32 
34  _In_ PVOID Object,
35  _In_ ULONG Flags
36  );
37 
39  _In_ PVOID Entry1,
40  _In_ PVOID Entry2
41  );
42 
44  _In_ PVOID Entry
45  );
46 
48  _In_opt_ PVOID Parameter,
49  _In_opt_ PVOID Context
50  );
51 
52 BOOLEAN EtDiskEnabled = FALSE;
53 
57 LIST_ENTRY EtDiskAgeListHead;
58 PH_CALLBACK_DECLARE(EtDiskItemAddedEvent);
59 PH_CALLBACK_DECLARE(EtDiskItemModifiedEvent);
60 PH_CALLBACK_DECLARE(EtDiskItemRemovedEvent);
61 PH_CALLBACK_DECLARE(EtDiskItemsUpdatedEvent);
62 
64 SLIST_HEADER EtDiskPacketListHead;
67 
68 static LARGE_INTEGER EtpPerformanceFrequency;
69 static PH_CALLBACK_REGISTRATION ProcessesUpdatedCallbackRegistration;
70 
72  VOID
73  )
74 {
75  LARGE_INTEGER performanceCounter;
76 
77  EtDiskItemType = PhCreateObjectType(L"DiskItem", 0, EtpDiskItemDeleteProcedure);
78  EtDiskHashtable = PhCreateHashtable(
79  sizeof(PET_DISK_ITEM),
82  128
83  );
85 
86  PhInitializeFreeList(&EtDiskPacketFreeList, sizeof(ETP_DISK_PACKET), 64);
87  RtlInitializeSListHead(&EtDiskPacketListHead);
88  EtFileNameHashtable = PhCreateSimpleHashtable(128);
89 
90  NtQueryPerformanceCounter(&performanceCounter, &EtpPerformanceFrequency);
91 
93 
94  // Collect all existing file names.
96 
100  NULL,
101  &ProcessesUpdatedCallbackRegistration
102  );
103 }
104 
106  VOID
107  )
108 {
109  PET_DISK_ITEM diskItem;
110 
111  diskItem = PhCreateObject(sizeof(ET_DISK_ITEM), EtDiskItemType);
112  memset(diskItem, 0, sizeof(ET_DISK_ITEM));
113 
114  return diskItem;
115 }
116 
118  _In_ PVOID Object,
119  _In_ ULONG Flags
120  )
121 {
122  PET_DISK_ITEM diskItem = Object;
123 
124  if (diskItem->FileName) PhDereferenceObject(diskItem->FileName);
125  if (diskItem->FileNameWin32) PhDereferenceObject(diskItem->FileNameWin32);
126  if (diskItem->ProcessName) PhDereferenceObject(diskItem->ProcessName);
127  if (diskItem->ProcessIcon) EtProcIconDereferenceProcessIcon(diskItem->ProcessIcon);
128  if (diskItem->ProcessRecord) PhDereferenceProcessRecord(diskItem->ProcessRecord);
129 }
130 
132  _In_ PVOID Entry1,
133  _In_ PVOID Entry2
134  )
135 {
136  PET_DISK_ITEM diskItem1 = *(PET_DISK_ITEM *)Entry1;
137  PET_DISK_ITEM diskItem2 = *(PET_DISK_ITEM *)Entry2;
138 
139  return diskItem1->ProcessId == diskItem2->ProcessId && PhEqualString(diskItem1->FileName, diskItem2->FileName, TRUE);
140 }
141 
143  _In_ PVOID Entry
144  )
145 {
146  PET_DISK_ITEM diskItem = *(PET_DISK_ITEM *)Entry;
147 
148  return (HandleToUlong(diskItem->ProcessId) / 4) ^ PhHashStringRef(&diskItem->FileName->sr, TRUE);
149 }
150 
152  _In_ HANDLE ProcessId,
153  _In_ PPH_STRING FileName
154  )
155 {
156  ET_DISK_ITEM lookupDiskItem;
157  PET_DISK_ITEM lookupDiskItemPtr = &lookupDiskItem;
158  PET_DISK_ITEM *diskItemPtr;
159  PET_DISK_ITEM diskItem;
160 
161  lookupDiskItem.ProcessId = ProcessId;
162  lookupDiskItem.FileName = FileName;
163 
164  PhAcquireQueuedLockShared(&EtDiskHashtableLock);
165 
166  diskItemPtr = (PET_DISK_ITEM *)PhFindEntryHashtable(
167  EtDiskHashtable,
168  &lookupDiskItemPtr
169  );
170 
171  if (diskItemPtr)
172  PhSetReference(&diskItem, *diskItemPtr);
173  else
174  diskItem = NULL;
175 
176  PhReleaseQueuedLockShared(&EtDiskHashtableLock);
177 
178  return diskItem;
179 }
180 
182  _In_ PET_DISK_ITEM DiskItem
183  )
184 {
185  RemoveEntryList(&DiskItem->AgeListEntry);
186  PhRemoveEntryHashtable(EtDiskHashtable, &DiskItem);
187  PhDereferenceObject(DiskItem);
188 }
189 
191  _In_ PET_ETW_DISK_EVENT Event
192  )
193 {
194  PETP_DISK_PACKET packet;
195 
196  if (!EtDiskEnabled)
197  return;
198 
199  packet = PhAllocateFromFreeList(&EtDiskPacketFreeList);
200  memcpy(&packet->Event, Event, sizeof(ET_ETW_DISK_EVENT));
201  packet->FileName = EtFileObjectToFileName(Event->FileObject);
202  RtlInterlockedPushEntrySList(&EtDiskPacketListHead, &packet->ListEntry);
203 }
204 
206  _In_ PET_ETW_FILE_EVENT Event
207  )
208 {
209  PH_KEY_VALUE_PAIR pair;
210  PPH_KEY_VALUE_PAIR realPair;
211 
212  if (!EtDiskEnabled)
213  return;
214 
215  if (Event->Type == EtEtwFileCreateType || Event->Type == EtEtwFileRundownType)
216  {
217  pair.Key = Event->FileObject;
218  pair.Value = NULL;
219 
220  PhAcquireQueuedLockExclusive(&EtFileNameHashtableLock);
221 
222  realPair = PhAddEntryHashtableEx(EtFileNameHashtable, &pair, NULL);
223  PhMoveReference(&realPair->Value, PhCreateString2(&Event->FileName));
224 
225  PhReleaseQueuedLockExclusive(&EtFileNameHashtableLock);
226  }
227  else if (Event->Type == EtEtwFileDeleteType)
228  {
229  pair.Key = Event->FileObject;
230 
231  PhAcquireQueuedLockExclusive(&EtFileNameHashtableLock);
232 
233  realPair = PhFindEntryHashtable(EtFileNameHashtable, &pair);
234 
235  if (realPair)
236  {
237  PhDereferenceObject(realPair->Value);
238  PhRemoveEntryHashtable(EtFileNameHashtable, &pair);
239  }
240 
241  PhReleaseQueuedLockExclusive(&EtFileNameHashtableLock);
242  }
243 }
244 
246  _In_ PVOID FileObject
247  )
248 {
249  PH_KEY_VALUE_PAIR pair;
250  PPH_KEY_VALUE_PAIR realPair;
251  PPH_STRING fileName;
252 
253  pair.Key = FileObject;
254  fileName = NULL;
255 
256  PhAcquireQueuedLockShared(&EtFileNameHashtableLock);
257 
258  realPair = PhFindEntryHashtable(EtFileNameHashtable, &pair);
259 
260  if (realPair)
261  PhSetReference(&fileName, realPair->Value);
262 
263  PhReleaseQueuedLockShared(&EtFileNameHashtableLock);
264 
265  return fileName;
266 }
267 
269  _In_ PETP_DISK_PACKET Packet,
270  _In_ ULONG RunId
271  )
272 {
273  PET_ETW_DISK_EVENT diskEvent;
274  PET_DISK_ITEM diskItem;
275  BOOLEAN added = FALSE;
276 
277  diskEvent = &Packet->Event;
278 
279  // We only process non-zero read/write events.
280  if (diskEvent->Type != EtEtwDiskReadType && diskEvent->Type != EtEtwDiskWriteType)
281  return;
282  if (diskEvent->TransferSize == 0)
283  return;
284 
285  // Ignore packets with no file name - this is useless to the user.
286  if (!Packet->FileName)
287  return;
288 
289  diskItem = EtReferenceDiskItem(diskEvent->ClientId.UniqueProcess, Packet->FileName);
290 
291  if (!diskItem)
292  {
293  PPH_PROCESS_ITEM processItem;
294 
295  // Disk item not found (or the address was re-used), create it.
296 
297  diskItem = EtCreateDiskItem();
298 
299  diskItem->ProcessId = diskEvent->ClientId.UniqueProcess;
300  PhSetReference(&diskItem->FileName, Packet->FileName);
301  diskItem->FileNameWin32 = PhGetFileName(diskItem->FileName);
302 
303  if (processItem = PhReferenceProcessItem(diskItem->ProcessId))
304  {
305  PhSetReference(&diskItem->ProcessName, processItem->ProcessName);
307  diskItem->ProcessRecord = processItem->Record;
309 
310  PhDereferenceObject(processItem);
311  }
312 
313  // Add the disk item to the age list.
314  diskItem->AddTime = RunId;
315  diskItem->FreshTime = RunId;
317 
318  // Add the disk item to the hashtable.
319  PhAcquireQueuedLockExclusive(&EtDiskHashtableLock);
320  PhAddEntryHashtable(EtDiskHashtable, &diskItem);
321  PhReleaseQueuedLockExclusive(&EtDiskHashtableLock);
322 
323  // Raise the disk item added event.
325  added = TRUE;
326  }
327 
328  // The I/O priority number needs to be decoded.
329 
330  diskItem->IoPriority = (diskEvent->IrpFlags >> 17) & 7;
331 
332  if (diskItem->IoPriority == 0)
333  diskItem->IoPriority = IoPriorityNormal;
334  else
335  diskItem->IoPriority--;
336 
337  // Accumulate statistics for this update period.
338 
339  if (diskEvent->Type == EtEtwDiskReadType)
340  diskItem->ReadDelta += diskEvent->TransferSize;
341  else
342  diskItem->WriteDelta += diskEvent->TransferSize;
343 
344  if (EtpPerformanceFrequency.QuadPart != 0)
345  {
346  // Convert the response time to milliseconds.
347  diskItem->ResponseTimeTotal += (FLOAT)diskEvent->HighResResponseTime * 1000 / EtpPerformanceFrequency.QuadPart;
348  diskItem->ResponseTimeCount++;
349  }
350 
351  if (!added)
352  {
353  if (diskItem->FreshTime != RunId)
354  {
355  diskItem->FreshTime = RunId;
356  RemoveEntryList(&diskItem->AgeListEntry);
358  }
359 
360  PhDereferenceObject(diskItem);
361  }
362 }
363 
365  _In_ PULONG64 Buffer,
366  _In_ ULONG BufferSize,
367  _In_ ULONG BufferPosition,
368  _In_ ULONG BufferCount,
369  _In_ ULONG NumberToConsider
370  )
371 {
372  ULONG64 sum;
373  ULONG i;
374  ULONG count;
375 
376  sum = 0;
377  i = BufferPosition;
378 
379  if (NumberToConsider > BufferCount)
380  NumberToConsider = BufferCount;
381 
382  if (NumberToConsider == 0)
383  return 0;
384 
385  count = NumberToConsider;
386 
387  do
388  {
389  sum += Buffer[i];
390  i++;
391 
392  if (i == BufferSize)
393  i = 0;
394  } while (--count != 0);
395 
396  return sum / NumberToConsider;
397 }
398 
400  _In_opt_ PVOID Parameter,
401  _In_opt_ PVOID Context
402  )
403 {
404  static ULONG runCount = 0;
405 
406  PSLIST_ENTRY listEntry;
407  PLIST_ENTRY ageListEntry;
408 
409  // Process incoming disk event packets.
410 
411  listEntry = RtlInterlockedFlushSList(&EtDiskPacketListHead);
412 
413  while (listEntry)
414  {
415  PETP_DISK_PACKET packet;
416 
417  packet = CONTAINING_RECORD(listEntry, ETP_DISK_PACKET, ListEntry);
418  listEntry = listEntry->Next;
419 
420  EtpProcessDiskPacket(packet, runCount);
421 
422  if (packet->FileName)
423  PhDereferenceObject(packet->FileName);
424 
425  PhFreeToFreeList(&EtDiskPacketFreeList, packet);
426  }
427 
428  // Remove old entries.
429 
430  ageListEntry = EtDiskAgeListHead.Blink;
431 
432  while (ageListEntry != &EtDiskAgeListHead)
433  {
434  PET_DISK_ITEM diskItem;
435 
436  diskItem = CONTAINING_RECORD(ageListEntry, ET_DISK_ITEM, AgeListEntry);
437  ageListEntry = ageListEntry->Blink;
438 
439  if (runCount - diskItem->FreshTime < HISTORY_SIZE) // must compare like this to avoid overflow/underflow problems
440  break;
441 
443 
444  PhAcquireQueuedLockExclusive(&EtDiskHashtableLock);
445  EtpRemoveDiskItem(diskItem);
446  PhReleaseQueuedLockExclusive(&EtDiskHashtableLock);
447  }
448 
449  // Update existing items.
450 
451  ageListEntry = EtDiskAgeListHead.Flink;
452 
453  while (ageListEntry != &EtDiskAgeListHead)
454  {
455  PET_DISK_ITEM diskItem;
456 
457  diskItem = CONTAINING_RECORD(ageListEntry, ET_DISK_ITEM, AgeListEntry);
458 
459  // Update statistics.
460 
461  if (diskItem->HistoryPosition != 0)
462  diskItem->HistoryPosition--;
463  else
464  diskItem->HistoryPosition = HISTORY_SIZE - 1;
465 
466  diskItem->ReadHistory[diskItem->HistoryPosition] = diskItem->ReadDelta;
467  diskItem->WriteHistory[diskItem->HistoryPosition] = diskItem->WriteDelta;
468 
469  if (diskItem->HistoryCount < HISTORY_SIZE)
470  diskItem->HistoryCount++;
471 
472  if (diskItem->ResponseTimeCount != 0)
473  {
474  diskItem->ResponseTimeAverage = (FLOAT)diskItem->ResponseTimeTotal / diskItem->ResponseTimeCount;
475 
476  // Reset the total once in a while to avoid the number getting too large (and thus losing precision).
477  if (diskItem->ResponseTimeCount >= 1000)
478  {
479  diskItem->ResponseTimeTotal = diskItem->ResponseTimeAverage;
480  diskItem->ResponseTimeCount = 1;
481  }
482  }
483 
484  diskItem->ReadTotal += diskItem->ReadDelta;
485  diskItem->WriteTotal += diskItem->WriteDelta;
486  diskItem->ReadDelta = 0;
487  diskItem->WriteDelta = 0;
490 
491  if (diskItem->AddTime != runCount)
492  {
493  BOOLEAN modified = FALSE;
494  PPH_PROCESS_ITEM processItem;
495 
496  if (!diskItem->ProcessName || !diskItem->ProcessIcon || !diskItem->ProcessRecord)
497  {
498  if (processItem = PhReferenceProcessItem(diskItem->ProcessId))
499  {
500  if (!diskItem->ProcessName)
501  {
502  PhSetReference(&diskItem->ProcessName, processItem->ProcessName);
503  modified = TRUE;
504  }
505 
506  if (!diskItem->ProcessIcon)
507  {
509 
510  if (diskItem->ProcessIcon)
511  modified = TRUE;
512  }
513 
514  if (!diskItem->ProcessRecord)
515  {
516  diskItem->ProcessRecord = processItem->Record;
518  }
519 
520  PhDereferenceObject(processItem);
521  }
522  }
523 
524  if (modified)
525  {
526  // Raise the disk item modified event.
528  }
529  }
530 
531  ageListEntry = ageListEntry->Flink;
532  }
533 
535  runCount++;
536 }