seq_file 인터페이스

seq_file은 커널 내부 데이터를 사용자 공간에 안전하게 출력하기 위한 표준 인터페이스입니다. 본 문서는 반복자(Iterator) 패턴의 4개 콜백(start/next/stop/show), 버퍼 자동 관리, 헬퍼 함수, RCU 통합, proc_create_seq API, 커널 소스 구현 분석을 상세히 다룹니다.

핵심 요약

한 줄 요약: seq_file은 커널 데이터를 사용자 공간에 순차적으로 출력하는 안전한 인터페이스입니다. 반복자 패턴(start/next/stop/show)으로 대용량 데이터를 자동 페이지네이션하며, 버퍼 오버플로를 투명하게 처리합니다.

단계별 이해

전제 조건: procfs/sysfs/debugfsVFS 문서를 먼저 읽으면 이 문서를 더 쉽게 이해할 수 있습니다.
일상 비유: seq_file은 자동 줄바꿈이 되는 프린터 버퍼와 비슷합니다. 용지(버퍼)가 가득 차면 더 큰 용지로 교체하고 처음부터 다시 인쇄하지만, 이미 전달된 페이지는 건너뜁니다.
  1. 목적: /proc, debugfs 등에서 커널 데이터를 사용자에게 텍스트로 출력
  2. 4개 콜백: start()show()next() → ... → stop()
  3. 버퍼 자동 관리: 초기 4KB, 부족 시 2배씩 확장 (최대 KMALLOC_MAX_SIZE)
  4. 간편 API: 소량 데이터는 single_open(), 대용량은 seq_operations 반복자

seq_file 개요

커널 2.6 이전에는 /proc 파일을 읽을 때 드라이버가 직접 copy_to_user()와 오프셋을 관리해야 했습니다. 출력이 한 페이지를 초과하면 데이터 누락이나 중복이 빈번했습니다. seq_file은 이 문제를 해결하기 위해 도입된 표준 인터페이스로, 버퍼 관리를 자동화하고 반복자(Iterator) 패턴으로 데이터 순회를 추상화합니다.

seq_file 아키텍처 개요 사용자 공간 cat /proc/my_entry VFS 계층 read() → proc_ops→proc_read seq_file 엔진 seq_read_iter() + 버퍼 관리 seq_operations (드라이버 제공) start(m, pos) → 이터레이터 초기화 show(m, v) → 현재 항목 출력 next(m, v, pos) → 다음 항목 stop(m, v) → 자원 해제 반복 커널 데이터 (list, array, hash...) seq_file 내부 버퍼 buf: show() 출력 누적 초기 4KB → 오버플로 시 ×2 확장 copy_to_user()로 사용자에게 전달

4개 콜백 상세

콜백 호출 시점 반환값 주요 역할
start(m, pos) read() 시작, 버퍼 재할당 후 재시작 첫 항목 포인터 / NULL(종료) lock 획득, pos 위치의 항목 탐색
show(m, v) 각 항목마다 (재호출 가능) 0(성공) / SEQ_SKIP / 음수(에러) seq_printf 등으로 출력만, 상태 변경 금지
next(m, v, pos) show() 성공 후 다음 항목 포인터 / NULL(종료) pos 증가, 다음 항목으로 이동
stop(m, v) 순회 완료 또는 오버플로 없음 (void) lock 해제, 리소스 정리
핵심 규칙: show()는 동일 항목에서 여러 번 호출될 수 있습니다(버퍼 오버플로 시 재시도). 따라서 show()에서 카운터 증가, 리스트 수정 등 부작용이 있는 코드는 절대 금지입니다.
static void *my_seq_start(struct seq_file *m, loff_t *pos)
{
    return seq_list_start(&my_list, *pos);
}

static void *my_seq_next(struct seq_file *m, void *v, loff_t *pos)
{
    return seq_list_next(v, &my_list, pos);
}

static void my_seq_stop(struct seq_file *m, void *v) { }

static int my_seq_show(struct seq_file *m, void *v)
{
    struct my_item *item = list_entry(v, struct my_item, list);
    seq_printf(m, "id=%d name=%s\\n", item->id, item->name);
    return 0;
}

static const struct seq_operations my_seq_ops = {
    .start = my_seq_start,
    .next  = my_seq_next,
    .stop  = my_seq_stop,
    .show  = my_seq_show,
};

seq_file API 선택 가이드

커널은 여러 가지 seq_file 편의 API를 제공합니다. 데이터 특성에 따라 적절한 API를 선택하면 보일러플레이트(Boilerplate) 코드를 크게 줄일 수 있습니다.

접근 방식 사용 시점 데이터 크기 보일러플레이트 핵심 API
single_open() 단일 값, 소량 통계 < PAGE_SIZE 낮음 single_open / single_release
DEFINE_SHOW_ATTRIBUTE() debugfs 파일, 단순 통계 < PAGE_SIZE 최소 (매크로) file_operations 자동 생성
proc_create_single() procfs 단일 값 (커널 4.18+) < PAGE_SIZE 매우 낮음 proc_ops 불필요
seq_operations 반복자 리스트, 배열, 대용량 데이터 무제한 중간 start/next/stop/show
proc_create_seq_private() 반복자 + per-open 상태 (4.18+) 무제한 낮음 seq_ops + private 크기
판단 기준: 100줄 미만의 고정 출력이면 single_open() 또는 proc_create_single()을 사용하십시오. 가변 길이 리스트나 대용량 데이터는 seq_operations 반복자 패턴이 필수입니다. debugfs 전용 파일이면 DEFINE_SHOW_ATTRIBUTE()가 가장 간결합니다.

DEFINE_SHOW_ATTRIBUTE 매크로

DEFINE_SHOW_ATTRIBUTE(name)name_show() 함수 하나만 작성하면 name_open()name_fops(file_operations)를 자동 생성하는 매크로입니다. debugfs 파일에서 가장 많이 사용됩니다.

#include <linux/debugfs.h>
#include <linux/seq_file.h>

/* 1. show 함수만 작성 */
static int my_stats_show(struct seq_file *m, void *v)
{
    seq_printf(m, "alloc_count: %lu\n", alloc_count);
    seq_printf(m, "free_count: %lu\n", free_count);
    seq_printf(m, "active: %lu\n", alloc_count - free_count);
    return 0;
}

/* 2. 매크로가 my_stats_open()과 my_stats_fops를 자동 생성 */
DEFINE_SHOW_ATTRIBUTE(my_stats);

