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

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;
}
}
정리하면 아래와 같다.
- 100ms 마다 CPU 와 디스크의 유휴 시간을 잰다.
- CPU 유휴율은 시스템 전체 유휴 시간 증가량을 논리 프로세서 수와 경과 시간으로 나눈 비율이다. (
NtQuerySystemInformation의SystemPerformanceInformation) - 디스크 유휴율은 Windows 가 설치된 볼륨이 올라가 있는 물리 디스크(
\\.\PhysicalDriveN)의 유휴 시간 증가량이다. (IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS로 디스크를 찾고IOCTL_DISK_GET_PERFORMANCE_INFO의IdleTime을 읽는다) - CPU 75%, 디스크 85% 중 하나라도 못 미치면 다시 처음부터 잰다.
- 두 조건을 모두 만족하는 상태가 300ms 연속되면 통과한다.
- 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://techshelps.github.io/MSDN/KB/Source/win95x/q137367.htm