Superblock 구조체는 리눅스 커널에서 가상 파일 시스템(VFS)의 핵심을 이루는 구조 체중 하나이다. VFS는 리눅스 커널에서 객체 지향 프로그래밍을 C로 구현한 가장 대표적인 예시인데, 때문에 Superblock 구조체를 Superblock 오브젝트라고도 많이 지칭한다. C에서는 구조체에 메소드를 삽입할 수 없기 때문에, Superblock 구조체에는 super_operations라는 함수 포인터를 담은 구조체를 멤버로 소유하고 있다. Superblock은 기본적으로 하나의 파일 시스템을 나타낸다. 즉 마운팅된 파일 시스템 하나당 Superblock 하나가 할당되게 되며, Superblock은 마운팅 된 파일 시스템에 대한 메타데이터를 지니고 있다. 이 포스팅에선 Superblock 구조체의 핵심적인 멤버를 알아본다.
[1]
struct super_block {
struct list_head s_list; /* Keep this first */
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned char s_blocksize_bits;
unsigned long s_blocksize;
loff_t s_maxbytes; /* Max file size */
struct file_system_type *s_type;
const struct super_operations *s_op;
const struct dquot_operations *dq_op;
const struct quotactl_ops *s_qcop;
const struct export_operations *s_export_op;
unsigned long s_flags;
unsigned long s_iflags; /* internal SB_I_* flags */
unsigned long s_magic;
struct dentry *s_root;
struct rw_semaphore s_umount;
int s_count;
atomic_t s_active;
#ifdef CONFIG_SECURITY
void *s_security;
#endif
const struct xattr_handler **s_xattr;
#ifdef CONFIG_FS_ENCRYPTION
const struct fscrypt_operations *s_cop;
struct key *s_master_keys; /* master crypto keys in use */
#endif
#ifdef CONFIG_FS_VERITY
const struct fsverity_operations *s_vop;
#endif
struct hlist_bl_head s_roots; /* alternate root dentries for NFS */
struct list_head s_mounts; /* list of mounts; _not_ for fs use */
struct block_device *s_bdev;
struct backing_dev_info *s_bdi;
struct mtd_info *s_mtd;
struct hlist_node s_instances;
unsigned int s_quota_types; /* Bitmask of supported quota types */
struct quota_info s_dquot; /* Diskquota specific options */
struct sb_writers s_writers;
/*
* Keep s_fs_info, s_time_gran, s_fsnotify_mask, and
* s_fsnotify_marks together for cache efficiency. They are frequently
* accessed and rarely modified.
*/
void *s_fs_info; /* Filesystem private info */
/* Granularity of c/m/atime in ns (cannot be worse than a second) */
u32 s_time_gran;
/* Time limits for c/m/atime in seconds */
time64_t s_time_min;
time64_t s_time_max;
#ifdef CONFIG_FSNOTIFY
__u32 s_fsnotify_mask;
struct fsnotify_mark_connector __rcu *s_fsnotify_marks;
#endif
char s_id[32]; /* Informational name */
uuid_t s_uuid; /* UUID */
unsigned int s_max_links;
fmode_t s_mode;
/*
* The next field is for VFS *only*. No filesystems have any business
* even looking at it. You had been warned.
*/
struct mutex s_vfs_rename_mutex; /* Kludge */
/*
* Filesystem subtype. If non-empty the filesystem type field
* in /proc/mounts will be "type.subtype"
*/
const char *s_subtype;
const struct dentry_operations *s_d_op; /* default d_op for dentries */
/*
* Saved pool identifier for cleancache (-1 means none)
*/
int cleancache_poolid;
struct shrinker s_shrink; /* per-sb shrinker handle */
/* Number of inodes with nlink == 0 but still referenced */
atomic_long_t s_remove_count;
/* Pending fsnotify inode refs */
atomic_long_t s_fsnotify_inode_refs;
/* Being remounted read-only */
int s_readonly_remount;
/* AIO completions deferred from interrupt context */
struct workqueue_struct *s_dio_done_wq;
struct hlist_head s_pins;
/*
* Owning user namespace and default context in which to
* interpret filesystem uids, gids, quotas, device nodes,
* xattrs and security labels.
*/
struct user_namespace *s_user_ns;
/*
* The list_lru structure is essentially just a pointer to a table
* of per-node lru lists, each of which has its own spinlock.
* There is no need to put them into separate cachelines.
*/
struct list_lru s_dentry_lru;
struct list_lru s_inode_lru;
struct rcu_head rcu;
struct work_struct destroy_work;
struct mutex s_sync_lock; /* sync serialisation lock */
/*
* Indicates how deep in a filesystem stack this SB is
*/
int s_stack_depth;
/* s_inode_list_lock protects s_inodes */
spinlock_t s_inode_list_lock ____cacheline_aligned_in_smp;
struct list_head s_inodes; /* all inodes */
spinlock_t s_inode_wblist_lock;
struct list_head s_inodes_wb; /* writeback inodes */
} __randomize_layout;
대부분의 멤버들은 이름과 주석으로 사용을 충분히 유추할 수 있다. 여기서는 몇가지 멤버들만 살펴보자. 가장 중요한 s_op는 [2]에서 다룬다.
1) s_dev
파일 시스템 identifier
2) s_blocksize, s_blocksize_bits
s_blocksize는 이 파일 시스템에서 사용하는 블록의 크기를 바이트단위로 나타낸다. s_blocksize_bits는 블록 사이즈가 몇 비트로 표기될 수 있는지 나타낸다. 즉 s_blocksize = 1024라면 s_blocksize_bits는 10이다. 물론 어느 한쪽 멤버만 있으면 간단한 계산을 통해 다른 한쪽을 알 수 있다.(1<<s_blocksize_bits = s_blocksize) 하지만 커널에서는 자주 사용하는 값에 대해서 메모리를 약간 손해 보더라도 퍼포먼스를 향상하는 선택을 한다. 이 변순들은 커널에서 time과 space의 trade-off에 대한 좋은 예시이다.
3) s_flag
슈퍼블록에 대한 flag. 즉 마운팅된 파일 시스템에 대한 flag이다.
4) s_root
마운팅된 root directory
[2]
super_block에 멤버로 포함되어 있는 super_operations 구조체의 함수 포인터들은 super_block 오브젝트의 메소드들이라고 보면 된다. super_operations에서는 Java 인터페이스처럼 타입만 정의해 놓고, 실질적인 구현은 실제 파일 시스템 구현 단계에서 이루어지게 된다.
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*free_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, struct writeback_control *wbc);
int (*drop_inode) (struct inode *);
void (*evict_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_super) (struct super_block *);
int (*freeze_fs) (struct super_block *);
int (*thaw_super) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct dentry *);
int (*show_devname)(struct seq_file *, struct dentry *);
int (*show_path)(struct seq_file *, struct dentry *);
int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
struct dquot **(*get_dquots)(struct inode *);
#endif
int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
long (*nr_cached_objects)(struct super_block *,
struct shrink_control *);
long (*free_cached_objects)(struct super_block *,
struct shrink_control *);
};
마찬가지로 대부분의 메소드들은 이름을 통해 기능을 유추할 수 있다. 몇 가지 메소드들을 살펴보자.
1) inode 관련 메소드들
alloc_inode(), destroy_inode(), write_inode(), delete_inode() 등이 해당한다. VFS에서 일반적으로 추상화 계층은 file-> dentry -> inode -> superblock이다.(dentry와 superblock 간의 인터페이스도 존재하긴 한다.) 따라서 superblock에 inode를 관리하는 메소드들이 포함되어 있다.
2) dirty_inode()
물론 이름대로 inode 관련 메소드이지만 약간 헷갈릴 수 있기 때문에 따로 기술한다. 이 메소드는 inode에 수정이 발생하였을 때 VFS에 의해 불리게 되며, 저널링 파일 시스템에서 저널 업데이트 목적으로 이 함수를 이용한다. 이 함수에 대해서는 [3]에서 코드 레벨로 따라가 보도록 하겠다.
3) put_super()
VFS가 파일 시스템을 언마운트하는 과정에서 호출된다. 인자로 주어진 superblock을 해제한다.
4) remount_fs()
새로운 옵션으로 리마운트될 때 호출된다.
5) sync_fs()
변경된 파일 시스템 메타데이터(in-memory)와 디스크 상의 메타데이터를 동기화한다.
[3]
메소드를 간략하게 분석만 하고 넘어가기는 아쉬우니, 코드 레벨에서 함수를 한 번 파헤쳐보자. 단, 위의 메소드는 인터페이스에 불과할 뿐이므로, 실제 구현은 파일 시스템마다 상이하다. 여기에서는 리눅스에서 가장 널리 사용되는 ext4의 구현을 따라가 보도록 하겠다.
우선 ext4에서는 dirt_inode()를 위와 같이 ext4_dirty_inode로 매핑해놓았다.
@ fs/ext4/inode.c
/*
* ext4_dirty_inode() is called from __mark_inode_dirty()
*
* We're really interested in the case where a file is being extended.
* i_size has been changed by generic_commit_write() and we thus need
* to include the updated inode in the current transaction.
*
* Also, dquot_alloc_block() will always dirty the inode when blocks
* are allocated to the file.
*
* If the inode is marked synchronous, we don't honour that here - doing
* so would cause a commit on atime updates, which we don't bother doing.
* We handle synchronous inodes at the highest possible level.
*
* If only the I_DIRTY_TIME flag is set, we can skip everything. If
* I_DIRTY_TIME and I_DIRTY_SYNC is set, the only inode fields we need
* to copy into the on-disk inode structure are the timestamp files.
*/
void ext4_dirty_inode(struct inode *inode, int flags)
{
handle_t *handle;
if (flags == I_DIRTY_TIME)
return;
handle = ext4_journal_start(inode, EXT4_HT_INODE, 2);
if (IS_ERR(handle))
goto out;
ext4_mark_inode_dirty(handle, inode);
ext4_journal_stop(handle);
out:
return;
}
ordered 저널링을 기본 옵션으로 사용하는 ext4의 특성을 여기에서도 확인할 수 있다. 우선 ext4_journal_start()를 통해 저널링 시작을 알리고 ext4_mark_inode_dirty()로 실제 inode를 dirty로 마킹한다. 이것이 종료되면 ext4_journal_stop()을 통해 저널링이 완료된다.
포인터는 프로그래밍에서 필수적인 요소이지만, 각종 메모리 취약점을 발생시키는 주범이다. Rust에선 레퍼런스라는 형태로 포인터를 사용할 수 있다. 레퍼런스는 Ownership-Borrow 체계를 통해서 메모리 취약점이 발생하지 않음을 보장한다. 반면 C/C++와 같은 방식의 일반적인 포인터는 주어지지 않는데, 대신에 스마트 포인터가 존재한다.
[1]
스마트 포인터는 Rust에서 처음 나온 개념은 아니다. 메모리 안정성을 위해 다양한 언어에서 빌트인으로, 혹은 패키지 등의 행태로 스마트 포인터가 제공된다. 스마트 포인터가 일반 포인터와 다른 점은 가리키는 대상에 대한 metadata를 기록한다는 점이다. 예컨대 reference counter를 기록하여 언제 해당 메모리 영역이 free 되어야 하는지 추적할 수 있다. Rust의 스마트 포인터 또한 이러한 종류의 metadata를 지니고 있으며, 일반적으로 구조체의 형태로 구현된다. Rust의 스마트 포인터가 가장 독특한 측면은 구조체에 Deref와 Drop trait을 구현하는 방식으로 스마트 포인터가 제공된다는 점이다. 이 포스팅에서는 Deref와 Drop trait에 대해 다룬다. 하지만 우선 가장 기본적인 형태의 스마트 포인터를 잠깐 살펴보자.
[2]
Rust의 가장 단순한 스마트 포인터는 Box<T>이다. 다음과 같은 식으로 선언할 수 있다.
let tmp = Box::new(1);
prinln!("{}", tmp); //print 1
tmp는 컴파일러에 의해 Box<i32> 타입으로 결정된다. 또한 println! 매크로에서 Box<T>의 T를 출력해주는 것을 확인할 수 있다.
Box<T>는 기본적으로 heap에 할당된 메모리를 가리키며, T에 대한 ownership을 소유한다. 즉 오브젝트를 레퍼런스가 아닌 ownership을 전달하고 싶지만, 오브젝트 사이즈가 너무 클 때 Box<T>를 전달할 수 있다. C에서 파라미터로 포인터를 넘겨주는 방식을 생각하면 된다.
[3]
많은 언어에서 쓰는 역참조 기호 *는 Rust에서는 기본적으로 레퍼런스에만 사용 가능하다. 하지만 오버라이딩 할 수 있는 방법을 제공하는데, Deref라는 trait으로 제공한다. Deref는 다음과 같이 구현할 수 있다.
use std::ops::Deref;
struct MyStruct<T> {
val: T,
}
impl<T> Deref for MyStruct<T> {
type Target = T;
fn deref(&self) -> &T {
&self.val
}
}
이제 아래와 같이 * 연산자를 통해 MyStruct를 역참조할 수 있다.
let obj = MyStruct{val:10};
println!("{}", *obj);
여기에서 *obj는 실제로는 *(obj.deref())로 변환된다.
[4]
Deref와 더불어서 스마트 포인트의 핵심 trait으로 Drop이 있다. Drop은 다음과 같이 구현할 수 있다.
impl<T> Drop for MyStruct<T>{
fn drop(&mut self){
//any cleanup code here!
}
}
drop()은 오브젝트가 scope 밖으로 나가서 소유권이 소멸될때 자동으로 실행된다. drop에 메모리 cleanup code를 넣어두는 식으로 보통 구현을 하게 된다. 이를 통해 사용자가 까먹거나, 혹은 2번 이상 호출해서 double-free가 발생하는 경우 등을 컴파일 타임에 찾아낼 수 있다.