seq_file 인터페이스

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

전제 조건: procfs/sysfs/debugfsVFS 문서를 먼저 읽으면 이 문서를 더 쉽게 이해할 수 있습니다.
일상 비유: seq_file은 자동 줄바꿈이 되는 프린터 버퍼와 비슷합니다. 용지(버퍼)가 가득 차면 더 큰 용지로 교체하고 처음부터 다시 인쇄하지만, 이미 전달된 페이지(Page)는 건너뜁니다.

핵심 요약

  • 목적/proc, debugfs 등에서 커널 데이터를 사용자에게 텍스트로 안전하게 출력합니다.
  • 반복자 패턴start()show()next()stop() 4개 콜백으로 순회를 구성합니다.
  • 자동 버퍼 관리 — 초기 4KB, 부족하면 2배씩 확장하며 오버플로를 투명하게 처리합니다.
  • 간편 API — 소량 데이터는 single_open(), 대용량은 seq_operations 반복자를 사용합니다.
  • 동시성 — 재호출(retry) 가능성을 전제로 start에서 잠금(Lock)을 잡고 stop에서 해제해야 합니다.

단계별 이해

  1. 개념 파악
    seq_file이 copy_to_user·버퍼 오버플로(Buffer Overflow)·오프셋 관리를 대신해 준다는 점을 이해합니다.
  2. 4개 콜백 역할
    start/show/next/stop이 각각 초기화·출력·이동·정리에 대응한다는 관계를 머릿속에 그립니다.
  3. 간편 API 선택
    단일 출력은 single_open, 반복 순회는 seq_operations+proc_create_seq를 골라 사용합니다.
  4. 잠금과 재시도
    버퍼가 커지면 엔진이 전체를 재호출하므로 잠금 범위와 재시도 가능 여부를 점검합니다.
  5. 디버깅 패턴
    cat로 직접 읽어 결과를 검증하고, 누락·중복 여부로 콜백 흐름을 역추적(Backtrace)합니다.

seq_file 개요

커널 2.6 이전에는 /proc 파일을 읽을 때 드라이버가 직접 copy_to_user()와 오프셋(Offset)을 관리해야 했습니다. 출력이 한 페이지를 초과하면 데이터 누락이나 중복이 빈번했습니다. 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() 시작, 버퍼 재할당 후 재시작(Reboot) 첫 항목 포인터 / 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 최소 (매크로(Macro)) 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)연결 리스트(Linked List)에서 pos 번째 항목을 찾아 반환합니다. 리스트 끝을 넘으면 NULL을 반환하여 순회를 종료합니다.
  • mutex_lock/unlock 위치lock은 반드시 start()에서 획득하고 stop()에서 해제합니다. 버퍼 오버플로 시 stop()→start()가 재호출되므로 lock/unlock 쌍이 자동으로 유지됩니다.
  • seq_open(file, &ops)file→private_dataseq_file 구조체(Struct)를 할당하고 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 콜백에서 자주 사용하는 리스트/해시 테이블(Hash Table) 순회를 위한 헬퍼 함수를 제공합니다. 직접 순회 코드를 작성하는 대신 이 함수들을 사용하면 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 해시(Hash) 버킷 순회 시작 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) 고정 폭 컬럼 정렬 (패딩(Padding) 후 문자 c 출력) /proc/net/tcp
seq_hex_dump() 16진수 덤프(Dump) 출력 레지스터(Register) 덤프, 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에서 마운트(Mount) 경로에 공백이 포함된 경우 파서가 혼동하지 않도록 이스케이프합니다.
  • 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 임계 영역(Critical Section)을 시작/종료합니다. 이 구간 내에서 RCU 보호 포인터를 안전하게 역참조(Dereference)할 수 있습니다. 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()을 다시 획득하므로 안전합니다.