/* 3. debugfs에 등록 (my_stats_fops 사용) */
debugfs_create_file("stats", 0444, parent_dir, NULL, &my_stats_fops);

/* 매크로 확장 결과 (참고):
 * static int my_stats_open(struct inode *i, struct file *f)
 * { return single_open(f, my_stats_show, inode->i_private); }
 * static const struct file_operations my_stats_fops = {
 *     .owner   = THIS_MODULE,
 *     .open    = my_stats_open,
 *     .read    = seq_read,
 *     .llseek  = seq_lseek,
 *     .release = single_release,
 * }; */

seq_file 내부 구현

seq_file은 커널 내부 데이터를 사용자 공간에 순차적으로 출력하는 표준 인터페이스입니다. 내부적으로 단일 페이지 크기 버퍼를 유지하며, 버퍼가 부족하면 자동으로 두 배 크기로 재할당 후 이터레이터를 처음부터 재시작(Reboot)합니다. 이 메커니즘을 이해하지 못하면 데이터 누락이나 중복 출력 버그가 발생할 수 있습니다.

seq_file 반복자 수명주기 상태 머신 read() start(pos) v≠NULL show(v) return 0 next(pos++) v≠NULL: 반복 v==NULL stop() 버퍼 오버플로 경로 show() 출력 > 버퍼 stop() buf ×2 확장 start(동일 pos) 재시도 — show()가 동일 항목에서 다시 호출됨 SEQ_SKIP 경로 show()가 SEQ_SKIP 반환 → 버퍼에 기록하지 않고 즉시 next() 호출 → 다음 항목으로 건너뜀 정상 흐름 NULL/종료 오버플로 재시도 건너뛰기 seq_file 동작 흐름과 버퍼 관리 read() 시스템 콜 seq_read() start(pos) show(v) next(pos++) v != NULL: 반복 stop() NULL 반환 시 copy_to_user 버퍼 관리 메커니즘 정상 경로 show() 출력 < 버퍼 크기 → seq_buf에 기록, pos 증가 → 다음 read()에서 이어서 출력 버퍼 오버플로우 경로 show() 출력 > 버퍼 크기 (SEQ_SKIP) → 버퍼 2배 확장 (kvmalloc) → start()/show() 재호출! 재시작 (동일 pos에서) seq_file 내부 버퍼 초기 크기: PAGE_SIZE (4KB) 최대: KMALLOC_MAX_SIZE buf + count + size + from 주의: show()는 동일 pos에서 여러 번 호출될 수 있음! show() 내부에서 상태를 변경하면 안 됨 (카운터 증가, 리스트 수정 등 금지) lock은 start()/stop()에서 잡고, show()는 순수 출력만 수행

seq_operations 완전한 예제

다음 예제는 seq_open()proc_ops를 직접 사용하는 전통적인 패턴입니다. 커널 4.18 이전 코드나 커스텀 open 로직이 필요한 경우에 사용합니다. 4.18 이후에는 proc_create_seq()가 더 간결합니다.

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/list.h>

struct my_data {
    struct list_head list;
    int value;
    char name[32];
};

static LIST_HEAD(data_list);
static DEFINE_MUTEX(data_mutex);

/* start: 이터레이터 시작 위치 설정 + lock 획득 */
static void *my_seq_start(struct seq_file *s, loff_t *pos)
{
    mutex_lock(&data_mutex);
    return seq_list_start(&data_list, *pos);
}

/* next: 다음 항목으로 이동 */
static void *my_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
    return seq_list_next(v, &data_list, pos);
}

/* stop: lock 해제 (show()와 무관하게 항상 호출됨) */
static void my_seq_stop(struct seq_file *s, void *v)
{
    mutex_unlock(&data_mutex);
}

/* show: 현재 항목 출력 (순수 출력만, 상태 변경 금지!) */
static int my_seq_show(struct seq_file *s, void *v)
{
    struct my_data *d = list_entry(v, struct my_data, list);
    seq_printf(s, "%s: %d\n", d->name, d->value);
    return 0;
}

static const struct seq_operations my_seq_ops = {
    .start = my_seq_start,
    .next  = my_seq_next,
    .stop  = my_seq_stop,
    .show  = my_seq_show,
};

static int my_proc_open(struct inode *inode, struct file *file)
{
    return seq_open(file, &my_seq_ops);
}

static const struct proc_ops my_pops = {
    .proc_open    = my_proc_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = seq_release,
};
코드 설명
  • seq_list_start(head, pos)연결 리스트에서 pos 번째 항목을 찾아 반환합니다. 리스트 끝을 넘으면 NULL을 반환하여 순회를 종료합니다.
  • mutex_lock/unlock 위치lock은 반드시 start()에서 획득하고 stop()에서 해제합니다. 버퍼 오버플로 시 stop()→start()가 재호출되므로 lock/unlock 쌍이 자동으로 유지됩니다.
  • seq_open(file, &ops)file→private_dataseq_file 구조체를 할당하고 seq_operations를 연결합니다. 이후 seq_read()가 이 구조체를 통해 콜백을 호출합니다.
  • proc_ops 구조커널 5.6+에서 file_operations 대신 사용합니다. proc_readseq_read, proc_lseekseq_lseek를 연결하는 것이 표준 패턴입니다.

single_open 패턴 (소량 데이터)

single_open()은 데이터가 적어 한 번의 show() 호출로 전체를 출력할 수 있을 때 사용합니다. start/next/stop 콜백을 정의할 필요 없이 show 함수 하나만 작성하면 됩니다. 출력이 PAGE_SIZE(4KB)를 초과할 가능성이 있으면 반복자 패턴을 대신 사용하십시오.

/* single_open: 데이터가 적을 때 간단한 패턴
 * start/next/stop 불필요, show 한 번만 호출 */
static int stats_show(struct seq_file *m, void *v)
{
    seq_printf(m, "total_ops: %lu\n", atomic_long_read(&total_ops));
    seq_printf(m, "errors: %lu\n", atomic_long_read(&error_count));
    seq_printf(m, "uptime_sec: %lu\n", jiffies / HZ);
    return 0;
}

static int stats_open(struct inode *inode, struct file *file)
{
    /* single_open은 내부적으로 seq_open + show 1회 호출 */
    return single_open(file, stats_show, pde_data(inode));
}

static const struct proc_ops stats_pops = {
    .proc_open    = stats_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,  /* seq_release가 아닌 single_release! */
};

대용량 데이터 페이지네이션

