Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

# Readahead (선행 읽기)

관련 소스: mm/readahead.c, include/linux/pagemap.h, include/linux/fs.h, include/linux/backing-dev-defs.h

개요 (Overview)

Readahead(선행 읽기)는 응용 프로그램이 명시적으로 요청하기 전에 파일의 내용을 page cache에 미리 로드하는 메커니즘이다. readahead.c는 address_space 수준에서 동작하며, 동기 읽기와 비동기 readahead를 결합하여 디스크 I/O와 애플리케이션 처리를 오버랩시킨다. 핵심 아이디어는 "readahead pipelining"으로, 응용 프로그램이 현재 readahead 윈도우의 마지막 async_size 페이지에 도달하면 다음 readahead를 비동기로 시작하여 디스크 대기 시간을 숨기는 것이다. 도서관에서 다음 손님이 읽을 가능성이 높은 책을 미리 카트에 꺼내 두는 것과 비슷하다.

Linux 7.0에서는 large folio 지원이 강화되어 page_cache_ra_order() 경로에서 order > 0인 large folio를 할당할 수 있으며, ra->order 필드를 통해 preferred folio order가 관리된다. readahead는 세 가지 트리거로 발동된다: (1) cache miss 시 동기 readahead (page_cache_sync_ra), (2) PG_readahead 플래그가 설정된 folio 접근 시 비동기 readahead (page_cache_async_ra), (3) readahead() 시스템 호출 및 posix_fadvise(POSIX_FADV_WILLNEED).

