Kernel version: 5.14.17
procfs이란?
procfs는 proc + fs = process file system의 약자다. 모든 것을 파일로 취급하는 유닉스 사상에 따라 이 특별한 파일 시스템은 시스템 내의 정보를 제공하기 위해 존재한다. 실제로 존재하는 하드웨어에 대한 파일 시스템이 아닌 메모리 상에서만 존재하고, 가상 파일 시스템(VFS)의 인터페이스의 형태이며 /proc 경로에 마운트 되어 사용된다. 예전에 정리했던 포스팅에서도 아주 잠깐 다룬 적이 있다. [wikipedia]
"ls /proc" 명령어를 사용해보면 뭔가 많이 있는 것을 확인할 수 있다. 숫자로 된 디렉터리들은 현재 올라가 있는 프로세스들의 PID를 나타내고, 나머지들도 각자의 결과를 반환하는 역할을 하는 함수들이다. 예를 들어 유닉스/리눅스 계열의 1번 프로세스인 init 프로세스의 정보를 알려면 "sudo ls /proc/1" 명령어를 사용하면 된다.
뜨는 여러 항목들 가운데 io를 불러봤다. 해당 파일은 조회하고 있는 프로세스의 I/O 통계를 보여준다. 이외에도 "cat /proc/uptime" 같은 명령어들로도 현재 시스템의 정보들을 조회할 수 있다.
Componenets of procfs
Data Structures
struct proc_dir_entry
/proc 밑에 있는 폴더 혹은 파일을 위한 전체적인 형태를 제공하는 구조체다. proc과 관련된 내부 함수에서 이 구조체를 통해 설정 및 조작한다.
/* fs/proc/internal.h */
/*
* This is not completely implemented yet. The idea is to
* create an in-memory tree (like the actual /proc filesystem
* tree) of these proc_dir_entries, so that we can dynamically
* add new files to /proc.
*
* parent/subdir are used for the directory structure (every /proc file has a
* parent, but "subdir" is empty for all non-directory entries).
* subdir_node is used to build the rb tree "subdir" of the parent.
*/
struct proc_dir_entry {
/*
* number of callers into module in progress;
* negative -> it's going away RSN
*/
atomic_t in_use;
refcount_t refcnt;
struct list_head pde_openers; /* who did ->open, but not ->release */
/* protects ->pde_openers and all struct pde_opener instances */
spinlock_t pde_unload_lock;
struct completion *pde_unload_completion;
const struct inode_operations *proc_iops;
union {
const struct proc_ops *proc_ops;
const struct file_operations *proc_dir_ops;
};
const struct dentry_operations *proc_dops;
union {
const struct seq_operations *seq_ops;
int (*single_show)(struct seq_file *, void *);
};
proc_write_t write;
void *data;
unsigned int state_size;
unsigned int low_ino;
nlink_t nlink;
kuid_t uid;
kgid_t gid;
loff_t size;
struct proc_dir_entry *parent;
struct rb_root subdir;
struct rb_node subdir_node;
char *name;
umode_t mode;
u8 flags;
u8 namelen;
char inline_name[];
} __randomize_layout;
struct proc_ops
file_operations와 유사하게 proc의 operations를 정의한 후 proc_create() 함수를 통해 등록할 수 있다.
/* include/linux/proc_fs.h */
struct proc_ops {
unsigned int proc_flags;
int (*proc_open)(struct inode *, struct file *);
ssize_t (*proc_read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*proc_read_iter)(struct kiocb *, struct iov_iter *);
ssize_t (*proc_write)(struct file *, const char __user *, size_t, loff_t *);
/* mandatory unless nonseekable_open() or equivalent is used */
loff_t (*proc_lseek)(struct file *, loff_t, int);
int (*proc_release)(struct inode *, struct file *);
__poll_t (*proc_poll)(struct file *, struct poll_table_struct *);
long (*proc_ioctl)(struct file *, unsigned int, unsigned long);
#ifdef CONFIG_COMPAT
long (*proc_compat_ioctl)(struct file *, unsigned int, unsigned long);
#endif
int (*proc_mmap)(struct file *, struct vm_area_struct *);
unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
} __randomize_layout;
APIs
proc_mkdir()
생성할 디렉터리의 이름(name)과 디렉터리의 부모 디렉터리를 지정해서 만들어주는 함수다. proc_mkdir_data() 함수의 wrapper 형태를 띠고 있다. 부모 디렉터리를 지정하지 않을 땐 NULL을 주면 된다.
/* fs/proc/generic.c */
struct proc_dir_entry *proc_mkdir(const char *name,
struct proc_dir_entry *parent)
{
return proc_mkdir_data(name, 0, parent, NULL);
}
EXPORT_SYMBOL(proc_mkdir);
proc_create()
procfs에서 실제 읽기, 쓰기를 수행할 파일을 생성한다. 3.10 버전 전까지만 해도 create_proc_entry() 함수를 사용했는데 deprecated 되었다.
파일 이름(name), 권한(mode; 기본으로 0444가 할당. 8진수 표기), 파일 생성할 디렉터리(parent; 필요하다면 proc_mkdir() 함수로 생성 후 넘기면 됨), 수행할 연산들(proc_ops)을 파라미터로 넘겨준다.
/* fs/proc/generic.c */
struct proc_dir_entry *proc_create(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct proc_ops *proc_ops)
{
return proc_create_data(name, mode, parent, proc_ops, NULL);
}
EXPORT_SYMBOL(proc_create);
proc_remove()
사용하던 proc_dir_entry 구조체를 주면 해제해준다.
/* fs/proc/generic.c */
void proc_remove(struct proc_dir_entry *de)
{
if (de)
remove_proc_subtree(de->name, de->parent);
}
EXPORT_SYMBOL(proc_remove);
여기까지만 알아도 충분히 간단한 커스텀 proc 파일을 생성할 수 있다.
The seq_file interface
Related references: [Document] [site-1] [site-2]
우리는 가상 파일들(예를 들어 procfs, debugfs 등)을 통해 특별한 프로그램 없이도 결괏값(output)을 얻는 게 가능하다. 읽고 쓰기라는 기능이 들어가 있는 이상 버퍼를 사용해서 이를 관리하는 일을 구현해주는 건 필수다. 하지만 사실 출력을 위한 버퍼를 관리하기란 쉽지 않다. 간단히 생각해보면 디스크로부터 데이터를 읽어온 후 유저 측으로 결과를 반환할 때 offset을 관리해준다든지 내부 버퍼의 크기가 유저가 요청한 데이터 크기보다 작을 경우(일반적으로 하나의 페이지 사이즈보다 큰 output이 필요할 때) 등 고려해야 할 부분이 많고 귀찮다. 이를 해결해주기 위해서 우리는 seq_file 인터페이스를 사용할 수 있다.
struct seq_file / struct seq_operations
/* include/linux/seq_file.h */
struct seq_file {
char *buf;
size_t size;
size_t from;
size_t count;
size_t pad_until;
loff_t index;
loff_t read_pos;
struct mutex lock;
const struct seq_operations *op;
int poll_event;
const struct file *file;
void *private;
};
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
seq_operations 구조체에 보이는 멤버들은 Iterator interface를 나타낸다. 각 이터레이터들은 위의 로직으로 동작한다. 결과적으로 seq_file 인터페이스의 의의대로 seq_show() 함수 내부에서 seq_printf() 함수를 통해 버퍼를 신경 쓰지 않고 원하는 포맷에 맞게 출력할 수 있게 해 준다. 결국 이 일렬의 동작들은 자동으로 내부 버퍼의 이터레이터를 관리해주기 위해 존재하는 것이다.
추가로, 구태여 각 이터레이터 인터페이스에 맞춰서 작성해줄 정도의 복잡하지 않은, 즉 매우 간단한 파일을 작성하고자 하는 경우를 위해 ready-made인 single_open() / single_release() 함수를 제공하고 있다. 이번 실습에서는 처음이기도 하니 직접 이터레이터 인터페이스에 맞게 작성해볼 생각이다.
Implementation of custom procfs
간단하게 만들면 재미없으니 내 proc 파일에 저장된 문자열을 변환해주는 동작을 구현하도록 했다 (홀수 인덱스-소문자, 짝수 인덱스-대문자/인덱스 0부터 시작).
my_proc.c
/*
* [ Print a string with a following rule ]
* - original string is kept -
* odd index char - lowercase
* even index char - uppercase
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/string.h>
#define DIR_NAME "karatus-dir"
#define FILE_NAME "karatus"
#define BUF_LEN 100
static uint8_t *buf; // will be initialized in proc_init()
static int len; // buf's current length
static struct proc_dir_entry *proc_dir = NULL;
static struct proc_dir_entry *proc_file = NULL;
/**
* normal functions -----------------------------------------------
*/
static inline bool is_odd(long long num)
{
return (num % 2 == 1) ? true : false;
}
static bool is_alpha(const uint8_t *c)
{
if (('A' <= *c && *c <= 'Z') || ('a' <= *c && *c <= 'z'))
return true;
return false;
}
static uint8_t *to_upper(uint8_t *c)
{
if (!is_alpha(c)) return NULL;
if ('a' <= *c && *c <= 'z')
*c -= 32;
return c;
}
static uint8_t *to_lower(uint8_t *c)
{
if (!is_alpha(c)) return NULL;
if ('A' <= *c && *c <= 'Z')
*c += 32;
return c;
}
static void convert_char(uint8_t *c, bool is_odd)
{
if (is_odd) // odd index
to_lower(c);
else // even index
to_upper(c);
}
/**
* seq_file interface ----------------------------------------------
*/
static void *seq_start(struct seq_file *m, loff_t *pos)
{
/* It is okay that
* c is defined inside of start(),
* because the pointer of c is alive
* until arriving stop().
*/
uint8_t *c;
printk(KERN_INFO "[%s] start(): pos=%lld, seq-file pos=%lu\n", FILE_NAME, *pos, m->count);
if (*pos >= len) { // are we done?
printk(KERN_INFO "[%s] start(): Done. Go to stop()\n", FILE_NAME);
return NULL;
}
// below-code's allocation is not efficient,
// but I just want to do like this.
c = kmalloc(sizeof(uint8_t), GFP_KERNEL);
if (!c) {
printk(KERN_ALERT "[%s] fatal kernel allocation!\n", __func__);
return NULL;
}
c = buf + *pos;
convert_char(c, is_odd(*pos));
return c;
}
static void *seq_next(struct seq_file *m, void *v, loff_t *pos)
{
static bool finished = false;
// printk(KERN_INFO "[%s] called.\n", __func__);
(*pos)++;
if (*pos == len) {
finished = true;
*(uint8_t*)v = '\n';
return v; // go to show() at the last time
}
if (finished) { // are you done?
printk(KERN_INFO "[%s] next(): Done. Go to stop()\n", FILE_NAME);
return NULL;
}
v = buf + *pos;
// printk(KERN_DEBUG "[DEBUG] before v=%c\n", *(uint8_t*)v);
convert_char((uint8_t*)v, is_odd(*pos));
// printk(KERN_DEBUG "[DEBUG] after v=%c\n", *(uint8_t*)v);
return v;
}
static void seq_stop(struct seq_file *m, void *v)
{
printk(KERN_INFO "[%s] called.\n", __func__);
if (v) {
kfree(v);
v = NULL;
}
}
/* at the very first time, v comes from start(),
* but after that, v comes from next().
*/
static int seq_show(struct seq_file *m, void *v)
{
// printk(KERN_INFO "[%s] called.\n", __func__);
seq_printf(m, "%c", *(char*)v);
return 0;
}
static const struct seq_operations seq_ops = {
.start = seq_start,
.stop = seq_stop,
.next = seq_next,
.show = seq_show
};
/**
* procfs interface ----------------------------------------------
*/
static int proc_open(struct inode *inode, struct file *file)
{
return seq_open(file, &seq_ops);
}
static ssize_t proc_write(struct file *file, const char __user *data, size_t length, loff_t *pos)
{
if (length > BUF_LEN)
length = BUF_LEN - 1; // -1 for EOF
len = length; // update it for seq_file iterator
if (copy_from_user(buf, data, length)) {
printk(KERN_ERR "copy_from_user failed!\n");
return -EFAULT;
}
return length;
}
static const struct proc_ops proc_ops = {
/* seq_{read,release,lseek}() are ready-made functions. */
.proc_read = seq_read,
.proc_write = proc_write,
.proc_open = proc_open,
.proc_release = seq_release,
.proc_lseek = seq_lseek
};
int __init proc_init(void)
{
if ((proc_dir = proc_mkdir(DIR_NAME, NULL)) == NULL) {
printk(KERN_ERR "Unable to create /proc/%s.\n", DIR_NAME);
goto err_return;
}
if ((proc_file = proc_create(FILE_NAME, 0666, proc_dir, &proc_ops)) == NULL) {
printk(KERN_ERR "Unable to create /proc/%s/%s.\n", DIR_NAME, FILE_NAME);
goto proc_dir_cleanup;
}
// dummy string initializing
buf = kmalloc(BUF_LEN, GFP_KERNEL);
if (!buf) {
printk(KERN_ERR "[%s] fatal kernel allocation: buf!\n", __func__);
goto proc_file_cleanup;
}
strcpy(buf, "I'm karatus01.");
len = strlen(buf);
printk(KERN_INFO "/proc/%s/%s loaded successfully\n", DIR_NAME, FILE_NAME);
return 0;
proc_file_cleanup:
proc_remove(proc_file);
proc_dir_cleanup:
proc_remove(proc_dir);
err_return:
return -ENOMEM;
}
void __exit proc_exit(void)
{
proc_remove(proc_dir);
printk(KERN_INFO "/proc/%s/%s unloaded successfully\n", DIR_NAME, FILE_NAME);
}
module_init(proc_init);
module_exit(proc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Karatus");
MODULE_DESCRIPTION("Get a string from user, and convert it.");
Makefile
M_NAME = my_proc
obj-m += $(M_NAME).o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
re: clean all
성공적. 궁금한 게 있다면 댓글로 알려주길 바란다.
Reference