배열 기반 데이터 구조에서는 loff_t *pos를 배열 인덱스로 사용합니다. seq_file은 read() 호출 사이에도 pos를 유지하므로, 버퍼가 가득 차면 copy_to_user 후 다음 read()에서 이어서 출력합니다. 드라이버가 최대 항목 수를 제한할 필요가 없습니다.

다중 read() 호출 시 pos/count/from 추적 커널 데이터: 10개 항목 (각 ~500B → 총 ~5KB) item[0] item[1] item[2] item[3] item[4] item[5] item[6] item[7] item[8] item[9] 1차 read(fd, buf, 4096) start(0) → show(0..3) → 버퍼 가득 → stop() copy_to_user: 4096B → pos=4, count=0, from=잔여 2차 read(fd, buf, 4096) start(4) → show(4..6) → 버퍼 가득 → stop() copy_to_user: 잔여B → pos=7 3차 read(fd, buf, 4096) start(7) → show(7..9) → NULL → stop() copy_to_user: 잔여B → 완료 seq_file 구조체 핵심 필드 index (loff_t) 다음 start()에 전달할 논리 위치 (항목 번호) count (size_t) 버퍼에 누적된 출력 바이트 수 from (size_t) 이전 read()에서 미전송 잔여 데이터 시작 오프셋 size (size_t) 현재 버퍼 크기 (초기 4KB → ×2 확장) read() 호출 간 상태 유지: from > 0이면 이전 버퍼 잔여분 먼저 전달 → from == 0이면 start(index)부터 새로 순회 사용자는 단순히 read()를 반복하면 됨 — seq_file이 pos, 버퍼, 분할을 모두 투명하게 관리
/* 대용량 데이터 처리: 배열 기반 이터레이터 */
static void *array_seq_start(struct seq_file *s, loff_t *pos)
{
    /* pos가 배열 범위를 벗어나면 NULL → stop() */
    if (*pos >= nr_entries)
        return NULL;
    return &entries[*pos];
}

static void *array_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
    ++(*pos);
    if (*pos >= nr_entries)
        return NULL;
    return &entries[*pos];
}

/* 핵심: loff_t *pos는 seq_file이 관리하는 논리 오프셋
 * read() 호출 사이에도 pos가 유지되어 자동 페이지네이션
 * 버퍼 가득 차면 → copy_to_user → 다음 read()에서 이어서 start(pos)
 *
 * 단일 read()에서 처리할 최대 항목 수를 제한하지 마세요!
 * seq_file이 버퍼 크기에 맞춰 자동으로 분할합니다. */

seq_file 순회 헬퍼 함수

커널은 seq_operations 콜백에서 자주 사용하는 리스트/해시 테이블 순회를 위한 헬퍼 함수를 제공합니다. 직접 순회 코드를 작성하는 대신 이 함수들을 사용하면 pos 관리 오류를 방지할 수 있습니다.

seq_file 헬퍼 함수 분류 순회 헬퍼 (start/next 콜백용) list_head 계열 seq_list_start() seq_list_next() seq_list_start_head() hlist_head 계열 seq_hlist_start() seq_hlist_next() + _rcu() 변형 RCU 변형: seq_list_start_rcu(), seq_list_next_rcu(), seq_hlist_start_rcu(), seq_hlist_next_rcu() 배열: 헬퍼 없음 — *pos를 인덱스로 직접 사용 (entries[*pos]) 출력 헬퍼 (show 콜백용) 텍스트 출력 seq_printf() — 포맷 seq_puts() — 정적 문자열 seq_putc() — 단일 문자 특수 출력 seq_escape() — 이스케이프 seq_pad() — 컬럼 정렬 seq_hex_dump() — 16진수 원시 바이트: seq_write(m, buf, len) — 바이너리 데이터 직접 출력 반환값: 0 = 성공, SEQ_SKIP = 건너뛰기 (show 전용), 음수 = 에러
함수 용도 시그니처
seq_list_start() list_head 순회 시작 seq_list_start(head, pos)
seq_list_next() list_head 다음 항목 seq_list_next(v, head, pos)
seq_list_start_head() 리스트 헤드를 pos=0 항목으로 포함 seq_list_start_head(head, pos)
seq_hlist_start() hlist_head 해시 버킷 순회 시작 seq_hlist_start(head, pos)
seq_hlist_next() hlist_head 다음 항목 seq_hlist_next(v, head, pos)
seq_hlist_start_rcu() RCU 보호 해시 순회 시작 seq_hlist_start_rcu(head, pos)
seq_hlist_next_rcu() RCU 보호 해시 다음 항목 seq_hlist_next_rcu(v, head, pos)
/* seq_list_start_head: 리스트 헤드를 첫 항목으로 포함
 * pos=0이면 head 자체를 반환 → show()에서 헤더 행 출력에 활용 */
static void *my_start(struct seq_file *m, loff_t *pos)
{
    mutex_lock(&my_mutex);
    return seq_list_start_head(&my_list, *pos);
}

static int my_show(struct seq_file *m, void *v)
{
    if (v == &my_list) {
        /* pos=0: 헤더 행 출력 */
        seq_puts(m, "Name\tValue\n");
        return 0;
    }
    struct my_item *item = list_entry(v, struct my_item, list);
    seq_printf(m, "%s\t%d\n", item->name, item->value);
    return 0;
}

/* seq_hlist: 해시 테이블 버킷 순회 예제 */
static void *ht_start(struct seq_file *m, loff_t *pos)
{
    return seq_hlist_start(&my_hash_bucket, *pos);
}

static void *ht_next(struct seq_file *m, void *v, loff_t *pos)
{
    return seq_hlist_next(v, &my_hash_bucket, pos);
}

seq_file 출력 유틸리티 함수

seq_printf() 외에도 특수한 출력 상황에 최적화된 유틸리티 함수들이 있습니다.

함수 용도 사용 예
seq_puts(m, str) 정적 문자열 출력 (포맷 파싱 없음) 헤더 행, 구분자
seq_putc(m, c) 단일 문자 출력 개행, 탭, 구분 문자
seq_write(m, buf, len) 원시 바이트 출력 바이너리 데이터
seq_escape(m, src, esc) 특수 문자를 8진수로 이스케이프 /proc/mounts 경로
seq_pad(m, c) 고정 폭 컬럼 정렬 (패딩 후 문자 c 출력) /proc/net/tcp
seq_hex_dump() 16진수 덤프 출력 레지스터 덤프, debugfs
/* seq_escape: /proc/mounts 스타일 경로 이스케이프 */
static int mount_show(struct seq_file *m, void *v)
{
    struct mount *mnt = list_entry(v, struct mount, mnt_list);
    /* 공백, 탭, 개행, 역슬래시를 8진수(\040 등)로 이스케이프 */
    seq_escape(m, mnt->mnt_devname, " \t\n\\");
    seq_putc(m, ' ');
    seq_escape(m, path_buf, " \t\n\\");
    seq_putc(m, '\n');
    return 0;
}