핵심 소스 파일:

  • mm/readahead.c — readahead 알고리즘 전체 구현 (841줄)
  • include/linux/pagemap.hstruct readahead_control, DEFINE_READAHEAD, readahead_folio() 등 accessor
  • include/linux/fs.hstruct file_ra_state (readahead 윈도우 상태 추적)
  • include/linux/backing-dev-defs.hstruct backing_dev_info (ra_pages, io_pages)
  • include/trace/events/readahead.h — tracepoint 4종

  • 빠른 점검 명령

    # readahead 관련 커널 심볼 확인
    cat /proc/kallsyms | grep -E 'page_cache_sync_ra|page_cache_async_ra|page_cache_ra_unbounded|page_cache_ra_order|readahead_expand'
    
    # readahead tracepoint 활성화
    echo 1 > /sys/kernel/debug/tracing/events/readahead/enable
    cat /sys/kernel/debug/tracing/trace_pipe | head -20
    
    # readahead tracepoint를 항목별로 확인
    echo 1 > /sys/kernel/debug/tracing/events/readahead/page_cache_sync_ra/enable
    echo 1 > /sys/kernel/debug/tracing/events/readahead/page_cache_async_ra/enable
    echo 1 > /sys/kernel/debug/tracing/events/readahead/page_cache_ra_order/enable
    echo 1 > /sys/kernel/debug/tracing/events/readahead/page_cache_ra_unbounded/enable
    cat /sys/kernel/debug/tracing/trace_pipe
    
    # 특정 BDI의 readahead 윈도우 크기 확인 (ra_pages)
    cat /sys/block/sda/queue/read_ahead_kb
    
    # 파일 시스템별 readahead 설정 확인
    cat /sys/block/<dev>/queue/read_ahead_kb
    
    # readahead 관련 procfs 통계
    cat /proc/diskstats | grep -E 'read_ahead|ra_'
    
    # readahead 시스템 호출 추적
    echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_readahead/enable
    cat /sys/kernel/debug/tracing/trace_pipe
    
    # 메모리 할당 실패 시 readahead 중단 확인
    dmesg | grep -i 'readahead\|oom'
    
    # 페이지 캐시 상태 확인
    cat /proc/meminfo | grep -E 'Cached|Buffers|Active'
    
    # 파일별 readahead 동작 관찰 (strace)
    strace -e trace=readahead,read,pread64 -p <PID>

    핵심 자료구조

    file_ra_state — 파일별 readahead 윈도우 상태

    // include/linux/fs.h:1212-1220
    struct file_ra_state {
    	pgoff_t start;              // 가장 최근 readahead 시작 페이지 인덱스
    	unsigned int size;          // 가장 최근 readahead에서 읽은 총 페이지 수
    	unsigned int async_size;    // 비동기 영역 크기 (이 수만큼 남았을 때 다음 readahead 트리거)
    	unsigned int ra_pages;      // 최대 readahead 윈도우 크기 (bdi->ra_pages에서 복사)
    	unsigned short order;       // large folio order (0이면 일반 4KB 페이지)
    	unsigned short mmap_miss;   // mmap 접근 시 page cache miss 횟수
    	loff_t prev_pos;           // 가장 최근 읽기 요청의 마지막 바이트 위치
    };
  • async_size: readahead 윈도우 내에서 "미리 읽은" 영역의 크기. 첫 번째 async 페이지에 PG_readahead 플래그가 설정됨
  • ra_pages: backing_dev_info.ra_pages에서 복사. 일반적으로 read_ahead_kb / 4 (예: 128KB → 32페이지)
  • order: Linux 7.0의 large folio 지원으로 page_cache_ra_order()에서 사용
  • readahead_control — readahead 요청 컨텍스트

    // include/linux/pagemap.h:1347-1358
    struct readahead_control {
    	struct file *file;                  // 대상 파일 (네트워크 FS에서 인증용, 내부 호출 시 NULL)
    	struct address_space *mapping;      // readahead 대상 address_space
    	struct file_ra_state *ra;           // 파일별 readahead 상태 (선택적)
    /* private: readahead_* accessor 사용 */
    	pgoff_t _index;                     // 현재 readahead 시작 페이지 인덱스
    	unsigned int _nr_pages;             // 남은 readahead 페이지 수
    	unsigned int _batch_count;          // 현재 배치에서 처리된 페이지 수
    	bool dropbehind;                    // dropbehind 플래그 (파일 끝에서 읽을 때 과거 페이지 해제)
    	bool _workingset;                   // workingset 플래그 감지 여부
    	unsigned long _pflags;              // PSI 메모리 스톨 플래그
    };

    backing_dev_info — BDI readahead 설정

    // include/linux/backing-dev-defs.h:168-175
    struct backing_dev_info {
    	u64 id;
    	/* ... */
    	unsigned long __data_racy ra_pages; /* 최대 readahead 크기 (페이지 단위) */
    	unsigned long io_pages;             /* 허용 최대 IO 크기 */
    	/* ... */
    };
    readahead 자료구조 관계도

    핵심 함수

    page_cache_sync_ra() — 동기 readahead 진입점

    // mm/readahead.c:557-631
    void page_cache_sync_ra(struct readahead_control *ractl,
    		unsigned long req_count)
    {
    	pgoff_t index = readahead_index(ractl);
    	bool do_forced_ra = ractl->file && (ractl->file->f_mode & FMODE_RANDOM);
    	struct file_ra_state *ra = ractl->ra;
    	unsigned long max_pages, contig_count;
    	pgoff_t prev_index, miss;
    
    	// readahead 비활성화 또는 블록 cgroup 혼잡 시 강제 읽기로 폴백
    	if (!ra->ra_pages || blk_cgroup_congested()) {
    		if (!ractl->file) return;
    		req_count = 1; do_forced_ra = true;
    	}
    
    	// FMODE_RANDOM 파일 (예: 데이터베이스)은 force_page_cache_ra로 처리
    	if (do_forced_ra) {
    		force_page_cache_ra(ractl, req_count);
    		return;
    	}
    
    	max_pages = ractl_max_pages(ractl, req_count);
    	prev_index = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
    
    	// 캐시 미스 또는 순차 읽기 감지: (index - prev_index) <= 1
    	if (!index || req_count > max_pages || index - prev_index <= 1UL) {
    		ra->start = index;
    		ra->size = get_init_ra_size(req_count, max_pages);
    		ra->async_size = ra->size > req_count ?
    			ra->size - req_count : ra->size >> 1;
    		goto readit;
    	}
    
    	// page cache에서 이전 캐시 이력 검색
    	rcu_read_lock();
    	miss = page_cache_prev_miss(ractl->mapping, index - 1, max_pages);
    	rcu_read_unlock();
    	contig_count = index - miss - 1;
    
    	// 독립적 작은 랜덤 읽기: readahead 상태 오염 방지
    	if (contig_count <= req_count) {
    		do_page_cache_ra(ractl, req_count, 0);
    		return;
    	}
    
    	// 파일 처음부터 캐시된 경우: 강한 순차 스트림 지시
    	if (miss == ULONG_MAX) contig_count *= 2;
    	ra->start = index;
    	ra->size = min(contig_count + req_count, max_pages);
    	ra->async_size = 1;
    readit:
    	ra->order = 0;
    	ractl->_index = ra->start;
    	page_cache_ra_order(ractl, ra);
    }

    역할: cache miss 시 readahead 윈도우를 계산하고 large folio 경로를 통해 I/O를 시작한다.

    분기 로직:

  • ra->ra_pages == 0 또는 blk_cgroup_congested()req_count = 1로 강제 읽기
  • FMODE_RANDOMforce_page_cache_ra()로 2MB 청크 단위 읽기
  • index - prev_index <= 1 → 순차 읽기 → get_init_ra_size()로 초기 윈도우
  • contig_count <= req_count → 랜덤 읽기 → readahead 상태 변경 없이 읽기
  • miss == ULONG_MAX → 파일 처음부터 캐시 → contig_count *= 2
  • page_cache_async_ra() — 비동기 readahead 트리거

    // mm/readahead.c:633-702
    void page_cache_async_ra(struct readahead_control *ractl,
    		struct folio *folio, unsigned long req_count)
    {
    	unsigned long max_pages;
    	struct file_ra_state *ra = ractl->ra;
    	pgoff_t index = readahead_index(ractl);
    	pgoff_t expected, start, end, aligned_end, align;
    
    	if (!ra->ra_pages) return;
    
    	// PG_readahead와 PG_reclaim 비트 공유 → writeback 중이면 건너뜀
    	if (folio_test_writeback(folio)) return;
    
    	folio_clear_readahead(folio);
    
    	if (blk_cgroup_congested()) return;
    
    	max_pages = ractl_max_pages(ractl, req_count);
    
    	// 순차 접근 예상 위치 확인
    	expected = round_down(ra->start + ra->size - ra->async_size,
    			folio_nr_pages(folio));
    	if (index == expected) {
    		// 순차 히트: 윈도우를 앞으로 밀고 크기 증가
    		ra->start += ra->size;
    		ra->size = max(ra->size, get_next_ra_size(ra, max_pages));
    		goto readit;
    	}
    
    	// 비순차 히트 (interleaved reads 등): page cache에서 async_size 추정
    	rcu_read_lock();
    	start = page_cache_next_miss(ractl->mapping, index + 1, max_pages);
    	rcu_read_unlock();
    
    	if (!start || start - index > max_pages) return;
    
    	ra->start = start;
    	ra->size = start - index;
    	ra->size += req_count;
    	ra->size = get_next_ra_size(ra, max_pages);
    readit:
    	ra->order += 2;
    	align = 1UL << min(ra->order, ffs(max_pages) - 1);
    	end = ra->start + ra->size;
    	aligned_end = round_down(end, align);
    	if (aligned_end > ra->start)
    		ra->size -= end - aligned_end;
    	ra->async_size = ra->size;
    	ractl->_index = ra->start;
    	page_cache_ra_order(ractl, ra);
    }

    역할: PG_readahead 플래그가 설정된 folio에 접근했을 때 다음 readahead 윈도우를 계산하고 시작한다.

    분기 로직:

  • folio_test_writeback(folio) → PG_reclaim 비트와 동일 → writeback 중이면 스킵
  • index == expected (순차 히트) → 윈도우 크기 증가 (get_next_ra_size)
  • index != expected (비순차 히트) → page cache에서 이전 비연속 구간 검색 후 윈도우 재설정
  • page_cache_ra_unbounded() — 기본 readahead 루프

    // mm/readahead.c:211-307
    void page_cache_ra_unbounded(struct readahead_control *ractl,
    		unsigned long nr_to_read, unsigned long lookahead_size)
    {
    	struct address_space *mapping = ractl->mapping;
    	unsigned long index = readahead_index(ractl);
    	gfp_t gfp_mask = readahead_gfp_mask(mapping);
    	unsigned long mark = ULONG_MAX, i = 0;
    	unsigned int min_nrpages = mapping_min_folio_nrpages(mapping);
    	unsigned int nofs = memalloc_nofs_save();
    
    	index = mapping_align_index(mapping, index);
    
    	// lookahead 크기 결정: lookahead 영역 시작 인덱스 계산
    	if (lookahead_size <= nr_to_read) {
    		unsigned long ra_folio_index;
    		ra_folio_index = round_up(readahead_index(ractl) +
    					  nr_to_read - lookahead_size,
    					  min_nrpages);
    		mark = ra_folio_index - index;
    	}
    
    	// 페이지 캐시에 folio 사전 할당 루프
    	while (i < nr_to_read) {
    		struct folio *folio = xa_load(&mapping->i_pages, index + i);
    
    		if (folio && !xa_is_value(folio)) {
    			// 이미 존재하는 folio → 현재 배치 플러시 후 건너뜀
    			read_pages(ractl);
    			ractl->_index += min_nrpages;
    			i = ractl->_index + ractl->_nr_pages - index;
    			continue;
    		}
    
    		folio = ractl_alloc_folio(ractl, gfp_mask,
    					mapping_min_folio_order(mapping));
    		if (!folio) break;
    
    		ret = filemap_add_folio(mapping, folio, index + i, gfp_mask);
    		if (ret < 0) {
    			folio_put(folio);
    			if (ret == -ENOMEM) break;
    			read_pages(ractl);
    			/* ... continue */
    		}
    		if (i == mark) folio_set_readahead(folio);  // PG_readahead 플래그 설정
    		ractl->_nr_pages += min_nrpages;
    		i += min_nrpages;
    	}
    
    	read_pages(ractl);
    	memalloc_nofs_restore(nofs);
    }

    역할: 파일 시스템의 readahead 경로에서 직접 호출하는 기본 readahead 구현. 페이지를 먼저 할당한 후 I/O를 제출하여 읽기-쓰기 간의 교차를 방지한다.

    분기 로직:

  • xa_load로 기존 folio 존재 확인 → 있으면 현재 배치 플러시 후 스킵
  • filemap_add_folio 실패 → -ENOMEM이면 중단, 기타 에러이면 플러시 후 계속
  • i == mark → lookahead 영역 시작 → folio_set_readahead()로 PG_readahead 설정
  • page_cache_ra_order() — large folio readahead

    // mm/readahead.c:467-540
    void page_cache_ra_order(struct readahead_control *ractl,
    		struct file_ra_state *ra)
    {
    	struct address_space *mapping = ractl->mapping;
    	pgoff_t start = readahead_index(ractl);
    	unsigned int min_order = mapping_min_folio_order(mapping);
    	pgoff_t limit = (i_size_read(mapping->host) - 1) >> PAGE_SHIFT;
    	pgoff_t mark = index + ra->size - ra->async_size;
    	unsigned int new_order = ra->order;
    
    	if (!mapping_large_folio_support(mapping)) {
    		ra->order = 0;
    		goto fallback;
    	}
    
    	new_order = min(mapping_max_folio_order(mapping), new_order);
    	new_order = min_t(unsigned int, new_order, ilog2(ra->size));
    	new_order = max(new_order, min_order);
    
    	while (index <= limit) {
    		unsigned int order = new_order;
    		// 인덱스 정렬에 따라 order 조정
    		if (index & ((1UL << order) - 1))
    			order = __ffs(index);
    		// EOF를 초과하지 않도록 order 축소
    		while (order > min_order && index + (1UL << order) - 1 > limit)
    			order--;
    		err = ra_alloc_folio(ractl, index, mark, order, gfp);
    		if (err) break;
    		index += 1UL << order;
    	}
    
    	read_pages(ractl);
    fallback:
    	if (ra->size > index - start)
    		do_page_cache_ra(ractl, ra->size - (index - start),
    				 ra->async_size);
    }

    역할: large folio 지원 시 higher-order folio로 readahead. ra->order를 기반으로 페이지 인덱스 정렬과 EOF 제한에 맞춰 order를 동적 조정한다.

    get_next_ra_size() — 다음 readahead 윈도우 크기 계산

    // mm/readahead.c:394-404
    static unsigned long get_next_ra_size(struct file_ra_state *ra,
    				      unsigned long max)
    {
    	unsigned long cur = ra->size;
    
    	if (cur < max / 16)
    		return 4 * cur;    // 작을 때: 4배 증가 (공격적 램프업)
    	if (cur <= max / 2)
    		return 2 * cur;    // 중간: 2배 증가
    	return max;                // 충분히 크면: 최대값 유지
    }

    역할: 순차 readahead에서 윈도우 크기를 점진적으로 증가시키는 ramp-up 함수.

    readahead_expand() — readahead 창 확장

    // mm/readahead.c:766-841
    void readahead_expand(struct readahead_control *ractl,
    		      loff_t new_start, size_t new_len)
    {
    	struct address_space *mapping = ractl->mapping;
    	struct file_ra_state *ra = ractl->ra;
    	pgoff_t new_index, new_nr_pages;
    	gfp_t gfp_mask = readahead_gfp_mask(mapping);
    	unsigned long min_nrpages = mapping_min_folio_nrpages(mapping);
    	unsigned int min_order = mapping_min_folio_order(mapping);
    
    	new_index = new_start / PAGE_SIZE;
    	/* readahead 코드는 호출 전에 ractl->_index를 min_nrpages로 정렬해야 한다. */
    	VM_BUG_ON(!IS_ALIGNED(ractl->_index, min_nrpages));
    
    	/* 앞쪽 경계를 아래로 확장한다. */
    	while (ractl->_index > new_index) {
    		unsigned long index = ractl->_index - 1;
    		struct folio *folio = xa_load(&mapping->i_pages, index);
    
    		if (folio && !xa_is_value(folio))
    			return; /* folio가 이미 존재하는 것으로 보인다 */
    
    		folio = ractl_alloc_folio(ractl, gfp_mask, min_order);
    		if (!folio)
    			return;
    
    		index = mapping_align_index(mapping, index);
    		if (filemap_add_folio(mapping, folio, index, gfp_mask) < 0) {
    			folio_put(folio);
    			return;
    		}
    		if (unlikely(folio_test_workingset(folio)) && !ractl->_workingset) {
    			ractl->_workingset = true;
    			psi_memstall_enter(&ractl->_pflags);
    		}
    		ractl->_nr_pages += min_nrpages;
    		ractl->_index = folio->index;
    	}
    
    	new_len += new_start - readahead_pos(ractl);
    	new_nr_pages = DIV_ROUND_UP(new_len, PAGE_SIZE);
    
    	/* 뒤쪽 경계를 위로 확장한다. */
    	while (ractl->_nr_pages < new_nr_pages) {
    		unsigned long index = ractl->_index + ractl->_nr_pages;
    		struct folio *folio = xa_load(&mapping->i_pages, index);
    
    		if (folio && !xa_is_value(folio))
    			return; /* folio가 이미 존재하는 것으로 보인다 */
    
    		folio = ractl_alloc_folio(ractl, gfp_mask, min_order);
    		if (!folio)
    			return;
    
    		index = mapping_align_index(mapping, index);
    		if (filemap_add_folio(mapping, folio, index, gfp_mask) < 0) {
    			folio_put(folio);
    			return;
    		}
    		if (unlikely(folio_test_workingset(folio)) && !ractl->_workingset) {
    			ractl->_workingset = true;
    			psi_memstall_enter(&ractl->_pflags);
    		}
    		ractl->_nr_pages += min_nrpages;
    		if (ra) {
    			ra->size += min_nrpages;
    			ra->async_size += min_nrpages;
    		}
    	}
    }
    EXPORT_SYMBOL(readahead_expand);

    역할: filesystem ->readahead() 경로에서 현재 요청 창을 앞뒤로 넓혀야 할 때 쓰는 보조 함수다. 중간에 이미 존재하는 folio를 만나면 더 이상 확장하지 않고 멈추며, THP가 끼어들면 요청한 길이보다 더 크게 늘어날 수 있다.


    호출 흐름

    readahead 호출 흐름
    read() / mmap() / fadvise()
      │
      ├─ [cache miss] → page_cache_sync_readahead()
      │                    └→ page_cache_sync_ra()
      │                         ├─ FMODE_RANDOM? → force_page_cache_ra()
      │                         │                    └→ do_page_cache_ra() → page_cache_ra_unbounded()
      │                         ├─ 순차 읽기 감지 → get_init_ra_size() → page_cache_ra_order()
      │                         └─ 랜덤 읽기 → do_page_cache_ra()
      │
      ├─ [PG_readahead 접근] → page_cache_async_readahead()
      │                          └→ page_cache_async_ra()
      │                               ├─ 순차 히트 → get_next_ra_size() → page_cache_ra_order()
      │                               └─ 비순차 히트 → page_cache_next_miss() → get_next_ra_size()
      │
      └─ readahead() 시스템 호출 → ksys_readahead()
                                      └→ vfs_fadvise(POSIX_FADV_WILLNEED)
                                           └→ page_cache_sync_readahead()
    
    page_cache_ra_order()
      ├─ mapping_large_folio_support() 확인
      ├─ ra_alloc_folio() 루프 (large folio 할당)
      │    └→ ractl_alloc_folio() → filemap_alloc_folio()
      │    └→ filemap_add_folio()
      └─ fallback: do_page_cache_ra() (일반 readahead 경로)
    
    page_cache_ra_unbounded()
      ├─ memalloc_nofs_save() (VM 재할당 방지)
      ├─ folio 사전 할당 루프
      │    ├─ xa_load() → 기존 folio 확인
      │    ├─ ractl_alloc_folio() → filemap_alloc_folio()
      │    └─ filemap_add_folio() → page cache에 추가
      └─ read_pages()
           ├─ aops->readahead() → 파일시스템 readahead 핸들러
           │    └→ readahead_folio() 반복
           └─ aops->read_folio() → 개별 folio 읽기 (폴백)
    
    filesystem ->readahead() 구현
      └─ 필요 시 readahead_expand()로 요청 창을 앞뒤로 확장

    조건별 비교

    readahead 트리거 비교

    조건동기 readahead비동기 readahead강제 읽기
    **진입점**`page_cache_sync_ra()``page_cache_async_ra()``force_page_cache_ra()`
    **트리거**cache missPG_readahead 플래그 접근FMODE_RANDOM 또는 ra_pages=0
    **윈도우 계산**`get_init_ra_size()``get_next_ra_size()`요청 크기 그대로
    **async_size**`size - req_count``size` (전체)0
    **large folio**지원지원비활성 (일반 4KB)
    **상태 변경**ra->start/size 설정윈도우 전진없음

    readahead 윈도우 ramp-up 비교

    현재 크기배율설명
    `cur < max/16`×4초기 공격적 램프업
    `cur ≤ max/2`×2점진적 증가
    `cur > max/2`max최대 윈도우 유지

    folio 처리 경로 비교

    시나리오처리 방식
    기존 folio 존재 (`xa_load` 성공)현재 배치 플러시 후 스킵
    새 folio 할당 성공filemap_add_folio로 page cache 추가
    할당 실패 (`-ENOMEM`)readahead 중단
    filemap_add_folio 실패 (기타)배치 플러시 후 계속
    PG_readahead 마크 도달`folio_set_readahead()` 설정

    readahead pipelining 원리

    |==================#===========================|
    ^start             ^PG_readahead               ^end
    |<----- async_size -------->|
    |<------------- size ----------------------->|
    
    1. 응용 프로그램이 start ~ # 사이를 읽음 (동기)
    2. # 에 도달하면 다음 readahead 시작 (비동기)
    3. 응용 프로그램이 나머지 async 영역을 읽는 동안
       디스크에서 다음 윈도우 로딩
    4. 다음 윈도우 로딩 완료 시까지 응용 프로그램이
       현재 윈도우를 소비하면 I/O 대기 없음
  • async_size가 클수록 파이프라이닝 효과가 크지만 메모리 사용량 증가
  • ra_pagesmax 역할을 하여 윈도우 상한 제한
  • 순차 스트림이 확립되면 동기 성분이 제거되고 전체가 비동기로 동작

  • 관련 문서

  • 메모리 관리 개요
  • Buddy Allocator
  • Folio / Page Cache
  • Workingset
  • Readahead (이 문서)
  • DAMON — 메모리 접근 패턴 모니터링과 연계
  • 페이지 회수 — readahead와 페이지 해제의 상호작용