Hwan'Story › @kim-hwan

Windows 11에서 시작 프로그램이 늦게 실행되는 이유와 WaitForIdleState

· (수정 ) · 조회 38

STARTUP DELAY

Windows 11에서 시작 프로그램이 늦게 실행되는 이유와 WaitForIdleState

예전에 레지스트리 Run 키에 프로그램을 자동 실행으로 등록해 두었는데, 로그인하고 바탕화면이 다 떴는데도 프로그램이 바로 실행되지 않고 1~2분, 길게는 몇 분 뒤에야 실행되는 증상이 있었다. 그때 구글에서 검색해 보니 Windows 11 부터 유휴 상태를 감지해서 시작 프로그램을 늦게 실행하는 동작이 있고, Serialize 레지스트리 키에 StartupDelayInMSec, WaitForIdleState 값을 0 으로 넣으면 해결된다는 내용이 있어서 그대로 적용했다. 실제로 해결은 됐지만 그때는 그 정도만 적용하고 넘어가서, 정확히 무엇을 기다리는 동작인지, 언제부터 생긴 것인지는 알아보지 않았다.

이번에 이 내용을 정리하면서 AI 를 이용해서 관련 자료를 다시 조사하고 explorer.exe 를 리버스 엔지니어링해서 실제 동작을 확인해 봤다. 자료를 조사해 보니 MS 공식 문서가 없는 값이라 무엇을 기다리는지, 언제부터 생긴 것인지는 글마다 설명이 조금씩 달랐다. 이 글에서는 시작 프로그램이 실행되는 흐름과 "유휴 상태" 판정 기준, 관련 레지스트리 값, 버전별 변화를 정리한다.

AI 사용 안내 이 글은 AI 를 이용해서 작성했다. 사용한 도구는 Anthropic 의 Claude Code, 모델은 Claude Opus 5.5 (claude-opus-5-5) 이다. 내가 직접 한 것은 예전에 증상을 겪고 구글 검색으로 관련 레지스트리 값을 찾아 적용한 것까지이다. 이 글에 나오는 자료 조사(웹 검색), explorer.exe 리버스 엔지니어링(바이너리·심볼 수집, 분석 스크립트 작성, 디스어셈블, 코드 해석), 본문 작성은 모두 AI 가 했다.

핵심 요약

  • Run 키, 시작프로그램 폴더에 등록한 프로그램은 로그인 후 explorer.exe 가 별도 스레드에서 실행한다.
  • 현재 Windows 11(24H2 이후)은 기본으로 CPU 유휴 75% 이상, 디스크 유휴 85% 이상인 상태가 0.3초 동안 이어질 때까지 기다렸다가 시작 프로그램을 실행한다. 한 번 기다릴 때 최대 30초이다.
  • 이 대기는 처음 한 번만 하는 것이 아니라 시작 프로그램을 하나 실행할 때마다 다시 한다. 그래서 시작 프로그램이 많고 무거울수록 지연이 쌓인다.
  • 관련 값은 모두 HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize 아래의 DWORD 값이다. 코드에 들어 있는 실제 이름은 WaitforIdleState 이다. (레지스트리 값 이름은 대소문자를 구분하지 않으므로 WaitForIdleState 로 써도 동작한다)
  • 유휴 대기 코드는 Windows 11 22H2 의 2023년 4월 업데이트(22621.1635)에 처음 들어갔고, 처음에는 기능 플래그로 꺼져 있다가 24H2 부터 기본으로 켜졌다. Windows 10 에는 이 코드가 없다.

시작 프로그램 실행 방식의 변화

본론에 앞서 Windows 에서 시작 프로그램을 실행하는 방식이 어떻게 바뀌어 왔는지 간단하게 정리하면 아래와 같다.