/* seq_pad: /proc/net/tcp 스타일 고정 폭 컬럼 정렬 */
static int conn_show(struct seq_file *m, void *v)
{
    seq_setwidth(m, 127);  /* 패딩 목표 폭 설정 */
    seq_printf(m, "%4d: %08X:%04X %08X:%04X",
               slot, src_addr, src_port, dst_addr, dst_port);
    seq_pad(m, '\n');  /* 127열까지 공백 채운 뒤 개행 */
    return 0;
}

/* seq_hex_dump: 하드웨어 레지스터 덤프 (debugfs 활용) */
static int regs_show(struct seq_file *m, void *v)
{
    u8 regs[64];
    read_device_regs(dev, regs, sizeof(regs));
    /* "  0000: xx xx xx ... | ascii..." 형식 출력 */
    seq_hex_dump(m, "  ", DUMP_PREFIX_OFFSET,
                 16, 1, regs, sizeof(regs), true);
    return 0;
}
코드 설명
  • seq_escape(m, src, esc)esc 문자열에 포함된 문자를 \040 형태의 8진수로 치환합니다. /proc/mounts에서 마운트 경로에 공백이 포함된 경우 파서가 혼동하지 않도록 이스케이프합니다.
  • seq_setwidth(m, 127) + seq_pad(m, '\n')seq_setwidth()로 목표 폭을 설정한 뒤 seq_pad()를 호출하면 현재 위치부터 목표 폭까지 공백으로 채운 뒤 지정 문자(여기서는 개행)를 출력합니다. /proc/net/tcp의 고정 폭 컬럼 정렬에 사용됩니다.
  • seq_hex_dump()print_hex_dump()의 seq_file 버전입니다. prefix, rowsize, groupsize, ascii 표시 여부를 지정하여 하드웨어 레지스터 덤프를 깔끔하게 출력합니다.
  • seq_puts vs seq_printf정적 문자열은 seq_puts()seq_printf()보다 빠릅니다. seq_printf()는 매번 포맷 문자열을 파싱하지만, seq_puts()는 단순히 memcpy()로 복사합니다.

RCU 보호 seq_file 패턴

RCU(Read-Copy-Update) 보호 데이터 구조를 seq_file로 출력할 때는 start()에서 rcu_read_lock()을 획득하고 stop()에서 해제합니다. RCU 전용 헬퍼 함수를 함께 사용해야 합니다.

/* RCU + list_head 순회 패턴 */
static void *rcu_list_start(struct seq_file *m, loff_t *pos)
{
    rcu_read_lock();
    return seq_list_start_rcu(&my_rcu_list, *pos);
}

static void *rcu_list_next(struct seq_file *m, void *v, loff_t *pos)
{
    return seq_list_next_rcu(v, &my_rcu_list, pos);
}

static void rcu_list_stop(struct seq_file *m, void *v)
{
    rcu_read_unlock();
}

/* RCU + hlist (해시 테이블) 순회 패턴 */
static void *rcu_hash_start(struct seq_file *m, loff_t *pos)
{
    rcu_read_lock();
    return seq_hlist_start_rcu(&hash_table[bucket], *pos);
}

static void *rcu_hash_next(struct seq_file *m, void *v, loff_t *pos)
{
    return seq_hlist_next_rcu(v, &hash_table[bucket], pos);
}

static void rcu_hash_stop(struct seq_file *m, void *v)
{
    rcu_read_unlock();
}

/* SRCU 패턴: show()에서 sleep이 필요한 경우 */
/* RCU read-side는 sleep 불가 → SRCU(Sleepable RCU) 사용 */
DEFINE_STATIC_SRCU(my_srcu);

static void *srcu_seq_start(struct seq_file *m, loff_t *pos)
{
    int idx = srcu_read_lock(&my_srcu);
    m->private = (void *)(long)idx;
    return seq_list_start(&my_srcu_list, *pos);
}

static void srcu_seq_stop(struct seq_file *m, void *v)
{
    int idx = (int)(long)m->private;
    srcu_read_unlock(&my_srcu, idx);
}
코드 설명
  • rcu_read_lock() / rcu_read_unlock()RCU read-side 임계 영역을 시작/종료합니다. 이 구간 내에서 RCU 보호 포인터를 안전하게 역참조할 수 있습니다. sleep이 불가하므로 show()에서 블로킹 연산을 사용하면 안 됩니다.
  • seq_list_start_rcu / seq_list_next_rcu일반 seq_list_start/next의 RCU 버전입니다. 내부적으로 list_for_each_entry_rcu()를 사용하여 RCU 보호 리스트를 안전하게 순회합니다.
  • SRCU (Sleepable RCU)RCU와 달리 read-side 임계 영역에서 sleep이 가능합니다. show()에서 I/O 대기 등 블로킹이 필요한 경우에 사용합니다. srcu_read_lock()이 반환하는 idxm→private에 저장하여 stop()에서 사용합니다.
버퍼 재할당과 RCU: 버퍼 오버플로 시 seq_file은 stop()을 호출한 뒤 버퍼를 확장하고 start()를 다시 호출합니다. stop()에서 rcu_read_unlock()이 호출되므로 재시도 사이에 RCU grace period가 발생할 수 있습니다. 이는 정상적인 동작이며, 재시작 시 start()에서 rcu_read_lock()을 다시 획득하므로 안전합니다.

성능과 메모리 관리

seq_file의 버퍼 관리 특성과 대용량 데이터 출력 시 성능 최적화 원칙을 설명합니다.

single_open vs seq_operations 메모리 사용 비교 single_open() — 전체 누적 seq_operations — 항목별 출력 4KB 8KB 16KB 32KB 64KB... 출력 항목이 늘어날수록 버퍼가 계속 커짐 전체 출력이 완료될 때까지 메모리 해제 불가 4KB 4KB 4KB 4KB 4KB 각 read()마다 버퍼 내용을 유저에게 전달 후 재사용 항목 수에 관계없이 버퍼 크기 일정 유지 수천 개 항목도 4KB 버퍼로 처리 가능

