這是一篇 自己實作 FUSE 過程的紀錄
FUSE (FileSystem in UserSpace) 大概是作業系統 (檔案系統) 中一個重要的發展。 檔案系統在實作上,都被歸屬在作業系統的核心當中,因此也較難 Debug。而 FUSE 提供一個 Kernel Module 的接口,可以在 User-Space 上實作整個 FS。因為需要在 Kernel-Space / User-Space 上做切換,在效能表現上會有一定的影響。
架構概念
架構上,實作一個自己的 FUSE 只需要了解如何引用現有的 Library 並且實作所有需要的 FS 指令 (像是 readdir / open / getattr 等),就可以完成一個客製化的 File System。 最簡單的 FUSE 就是一個 LoopBack 。他的功能很單純,就是把 Mount Point 重新導向到其他的資料夾。複雜點的就可以像是把 SFTP 等其他存取檔案的方式, 用 POSIX-Like 的方式來存取。
實作
這次 FUSE 實作的目標需要滿足以下條件:
- 允許 mount point 存在。如果存在,則將舊的檔案/資料移到其他地方。
- 支援額外的設定檔 (.rule),並且根據設定檔來決定如何存取 Remote 端的檔案。
- FUSE 可以用來存取 Web 上的檔案 (e.g. Image 檔)。
- FUSE 從 Web 上抓到的檔案,會自動儲存到 Local 端。
- 有 Cache 機制,如果 Local 端有檔案則會優先存取。如果存取不在 Local 端的檔案,則嘗試存取 Remote 端。
- Cache 機制可以決定最大容量/最多檔案數量 (藉由 .rule 設定),並且提供多種 Rotation 機制。
- 每次列舉 (listdir) 的時候,重新列舉 Remote 端檔案的狀態,並且更新 Local 端。
框架
根據上面的設定,我們先定出 .rule 檔的輪廓:他必須可以決定多個 Section 表示從哪一個 Remote 端存取檔案。
[example]
site = www.example.com
protocol = http
pattern = "www.example.com/{0:03d}.jpg"
param = int
## Options
port = 8080
direction = DESC ## ASC, DESC, RAND, BOTH
quota = 1G ## Limit by size (M, G, T) or number of file (F)
接著,我們提供如何使用 FUSE 的指令集 (binary command)。
mount.fuse-example -h
Usage: mount.fuse-example <mount point> [-c rule]
Mount the remote target by the rule file. Default is the loopback FUSE.
至於程式的部分,先寫出一個簡單的框架可以滿足基本 FUSE 的操作,之後按照之前定的 SPEC 來增加所有需要的功能。下面的範例程式提供一個完整但每一個操作都尚未實作的 FUSE 介面。
#! /usr/bin/env python
#! coding: utf-8
from fuse import FUSE, Operations
class FUSE_EXAMPLE(Operations):
''' Inherit all FUSE operation from Operations which all are NOP '''
def __init__(self, mountPoint):
pass
if __name__ == "__main__":
import sys
MNT = sys.argv[1]
_fuse_ = FUSE(FUSE_EXAMPLE(MNT), MNT, foreground=True)
有 Cache 跟 Error handling 的 FUSE
接下來,我們需要修改 __init__ 讓他可以支援:如果 mount point 存在, 將他搬到一個暫存的位子,並且在 umount 的時候搬回來;如果 mount point 不存在, 則自動建立一個資料夾。
import os
from threading import Lock
from fuse import FUSE, Operations
class FUSE_EXAMPLE(Operations):
''' Inherit all FUSE operation from Operations which all are NOP '''
def __init__(self, mountPoint):
self.rwlock = Lock()
self.mountPoint = mountPoint
## Create the mount point if need
try:
os.mkdir(mountPoint)
except OSError as e:
pass
if os.access(mountPoint, os.O_RDWR):
os.rename(mountPoint, self.tmpMountPoint)
os.mkdir(mountPoint)
else:
raise SystemError("You cannot access the mount point")
def __del__(self):
os.rename(self.tmpMountPoint, self.mountPoint)
@property
def mountPoint(self):
""" Save the mount point as the private method """
return self._mountPoint
@mountPoint.setter
def mountPoint(self, v):
self._mountPoint = v
@property
def tmpMountPoint(self):
""" Always return the tmp mount path related on actally mount point """
return ".%s-%d" %(self.mountPoint, os.getpid())
提供基本 I/O 能力的 FUSE
在上面的架構中只有實作出一個空殼,我們並不能對這個 mount point 真的做 I/O 存取。 所以針對 POSIX 介面我們實作出最簡單的功能:可以將檔案寫入到我們暫存資料夾當中。 在這個架構下,我們可以直接讀取暫存檔的檔案 (也就是 Cache 機制)。
class FUSE_EXAMPLE(Operations):
''' Inherit all FUSE operation from Operations which all are NOP '''
""" ... Ignore the old parts ... """
def _path_(self, path):
return "%s%s" %(self.tmpMountPoint, path)
def release(self, path, fh):
return os.close(fh)
def open(self, path, flag, mode=0777):
return os.open(self._path_(path), flag, mode)
def create(self, path, mode):
return os.open(self._path_(path), os.O_WRONLY | os.O_CREAT, mode)
def write(self, path, data, offset, fh):
with self.rwlock:
os.lseek(fh, offset, 0)
return os.write(fh, data)
def getattr(self, path, fh=None):
st = os.stat(self._path_(path))
return {_: getattr(st, _) for _ in [n for n in dir(st) if n.startswith('st_')]}
def readdir(self, path, fh):
return [".", ".."] + os.listdir(self._path_(path))
def unlink(self, path):
return os.unlink(self._path_(path))
很可怕的部分
接下來就是比較可怕的部分了。我們需要從網路上抓東西下來,所以需要選定一個目標來抓。 我一直尊崇著寫程式就要寫自己有興趣的,所以範例程式的目標就交給 SOD 這個陪伴我多年的好夥伴了。這部分的程式碼其實比較偏向於 網路爬蟲 的部分, 目的只是從網路上把資料抓到 Local 端。如果有其他使用上的需求,也可以改用其他的 Protocol 抓自己想要的資料。
class FUSE_EXAMPLE(Operations):
''' Inherit all FUSE operation from Operations which all are NOP '''
""" ... Ignore the old parts ... """
def catpure(self, ID):
""" Capture from the particular web and download the particular pic"""
import re
import urllib2
path = self._path_("IENE-%03d.jpg" %ID)
if os.access(path, os.R_OK):
return
url = "http://ec.sod.co.jp/detail/index/-_-/iid/IENE-{id:03}"
url = url.format(id=ID)
ret = urllib2.urlopen(url).read()
pic = re.search(ur'"(http://pics.*?)"', ret).groups()[0]
pic = urllib2.urlopen(pic).read()
with open(path, "wb") as f:
f.write(pic)
觸發機制
現在,就需要設計如何處發重新抓取檔案的部分。想單然我們不想重複抓已經存在於 Cache 上的圖片,所以抓圖片的時候我們當然需要先列舉已經存在於 Cache 上有的圖片, 所以放在 readdir 上似乎是個不錯的主意 (?)。
class FUSE_EXAMPLE(Operations):
''' Inherit all FUSE operation from Operations which all are NOP '''
""" ... Ignore the old parts ... """
def __init__(self, mountPoint):
self._id_ = 400
""" ... Skip the old parts ... """
def readdir(self, path, fh):
""" Get the new image from weg """
try:
self.catpure(self._id_)
self._id_ += 1
except Exception as e:
pass
return [".", ".."] + os.listdir(self._path_(path))
收尾工作
[To be continue]