시기 내용
Windows 3.x win.ini 의 [windows] 섹션 load=, run= 과 프로그램 관리자의 StartUp 그룹
Windows NT win.ini 의 load=, run= 이 레지스트리 HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows 의 Load, Run 값에 대응
Windows 95 레지스트리 ...\Windows\CurrentVersion\Run, RunOnce 키 (HKLM, HKCU)
Windows Vista 시작 프로그램을 처음 60초 동안 낮은 CPU, I/O 우선순위로 실행 (boxing)
Windows 7 boxing 지연 값(Delay_Sec)이 0 이 되어 사실상 꺼짐
Windows 8 시작 프로그램을 몇 초 늦게 실행하고, 앞 프로그램이 안정된 뒤 다음 프로그램을 실행
Windows 11 시스템 전체가 유휴 상태가 될 때까지 기다림 (이 글의 주제)

Run 키가 Windows 95 에서 처음 생겼다고 직접 써 있는 MS 문서는 찾지 못했다. (Windows 95 대상 KB 문서에 Run 계열 키가 정리되어 있고, 3.x 는 win.ini 방식이었다)

재밌는 점은 지금 Windows 11 의 explorer.exe 에도 win.ini 시절의 Load, Run 값을 읽어서 실행하는 코드(ProcessOldRunAndLoadEquals)와 Vista 의 boxing 코드(CAppBox)가 아직 남아 있다는 것이다.

왜 시작 프로그램을 늦게 실행할까

MS 의 Raymond Chen 이 2016년에 쓴 Startup apps start up when I tell them 글에 이유가 나와 있다.

  • 탐색기는 로그인 후 눈에 보이는 것부터 준비하도록 되어 있고, 시작 프로그램은 가장 낮은 우선순위로 처리한다.
  • Run 키와 시작프로그램 폴더의 프로그램들이 한꺼번에 디스크에서 읽히면 시스템이 버벅여서 시작 메뉴조차 열리지 않는 상황이 생긴다.
  • 그래서 지금까지 낮은 우선순위로 실행하기(Vista), 앞 프로그램이 안정된 뒤 다음 프로그램 실행하기(Windows 8), 그냥 더 오래 기다리기 같은 방법을 써 왔다고 한다.

Windows 11 의 유휴 대기도 이 연장선으로 보인다.

분석 방법

MS 는 Windows 바이너리의 공개 심볼(PDB)을 심볼 서버로 제공한다. 공개 심볼에는 함수 이름이 들어 있어서 디스어셈블한 코드에 RunStartupAppsThreadProc, CStartupTracker::WaitForIdleState 같은 이름을 붙여서 볼 수 있다.

  • 분석 대상: Windows 11 25H2 PC 의 C:\Windows\explorer.exe (10.0.26100.9444)
  • 심볼: MS 심볼 서버(msdl.microsoft.com)의 공개 PDB
  • 과거 버전: Winbindex 에서 버전별 explorer.exe 정보를 찾아서 MS 심볼 서버에서 원본 파일을 받았다. (Windows 10 1507 부터 Windows 11 24H2 까지 17개 빌드)
  • 도구: Python 의 pefile, capstone 으로 PE 파일을 읽고 디스어셈블했다.
  • 분석 수행: 위 과정은 모두 Claude Code 에서 Claude Opus 5.5 가 수행했다. PDB 파서와 디스어셈블 스크립트를 AI 가 작성해서 실행했고, 이 글의 코드 해석과 의사 코드도 AI 가 디스어셈블 결과를 읽고 정리한 것이다.

심볼 서버에서 파일을 받는 주소는 아래와 같은 형식이다. exe 는 PE 헤더의 TimeDateStamp, SizeOfImage 로, pdb 는 Debug Directory 의 RSDS 레코드에 있는 GUID 와 Age 로 찾는다. (PE 구조는 Windows PE(Portable Executable) File Format 시리즈에서 정리한 적이 있다)

https://msdl.microsoft.com/download/symbols/explorer.exe/{TimeDateStamp:08X}{SizeOfImage:x}/explorer.exe
https://msdl.microsoft.com/download/symbols/explorer.pdb/{GUID}{Age}/explorer.pdb

정적 분석만 했고, 실제로 부팅해서 대기 시간을 잰 것은 아니다.

시작 프로그램 실행 흐름

로그인하면 explorer.exe 는 RunStartupAppsThreadProc 스레드에서 시작 프로그램을 실행한다. 별도 스레드라서 바탕화면과 작업 표시줄은 먼저 사용할 수 있고, 시작 프로그램만 뒤에서 순서대로 실행된다.