seq_file 메모리 사용

seq_file은 출력을 임시 버퍼에 저장한 뒤 사용자 공간으로 복사합니다. 대량 데이터 출력 시 메모리 사용이 급증할 수 있습니다:

/* seq_file 내부 버퍼 관리 */
/*
 * seq_read() → traverse() → show()
 * 초기 버퍼: PAGE_SIZE (4KB)
 * 부족 시 2배씩 증가: 4K → 8K → 16K → ... → kmalloc 한계까지
 *
 * 주의: single_open()은 전체 출력을 한 번에 메모리에 저장
 *       대량 데이터 시 seq_operations (start/next/stop/show) 사용 필수
 */

/* 나쁜 예: single_open으로 대량 데이터 출력 */
static int bad_show(struct seq_file *m, void *v)
{
    struct my_item *item;
    list_for_each_entry(item, &huge_list, list)
        seq_printf(m, "%d %s\\n", item->id, item->name);
    return 0;
    /* → 리스트가 크면 버퍼가 수십 MB까지 증가 */
}

/* 좋은 예: seq_operations으로 반복자 패턴 사용 */
/* → 한 번에 하나의 항목만 출력, 버퍼 크기 일정 유지 */

seq_file 성능 최적화

대용량 데이터를 seq_file로 출력할 때 다음 최적화 원칙을 적용하면 CPU 사용량과 메모리 소비를 줄일 수 있습니다.

/* 1. show()는 재호출될 수 있으므로 작업을 최소화하십시오 */
/*    버퍼 오버플로 시 stop() → 버퍼 확장 → start() → show() 재호출 */
/*    비용이 큰 계산은 start()에서 수행하고 seq_file->private에 저장 */

/* 2. 정적 문자열은 seq_puts()를 사용하십시오 (seq_printf보다 빠름) */
seq_puts(m, "Name\tPID\tState\n");     /* 빠름: 포맷 파싱 없음 */
seq_printf(m, "%s", "Name\tPID\n"); /* 느림: 불필요한 포맷 파싱 */

/* 3. 필터링에는 SEQ_SKIP을 사용하십시오 */
static int filter_show(struct seq_file *m, void *v)
{
    struct my_item *item = list_entry(v, struct my_item, list);
    if (!item->active)
        return SEQ_SKIP;  /* 버퍼에 아무것도 기록하지 않고 건너뜀 */
    seq_printf(m, "%d %s\n", item->id, item->name);
    return 0;
}

/* 4. single_open 대용량 경고: 전체 출력이 메모리에 누적됨 */
/*    출력이 PAGE_SIZE(4KB)를 초과할 가능성이 있으면 */
/*    반드시 seq_operations 반복자 패턴을 사용하십시오 */
/*    반복자 패턴은 항목별로 버퍼를 재사용하므로 메모리 일정 유지 */

흔한 실수와 함정

seq_file 콜백에서 자주 발생하는 실수와 올바른 패턴을 정리합니다.

실수 5: seq_file 콜백에서 sleep/mutex

/* ✗ 위험한 패턴: show 콜백에서 장시간 블로킹 */
static int my_show(struct seq_file *m, void *v)
{
    mutex_lock(&global_mutex);  /* 핫 패스 mutex를 잡음 */
    seq_printf(m, "%d\\n", shared_counter);
    mutex_unlock(&global_mutex);
    return 0;
    /* 문제:
     * - 사용자가 cat /proc/my_entry 할 때마다 global_mutex 경합
     * - seq_file이 버퍼 리사이즈하면 show()가 재호출됨!
     *   → start() → show() [버퍼 부족] → stop() → start() → show() [재시도]
     *   → mutex를 두 번 잡으려고 시도할 수 있음 (deadlock 아니면 데이터 불일치)
     */
}

/* ✓ 개선 패턴: start/stop에서 lock, show에서는 lock 없이 */
static void *my_start(struct seq_file *m, loff_t *pos)
{
    mutex_lock(&my_mutex);
    return seq_list_start(&my_list, *pos);
}
static void my_stop(struct seq_file *m, void *v)
{
    mutex_unlock(&my_mutex);
}
/* start()~stop() 사이에서 show()가 여러 번 호출되어도 lock은 한 번만 */

실수 2: show()에서 부작용 코드

/* ✗ 위험: show()에서 카운터 증가 */
static int bad_show(struct seq_file *m, void *v)
{
    struct my_item *item = list_entry(v, struct my_item, list);
    item->read_count++;  /* 버퍼 오버플로 시 같은 항목에서 2번 증가! */
    seq_printf(m, "%s: reads=%d\n", item->name, item->read_count);
    return 0;
}

/* ✓ show()는 순수 출력만 수행 */
static int good_show(struct seq_file *m, void *v)
{
    struct my_item *item = list_entry(v, struct my_item, list);
    seq_printf(m, "%s: reads=%d\n", item->name,
               atomic_read(&item->read_count));
    return 0;
    /* 카운터 증가가 필요하면 open 콜백 또는 외부에서 수행 */
}

실수 3: single_release와 seq_release 혼동

/* ✗ single_open()으로 열었는데 seq_release()로 닫음 → 메모리 누수 */
static int my_open(struct inode *i, struct file *f)
{
    return single_open(f, my_show, NULL);
}
static const struct proc_ops my_ops = {
    .proc_open    = my_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = seq_release,  /* ✗ 잘못됨! single_release 사용 필수 */
};

/* 규칙:
 * - single_open()  → single_release()
 * - seq_open()     → seq_release()
 * - seq_open_private() → seq_release_private()
 * 짝이 맞지 않으면 메모리 누수 또는 이중 해제 발생 */

함정 요약 체크리스트

seq_file 코드 리뷰 체크리스트:
  • show()에서 상태를 변경하지 않는가? (카운터, 리스트, 플래그 수정 금지)
  • lock을 start()/stop()에서 관리하는가? (show()에서 lock 금지)
  • single_open()single_release(), seq_open()seq_release() 짝이 맞는가?
  • 대용량 데이터에 single_open() 대신 seq_operations 반복자를 사용하는가?
  • next()에서 (*pos)++를 빠뜨리지 않았는가? (무한 루프 원인)
  • start()*pos 범위 초과 시 NULL을 반환하는가?

proc_create_seq_private: private 데이터 패턴

