Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)

ClrMD를 이용해 메모리 덤프 파일로부터 특정 인스턴스를 참조하고 있는 소유자 확인

지난 글에서,

windbg - 닷넷 응용 프로그램의 메모리 누수 분석
; https://www.sysnet.pe.kr/2/0/11808

문자열이 쌓이고 있는 경우를 살펴봤는데, 분석 시 약간 아쉬운 점이 있습니다. 그 덤프의 힙 상태를 다시 보면,

0:000> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ff90f456748        1           24 Elasticsearch.Net.ElasticsearchDefaultSerializer
00007ff90f192f78        1           24 System.Collections.Generic.ObjectEqualityComparer`1[[System.Runtime.Caching.MemoryCache, System.Runtime.Caching]]
00007ff90f143038        1           24 System.Collections.Generic.Dictionary`2+ValueCollection[[System.String, mscorlib],[OracleInternal.SelfTuning.OracleTuner+OracleTunerInput, Oracle.ManagedDataAccess]]
00007ff90f0120b8        1           24 System.Net.SSPISecureChannelType
00007ff90f011e78        1           24 System.Net.SSPIAuthType
...[생략]...                
00007ff90d3869b8    12753      5127112 System.Int32[]
00007ff90d4ba6e0      159     14555944 System.Int64[]
00007ff90d1aba28   148673     16952928 System.Object[]
00007ff90d1fe828    75889     20114243 System.Byte[]
00007ff90ebd41e0   376168     36112128 TestApi.Data.AddrInfo
00007ff90d1f46f0  2726523    208786640 System.String
000000b69ba94870    33952    274313352      Free
Total 4063087 objects
Fragmented blocks larger than 0.5 MB:
            Addr     Size      Followed by
000000b6a40f38b0   11.4MB 000000b6a4c4d630 System.Byte[]

System.String을 소유하고 있는 객체로 (사용자가 만든 타입인) TestApi.Dat.AddrInfo 인스턴스가 많은 걸로 쉽게 짐작을 했는데요. 이런 식으로 찾아낼 수 없는 경우도 더러 있습니다. 예를 들어, 다음의 소스 코드를 덤프 떠서 살펴보면 어떨까요?

using System;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        static List<string> _list = new List<string>();

        static void Main(string[] args)
        {
            CreateObjects();
            GC.Collect(2);
            GC.Collect(2);

            Console.WriteLine("Dump!");
            Console.ReadKey();
        }

        private static void CreateObjects()
        {
            for (int i = 0; i < 100000; i ++)
            {
                _list.Add(Guid.NewGuid().ToString());
            }
        }
    }
}

string에 대한 소유를 1개의 List 객체가 담당하고 있으므로 heap 상태를 봐도 그 소유자를 쉽게 확인할 수 없습니다.

