guest@blog.cmj.tw: ~/posts $

FUSE


這是一篇 自己實作 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]