RunStartupAppsThreadProc
 ├─ 안전 모드이면 Run 키, 시작프로그램 폴더를 건너뜀
 ├─ waitIdle = WaitforIdleState 값 (값이 없으면 1)
 ├─ delay = StartupDelayInMSec 값 (최대 1시간)
 │    값이 없으면 waitIdle 이 켜져 있을 때 0, 꺼져 있을 때 10초
 ├─ delay 만큼 대기
 ├─ waitIdle 이면 WaitForIdleState()               ← 첫 번째 유휴 대기
 └─ 시작 프로그램 실행
      1. win.ini 시절의 Load, Run 값
      2. HKLM\...\Policies\Explorer\Run
      3. HKLM\...\CurrentVersion\Run
      4. HKCU\...\Policies\Explorer\Run
      5. HKCU\...\CurrentVersion\Run
      6. 패키지 앱(MSIX)의 StartupTask
      7. WOW6432Node 의 Run (SysWOW64\runonce.exe 에 맡김)
      8. 시작프로그램 폴더
      9. 다시 시작 가능한 앱 복원
     10. HKCU\...\CurrentVersion\RunOnce

여기서 Run 키(2~5), StartupTask(6), 시작프로그램 폴더(8)는 프로그램을 하나 실행할 때마다 CStartupTracker::WaitForSteadyState() 를 호출한다. 이 함수는 WaitforIdleState 가 켜져 있으면 방금 실행한 프로세스와 상관없이 시스템 전체 유휴 대기(WaitForIdleState)를 하고, 꺼져 있으면 방금 실행한 그 프로세스가 잠잠해질 때까지 기다린다. (뒤에서 따로 정리한다)

10초는 어디서 나오나

"Windows 시작 프로그램 지연은 10초" 라는 내용을 많이 볼 수 있는데, 코드에서 10초(0x2710)가 나오는 부분은 아래와 같다. (필요한 부분만 남기고 주소는 생략했다)

mov    rcx, 0xffffffff80000001     ; HKEY_CURRENT_USER
call   RegGetValueW                ; Serialize\WaitforIdleState (없으면 waitIdle = 1 유지)
...
call   RegGetValueW                ; Serialize\StartupDelayInMSec
test   eax, eax
je     use_value                   ; 값이 있으면 그 값 사용
call   AppSaver::IsAppRestartEnabled
jne    delay_zero                  ; 다시 시작 가능한 앱 자동 저장이 켜져 있으면 0
cmp    dword ptr [waitIdle], 0
jne    delay_zero                  ; 유휴 대기가 켜져 있으면 0
mov    r8d, 0x2710                 ; 10000ms

즉 10초는 StartupDelayInMSec 값이 없고, 유휴 대기가 꺼져 있고, "다시 시작 가능한 앱 자동 저장" 설정(로그인 옵션)이 꺼져 있을 때만 쓰인다. 유휴 대기가 기본으로 켜진 현재 Windows 11 에서는 고정 대기가 0 이고, 대신 유휴 대기를 한다.

참고로 RegGetValueW 에 RRF_RT_REG_DWORD 를 지정해서 읽기 때문에 값을 문자열(REG_SZ)로 만들면 값이 없는 것으로 처리된다. 또 HKCU 만 읽으므로 HKLM 에 만들면 아무 효과가 없다.

"유휴 상태" 판정 기준

CStartupTracker::WaitForIdleState 를 디스어셈블한 코드를 읽기 쉽게 C 로 옮기면 아래와 같다. (변수 이름은 임의로 붙였고, 시간 단위는 ms 로 단순화했다)