0:000> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class Name
719025c4        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.String, mscorlib]]
71900414        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type, mscorlib]]
718fde9c        1           16 System.Security.Policy.AssemblyEvidenceFactory
7190be18        1           20 System.IO.Stream+NullStream
71901888        1           20 Microsoft.Win32.SafeHandles.SafeFileHandle
718fdde8        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
7190c3dc        1           24 System.IO.TextWriter+SyncTextWriter
71902298        1           24 System.Version
71900994        2           24 System.Int32
71461e00        1           24 System.Collections.Generic.List`1[[System.String, mscorlib]]
7190c2e4        1           28 System.IO.__ConsoleStream
7190c29c        1           28 Microsoft.Win32.Win32Native+InputRecord
...[생략]...
71900958        6          452 System.Int32[]
718fff54       22          616 System.RuntimeType
718ff54c        6          626 System.Char[]
7190316c        1          783 System.Byte[]
71901e50        3          924 System.Globalization.CultureData
718fef34        4        17604 System.Object[]
718ff698       19       524868 System.String[]
00f2b548       24       525636      Free
718feb40   100154      8604850 System.String
Total 100299 objects

물론, "!dumpheap -mt 718feb40" 명령어를 이용해 문자열 인스턴스를 나열할 수 있지만 windbg에서 이 명령어를 수행하면 10만 개가 넘는 인스턴스를 출력하느라 한 세월이 걸립니다. 게다가 그중에서 몇 개를 골라 "!gcroot"로 찾아볼 수 있겠지만 무작위로 선정하는 것이므로 운이 좋아야 List를 가리키는 객체를 알아낼 수 있습니다. 또한, 그 객체가 문자열을 보관하는 (Leak이 없는) 또 다른 List 객체일 수도 있다는 점을 감안하면 찾기가 그리 쉽지 않습니다.




이런 상황을 해결하려면, windbg에서 스크립트를 이용해 10만 개의 System.String에 대한 참조를 유지하고 있는 객체 리스트를 작성하는 것입니다. 대표적으로 pykd를 이용하면 될 텐데요,

windbg에서 python 스크립트 실행하는 방법 - pykd
; https://www.sysnet.pe.kr/2/0/11227

아쉽게도 실제로 수행해보면 출력 결과에 대해 일일이 sos.dll의 명령어로 다시 수행하는 방식이어서 전체 과정이 꽤나 긴 시간을 요구하게 됩니다. 그래서 좀 더 빠른 방법을 찾게 되는데, 바로 그 해답이 ClrMD입니다.

Microsoft.Diagnostics.Runtime

Install-Package Microsoft.Diagnostics.Runtime -Version 1.0.2 

아울러 이미 제공되고 있는 풍부한 예제 코드에 따라,

Tutorials - 1. Getting Started - A brief introduction to the API and how to create a CLRRuntime instance.
; https://github.com/Microsoft/clrmd/blob/master/Documentation/GettingStarted.md

Tutorials - 3. Walking objects on the GC heap, working with types in CLR MD.
; https://github.com/Microsoft/clrmd/blob/master/Documentation/WalkingTheHeap.md

이거저거 짜깁기하면 우리가 원하는 목적의 코드를 다음과 같이 간단하게 만들 수 있습니다.

using Microsoft.Diagnostics.Runtime;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace RefOwner
{
    class Program
    {
        static string _platformPostfix = (IntPtr.Size == 4) ? "32" : "64";

        static void Main(string[] args)
        {
            if (args.Length < 2)
            {
                Console.WriteLine("RefOwner" + _platformPostfix + " [/d] [dumppath] [typename | address]");
                Console.WriteLine("[Sample]");
                Console.WriteLine("\tRefOwner" + _platformPostfix + " c:\\temp\\test.dmp System.String");
                Console.WriteLine("\tRefOwner" + _platformPostfix + "  /d c:\\temp\\test.dmp System.String");
                return;
            }

            string filePath = (args.Length == 3) ? args[1] : args[0];
            if (File.Exists(filePath) == false)
            {
                Console.WriteLine("FILE-NOT-FOUND: " + filePath);
                return;
            }

            string basePath = Path.GetDirectoryName(filePath);
            string dmpPath = filePath;
            string dacPath = Path.Combine(basePath, "mscordacwks.dll");

            string typeNameOrAddress = (args.Length == 3) ? args[2] : args[1];

            bool detailed = args.Length == 3 && args[0] == "/d";

            using (DataTarget dataTarget = DataTarget.LoadCrashDump(dmpPath))
            {
                foreach (ClrInfo version in dataTarget.ClrVersions)
                {
                    Console.WriteLine("Found CLR Version: " + version.Version);

                    // This is the data needed to request the dac from the symbol server:
                    ModuleInfo dacInfo = version.DacInfo;
                    Console.WriteLine("Filesize:  {0:X}", dacInfo.FileSize);
                    Console.WriteLine("Timestamp: {0:X}", dacInfo.TimeStamp);
                    Console.WriteLine("Dac File:  {0}", dacInfo.FileName);

                    string dacLocation = version.LocalMatchingDac;
                    if (!string.IsNullOrEmpty(dacLocation))
                        Console.WriteLine("Local dac location: " + dacLocation);

                    Console.WriteLine();

                    ClrRuntime runtime;

                    if (File.Exists(dacPath) == false)
                    {
                        runtime = version.CreateRuntime();
                    }
                    else
                    {
                        runtime = version.CreateRuntime(dacPath);
                    }

                    // DumpHeapStat(runtime);
                    // Console.WriteLine();
                    DumpHeapRefHierachy(runtime, typeNameOrAddress, detailed, 0);
                }
            }
        }

        private static void DumpHeapRefHierachy(ClrRuntime runtime, string typeNameOrAddress, bool detailed, int depth)
        {
            HashSet<ulong> instances = GetObjectListByName(runtime, typeNameOrAddress);
            Console.WriteLine(typeNameOrAddress + ", # of instances: " + instances.Count);

            Dictionary<string, HeapObjectCounter> owners = GetObjectOwners(runtime, instances);
            List<ObjectHistogramItem> histogram = new List<ObjectHistogramItem>();

            foreach (var owner in owners)
            {
                ObjectHistogramItem item = new ObjectHistogramItem { Key = owner.Key, Counter = owner.Value };
                histogram.Add(item);
            }

            histogram.Sort();

            int count = 0;

            foreach (var item in histogram)
            {
                Console.WriteLine($"{item.Counter.Total} {item.Key}({item.Counter.OwnerCount})");

                if (detailed == true)
                {
                    foreach (var instance in item.Counter)
                    {
                        Console.WriteLine($"\t[{instance.Key:x}, {instance.Value}]");
                    }
                }

                count += item.Counter.Total;
            }

            Console.WriteLine("Total: " + count);
        }

        private static ulong GetFieldAddressValue(ClrRuntime runtime, ClrInstanceField field, ulong addr)
        {
            ulong fieldAddress = field.GetAddress(addr);
            return ReadMemory(runtime, fieldAddress);
        }

        private static ulong ReadMemory(ClrRuntime runtime, ulong address)
        {
            bool x86 = IntPtr.Size == 4;

            unsafe
            {
                byte[] refAddr = null;

                if (x86 == true)
                {
                    refAddr = new byte[4];
                }
                else
                {
                    refAddr = new byte[8];
                }

                runtime.Heap.ReadMemory(address, refAddr, 0, IntPtr.Size);

                if (x86 == true)
                {
                    return BitConverter.ToUInt32(refAddr, 0);
                }
                else
                {
                    return BitConverter.ToUInt64(refAddr, 0);
                }                    
            }
        }

        private static Dictionary<string, HeapObjectCounter> GetObjectOwners(ClrRuntime runtime, HashSet<ulong> instances)
        {
            Dictionary<string, HeapObjectCounter> dict = new Dictionary<string, HeapObjectCounter>();

            if (!runtime.Heap.CanWalkHeap)
            {
                Console.WriteLine("Cannot walk the heap!");
            }
            else
            {
                foreach (ClrSegment seg in runtime.Heap.Segments)
                {
                    for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                    {
                        ClrType type = runtime.Heap.GetObjectType(obj);

                        if (type == null || type.IsFree == true)
                        {
                            continue;
                        }

                        string typeName = type.ToString();

                        int gen = runtime.Heap.GetGeneration(obj);
                        if (gen != 2)
                        {
                            continue;
                        }

                        if (type.IsArray == true)
                        {
                            int arrayLength = type.GetArrayLength(obj);
                            for (int i = 0; i < arrayLength; i++)
                            {
                                ulong elemAddress = type.GetArrayElementAddress(obj, i);

                                ClrType elemType = type.ComponentType;
                                if (elemType.IsValueClass == false)
                                {
                                    ulong elemRef = ReadMemory(runtime, elemAddress);
                                    if (instances.Contains(elemRef) == true)
                                    {
                                        if (dict.ContainsKey(typeName) == true)
                                        {
                                            dict[typeName].AddCount(obj);
                                        }
                                        else
                                        {
                                            dict.Add(typeName, new HeapObjectCounter(obj));
                                        }
                                    }
                                }
                                else
                                {
                                    AddItem(type, typeName, obj);
                                }
                            }
                        }
                        else
                        {
                            AddItem(type, typeName, obj);
                        }
                    }
                }
            }

            return dict;

            void AddItem(ClrType type, string typeName, ulong objAddress)
            {
                foreach (ClrInstanceField field in type.Fields)
                {
                    ulong fieldAddress = GetFieldAddressValue(runtime, field, objAddress);

                    if (instances.Contains(fieldAddress) == true)
                    {
                        if (dict.ContainsKey(typeName) == true)
                        {
                            dict[typeName].AddCount(objAddress);
                        }
                        else
                        {
                            dict.Add(typeName, new HeapObjectCounter(objAddress));
                        }
                    }
                }
            }
        }

        private static HashSet<ulong> GetObjectListByName(ClrRuntime runtime, string typeNameOrAddress)
        {
            HashSet<ulong> list = new HashSet<ulong>();

            ulong result;
            if (UInt64.TryParse(typeNameOrAddress, System.Globalization.NumberStyles.AllowHexSpecifier, null, out result) == true)
            {
                list.Add(result);
                return list;
            }

            if (!runtime.Heap.CanWalkHeap)
            {
                Console.WriteLine("Cannot walk the heap!");
            }
            else
            {
                foreach (ClrSegment seg in runtime.Heap.Segments)
                {
                    for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                    {
                        ClrType type = runtime.Heap.GetObjectType(obj);

                        if (type == null)
                        {
                            continue;
                        }

                        int gen = runtime.Heap.GetGeneration(obj);
                        if (gen != 2)
                        {
                            continue;
                        }

                        if (type.ToString() != typeNameOrAddress)
                        {
                            continue;
                        }

                        list.Add(obj);
                    }
                }
            }

            return list;
        }

        private static void DumpHeapObject(ClrRuntime runtime)
        {
            if (!runtime.Heap.CanWalkHeap)
            {
                Console.WriteLine("Cannot walk the heap!");
            }
            else
            {
                foreach (ClrSegment seg in runtime.Heap.Segments)
                {
                    for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                    {
                        ClrType type = runtime.Heap.GetObjectType(obj);

                        if (type == null)
                        {
                            continue;
                        }

                        ulong size = type.GetSize(obj);
                        Console.WriteLine("{0,12:X} {1,8:n0} {2,1:n0} {3}", obj, size, seg.GetGeneration(obj), type.Name);
                    }
                }
            }
        }

        private static void DumpHeapStat(ClrRuntime runtime)
        {
            Console.WriteLine("{0,12} {1,12} {2,12} {3,12} {4,4} {5}", "Start", "End", "CommittedEnd", "ReservedEnd", "Heap", "Type");

            foreach (ClrSegment segment in runtime.Heap.Segments)
            {
                string type;
                if (segment.IsEphemeral)
                    type = "Ephemeral";
                else if (segment.IsLarge)
                    type = "Large";
                else
                    type = "Gen2";

                Console.WriteLine("{0,12:X} {1,12:X} {2,12:X} {3,12:X} {4,4} {5}", segment.Start, segment.End, segment.CommittedEnd, segment.ReservedEnd, segment.ProcessorAffinity, type);
            }

            ClrHeap heap = runtime.Heap;
            foreach (var item in (from seg in heap.Segments
                                  group seg by seg.ProcessorAffinity into g
                                  orderby g.Key
                                  select new
                                  {
                                      Heap = g.Key,
                                      Size = g.Sum(p => (uint)p.Length)
                                  }))
            {
                Console.WriteLine("Heap {0,2}: {1:n0} bytes", item.Heap, item.Size);
            }
        }
    }

    public class ObjectHistogramItem : IComparable<ObjectHistogramItem>
    {
        public string Key;
        public HeapObjectCounter Counter;

        public int CompareTo(ObjectHistogramItem other)
        {
            return Counter.Total.CompareTo(other.Counter.Total);
        }
    }

    public class HeapObjectCounter : IEnumerable<KeyValuePair<ulong, int>>
    {
        Dictionary<ulong, int> _owners = new Dictionary<ulong, int>();
        int _total;

        public HeapObjectCounter(ulong address)
        {
            AddCount(address);
        }

        public int OwnerCount
        {
            get
            {
                return _owners.Keys.Count;
            }
        }

        public int Total
        {
            get
            {
                return _total; // _owners.Values.Sum();
            }
        }

        public IEnumerator<KeyValuePair<ulong, int>> GetEnumerator()
        {
            return this._owners.GetEnumerator();
        }

        internal void AddCount(ulong objAddress)
        {
            if (_owners.ContainsKey(objAddress) == true)
            {
                _owners[objAddress]++;
            }
            else
            {
                _owners[objAddress] = 1;
            }

            _total++;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this._owners.GetEnumerator();
        }
    }
}

그래서 위의 프로그램을 이 글에서 만든 예제 프로젝트의 덤프에 대해 실행하면,

c:\temp\RefOwner\bin> RefOwner32.exe c:\tmp\ConsoleApp1.dmp System.String
Found CLR Version: v4.7.3260.00
Filesize:  6EF000
Timestamp: 5BB7BCB7
Dac File:  mscordacwks_X86_X86_4.7.3260.00.dll
Local dac location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll

System.String, # of instances: 100034
1 System.AppDomainSetup(1)
31 System.Object[](3)
100003 System.String[](2)
Total: 100035

총 100,034개의 문자열 인스턴스 중에 무려 100,003개가 단 2개의 System.String[] 객체에 참조되고 있는 것을 볼 수 있습니다. 여기서 "/d" 옵션을 주면 2개의 System.String[]에 대한 인스턴스 주소와 그것들이 각각 참조하고 있는 System.String 객체의 수를 알 수 있습니다.

c:\temp\RefOwner\bin> RefOwner32.exe /d c:\tmp\ConsoleApp1.dmp System.String
...[생략]...
System.String, # of instances: 100034
System.String, # of instances: 100034
1 System.AppDomainSetup(1)
        [2b714e0, 1]
31 System.Object[](3)
        [3b71020, 1]
        [3b72300, 2]
        [3b72520, 28]
100003 System.String[](2)
        [2b7156c, 3]
        [3bd5570, 100000]
Total: 100035

보는 바와 같이, 두 번째 인스턴스인 3bd5570 객체가 100,000개의 String을 담고 있는데, 테스트 코드에서 100,000번의 루프로 Guid 문자열을 담은 것과 일치하는군요. ^^

windbg에서 위의 주소에 따라 dump를 해보면,

0:000> !do 3bd5570
Name:        System.String[]
MethodTable: 718ff698
EEClass:     714d4b80
Size:        524300(0x8000c) bytes
Array:       Rank 1, Number of elements 131072, Type CLASS (Print Array)
Fields:
None

배열의 크기 자체는 131,072라고 합니다. 즉, 그 배열 중에 String이 할당된 것은 100,000개이고 이후의 요소들은 모두 null입니다. 그럼, 여기서 다시 3bd5570 객체의 소유주를 보면,

c:\temp\RefOwner\bin\x86> RefOwner32.exe /d c:\tmp\ConsoleApp1.dmp 3bd5570
Found CLR Version: v4.7.3260.00
Filesize:  6EF000
Timestamp: 5BB7BCB7
Dac File:  mscordacwks_X86_X86_4.7.3260.00.dll
Local dac location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll

3bd5570, # of instances: 1
1 System.Collections.Generic.List<System.String>(1)
        [2b720a4, 1]
Total: 1

답이 나왔습니다. (만들어 놓고 보니 그런대로 쓸만한 도구가 된 듯합니다. ^^)




참고로, ClrMD 라이브러리 사용 시 다음과 같은 오류가 발생할 수 있습니다.

System.InvalidOperationException
  HResult=0x80131509
  Message=Mismatched architecture between this process and the dac.
  Source=Microsoft.Diagnostics.Runtime
  StackTrace:
   at Microsoft.Diagnostics.Runtime.DataTarget.ConstructRuntime(ClrInfo clrInfo, String dac) in /_/src/Microsoft.Diagnostics.Runtime/src/DataTargets/DataTarget.cs:line 326
   at Microsoft.Diagnostics.Runtime.DataTarget.CreateRuntime(ClrInfo clrInfo, String dacFilename, Boolean ignoreMismatch) in /_/src/Microsoft.Diagnostics.Runtime/src/DataTargets/DataTarget.cs:line 320
   at Microsoft.Diagnostics.Runtime.ClrInfo.CreateRuntime(String dacFilename, Boolean ignoreMismatch) in /_/src/Microsoft.Diagnostics.Runtime/src/DataTargets/ClrInfo.cs:line 115
   at RefOwner.Program.Main(String[] args) in c:\temp\RefOwner\RefOwner\RefOwner\Program.cs:line 39

이유는? 현재 프로세스는 x86이면서, 분석 대상이 되는 덤프는 x64 프로세스를 뜬 경우에 해당합니다. 따라서 분석하려는 프로세스를 x64로 빌드해 다시 실행하면 정상적으로 동작합니다.




이 글의 소스 코드는 https://github.com/stjeong/RefOwner/에도 있습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/11/2019]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 16  17  18  19  20  [21]  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13099정성태7/14/20227799.NET Framework: 2031. C# 11 - 사용자 정의 checked 연산자파일 다운로드1
13098정성태7/13/20226083개발 환경 구성: 647. Azure - scale-out 상태의 App Service에서 특정 인스턴스에 요청을 보내는 방법 [1]
13097정성태7/12/20225491오류 유형: 817. Golang - binary.Read: invalid type int32
13096정성태7/8/20228246.NET Framework: 2030. C# 11 - UTF-8 문자열 리터럴
13095정성태7/7/20226322Windows: 208. AD 도메인에 참여하지 않은 컴퓨터에서 Kerberos 인증을 사용하는 방법
13094정성태7/6/20226025오류 유형: 816. Golang - "short write" 오류 원인
13093정성태7/5/20226951.NET Framework: 2029. C# - HttpWebRequest로 localhost 접속 시 2초 이상 지연
13092정성태7/3/20227888.NET Framework: 2028. C# - HttpWebRequest의 POST 동작 방식파일 다운로드1
13091정성태7/3/20226709.NET Framework: 2027. C# - IPv4, IPv6를 모두 지원하는 서버 소켓 생성 방법
13090정성태6/29/20225842오류 유형: 815. PyPI에 업로드한 패키지가 반영이 안 되는 경우
13089정성태6/28/20226321개발 환경 구성: 646. HOSTS 파일 변경 시 Edge 브라우저에 반영하는 방법
13088정성태6/27/20225442개발 환경 구성: 645. "Developer Command Prompt for VS 2022" 명령행 환경의 폰트를 바꾸는 방법
13087정성태6/23/20228399스크립트: 41. 파이썬 - FastAPI / uvicorn 호스팅 환경에서 asyncio 사용하는 방법 [1]
13086정성태6/22/20227816.NET Framework: 2026. C# 11 - 문자열 보간 개선 2가지파일 다운로드1
13085정성태6/22/20227880.NET Framework: 2025. C# 11 - 원시 문자열 리터럴(raw string literals)파일 다운로드1
13084정성태6/21/20226518개발 환경 구성: 644. Windows - 파이썬 2.7을 msi 설치 없이 구성하는 방법
13083정성태6/20/20227092.NET Framework: 2024. .NET 7에 도입된 GC의 메모리 해제에 대한 segment와 region의 차이점 [2]
13082정성태6/19/20226136.NET Framework: 2023. C# - Process의 I/O 사용량을 보여주는 GetProcessIoCounters Win32 API파일 다운로드1
13081정성태6/17/20226211.NET Framework: 2022. C# - .NET 7 Preview 5 신규 기능 - System.IO.Stream ReadExactly / ReadAtLeast파일 다운로드1
13080정성태6/17/20226831개발 환경 구성: 643. Visual Studio 2022 17.2 버전에서 C# 11 또는 .NET 7.0 preview 적용
13079정성태6/17/20224570오류 유형: 814. 파이썬 - Error: The file/path provided (...) does not appear to exist
13078정성태6/16/20226590.NET Framework: 2021. WPF - UI Thread와 Render Thread파일 다운로드1
13077정성태6/15/20226923스크립트: 40. 파이썬 - PostgreSQL 환경 구성
13075정성태6/15/20225883Linux: 50. Linux - apt와 apt-get의 차이 [2]
13074정성태6/13/20226185.NET Framework: 2020. C# - NTFS 파일에 사용자 정의 속성값 추가하는 방법파일 다운로드1
13073정성태6/12/20226391Windows: 207. Windows Server 2022에 도입된 WSL 2
... 16  17  18  19  20  [21]  22  23  24  25  26  27  28  29  30  ...