IT EXPORT, 리눅스 커널 프로그래밍을 읽고 있다. 책에서 다루는 커널 버전은 2.6인데 이걸 5.8에서 따라하려니 인터페이스가 많이 바뀌었다. 책 따라해보다 짜증나서 정리해보려고 한다. (아, 책은 정말 좋다.) 이 글은 5.8 기준으로 작성되었다.
proc 파일시스템이란
procfs는 말 그대로 파일시스템이다. 하지만 디스크 기반 파일시스템과는 달리, 실제로 하드디스크 상에 존재하지는 않는다. 다만, 커널 상에서 VFS 인터페이스를 활용해 특수한 파일시스템을 구현한 것이다. procfs는 일반적으로 /proc에 마운트되어있다. 비슷하게 sysfs, debugfs 등이 존재한다. 한 번 쉘에서 /proc에 무엇이 들어있는지 확인해보자.
ls /proc
숫자인 것과 아닌 것이 있다. 숫자된 폴더들은 프로세스의 pid이고 폴더 안으로 들어가면 프로세스의 정보를 확인할 수 있다. 숫자가 아닌 것들도 리눅스에서 제공하는 다양한 정보가 들어있다. 시험삼아 pid = 1인 init 프로세스의 환경변수를 조회해보자.
ls /proc/1
# 루트 권한이 있다면 실행해보자. init 프로세스의 환경변수를 확인할 수 있다.
sudo cat /proc/1/environ
이처럼 숫자로 된 폴더에는 프로세스에 대한 정보가 파일 형태로 존재한다. 프로세스 외에도 cpu, memory, device, disk 사용량 통계 등 다양한 정보를 파일 형태로 제공한다. 이번엔 /proc/cpuinfo를 확인해보자. cpu 클럭, 캐시 사이즈, 코어 등 다양한 정보를 확인해볼 수 있다.
이처럼 proc 파일시스템은 현재 시스템에 대한 다양한 정보를 제공한다. procfs의 사용법에 들어가기 전에 다시 짚고 넘어가자면, procfs는 시스템이 사용자 프로그램에게 정보를 제공하기 위한 일종의 수단이다. 모듈, 디바이스 드라이버에서 정보를 제공하기 위해 구현할 수도 있고, 커널 자체에서 구현할 수도 있다.
procfs API, 자료구조
struct proc_dir_entry
procfs의 entry를 (폴더 or 파일) 표현하기 위한 자료구조이다. 나도 멤버들이 정확히 무슨 역할인지는 아직 모른다. (대충 in_use, refcnt, data, subdir, subdir_node, name, mode 이런 것들은 대충 트리구조와 entry와 관련된 정보들이 들어있을 것이다.)
/* 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
proc_ops는 proc_dir_entry에 read/write/open/lseek/close 등의 연산을 했을 때의 함수를 구현한다. 원래는 VFS에서 사용하는 file_operations를 사용했었는데, 이 패치에서 성능 향상을 목적으로 proc_ops로 바뀌었다. 무려 1년 전에 적용된 패치이다. 이렇게 실시간으로 바뀌는구나 하는 생각이 들었다.
/* 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 *);
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;
proc_create
proc_dir_entry를 반환하는 함수이다. procfs에 파일을 만들때 사용한다. 2.6에서는 create_proc_entry를 사용했지만deprecated되면서 함수가 사라졌다.habr.com/en/post/444620/
struct proc_dir_entry *proc_create(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct proc_ops *proc_ops);
/*
* name: 파일 이름
* mode: 파일 권한을 설정함 (0을 넣으면 기본적으로 0444로 설정됨)
* parent: 부모 디렉토리의 포인터이다. NULL인 경우 /proc에 생성된다.
* proc_ops: 해당 entry에 대한 read, write, open, release 등의 연산에 대한 함수 포인터를 저장하는 구조체다.
* 만약 /proc/my_folder/my_proc 이런식으로 특정 폴더안에 만들려면
* proc_mkdir을 한 다음에 parent에 넘기면 된다.
*/
proc_remove
remove_proc_subtree를 호출하는 함수이다. proc_dir_entry를 해제한다.
void proc_remove(struct proc_dir_entry *de)
{
if (de)
remove_proc_subtree(de->name, de->parent);
}
자, 이제 우리는 proc_dir_entry를 만들 수 있다. 이제 모듈을 만들을 만들 수 있을까? 아직 아니다. (ㅠㅠ...) seq_file 인터페이스를 알아야 한다.
seq_file 인터페이스
이 파트는 어떻게 설명할지 고민이 많이 되어서 Documentation을 많이 참고하였다. 이해가 안된다면 저 문서를 보자.
우리가 모듈을 만들어서 number라는 정수형 변수를 출력하는 간단한 proc_dir_entry를 만들었다고 생각해보자. number의 값은 4242라고 하자. number는 정수형이므로 버퍼에 복사하려면 담으려면 문자열로 변환해야한다. 그렇게 변환한 문자열을 copy_to_user와 같은 함수로 사용자 프로그램의 버퍼에 복사해야 한다. 그런데 만약 버퍼의 크기가 1바이트라면? number는 4바이트인데 나머지 1바이트는 어디다 써야하는가? 우리가 변환했던 "4242"라는 문자열은, 다음 read가 호출될 때까지 어디에 존재해야 할까?
일반적인 디스크상의 파일이었다면 그냥 현재까지 읽은 위치를 offset 같은 변수로 저장해두고 다음 read에서 해당 offset부터 읽으면 된다. 하지만 procfs의 데이터는 디스크 상에 존재하지 않기 때문에 그런게 불가능하다. 이런 문제를 해결하려면 우리는 모듈 안에서 각각의 사용자 프로세스별로 출력 버퍼를 관리해야할 것이다. 하지만 다행히 우리에겐 seq_file 인터페이스가 존재한다! seq_file 인터페이스는 출력 버퍼를 신경쓰지 않고도 포맷이 있는 출력(printf의 %d, %s와 같은 포맷)을 하도록 도와준다.
iterator interface
기본적으로 seq_file에서는 seq_printf라는 함수를 통해서 포맷을 갖는 문자열을 출력하면서도, 출력 버퍼를 신경쓰지 않도록 인터페이스를 제공한다.
struct seq_operations
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); // 현재 이터레이터가 가리키고 있는 데이터를 보여주는 함수
};
사용자 프로세스가 파일을 open하고 close하기까지의 시간을 "세션"이라고 해보자. 그럼 우리는 이 세션별로 어디까지 데이터를 출력했는지 상대적인 위치를 이터레이터로 저장할 수 있다. seq_file에서는 이러한 이터레이터를 start, stop, next, show 함수로써 정의한다. 따라서 seq_file 인터페이스를 사용하려면 seq_operations에 존재하는 함수들을 구현해야 한다. 이 때 이터레이터는 배열의 인덱스가 될 수도, 리스트 노드에 대한 포인터가 될 수도 있다.
등록된 함수는 start -> show -> next -> show -> next -> show -> ... -> stop 순으로 호출된다.
A complete example
간단하게 data라는 배열에 든 데이터를 출력하는 my_proc을 만들어보았다.
/* my_proc.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/proc_fs.h>
#include <linux/fs.h>
#include <linux/seq_file.h>
#define DEVICE_NAME "my_proc"
int data[] = {1, 2, 3, 4, 5};
const int size = 5;
static struct proc_dir_entry *my_proc = NULL;
void *device_start(struct seq_file *s, loff_t *pos) {
printk(KERN_INFO "[%s] %s - pos = %lld\n",
DEVICE_NAME, __func__, *pos);
if (*pos >= size) {
printk(KERN_INFO "[%s] %s - we're finished\n", DEVICE_NAME,
__func__);
return NULL;
}
return data + *pos;
}
void device_stop(struct seq_file *s, void *v) {
}
void *device_next(struct seq_file *s, void *v, loff_t *pos) {
printk(KERN_INFO "[%s] %s - pos = %lld", DEVICE_NAME, __func__,
*pos);
(*pos)++;
if (*pos >= size) {
return NULL;
}
printk("[%s] %s - returned data: %d\n", DEVICE_NAME, __func__,
(data[*pos]));
return data + *pos;
}
/* v is what returned by next */
int device_show(struct seq_file *m, void *v) {
printk(KERN_INFO "[%s] - %s printed %d\n", DEVICE_NAME, __func__,
*(int*)v);
seq_printf(m, "%d\n", *(int*)v);
return 0;
}
static const struct seq_operations s_ops = {
.start = device_start,
.next = device_next,
.stop = device_stop,
.show = device_show
};
int device_open(struct inode *inode, struct file *file) {
/* open using seq_file interface */
return seq_open(file, &s_ops);
}
static const struct proc_ops p_ops = {
/* read, lseek, release is processed by seq_file interface */
.proc_read = seq_read,
.proc_open = device_open,
.proc_release = seq_release,
.proc_lseek = seq_lseek,
};
int __init proc_init(void) {
if ((my_proc = proc_create(DEVICE_NAME, 0666, NULL, &p_ops)) == NULL) {
printk(KERN_ALERT "[%s] %s - proc_create failed\n",
DEVICE_NAME, __func__);
return -ENOMEM;
}
printk(KERN_ALERT "[%s] %s - successfully loaded!\n",
DEVICE_NAME, __func__);
return 0;
}
void __exit proc_exit(void) {
proc_remove(my_proc);
printk(KERN_ALERT "[%s] %s - successfully unloaded!\n",
DEVICE_NAME, __func__);
}
module_init(proc_init);
module_exit(proc_exit);
MODULE_LICENSE("GPL");
Makefile
NAME = my_proc
obj-m += ${NAME}.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm -f Module.symvers modules.order
rm -f ${NAME}.o ${NAME}.mod ${NAME}.mod.c ${NAME}.mod.o
fclean: clean
rm -f ${NAME}.ko
re: fclean all
Test
make
sudo insmod my_proc.ko
cat /proc/my_proc
글을 마치며
필자의 글이 미숙해 설명에 부족한 부분이 있을 수 있다. 글을 읽으면서 부족함을 느낀다면 아래 참고 문서를 하나씩 차근차근 읽어보는 걸 추천한다. 아마 필요한 대부분의 내용이 있을 것이다.
참고 문서
'Kernel' 카테고리의 다른 글
[Linux Kernel] 시간과 타이머 (1) | 2021.05.04 |
---|---|
[Linux Kernel] 인터럽트와 후반부 처리의 개념 (0) | 2021.04.28 |
[LInux Kernel] 문자 디바이스 드라이버 작성 (5) | 2021.04.17 |
[Linux Kernel] 가상 파일시스템이란 (VFS, Virtual Filesystem Switch) (0) | 2021.04.13 |
[Linux Kernel] 리눅스 커널 모듈 작성 (0) | 2021.04.13 |
댓글