HRESULT CStartupTracker::WaitForIdleState()
{
    if (ProcessWaitTimeoutInMSec == 0)
        return S_OK;

    HANDLE disk = _GetSystemDiskHandle();   // Windows 가 설치된 볼륨의 물리 디스크
    IDLE_STATISTICS prev, cur;
    _GetIdleStatistics(&prev, disk);        // 현재 시각, CPU 유휴 시간, 디스크 유휴 시간

    ULONGLONG start = prev.time;
    ULONGLONG idleSince = prev.time;

    for (;;)
    {
        if (prev.time - start >= ProcessWaitTimeoutInMSec)          // 기본 30초
            return HRESULT_FROM_WIN32(ERROR_TIMEOUT);

        SHProcessMessagesUntilEvent(NULL, NULL, 100);               // 메시지를 처리하면서 100ms 대기
        _GetIdleStatistics(&cur, disk);

        DWORD elapsed = cur.time - prev.time;
        if (elapsed < 10)
            continue;

        DWORD cpuIdle  = (cur.cpuIdle - prev.cpuIdle) / cpuCount * 100 / elapsed;
        DWORD diskIdle = (cur.diskIdle - prev.diskIdle) * 100 / elapsed;

        if (cpuIdle < IdleCPUPctMin ||                               // 기본 75
            (disk != INVALID_HANDLE_VALUE && diskIdle < IdleDiskPctMin))  // 기본 85
        {
            idleSince = cur.time;                                    // 바쁘면 처음부터 다시
        }

        if (cur.time - idleSince >= SteadyTimeNeededInMSec)          // 기본 300ms
            return S_OK;

        prev = cur;
    }
}

정리하면 아래와 같다.

  1. 100ms 마다 CPU 와 디스크의 유휴 시간을 잰다.
  2. CPU 유휴율은 시스템 전체 유휴 시간 증가량을 논리 프로세서 수와 경과 시간으로 나눈 비율이다. (NtQuerySystemInformation 의 SystemPerformanceInformation)
  3. 디스크 유휴율은 Windows 가 설치된 볼륨이 올라가 있는 물리 디스크(\\.\PhysicalDriveN)의 유휴 시간 증가량이다. (IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS 로 디스크를 찾고 IOCTL_DISK_GET_PERFORMANCE_INFO 의 IdleTime 을 읽는다)
  4. CPU 75%, 디스크 85% 중 하나라도 못 미치면 다시 처음부터 잰다.
  5. 두 조건을 모두 만족하는 상태가 300ms 연속되면 통과한다.
  6. 30초가 지나도 통과하지 못하면 포기하고 그냥 다음으로 진행한다. (타임아웃 오류를 반환하지만 호출하는 쪽에서 무시한다)

특이사항으로는 디스크 성능 정보를 읽을 때 DiskPerformanceTypeAllPriority 를 지정해서 우선순위가 낮은 I/O 까지 모두 "바쁨" 으로 센다. 로그인 직후에는 백신 검사, 검색 인덱싱, 업데이트, 클라우드 동기화 같은 낮은 우선순위의 백그라운드 I/O 가 많은데 이것도 모두 디스크가 바쁜 것으로 판정된다. 디스크가 0.3초 동안 85% 이상 쉬는 순간이 생각보다 잘 안 오는 것이다.

여기에 앞에서 본 것처럼 시작 프로그램을 하나 실행할 때마다 이 대기를 다시 하므로, 계산상 최악의 경우 대기 시간은 30초 + 시작 프로그램 수 × 30초 이다. 시작 프로그램이 5개이고 매번 타임아웃까지 간다면 약 3분이 된다. (몇 분 늦게 실행된다는 증상과 맞는다)

유휴 대기를 끄면: 프로세스 안정 대기

WaitforIdleState 를 0 으로 하거나 Windows 10 에서는 시스템 전체가 아니라 방금 실행한 프로세스만 본다. (CStartupTracker::_WaitForSteadyStateEx)

  • 방금 실행한 프로세스의 CPU 사용 시간을 100ms 마다 잰다.
  • 그 프로세스의 CPU 사용률이 ProcessCPUPercentMax(10%, 코어 1개 기준) 이하인 상태가 300ms 이어지거나 프로세스가 종료되면 통과한다.
  • 최대 ProcessWaitTimeoutInMSec 만큼 기다린다. 유휴 대기가 꺼져 있으면 기본값이 10초이다.
  • ProcessHardFaultsMax 값도 읽기는 하지만 현재 빌드에서는 하드 폴트 수를 항상 0 으로 채우고 있어서 사실상 의미가 없다.

Raymond Chen 의 글에서 말한 "앞 프로그램이 안정된 뒤 다음 프로그램을 실행한다" 가 이 동작이다.

