Dev/Linux

Linux/proc 파일시스템, 커널과 대화하기

onepaperhoon 2026. 4. 3. 17:36
`cat /proc/cpuinfo`,`cat /proc/meminfo` - 써본 적 있으신가요?
이 파일들은 디스크에 없습니다. 커널이 즉석에서 만들어냅니다. 오늘은 그 원리를 직접 구현해 봅니다. 

1. /proc 파일시스템이란?

터미널에서 이런 명령을 써본 적 있을 겁니다.

$ cat /proc/cpuinfo      # CPU 정보
$ cat /proc/meminfo      # 메모리 사용량
$ cat /proc/uptime       # 부팅 후 경과 시간

 

이 파일들은 실제로 디스크에 존재하지 않습니다. `/proc`은 가상 파일 시스템(virtual File System)입니다.
커널이 메모리 위에 만들어 놓은 인터페이스로,  `cat`으로 읽으면 커널 함수가 호출되어 데이터를 즉석에서 생성합니다.

 

디스크
┌──────────────────┐
│ /home, /etc, ...│  ← 실제 파일
└──────────────────┘

메모리 (커널)
┌──────────────────┐
│ /proc/cpuinfo   │  ← cat 하면 커널 함수 호출
│ /proc/meminfo   │
│ /proc/mymodule  │  ← 우리가 만들 것
└──────────────────┘

이 메커니즘을 이용하면 커널 모듈이 유저스페이스 앱과 파일처럼 통신할 수 있습니다.


2. 동작 원리 - cat 하면 무슨 일이 생기나?

유저스페이스                     커널스페이스

cat /proc/onepaperhoon_info
  │
  │  open() 시스템 콜
  ▼
  proc_open()  ─────────────►  single_open(file, proc_show, NULL)
                                  seq_file 초기화

  │  read() 시스템 콜
  ▼
  seq_read()   ─────────────►  proc_show(seq_file *m, void *v)
                                  seq_printf(m, "Access count: %lu\n", ...)
                                  버퍼에 데이터 채움

  │  결과 출력
  ▼
Access count: 1
Current jiffies: 4298765432
...


echo reset > /proc/onepaperhoon_info
  │
  │  write() 시스템 콜
  ▼
  proc_write() ─────────────►  copy_from_user(cmd, buffer, count)
                                  "reset" 감지 → access_count = 0

핵심은 `cat`과 `echo`가 일반 파일처럼 동작하지만, 실제로는 커널 함수를 호출한다는 점입니다.


3. 핵심 API 설명

① proc_ops — 커널 5.6부터 필수