proc_create_seq_private()는 seq_file에 private 데이터를 연결하는 가장 간결한 방법입니다. seq_open_private()와 달리 proc_ops를 직접 정의할 필요 없이, seq_operations와 private 크기만 지정하면 됩니다.

/* proc_create_seq_private: 가장 간결한 private data 패턴 (커널 4.18+) */

struct filter_state {
    int  min_value;
    int  max_value;
    bool show_inactive;
};

static void *filter_start(struct seq_file *s, loff_t *pos)
{
    /* seq_file→private에 filter_state가 자동 할당됨 */
    struct filter_state *f = s->private;

    /* 첫 호출 시 초기화 */
    if (*pos == 0) {
        f->min_value = 0;
        f->max_value = 1000;
        f->show_inactive = false;
    }
    return seq_list_start(&device_list, *pos);
}

static int filter_show(struct seq_file *s, void *v)
{
    struct filter_state *f = s->private;
    struct my_device *dev = list_entry(v, struct my_device, list);

    /* private 상태를 활용한 필터링 */
    if (dev->value < f->min_value || dev->value > f->max_value)
        return SEQ_SKIP;
    if (!f->show_inactive && !dev->active)
        return SEQ_SKIP;

    seq_printf(s, "%s: value=%d active=%d\n",
               dev->name, dev->value, dev->active);
    return 0;
}

static const struct seq_operations filter_seq_ops = {
    .start = filter_start,
    .next  = filter_next,
    .stop  = filter_stop,
    .show  = filter_show,
};

/* proc_create_seq_private: proc_ops 정의 불필요! */
proc_create_seq_private("filtered_list", 0444,
    my_proc_dir, &filter_seq_ops,
    sizeof(struct filter_state), NULL);

/* 내부 동작:
 * 1. open 시 seq_open_private()가 filter_state 크기만큼 할당
 * 2. seq_file→private = kmalloc(sizeof(filter_state))
 * 3. start/show에서 s→private으로 접근
 * 4. release 시 seq_release_private()가 자동 해제 */
코드 설명
  • proc_create_seq_private()커널 4.18에서 도입된 편의 함수입니다. proc_ops, open, release 콜백을 직접 정의할 필요 없이 seq_operations와 private 데이터 크기만 전달합니다.
  • s→privateseq_open_private()가 내부적으로 kmalloc()한 영역의 포인터입니다. open부터 release까지 유지되므로, 이터레이션 간 상태를 안전하게 전달할 수 있습니다.
  • SEQ_SKIPshow()가 SEQ_SKIP을 반환하면 현재 항목의 출력이 seq_buf에서 제거됩니다. 조건부 필터링에 유용한 패턴으로, 별도의 임시 버퍼 없이 선택적 출력이 가능합니다.

파일 수명주기: open → read → release

사용자 공간에서 cat /proc/my_entry를 실행하면 내부적으로 open()→read()→...→read()→release() 시스템 콜이 순차적으로 호출됩니다. seq_file은 이 과정에서 버퍼를 할당·관리하고, 반복자 상태를 유지합니다.

seq_file 파일 수명주기 1. open() seq_open() 또는 single_open() → struct seq_file 할당 → seq_operations 연결 → file→private_data에 저장 2. read() — 반복 호출 seq_read_iter() → start()→show()→next()→...→stop() → 버퍼 → copy_to_user() → 다음 read()에서 pos 이어서 진행 데이터가 남아있으면 반복 3. release() seq_release() / single_release() → 버퍼(buf) kvfree() → struct seq_file kfree() → private 데이터 해제 (if any) struct seq_file 내부 상태 변화 buf = NULL, size = 0 index = 0, count = 0 op = &my_seq_ops buf = kvmalloc(4096) count = show() 출력 바이트 index = 마지막 성공 pos + 1 from += copied (유저에게 전달한 양) count = 0이면 다음 항목부터 재개 오버플로 시: size <<= 1, 재시도 release() 호출 시 buf와 seq_file 구조체 모두 해제 → 메모리 반환 완료
open과 release의 짝: seq_open()으로 열었으면 seq_release(), single_open()으로 열었으면 single_release(), seq_open_private()로 열었으면 seq_release_private()를 사용해야 합니다. 짝이 맞지 않으면 메모리 누수 또는 이중 해제가 발생합니다.

seq_file 핵심 함수 구현 분석

seq_read_iter()는 seq_file의 실질적인 엔진입니다. 이 함수가 이터레이터를 구동하고, 버퍼 오버플로 시 자동 재시도를 처리합니다. traverse()lseek() 후 특정 위치로 빠르게 이동하는 내부 함수입니다.

/* fs/seq_file.c - seq_read_iter() 핵심 루프 (간략화) */
ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
    struct seq_file *m = iocb->ki_filp->private_data;
    size_t copied = 0;
    void *p;

    /* 버퍼에 이전 데이터가 남아있으면 먼저 출력 */
    if (m->count) {
        /* copy_to_iter()로 유저 공간에 복사 */
        copied = copy_to_iter(m->buf + m->from, m->count, iter);
        m->count -= copied;
        m->from  += copied;
        if (m->count)      /* 유저 버퍼 부족: 나중에 재개 */
            goto done;
    }

    /* 이터레이터 시작 */
    p = m->op->start(m, &m->index);
    while (p) {
        /* show()로 seq_buf에 기록 */
        int err = m->op->show(m, p);
        if (err == SEQ_SKIP)
            goto skip;

        /* 버퍼 오버플로 감지 */
        if (seq_has_overflowed(m)) {
            /* 핵심: 버퍼 2배 확장 후 재시도 */
            m->op->stop(m, p);
            kvfree(m->buf);
            m->count = 0;
            m->buf = kvmalloc(m->size <<= 1, GFP_KERNEL);
            if (!m->buf)
                goto enomem;
            /* 동일 index에서 start() 재호출! */
            p = m->op->start(m, &m->index);
            continue;
        }
skip:
        /* 다음 항목으로 이동 */
        p = m->op->next(m, p, &m->index);
    }
    m->op->stop(m, p);