Serialize 키에서 읽는 값

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize 아래에서 읽는 값은 아래와 같다. (모두 REG_DWORD, 기본 설치 상태에서는 키 자체가 없다)

값 기본값 최대 의미
StartupDelayInMSec 유휴 대기 켜짐 0 / 꺼짐 10000 3600000 시작 프로그램 실행 전 고정 대기
WaitforIdleState 1 - 0 이 아니면 유휴 대기 사용
ProcessWaitTimeoutInMSec 유휴 대기 켜짐 30000 / 꺼짐 10000 60000 대기 1회의 최대 시간. 0 이면 대기하지 않음
IdleCPUPctMin 75 - CPU 유휴율 기준(%)
IdleDiskPctMin 85 - 디스크 유휴율 기준(%)
SteadyTimeNeededInMSec 300 - 조건을 연속으로 만족해야 하는 시간
ProcessCPUPercentMax 10 100 유휴 대기가 꺼져 있을 때 프로세스 CPU 사용률 기준(%)
ProcessHardFaultsMax 16 - 읽기만 하고 사실상 사용되지 않음

IdleCPUPctMin, IdleDiskPctMin 은 유휴 대기가 켜져 있을 때만 읽는다.

버전별 변화

Winbindex 에서 버전별 explorer.exe 를 받아 WaitforIdleState 관련 코드가 언제 생겼는지 확인했다.

버전 내용
Windows 10 1507 ~ 22H2 마지막 빌드 (19041.6392, 2025년 9월) 유휴 대기 코드 없음. 고정 10초 + 프로그램마다 프로세스 안정 대기(최대 10초)
Windows 11 21H2 (22000.120 ~ 22000.3079) 유휴 대기 코드 없음
Windows 11 22H2 (22621.317 ~ 22621.1485) 유휴 대기 코드 없음. 고정 10초
22621.1635 (2023년 4월 25일 미리 보기 KB5025305, 5월 정기 업데이트 KB5026372 부터 모든 22H2 에 배포) 유휴 대기 코드 추가. Feature_ModerateStartupApps 기능 플래그로 감싸져 있고 상위 플래그가 기본 꺼짐
Windows 11 23H2 (22621.2428, 2023년 10월 31일) 위와 같음
Windows 11 24H2 (26100.863, 2024년 6월, Winbindex 에서 가장 이른 24H2 빌드) 기능 플래그가 기본으로 켜짐
Windows 11 25H2 (분석한 PC, 26100.9444) 기능 플래그 검사 자체가 사라짐. 항상 유휴 대기

22H2 와 23H2 는 같은 바이너리를 쓰고, 23H2 는 기능을 켜 주는 활성화 패키지 형태로 배포되었다. 2024년 초부터 "23H2 로 업데이트한 뒤 시작 프로그램이 1~3분 늦게 실행된다" 는 글이 올라오기 시작한 것과 시기가 맞는다. 다만 기능 플래그는 MS 가 단계적으로 켜는 방식이라 정확히 언제 켜졌는지는 바이너리만으로는 알 수 없다.

설정 조합별 동작

코드를 기준으로 설정 조합별 동작을 정리하면 아래와 같다.

설정 동작
아무 값도 없음 (기본) 고정 대기 없음 + 첫 유휴 대기(최대 30초) + 프로그램마다 유휴 대기(최대 30초)
WaitforIdleState = 0 만 고정 10초 + 프로그램마다 프로세스 안정 대기(최대 10초)
StartupDelayInMSec = 0, WaitforIdleState = 0 고정 대기 없음 + 프로그램마다 프로세스 안정 대기(최대 10초)
ProcessWaitTimeoutInMSec = 0 유휴 대기, 프로세스 안정 대기 모두 바로 통과 (코드상 그렇고 실제로 확인하지는 않았다)

WaitforIdleState 만 0 으로 하면 10초 고정 대기가 다시 생기기 때문에 두 값을 같이 0 으로 넣어야 한다고 알려진 것이다. (다시 시작 가능한 앱 자동 저장 설정이 켜져 있으면 이 10초도 0 이 된다)