static const struct proc_ops proc_fops = {
    .proc_open    = proc_open,
    .proc_read    = seq_read,
    .proc_write   = proc_write,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

커널 5.6 이전에는 `file_operations`를 썼습니다. 5.6부터 `/proc`전용 `struct proc_ops`가 도입됐습니다.
Raspberry Pi의 커널 6.x에서는 반드시 `proc_ops`를 써야 합니다. `file_operations`를 쓰면 컴파일 에러가 납니다.

② seq_file — 버퍼 걱정 없이 출력

`/proc`파일을 읽을 때 단순히 문자열을 반환하면 대용량 데이터에서 페이지 경계 문제가 생깁니다.

`seq_file`은 커널이 제공하는 순차 출력 인터페이스로, 버퍼 관리를 자동으로 처리해 줍니다.

함수 역할
single_open(file, show_fn, data) 단일 페이지 출력용 seq_file 초기화
seq_printf(m, fmt, ...) seq_file 버퍼에 포맷 출력
seq_read 표준 read 핸들러 (직접 구현 불필요)
single_release single_open 대응 release 핸들러

③ copy_from_user() — 유저 → 커널 안전 복사

if (copy_from_user(cmd, buffer, count))
    return -EFAULT;

유저스페이스와 커널스페이스는 메모리 주소 공간이 분리되어 있습니다. `echo reset` 으로 데이터를 쓰면 그 데이터는 유저 공간 메모리에 있습니다. 커널에서 직접 포인터로 접근하면 페이지 폴트나 보안 문제가 생깁니다. `copy_from_user()`가 유저 공간 -> 커널 공간으로 안전하게 복사해 줍니다.

④ `__user` 어노테이션

static ssize_t proc_write(struct file *file, const char __user *buffer, ...)

`__user`는 실행에 영향을 주지 않는 컴파일러 어노테이션입니다. `sparse` (커널 정적 분성 도구)가 이 포인터를 유저 공간 주소로 인식해, `copy_from_user()` 없이 직접 역참조 하면 경고를 발생시킵니다. 빌드 타임에 실수를 잡아주는 안전장치입니다.

⑤ jiffies — 커널의 시간 단위

seq_printf(m, "Current jiffies: %lu\n", jiffies);

`jiffies`는 커널 부팅 이후 타이머 인터럽트가 발생한 횟수입니다. 1초에 `HZ`번 증가합니다. (보통 250 또는 1000).

`/proc/uptime` 같은 파일이 이 값으로 경과 시간을 계산합니다.


4. 코드 전체 구조

#include <linux/init.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/uaccess.h>

#define PROC_NAME "onepaperhoon_info"

static unsigned long access_count = 0;

/* ① cat 할 때 호출 — 출력 내용 정의 */
static int proc_show(struct seq_file *m, void *v)
{
    access_count++;
    seq_printf(m, "====================================\n");
    seq_printf(m, "OnePaperHoon Kernel Module Info\n");
    seq_printf(m, "====================================\n");
    seq_printf(m, "Access count: %lu\n", access_count);
    seq_printf(m, "Current jiffies: %lu\n", jiffies);
    seq_printf(m, "Module: %s\n", THIS_MODULE->name);
    seq_printf(m, "====================================\n");
    return 0;
}

/* ② open 시 seq_file 초기화 */
static int proc_open(struct inode *inode, struct file *file)
{
    return single_open(file, proc_show, NULL);
}

/* ③ echo 할 때 호출 — "reset" 명령 처리 */
static ssize_t proc_write(struct file *file, const char __user *buffer,
                           size_t count, loff_t *pos)
{
    char cmd[10];

    if (count > sizeof(cmd) - 1)
        return -EINVAL;

    if (copy_from_user(cmd, buffer, count))
        return -EFAULT;

    cmd[count] = '\0';

    if (strncmp(cmd, "reset", 5) == 0) {
        access_count = 0;
        printk(KERN_INFO "proc_basic: Counter reset!\n");
    }
    return count;
}

/* ④ proc_ops 등록 (커널 5.6+) */
static const struct proc_ops proc_fops = {
    .proc_open    = proc_open,
    .proc_read    = seq_read,
    .proc_write   = proc_write,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

static struct proc_dir_entry *proc_entry;

/* ⑤ insmod 시 /proc 파일 생성 */
static int __init proc_basic_init(void)
{
    proc_entry = proc_create_data(PROC_NAME, 0666, NULL, &proc_fops, NULL);
    if (!proc_entry) {
        printk(KERN_ERR "proc_basic: Failed to create /proc/%s\n", PROC_NAME);
        return -ENOMEM;
    }
    printk(KERN_INFO "proc_basic: Created /proc/%s\n", PROC_NAME);
    return 0;
}

/* ⑥ rmmod 시 /proc 파일 제거 */
static void __exit proc_basic_exit(void)
{
    if (proc_entry)
        remove_proc_entry(PROC_NAME, NULL);
    printk(KERN_INFO "proc_basic: Total accesses: %lu\n", access_count);
}

module_init(proc_basic_init);
module_exit(proc_basic_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("OnePaperHoon");
MODULE_DESCRIPTION("Basic /proc filesystem example - Week 5 Day 3");
MODULE_VERSION("1.0");

5. 자주 하는 실수 모음

❌ 실수 1: 커널 5.6 미만 API 사용

/* 나쁜 예 — 5.6 이상에서 컴파일 에러 */
static const struct file_operations proc_fops = {
    .open    = proc_open,
    .read    = seq_read,
    ...
};

/* 좋은 예 — 5.6 이상 필수 */
static const struct proc_ops proc_fops = {
    .proc_open = proc_open,
    .proc_read = seq_read,
    ...
};

Raspberry Pi 커널 6.x에서는 반드시 `proc_ops`를 써야 합니다.

❌ 실수 2: copy_from_user() 없이 직접 접근

/* 나쁜 예 — 커널 패닉 또는 보안 취약점 */
static ssize_t proc_write(struct file *file, const char __user *buffer, ...)
{
    char cmd[10];
    memcpy(cmd, buffer, count);  /* 직접 접근 — 절대 금지 */
}

/* 좋은 예 */
if (copy_from_user(cmd, buffer, count))
    return -EFAULT;

`__user` 포인터를 직접 역참조하면 커널 패닉이 발생합니다.

❌ 실수 3: remove_proc_entry() 누락

/* 나쁜 예 — exit에서 제거 안 함 */
static void __exit proc_basic_exit(void)
{
    /* remove_proc_entry() 깜빡! */
    printk(KERN_INFO "Unloaded\n");
}

모듈이 제거된 후에도 `/proc` 엔트리가 남아 있으면, `cat /proc/onepaperhoon_info` 시 커널 패닉이 발생합니다. `exit` 함수에서 반드시 `remove_proc_entry()`를 호출하세요.

❌ 실수 4: count 검증 없이 copy_from_user()

/* 나쁜 예 — 버퍼 오버플로우 가능 */
if (copy_from_user(cmd, buffer, count))
    return -EFAULT;

/* 좋은 예 — 크기 검증 먼저 */
if (count > sizeof(cmd) - 1)
    return -EINVAL;
if (copy_from_user(cmd, buffer, count))
    return -EFAULT;

`count`를 먼저 검증하지 않으면 버퍼 오버플로우가 생길 수 있습니다.


7. 마치며

`/proc` 파일시스템은 커널 모듈과 유저스페이스가 대화하는 가장 단순한 방법입니다.

/proc 파일은 디스크에 없다 — cat 하면 커널 함수가 실행된다.
커널 5.6부터 proc_ops를 써야 한다.
유저 → 커널 데이터 복사는 반드시 copy_from_user()를 거쳐야 한다.
remove_proc_entry() 없이 rmmod 하면 커널 패닉이 난다.

다음 글에서는 `/dev` 디렉터리에 파일을 만들어 유저스페이스 앱과 `read()`/`write()`/`ioctl()`로 통신하는 Character Device Driver를 다룰 예정입니다.


참고 자료

  • Linux Kernel Documentation: Documentation/filesystems/proc.rst
  • linux/proc_fs.h, linux/seq_file.h 커널 소스
  • linux/uaccess.h — copy_from_user() 구현