[LKM] procfs & seq_file
Kernel/Development

[LKM] procfs & seq_file

반응형
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);
};

https://habr.com/en/post/444620/

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

 

The seq_file Interface — The Linux Kernel documentation

There are numerous ways for a device driver (or other kernel component) to provide information to the user or system administrator. One useful technique is the creation of virtual files, in debugfs, /proc or elsewhere. Virtual files can provide human-reada

www.kernel.org

 

Using Linux Kernel Sequence Files

A characteristic feature of modern programming is the use of the global network as a source of reference information, in particular, a source of patterns for solving unknown or little-known problems...

habr.com

 

[Linux Kernel 5] proc & seq_file

procfs는 Process File System을 줄인 것으로, Processes as Files의 의미이다 커널 및 디바이스 정보 (시스템 정보)를 유저 스페이스에 제공하기 위해 사용된다  ls /proc 으로 리스팅 해보면 다음과 같이 여러

pr0gr4m.tistory.com

 

[Linux Kernel] proc 파일시스템과 seq_file 인터페이스

IT EXPORT, 리눅스 커널 프로그래밍을 읽고 있다. 책에서 다루는 커널 버전은 2.6인데 이걸 5.8에서 따라하려니 인터페이스가 많이 바뀌었다. 책 따라해보다 짜증나서 정리해보려고 한다. (아, 책은

hyeyoo.com

 

Driver porting: The seq_file interface [LWN.net]

There are numerous ways for a device driver (or other kernel component) to provide information to the user or system administrator. One very useful technique is the creation of virtual files, in /proc or elsewhere. Virtual files can provide human-readable

lwn.net

 

GitHub - linux-kernel-labs/linux: Linux kernel source tree

Linux kernel source tree. Contribute to linux-kernel-labs/linux development by creating an account on GitHub.

github.com

 

반응형