일반적으로 알려진 방법대로 두 값을 0 으로 넣으려면 명령 프롬프트에서 아래 명령어를 실행하면 된다.

reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /t REG_DWORD /d 0 /f
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v WaitforIdleState /t REG_DWORD /d 0 /f

이렇게 해도 프로그램마다 최대 10초의 프로세스 안정 대기는 남는다. HKCU 설정이므로 사용자마다 따로 넣어야 하고, 원래 이 대기가 로그인 직후 시스템이 버벅이지 않게 하려고 있는 것이라 시작 프로그램이 많은 PC 에서는 로그인 직후가 더 무거워질 수 있다.

이 대기는 explorer.exe 가 시작 프로그램을 실행할 때만 하는 것이므로, 작업 스케줄러의 로그온 트리거로 등록한 프로그램은 영향을 받지 않는다.

인터넷에 알려진 내용 중 코드와 다른 부분

  • Windows 10 22H2 에서도 WaitForIdleState 를 넣어야 한다
    • Windows 10 마지막 빌드(19041.6392)에도 이 코드가 없어서 넣어도 아무 효과가 없다.
  • 값 이름의 대소문자를 정확히 써야 한다
    • 코드에 있는 이름은 WaitforIdleState 이지만 레지스트리 값 이름은 대소문자를 구분하지 않는다.
  • Windows 11 의 기본 지연은 10초이다
    • Windows 11 초기 빌드(22H2 의 2023년 3월 빌드까지)는 맞다. 24H2 이후 기본은 고정 대기 0 + 유휴 대기이다.
  • StartupDelayInMSec 는 0 이 아니라 1 로 넣어야 한다
    • 코드상 0 이면 대기하지 않고, 1 이면 1ms 를 기다릴 뿐이다.
  • 시작프로그램 폴더에만 적용된다
    • HKLM, HKCU 의 Run 키와 정책 Run 키, 패키지 앱의 StartupTask 모두 적용된다.

확인하지 못한 부분

  • 정적 분석만 했고 실제 부팅 시간은 재지 않았다. explorer.exe 가 유휴 대기 시작과 끝에 Microsoft-Windows-Shell-Core 공급자의 ETW 이벤트(Explorer_Startup_SerializationWait_Start, Explorer_Startup_SerializationWait_Stop)를 남기므로 WPR 로 이 공급자를 기록하면 실제 대기 시간을 확인할 수 있다.
  • WOW6432Node 의 Run 키를 실행하는 SysWOW64\runonce.exe 에도 같은 값 이름이 들어 있지만 이쪽은 분석하지 않았다.
  • Windows 8, 8.1 은 Winbindex 에 바이너리가 없어서 확인하지 못했다. 표의 Windows 8 내용은 Raymond Chen 의 글을 기준으로 했다.
  • 디스어셈블 결과의 해석은 AI 가 한 것이라 틀린 부분이 있을 수 있다. 잘못된 내용이 있으면 댓글로 알려 주면 고치겠다.

마무리

시작 프로그램을 늦게 실행하는 것 자체는 로그인 직후 시스템이 버벅이지 않게 하려는 의도이다. 하지만 디스크 유휴 85% 라는 기준이 꽤 엄격하고, 이 대기를 시작 프로그램마다 다시 하기 때문에 시작 프로그램이 많은 환경에서는 몇 분씩 늦어질 수 있다.

로그인하자마자 바로 떠야 하는 프로그램이라면 StartupDelayInMSec, WaitforIdleState 를 0 으로 넣거나 작업 스케줄러의 로그온 트리거로 실행하는 방법을 생각해 볼 수 있다.

참고

https://devblogs.microsoft.com/oldnewthing/20160502-00/?p=93395

https://devblogs.microsoft.com/oldnewthing/20110817-00/?p=9883

https://techcommunity.microsoft.com/blog/askperf/startup-programs-on-windows-vista-inside-the-box/372954

https://learn.microsoft.com/en-us/answers/questions/4059183/startup-apps-artificially-delayed-on-windows-11

https://techshelps.github.io/MSDN/KB/Source/win95x/q137367.htm

https://jeffpar.github.io/kbarchive/kb/069/Q69844

https://winbindex.m417z.com/

관련 게시글