성능과 메모리 관리(Memory Management)

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() 시스템 콜(System Call)이 순차적으로 호출됩니다. 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()를 사용해야 합니다. 짝이 맞지 않으면 메모리 누수 또는 이중 해제(Double Free)가 발생합니다.

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동일 파일 디스크립터(File Descriptor)에서 멀티스레드 read()를 직렬화(Serialization)합니다. 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 파일을 생성하는 완전한 커널 모듈(Kernel Module)입니다. 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() 파싱 오버헤드(Overhead)가 없어 빠릅니다. 단일 문자(\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()로 헤더 행 출력 후 소켓(Socket) 순회
/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 캐시(Cache) 순회
/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+ 파일

v6.12~v6.14 최신 변경사항

v6.12~v6.14에서 seq_file의 핵심 엔진(seq_read_iter(), 버퍼 관리, C API)은 변경되지 않았습니다. 주요 변경사항은 Rust 언어 지원 추가와 procfs 보안 수정입니다.

Rust seq_file 추상화 (v6.13)

커널 6.13에서 Rust Binder 드라이버 지원을 위해 rust/kernel/seq_file.rs가 도입되었습니다. C struct seq_file을 래핑하는 SeqFile 구조체와 seq_print! 매크로를 제공하며, Rust 코드에서 seq_file 인터페이스를 관용적(Idiomatic)으로 사용할 수 있습니다.

// rust/kernel/seq_file.rs (v6.13 신규)
// SeqFile: C struct seq_file의 투명 래퍼
// NotThreadSafe로 표시 — 동일 파일 디스크립터에서 seq_read_iter()가 mutex로 직렬화

use crate::seq_file::SeqFile;

// seq_print! 매크로: Rust의 format! 스타일로 seq_file에 출력
// 내부적으로 C seq_printf()의 %pA 포맷 지정자를 사용하여 호출
fn my_show(m: &SeqFile) {
    seq_print!(m, "hello from Rust: value={}\n", 42);
}

// SeqFile::from_raw(): C 포인터에서 안전하지 않은(unsafe) 생성
// SeqFile::call_printf(): C seq_printf 호출 (내부용)
코드 설명
  • SeqFileC struct seq_file에 대한 투명(Transparent) 래퍼입니다. NotThreadSafe로 표시되어 있으며, 이는 C 측의 seq_file→lock 뮤텍스(Mutex)가 동시 접근을 직렬화하는 것에 대응합니다.
  • seq_print!Rust format! 매크로와 동일한 문법으로 seq_file 버퍼에 출력합니다. 내부적으로 C의 seq_printf()%pA 포맷 지정자와 함께 호출하여 Rust fmt::Arguments를 전달합니다.
Rust 사용 범위: Rust seq_file 추상화는 Binder 드라이버의 debugfs 출력에서 처음 활용되었습니다. Rust로 커널 모듈을 작성할 때 /proc이나 debugfs 파일을 생성하려면 이 추상화를 사용할 수 있습니다. 단, CONFIG_RUST=y 빌드 설정이 필요합니다.

seq_puts() 컴파일 타임 최적화 (v6.10)

커널 6.10에서 seq_puts()가 인라인 래퍼로 변경되었습니다. __builtin_constant_p()를 사용하여 문자열 리터럴의 길이를 컴파일 타임에 계산하고, 단일 문자면 seq_putc(), 길이가 알려진 문자열이면 seq_write()로 직접 라우팅(Routing)합니다. 런타임 strlen() 호출을 제거하여 커널 전체 약 3,400개 이상의 호출 사이트에서 성능이 개선되었습니다.

/* include/linux/seq_file.h (v6.10 변경) */
/* 기존: seq_puts() → strlen() + memcpy */
/* 변경: 인라인 래퍼가 컴파일 타임에 분기 */

/* seq_puts("X") → seq_putc(m, 'X')         (1문자: 최적) */
/* seq_puts("hello\n") → seq_write(m, s, 6)  (길이 확정: strlen 불필요) */
/* seq_puts(dynamic_str) → __seq_puts(m, s)  (동적 문자열: 기존 경로) */

static inline void seq_puts(struct seq_file *m, const char *s)
{
    if (__builtin_constant_p(*s) && !s[0])
        return;  /* 빈 문자열: 아무것도 하지 않음 */
    if (__builtin_constant_p(*s) && !s[1])
        seq_putc(m, s[0]);  /* 단일 문자 */
    else if (__builtin_constant_p(strlen(s)))
        seq_write(m, s, strlen(s));  /* 컴파일 타임 길이 */
    else
        __seq_puts(m, s);  /* 런타임 strlen 필요 */
}

proc_get_inode() UAF 수정 (v6.14, CVE-2025-21999)

커널 6.14에서 proc_get_inode()의 Use-After-Free(UAF) 취약점(Vulnerability)이 수정되었습니다. 모듈 언로드와 /proc inode 인스턴스화 사이의 경합(Contention) 조건(Race Condition)이 원인이었습니다. proc_ops 포인터가 모듈에 속해 있어, 모듈 언로드 후 접근하면 해제된 메모리를 참조할 수 있었습니다.

/* include/linux/proc_fs.h (v6.14 변경) */
/* 수정 전: proc_register() 후 proc_ops 포인터를 역참조 → UAF 위험 */
/* 수정 후: proc_register() 전에 inode 설정 정보를 PDE에 캐싱 */

/* 새 플래그: proc_dir_entry에 콜백 존재 여부를 사전 기록 */
#define PROC_ENTRY_proc_read_iter    (1U << 1)
#define PROC_ENTRY_proc_compat_ioctl  (1U << 2)

/* proc_register() 전에 플래그를 설정하여
 * proc_get_inode()가 모듈의 proc_ops 포인터 없이도
 * inode의 file_operations를 올바르게 구성할 수 있음 */
영향 범위: CVE-2025-21999는 커널 6.2~6.13에 영향을 미칩니다. proc_create_seq()로 등록한 proc 파일에서도 모듈 언로드 타이밍에 따라 발생할 수 있었습니다. 커널 6.14 이상 또는 6.13.9 이상 안정 버전에서 수정되었습니다.

변경사항 요약

버전변경 내용유형영향 범위
v6.10seq_puts() 컴파일 타임 최적화성능 개선커널 전체 ~3,400+ 호출 사이트
v6.13Rust SeqFile + seq_print! 매크로신규 APIRust 커널 모듈 (CONFIG_RUST)
v6.13seq_release() kerneldoc 매개변수 순서 수정문서 수정문서 전용, 기능 변경 없음
v6.14proc_get_inode() UAF 수정 (CVE-2025-21999)보안 수정모듈 언로드 경합 조건
핵심 요점: seq_file의 C API, 반복자 패턴, 버퍼 관리 메커니즘은 v6.12~v6.14에서 변경되지 않았습니다. 기존 seq_operations, proc_create_seq(), DEFINE_SHOW_ATTRIBUTE() 패턴은 그대로 유효합니다.

외부 참고 자료

seq_file 최신 변화 (v6.8~v6.15)

seq_file 계층은 Rust 추상화 도입, 오버플로우 방어 강화, single_open 관용구 정리를 중심으로 업데이트되었습니다.

Rust seq_file abstraction (v6.13~)

오버플로우 보호 강화 (v6.14)

single_open 관용구 정리 (v6.10~v6.14)

static int my_show(struct seq_file *m, void *v)
{
    struct my_ctx *ctx = pde_data(file_inode(m->file));
    seq_printf(m, "count=%u\n", ctx->count);
    return 0;
}

static int my_open(struct inode *ino, struct file *f)
{
    return single_open(f, my_show, pde_data(ino));
}
: seq_printf가 -1을 반환하면 버퍼가 가득 찬 것이며, VFS가 더 큰 버퍼로 재시도합니다. 루프 내부에서 오버플로우 발생 시 조기 종료하도록 seq_has_overflowed()를 검사하는 것이 관용구로 자리잡았습니다.

seq_file 추가 변화 (v6.15~v6.18)

기존 섹션에서 다룬 Rust 추상화 도입(v6.13), seq_puts() 컴파일 타임 최적화(v6.10), CVE-2025-21999 UAF 수정(v6.14) 이후에도 seq_file 생태계는 안정세를 유지하며 주변 인프라가 점진적으로 개선되었습니다.

Rust SeqFile 확장 (v6.15~)

v6.13에서 도입된 rust/kernel/seq_file.rs는 이후 버전에서 Binder 이외 Rust 드라이버들도 채택하기 시작했습니다. SeqFile 래퍼와 seq_print! 매크로는 API 변경 없이 유지되며, Rust로 작성하는 /procdebugfs 파일의 표준 방식으로 자리잡고 있습니다.

Rust 추상화를 사용할 때 주요 패턴은 아래와 같습니다.

// rust/kernel/seq_file.rs 활용 패턴 (v6.13 이후 공통)
use kernel::seq_file::SeqFile;

fn show_info(m: &SeqFile, data: &MyData) -> Result {
    seq_print!(m, "count: {}\n", data.count);
    seq_print!(m, "state: {}\n", data.state);
    Ok(())
}
// CONFIG_RUST=y 빌드 필요

오버플로우 검사 관용구 정착 (v6.14 이후)

CVE-2025-21999 수정 이후 커널 전반에서 seq_file 루프 내 seq_has_overflowed() 조기 탈출 패턴이 코딩 관용구(Idiom)로 명확히 권장됩니다. 새로 작성하는 show() 콜백에서는 아래처럼 루프 중간에 체크하여 불필요한 반복을 줄이는 것이 표준 방식입니다.

static int my_show(struct seq_file *m, void *v)
{
    struct my_entry *e = v;

    while (condition) {
        seq_printf(m, "%s %lu\n", e->name, e->val);
        if (seq_has_overflowed(m))
            return 0;  /* VFS가 더 큰 버퍼로 재시도함 */
        e = next_entry(e);
    }
    return 0;
}

코어 API 안정성

v6.15~v6.18 구간에서 seq_read_iter(), seq_operations, proc_create_seq(), DEFINE_SHOW_ATTRIBUTE() 등 핵심 C API는 변경되지 않았습니다. 기존 드라이버와 모듈 코드는 재컴파일 없이 동작하며, 새로운 모듈 작성 시에도 기존 패턴을 그대로 사용할 수 있습니다.

요약: seq_file v6.15~v6.18의 핵심은 API 안정성 유지입니다. Rust 기반 드라이버라면 SeqFile 래퍼를 적극적으로 활용하고, C 기반 드라이버는 seq_has_overflowed() 조기 탈출 패턴을 루프에 반드시 적용하세요.