seq_file 인터페이스
seq_file은 커널 내부 데이터를 사용자 공간에 안전하게 출력하기 위한 표준 인터페이스입니다. 본 문서는 반복자(Iterator) 패턴의 4개 콜백(start/next/stop/show), 버퍼 자동 관리, 헬퍼 함수, RCU 통합, proc_create_seq API, 커널 소스 구현 분석을 상세히 다룹니다.
핵심 요약
seq_file은 커널 데이터를 사용자 공간에 순차적으로 출력하는 안전한 인터페이스입니다. 반복자 패턴(start/next/stop/show)으로 대용량 데이터를 자동 페이지네이션하며, 버퍼 오버플로를 투명하게 처리합니다.
단계별 이해
- 목적:
/proc,debugfs등에서 커널 데이터를 사용자에게 텍스트로 출력 - 4개 콜백:
start()→show()→next()→ ... →stop() - 버퍼 자동 관리: 초기 4KB, 부족 시 2배씩 확장 (최대 KMALLOC_MAX_SIZE)
- 간편 API: 소량 데이터는
single_open(), 대용량은seq_operations반복자
seq_file 개요
커널 2.6 이전에는 /proc 파일을 읽을 때 드라이버가 직접 copy_to_user()와 오프셋을 관리해야 했습니다. 출력이 한 페이지를 초과하면 데이터 누락이나 중복이 빈번했습니다. seq_file은 이 문제를 해결하기 위해 도입된 표준 인터페이스로, 버퍼 관리를 자동화하고 반복자(Iterator) 패턴으로 데이터 순회를 추상화합니다.
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 크기 |
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_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_data에seq_file구조체를 할당하고seq_operations를 연결합니다. 이후seq_read()가 이 구조체를 통해 콜백을 호출합니다. - proc_ops 구조커널 5.6+에서
file_operations대신 사용합니다.proc_read에seq_read,proc_lseek에seq_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()에서 이어서 출력합니다. 드라이버가 최대 항목 수를 제한할 필요가 없습니다.
/* 대용량 데이터 처리: 배열 기반 이터레이터 */
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_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()이 반환하는idx를m→private에 저장하여stop()에서 사용합니다.
stop()을 호출한 뒤 버퍼를 확장하고 start()를 다시 호출합니다. stop()에서 rcu_read_unlock()이 호출되므로 재시도 사이에 RCU grace period가 발생할 수 있습니다. 이는 정상적인 동작이며, 재시작 시 start()에서 rcu_read_lock()을 다시 획득하므로 안전합니다.
성능과 메모리 관리
seq_file의 버퍼 관리 특성과 대용량 데이터 출력 시 성능 최적화 원칙을 설명합니다.
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()
* 짝이 맞지 않으면 메모리 누수 또는 이중 해제 발생 */
함정 요약 체크리스트
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→private
seq_open_private()가 내부적으로kmalloc()한 영역의 포인터입니다. open부터 release까지 유지되므로, 이터레이션 간 상태를 안전하게 전달할 수 있습니다. - SEQ_SKIP
show()가 SEQ_SKIP을 반환하면 현재 항목의 출력이 seq_buf에서 제거됩니다. 조건부 필터링에 유용한 패턴으로, 별도의 임시 버퍼 없이 선택적 출력이 가능합니다.
파일 수명주기: open → read → release
사용자 공간에서 cat /proc/my_entry를 실행하면 내부적으로 open()→read()→...→read()→release() 시스템 콜이 순차적으로 호출됩니다. seq_file은 이 과정에서 버퍼를 할당·관리하고, 반복자 상태를 유지합니다.
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→from
count는 버퍼에 남은 유효 데이터 크기,from은 버퍼 내 읽기 시작 오프셋입니다. 이전read()에서 유저 버퍼가 작아 미전송된 데이터가 남아있을 수 있습니다. - seq_has_overflowed(m)
m→count >= m→size를 확인합니다.seq_printf()내부에서seq_buf_printf()가 버퍼를 초과하면count를size이상으로 설정합니다. - m→size <<= 1버퍼 크기를 2배로 증가시킵니다. 초기 크기는
PAGE_SIZE(4KB)이며, 최대KMALLOC_MAX_SIZE까지 확장됩니다. 이로 인해show()가 동일 항목에서 여러 번 호출될 수 있습니다. - SEQ_SKIP
show()가SEQ_SKIP을 반환하면 현재 항목의 출력을 건너뜁니다. 필터링이 필요한 경우에 활용합니다. - start() 재호출오버플로 시 동일
index에서start()를 다시 호출합니다. 따라서show()는 반드시 부작용이 없는 순수 출력 함수여야 합니다.
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_pos
index는 이터레이터의 논리 위치(항목 번호)이고,read_pos는 유저가 읽은 바이트 수입니다.lseek()시traverse()가read_pos를 기반으로index를 재계산합니다. - lock동일 파일 디스크립터에서 멀티스레드
read()를 직렬화합니다.seq_read_iter()진입 시mutex_lock()을 획득합니다. - private
seq_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;
}
traverse()는 처음부터 offset까지 모든 항목을 순회하므로 O(n) 비용이 발생합니다. 대용량 데이터에서 lseek를 빈번하게 사용하면 성능이 크게 저하됩니다. 대부분의 /proc 파일은 처음부터 순차 읽기만 하므로 실제로 문제가 되는 경우는 드뭅니다.
종합 모듈 예제
다음은 seq_file을 사용하여 /proc/my_items 파일을 생성하는 완전한 커널 모듈입니다. insmod 후 cat /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 전용 순회 헬퍼를 제공하여 안전한 통합을 지원합니다.
/* 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;
}
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+ 파일
외부 참고 자료
- docs.kernel.org — The seq_file Interface: seq_file 인터페이스의 공식 문서입니다. seq_operations 콜백, single_open 패턴, 대용량 데이터 출력 방법을 설명합니다.
- LWN.net — Driver porting: The seq_file interface: seq_file 인터페이스가 처음 도입되었을 때의 설명 기사입니다. 기존 /proc 읽기 방식의 한계와 seq_file이 해결하는 문제를 다룹니다.
- Bootlin Elixir — fs/seq_file.c: seq_file 인터페이스의 핵심 구현 소스입니다. 버퍼 관리, 반복자 패턴, 오버플로 처리를 확인할 수 있습니다.
- Bootlin Elixir — include/linux/seq_file.h: seq_file 구조체와 seq_operations, 헬퍼 함수 선언 헤더입니다.
- Bootlin Elixir — include/linux/proc_fs.h: proc_create_seq, proc_create_single 등 procfs 연동 API 선언 헤더입니다.