done:
    return copied;
}
코드 설명
  • m→count / m→fromcount는 버퍼에 남은 유효 데이터 크기, from은 버퍼 내 읽기 시작 오프셋입니다. 이전 read()에서 유저 버퍼가 작아 미전송된 데이터가 남아있을 수 있습니다.
  • seq_has_overflowed(m)m→count >= m→size를 확인합니다. seq_printf() 내부에서 seq_buf_printf()가 버퍼를 초과하면 countsize 이상으로 설정합니다.
  • m→size <<= 1버퍼 크기를 2배로 증가시킵니다. 초기 크기는 PAGE_SIZE(4KB)이며, 최대 KMALLOC_MAX_SIZE까지 확장됩니다. 이로 인해 show()가 동일 항목에서 여러 번 호출될 수 있습니다.
  • SEQ_SKIPshow()SEQ_SKIP을 반환하면 현재 항목의 출력을 건너뜁니다. 필터링이 필요한 경우에 활용합니다.
  • start() 재호출오버플로 시 동일 index에서 start()를 다시 호출합니다. 따라서 show()는 반드시 부작용이 없는 순수 출력 함수여야 합니다.
seq_file 버퍼 재할당과 재시도 메커니즘 User Space seq_read_iter() seq_operations 버퍼 상태 read(fd, buf, 4096) 4KB 할당 start(pos=0) show(item_0) 성공: 버퍼에 기록 next() → pos=1 show(item_1) 오버플로! stop() 4K→8KB 확장 index=1 유지 (item_0은 이미 출력됨) start(pos=1) show(item_1) 성공: 8K 버퍼에 기록 next() → ... → stop() copy_to_user() 데이터 수신 완료 핵심: 오버플로 시 이미 성공한 항목(item_0)은 건너뛰고, 실패한 항목(item_1)부터 재시도합니다

struct seq_file 핵심 필드

/* include/linux/seq_file.h - seq_file 구조체 */
struct seq_file {
    char           *buf;    /* 출력 버퍼 (kvmalloc 할당) */
    size_t          size;   /* 버퍼 할당 크기 */
    size_t          from;   /* 유저에게 복사할 시작 오프셋 */
    size_t          count;  /* 버퍼 내 유효 데이터 크기 */
    size_t          pad_until; /* seq_pad()가 채울 위치 */
    loff_t          index;  /* 현재 이터레이터 위치 (논리 오프셋) */
    loff_t          read_pos; /* 유저 공간 read 위치 (바이트) */
    struct mutex    lock;   /* 동시 read() 직렬화 */
    const struct seq_operations *op; /* start/next/stop/show 콜백 */
    int             poll_event; /* poll 이벤트 상태 */
    const struct file *file;    /* 연결된 파일 포인터 */
    void           *private; /* seq_open_private()의 private 데이터 */
};
코드 설명
  • index vs read_posindex는 이터레이터의 논리 위치(항목 번호)이고, read_pos는 유저가 읽은 바이트 수입니다. lseek()traverse()read_pos를 기반으로 index를 재계산합니다.
  • lock동일 파일 디스크립터에서 멀티스레드 read()를 직렬화합니다. seq_read_iter() 진입 시 mutex_lock()을 획득합니다.
  • privateseq_open_private()로 할당된 드라이버별 데이터입니다. seq_release_private()가 자동으로 해제합니다.

traverse() — lseek 지원 함수

사용자가 lseek(fd, offset, SEEK_SET)를 호출하면 seq_lseek()가 내부적으로 traverse()를 호출합니다. traverse()는 이터레이터를 처음부터 다시 시작하여 offset 바이트까지 show()를 반복 호출하고, 해당 위치의 index를 계산합니다.

/* fs/seq_file.c - traverse() 개념 (간략화) */
static loff_t traverse(struct seq_file *m, loff_t offset)
{
    loff_t pos = 0;
    void *p;
    int error = 0;

    m->index = 0;
    m->count = m->from = 0;
    p = m->op->start(m, &m->index);
    while (p) {
        error = m->op->show(m, p);
        if (error < 0)
            break;
        /* 누적 바이트 수가 offset에 도달하면 중단 */
        if (m->count == m->size)  /* 오버플로 시 버퍼 확장 */
            goto Eoverflow;
        pos += m->count;
        if (pos >= offset) {
            /* offset 위치 도달 → from 조정 */
            m->from = m->count - (pos - offset);
            m->count = pos - offset;
            break;
        }
        m->count = 0;
        p = m->op->next(m, p, &m->index);
    }
    m->op->stop(m, p);
    return error;
}
lseek 성능 주의: traverse()는 처음부터 offset까지 모든 항목을 순회하므로 O(n) 비용이 발생합니다. 대용량 데이터에서 lseek를 빈번하게 사용하면 성능이 크게 저하됩니다. 대부분의 /proc 파일은 처음부터 순차 읽기만 하므로 실제로 문제가 되는 경우는 드뭅니다.

종합 모듈 예제

다음은 seq_file을 사용하여 /proc/my_items 파일을 생성하는 완전한 커널 모듈입니다. insmodcat /proc/my_items로 동작을 확인할 수 있습니다.

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
#include <linux/list.h>

struct item {
    struct list_head list;
    int id;
    char name[16];
};

static LIST_HEAD(items);
static DEFINE_MUTEX(items_lock);

/* --- seq_operations 콜백 --- */
static void *items_start(struct seq_file *m, loff_t *pos)
{
    mutex_lock(&items_lock);
    return seq_list_start(&items, *pos);
}

static void *items_next(struct seq_file *m, void *v, loff_t *pos)
{
    return seq_list_next(v, &items, pos);
}

static void items_stop(struct seq_file *m, void *v)
{
    mutex_unlock(&items_lock);
}

static int items_show(struct seq_file *m, void *v)
{
    struct item *it = list_entry(v, struct item, list);
    seq_printf(m, "[%3d] %s\n", it->id, it->name);
    return 0;
}

static const struct seq_operations items_sops = {
    .start = items_start,
    .next  = items_next,
    .stop  = items_stop,
    .show  = items_show,
};

/* --- 모듈 초기화/종료 --- */
static int __init my_init(void)
{
    int i;
    for (i = 0; i < 100; i++) {
        struct item *it = kmalloc(sizeof(*it), GFP_KERNEL);
        it->id = i;
        snprintf(it->name, sizeof(it->name), "item_%03d", i);
        list_add_tail(&it->list, &items);
    }
    /* proc_create_seq: proc_ops 직접 정의 불필요 (커널 4.18+) */
    proc_create_seq("my_items", 0444, NULL, &items_sops);
    return 0;
}

static void __exit my_exit(void)
{
    struct item *it, *tmp;
    remove_proc_entry("my_items", NULL);
    list_for_each_entry_safe(it, tmp, &items, list) {
        list_del(&it->list);
        kfree(it);
    }
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
코드 설명
  • proc_create_seq()커널 4.18+ 편의 함수입니다. proc_ops를 직접 정의하지 않고 seq_operations만 전달합니다. 내부적으로 seq_open()/seq_read()/seq_lseek()/seq_release()를 연결합니다.
  • mutex_lock/unlock 위치lock은 start()에서 획득하고 stop()에서 해제합니다. 버퍼 오버플로 시 stop()→start()가 다시 호출되므로 lock/unlock이 자연스럽게 쌍을 이룹니다.
  • remove_proc_entry 순서proc 엔트리를 먼저 제거한 뒤 리스트를 해제합니다. 순서가 바뀌면 제거 중인 리스트를 cat이 읽을 수 있습니다.

seq_file 출력 헬퍼 비교

show() 콜백 내에서 사용할 수 있는 출력 헬퍼 함수를 정리합니다. 상황에 맞는 헬퍼를 선택하면 코드가 간결해지고 성능도 향상됩니다.

함수용도예시내부 동작
seq_printf(m, fmt, ...)서식 문자열 출력seq_printf(m, "pid=%d\n", pid)vsnprintf → 버퍼 기록
seq_puts(m, s)고정 문자열 출력seq_puts(m, "header\n")memcpy (printf보다 빠름)
seq_putc(m, c)단일 문자 출력seq_putc(m, '\n')buf[count++] = c
seq_write(m, data, len)바이너리 데이터 출력seq_write(m, buf, 16)memcpy (길이 지정)
seq_escape(m, s, esc)특수문자 이스케이프 출력seq_escape(m, path, " \t\n")esc 문자를 \ooo로 변환
seq_pad(m, c)컬럼 정렬 패딩seq_printf(m, "%-20s", name); seq_pad(m, ' ')m→pad_until까지 c로 채움
seq_hex_dump(m, ...)16진수 덤프 출력seq_hex_dump(m, " ", DUMP_PREFIX_OFFSET, 16, 1, data, len, true)hex_dump_to_buffer
seq_file_path(m, file, esc)파일 경로 출력seq_file_path(m, file, " \t\n")d_path + seq_escape
성능 팁: 고정 문자열은 seq_printf(m, "header\n") 대신 seq_puts(m, "header\n")를 사용하세요. seq_puts()vsnprintf() 파싱 오버헤드가 없어 빠릅니다. 단일 문자(\n, \t)는 seq_putc()가 최적입니다.

RCU 통합 패턴

lock-free 읽기가 필요한 고성능 경로에서는 mutex 대신 RCU(Read-Copy-Update)를 사용합니다. seq_file은 RCU 전용 순회 헬퍼를 제공하여 안전한 통합을 지원합니다.

mutex vs RCU: seq_file 보호 패턴 비교 Mutex 보호 (일반 패턴) start(): lock show()+next() stop(): unlock 장점: 구현 간단, 데이터 일관성 보장 단점: 읽기 중 쓰기 차단, sleep 가능 용도: 대부분의 /proc 파일, 빈번하지 않은 읽기 예: /proc/modules, /proc/my_items RCU 보호 (고성능 패턴) start(): rcu_read_lock show()+next() stop(): rcu_read_unlock 장점: 읽기 lock-free, 쓰기와 동시 실행 가능 단점: 읽기 중 항목이 삭제될 수 있음 (stale data) 용도: /proc/net/*, 고빈도 읽기 + 드문 쓰기 예: /proc/net/tcp, /proc/net/arp
/* RCU 보호 seq_file 패턴 (/proc/net/tcp 스타일) */
static void *my_rcu_start(struct seq_file *m, loff_t *pos)
{
    rcu_read_lock();
    return seq_hlist_start_rcu(&my_hash[*pos % HASH_SIZE], *pos);
}

static void *my_rcu_next(struct seq_file *m, void *v, loff_t *pos)
{
    return seq_hlist_next_rcu(v, &my_hash[*pos % HASH_SIZE], pos);
}

static void my_rcu_stop(struct seq_file *m, void *v)
{
    rcu_read_unlock();
}

static int my_rcu_show(struct seq_file *m, void *v)
{
    struct my_entry *e = hlist_entry(v, struct my_entry, hnode);
    /* RCU 읽기 중: 항목 멤버는 읽기 가능, 수정 금지 */
    seq_printf(m, "%pI4:%u %pI4:%u %s\n",
               &e->saddr, e->sport, &e->daddr, e->dport,
               tcp_state_name(e->state));
    return 0;
}
RCU + seq_file 주의사항: RCU 읽기 섹션 내에서는 show()가 sleep할 수 없습니다. 따라서 seq_printf()가 버퍼 오버플로를 유발하면 stop()(rcu_read_unlock) → 버퍼 확장 → start()(rcu_read_lock)이 자동으로 실행됩니다. 이 사이에 데이터가 변경될 수 있으므로, RCU 패턴에서는 순회 중 항목 삭제에 대한 내성이 필요합니다.

커널 실전 사용 사례

리눅스 커널 소스에서 seq_file을 활용하는 대표적인 예시를 정리합니다. 이 사례들은 실제 코드를 참고할 때 유용합니다.

파일 소스 위치 패턴 설명
/proc/modules kernel/module/procfs.c 반복자 로드된 모듈 리스트를 seq_list_start()로 순회
/proc/net/tcp net/ipv4/tcp_ipv4.c 반복자 + 헤더 seq_list_start_head()로 헤더 행 출력 후 소켓 순회
/proc/mounts fs/proc_namespace.c 반복자 + escape seq_escape()로 경로의 특수 문자 이스케이프
/proc/interrupts kernel/irq/proc.c 반복자 (배열) IRQ 번호를 인덱스로 배열 순회
/proc/version fs/proc/version.c single_open 단일 문자열 출력, 가장 단순한 사례
/proc/meminfo fs/proc/meminfo.c single_open 메모리 통계를 한 번에 출력
/proc/slabinfo mm/slab_common.c 반복자 + pad seq_pad()로 컬럼 정렬, SLAB 캐시 순회
/sys/kernel/debug/* (각 드라이버) DEFINE_SHOW_ATTRIBUTE debugfs 파일의 표준 패턴
# 커널 소스에서 seq_file 사용 패턴 검색하기
git grep 'seq_operations' -- '*.c' | wc -l    # ~600+ 파일
git grep 'DEFINE_SHOW_ATTRIBUTE' -- '*.c' | wc -l # ~400+ 파일
git grep 'single_open' -- '*.c' | wc -l          # ~300+ 파일

외부 참고 자료