Реализация mumufs для Linux

Author / Автор: Сергей Сацкий
NCBI
Publication date / Опубликовано: 12.07.2009
Version / Версия текста: 1.0

Памяти Ромы Плеханова, хорошего человека

Введение

На момент написания статьи было трудно найти подробное описание реализации файловых систем для Linux. Эта статья – результат работы автора в процессе поиска материалов и реализации mumufs. Статья ставит целью описать в деталях процесс разработки Linux файловой системы, целиком находящейся в памяти, а так же познакомить читателя с особенностями mumufs. Стоить, однако, заметить, что автор не является опытным специалистом в разработке модулей ядра, поэтому в статье возможны неточности.

Следующая глава кратко описывает mumufs, а более подробное описание можно найти в [1].

Краткое описание mumufs

Для некоторых задач бывает удобной модель обмена данными между процессами со следующими характеристиками:

  • обмен производится блоками произвольного размера
  • каждый блок имеет уникальный идентификатор
  • хранится только последний записанный блок для каждого идентификатора
  • допускатся одновременная запись и одновременное чтение многими процессами

Такая модель хорошо подойдет для систем, когда история обмена не имеет значения. Примером могут служить приложения отображения данных, когда поставщики и потребители (то есть средства отображения) являются разныими процессами. Представим, что идентификация блоков данных производится способом, принятым для файловых систем, а в качестве измеряемой величины и спользуется скорость ветра. Тогда схема взаимодействия может выглядеть так, как показано на рисунке ниже.

Рисунок 1. Пример обмена данными между процессами

Датчики скорости ветра могут подключаться по различным интерфейсам, которые обслуживаются разными процессами (процедуру выбора датчика, который должен поставлять значение в текущий момент времени, оставим за рамками обсуждения реализации mumufs). Как только новое значение получено, производится его запись в блок данных с идентификатором /mnt/mumu/wind. Процесс 3 может обслуживать интерфейс пользователя и, как только новое значение получено, производить обновление данных на экране. Процесс 4 может использовать значение скорости ветра для каких-либо расчетов.

Каждый процесс, открывший и прочитавший из /mnt/mumu/wind получит текущее значение. При этом постоянное наличие ни потребителей, ни поставщиков данных не требуется.

Поскольку базовыми операциями с блоком данных являются операции чтения и записи, то интерфейс к ним очень похож на интерфейс, предоставляемый файловыми системами. Удобным является и тот факт, что файловые системы подразумевают уникальность имен файлов. Имена файлов могут служить идентификаторами блоков данных. Еще одним положительным моментом является возможность использования штатных утилит UNIX для работы с файловыми системами, таких как mkdir, ls, rm, cat, touch, ln и т.д.

Ядро операционной системы Linux предоставляет удобную подсистему VFS (virtual file system switcher) для разработки новых файловых систем, поэтому оставшаяся часть статьи описывает реализацию файловой системы, поддерживающей блочный обмен данными между процессами, допускающий много поставщиков и потребителей и размещающейся целиком в памяти.

Внешние требования к модулю

Рассмотрим подробнее, как должна выглядеть файловая система с точки зрения администратора системы. Поскольку файловая система располагается целиком в памяти, то удобно иметь средство контроля за количеством памяти, использованной файловой системой. В терминах mumufs интерес представляют два параметра – максимальный размер блока и максимальное количество блоков данных. Принимая во внимание, что файловая система может быть смонтирована произвольное количество раз, эти параметры должны быть параметрами монтирования, а не параметрами модуля в момент его загрузки. То есть администратор должен иметь возможность запустить такую команду:

mount –t mumufs nodev /mnt/mumu –o max_entries=2048,max_block_size=1024

Модуль, в свою очередь, может предоставить информацию через /proc файловую систему. Удобно было бы получать информацию о версии модуля, а так же обо всех смонтированных экземплярах mumufs. Структура может быть такой:

/proc/mumu/version
          /<смонтированная mumufs #1>
          . . .
          /<смонтированная mumufs #n>/mountpoint
                                     /maxentries
                                     /maxblocksize
                                     /entries

К сожалению, на одну и ту же точку монтирования потенциально можно смонтировать несколько разных экземпляров mumufs (хотя будет доступен для использования только последний смонтированный). Из-за этого, в качестве имени <смонтированная mumufs> нельзя использовать просто декорированный путь к точке монтирования. Нужно добавлять что-нибудь еще, например порядковый номер. Для простоты реализации, однако, код использует строковое представление адреса экземпляра структуры, описывающей точку монтирования.

Теперь можно приступить к проектированию структур данных.

Структуры данных

Для хранения одного блока данных подойдет такая структура:

struct Storage
{
    unsigned char *      Buffer;         /* Data block             */

    unsigned int         BufferSize;     /* Buffer capacity        */
    unsigned int         BufferVersion;  /* Buffer version         */
    struct rw_semaphore  BufferLock;     /* RW lock for the buffer */

    wait_queue_head_t    BlockedReaders; /* Blocked processes list */
};

Поле Buffer указывает на блок данных, размер которого не должен превышать лимит, указанный в момент монтирования файловой системы. Поле BufferSize содержит текущий размер блока данных. Транзакцией обмена в mumufs является блок, и каждый новый блок не обязан быть точно такого же размера как и предыдущий, поэтому текущий размер надо запоминать. Поле BufferVersion содержит версию блока. Каждая новая запись увеличивает версию, а значение 0 говорит о том, что еще никаких данных нет.

Для обеспечения безопасного обновления перечисленных выше полей потребуется блокировка BufferLock. Здесь подойдет rw_semaphore, которая обеспечивает множественное чтение и монопольную запись. При этом записи отдается приоритет.

Последнее поле BlockedReaders необходимо для случая, когда используется блокируемое чтение, а данных либо нет, либо через данный файловый дескриптор последняя версия блока уже была прочитана. В этом случае процесс должен быть заблокирован. Поле BlockedReaders содержит список процессов, заблокированных на операции чтения блока данных.

Кроме описанной выше структуры потребуется еще одна, в которой будет храниться информация о смонтированном экземпляре mumufs. Подойдет такая структура:

struct mumu_mount_data
{
    struct vfsmount *        Mount;            /* fs mount point info                    */
    unsigned int             MaxBlockSize;     /* Maximum block size                     */

    unsigned int             MaxEntries;       /* Maximum number of files                */
    atomic_t                 NumberOfEntries;  /* The current number of files            */
    struct proc_dir_entry *  parent;           /* Used at the time when entry is removed */

};

Поле Mount содержит указатель на структуру ядра, описывающую точку монтирования. Указатель на эту структуру не часто появляется в вызовах VFS, в часности он появляется в момент монтирования файловой системы. Без этой информации невозможно узнать, на какую точку была смонтирована файловая система, а модуль mumufs должен показать эту точку через /proc, поэтому указатель надо сохранить.

Следующие два поля – MaxBlockSize и MaxEntries – сохраняют текущие параметры монтирования файловой системы. А поле NumberOfEntries хранит текущее количество блоков данных на смонтированном экземпляре mumufs.

Последнее поле – parent – необходимо для корректного удаления записей в /proc, созданных для экземпляра mumufs. Удаление понадобится в момент размонтирования экземпляра mumufs.

Остальныйе важные структуры данных предоставляются подстемой VFS.

Модуль ядра

Для оформления модуля ядра 2.6 потребуются минимальные усилия. Необходимо определить несколько макросов:

MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "Sergey Satskiy" );
MODULE_DESCRIPTION( "MuMu filesystem supports multiple writers and multiple readers" );

И две функции, которые вызываются VFS в момент загрузки и выгрузки модуля:

static int __init init_mumufs_fs( void )
{
    . . .
}

static void __exit exit_mumufs_fs( void )
{
    . . .
}

module_init( init_mumufs_fs )
module_exit( exit_mumufs_fs )

В момент загрузки модуля потребуется выполнить регистрацию файловой системы в операционной системе, а в момент выгрузки де-регистрацию. Кроме этого нужно создать/удалить корневую запись в /proc (работа со /proc будет описана в отдельной секции).

Для регистрации файловой системы требуется подготовить структуру типа file_system_type:

static struct file_system_type      mumufs_fs_type =
{
    .owner      = THIS_MODULE,
    .name       = "mumufs",
    .get_sb     = mumufs_get_sb,
    .kill_sb    = mumufs_kill_super,
};

И сделать вызов

register_filesystem( &mumufs_fs_type );

Присвоение полю owner значения THIS_MODULE позволяет ядру корректно вести счетчик ссылок на модуль. Функции mumufs_get_sb и mumufs_kill_super должны создавать и уничтожать суперблок файловой системы соответственно.

Функция init_mumufs_fs может быть реализована так:

static int __init init_mumufs_fs( void )
{
    int         RetVal;

    if ( InitialiseProcEntries() != 0 )
    {
        return -EFAULT;
    }

    RetVal = register_filesystem( &mumufs_fs_type );
    if ( RetVal != 0 )
    {
        RemoveProcEntries();
    }
    return RetVal;
}

А функция exit_mumufs_fs так:

static void __exit exit_mumufs_fs( void )
{
    RemoveProcEntries();
    unregister_filesystem( &mumufs_fs_type );
}

Монтирование и размонтирование mumufs

Функции mumufs_get_sb и mumufs_kill_super, указатели на которые были предоставлены в момент регистрации файловой системы, будут использоваться VFS в момент монтирования и размонтирования mumufs.

Для создания суперблока можно вопользоваться функцией VFS get_sb_nodev(), которая предназначена для случаев, когда файловая система не связана ни с каким устройством. Функция mumufs_get_sb должна выполнить еще одно действие – запомнить указатель на vsmount, который понадобится для /proc записей и который больше нигде не встретится. Таким образом функция может выглядеть так:

int  mumufs_get_sb( struct file_system_type *      fs_type,
                    int                            flags,
                    const char *                   dev_name,
                    void *                         data,
                    struct vfsmount *              mnt )
{
    int     ret;

    ret = get_sb_nodev( fs_type, flags, data, mumufs_fill_super, mnt );
    if ( ret == 0 )
    {
        struct super_block *        sb = mnt->mnt_sb;

        /* Memorize vfsmount pointer. It is required to display the mount */

        /* point in the /proc/mumufs/...                                  */
        ((struct mumu_mount_data *)(sb->s_fs_info))->Mount = mnt;
    }
    return ret;
}

VFS функция get_sb_nodev принимает в числе прочего указатель на функцию, которая должна заполнить структуру super_block. В случае с mumufs это будет делать mumufs_fill_super(). В этой функции, помимо всего прочего, размещается экземпляр структуры mumu_mount_data, указатель на который записывается в поле s_fs_info структуры super_block. Это поле специально зарезервировано для специфических данных файловой системы.

Фунция mumufs_fill_super может быть реализована так (контроль ошибок опущен для краткости):

static int mumufs_fill_super( struct super_block *      sb,
                              void *                    data,
                              int                       silent )
{
    struct inode *              inode;
    struct dentry *             root;
    struct mumu_mount_data *    mount_data;


    mount_data = kmalloc( sizeof( struct mumu_mount_data ), GFP_KERNEL );

    memset( mount_data, 0, sizeof( struct mumu_mount_data ) );

    mumufs_parse_opt( (char *)data, mount_data );

    sb->s_fs_info = mount_data;
    sb->s_magic = MUMUFS_MAGIC;
    sb->s_op = &mumufs_ops;

    inode = mumufs_get_inode( sb, S_IFDIR | 0755, 0 );

    root = d_alloc_root( inode );
    sb->s_root = root;

        /* It is the point when a file system is mounted */

    CreateMountInfo( sb );
    return 0;
}

Концептуально mumufs_fill_super() ответственна за несколько вещей. Во-первых, она размещает экземпляр структуры mumu_mount_point, затем разбирает параметры монтирования файловой системы с помощью функции mumufs_parse_opt() и записывает указатель на эту структуру в поле s_fs_info суперблока. Далее заполняются два важных поля – магический номер mumufs и указатель на структуру, описывающую операции с mumufs. После этого функция создает inode для корневого элемента смонтированной файловой системы с помощью функции mumufs_get_inode(). Поскольку операция создания inode нужна часто – в момент создания файлов, каталогов, ссылок – имеет смысл оформить эту функциональность в виде функции, что и сделано. Последнее, что делает mumufs_fill_super(), она создает записи в /proc с помощью функции CreateMountInfo().

Структура с операциями mumufs и магический номер выглядят так:

#define MUMUFS_MAGIC    0x710110

static struct super_operations              mumufs_ops =
{
    .statfs         = mumufs_statfs,
    .drop_inode     = generic_delete_inode,
    .put_super      = mumufs_put_super,
    .show_options   = mumufs_show_options,
};

Разбор параметров монтирования, создание inode и работа с записями в /proc обсуждаются в деталях в отдельных главах.

Функция mumufs_put_super() освободит память, занимаемую структурой, описывающей точку монтипрвания:

static void mumufs_put_super( struct super_block *  sb )
{
    RemoveMountInfo( sb );          /* /proc/ entry removal           */

    mumufs_umount_cleanup( sb );
    kfree( sb->s_fs_info );         /* mumu_mount_data removal        */
}

В момент размонтирования mumufs подсистема VFS вызовет функцию mumufs_kill_super(). Может оказаться так, что на файловой системе все еще существуют файлы, то есть все еще есть размещенные блоки памяти, ассоциированные с ними. Память нужно освободить, о чем и должна позаботиться mumufs_kill_super().

Здесь надо немного забежать вперед. Информация о блоке данных содержится в структуре Storage, которая будет размещаться в памяти в момент создания обычного файла, а указатель на нее будет записываться в поле i_private структуры inode. Принимая во внимание факт, что супер блок хранит список всех своих inode, имеется достаточно информации для реализации функции:

void  mumufs_kill_super( struct super_block *  sb )
{
    struct inode *      inode;
    struct inode *      next_i;

        /* Iterate over the fs inodes */

    list_for_each_entry_safe(inode, next_i, & sb->s_inodes, i_sb_list)
    {
        if ( inode->i_mode && S_IFREG )
        {
            if ( inode->i_private != NULL )
            {
                FreeStorage( (struct Storage *)( inode->i_private ) );
            }
        }
    }

    return kill_litter_super( sb );
}

Разбор параметров монтирования

Для разбора параметров монтирования VFS предлагает несколько функций, для работы которых необходимо подготовить структуру данных, описывающую возможные параметры:

enum
{
    OPT_MAX_BLOCK_SIZE,     /* max block size limitation   */

    OPT_MAX_ENTRIES,        /* max # of entries limitation */
    OPT_ERROR               /* end of list marker          */
};


static match_table_t    tokens =
{
    {OPT_MAX_BLOCK_SIZE,    "max_block_size=%u"},
    {OPT_MAX_ENTRIES,       "max_entries=%u"},
    {OPT_ERROR,             NULL}
};

Теперь можно реализовать функцию mumufs_parse_opt():

int mumufs_parse_opt( char *                      opt,
                    struct mumu_mount_data *    d )
{
    char *      p;

    d->MaxBlockSize = DEFAULTMAXBLOCKSIZE;
    d->MaxEntries   = DEFAULTMAXENTRIES;

    while ( (p = strsep( &opt, "," )) != NULL )
    {
        int             token;
        int             value;
        substring_t     args[ MAX_OPT_ARGS ];


        if ( !*p )      continue;

        token = match_token( p, tokens, args );
        switch ( token )
        {
            case OPT_MAX_BLOCK_SIZE:
                if ( match_int( &args[0], &value ) )    return 0;
                d->MaxBlockSize = value;
                break;

            case OPT_MAX_ENTRIES:
                if ( match_int( &args[0], &value ) )    return 0;
                d->MaxEntries = value;
                break;

            default:
                return 0;
        }
    }

    return 1;
}

Для корректного отображения параметров монтирования, например по команде mount, необходимо реализовать функцию mumufs_show_options(), указатель на которую был записан в структуру операций mumufs.

int mumufs_show_options( struct seq_file *    m,
                         struct vfsmount *    mnt )
{
    struct mumu_mount_data *    d = (struct mumu_mount_data *)

                                            ( mnt->mnt_sb->s_fs_info );

    seq_printf( m, ",max_block_size=%u", d->MaxBlockSize );
    seq_printf( m, ",max_entries=%u", d->MaxEntries );

    return 0;
}

Записи в /proc

В момент загрузки модуля необходимо создать папку /proc/mumu, в которой будут создаваться все остальные записи. В этот же момент стоит создать файл /proc/mumu/version, чтение из которого выдаст версию mumufs. Это сделает функция InitialiseProcEntries():

static const char *             EntryPoint = "mumu";

static const char *             Version = "0.1.0";
static struct proc_dir_entry *  MainProcEntry = NULL;

int  InitialiseProcEntries( void )
{
    struct proc_dir_entry *     Entry;
    int                         Flags;

    MainProcEntry = proc_mkdir( EntryPoint, NULL );
    if ( MainProcEntry == NULL )
    {
        return -ENOMEM;
    }
    MainProcEntry->owner = THIS_MODULE;


    Flags = S_IFREG | S_IRUGO;              // Anybody can read the file

    Entry = create_proc_entry( "version", Flags, MainProcEntry);
    Entry->owner = THIS_MODULE;
    Entry->read_proc = ReadVersion;

    return 0;
}

Присвоение THIS_MODULE полю owner позволяет ядру корректно вести подсчет ссылок на модуль. Поле read_proc содержит указатель на функцию, которая будет вызвана, когда пользователь сделает попытку прочитать из соответствующего файла.

Разумеется, в момент выгрузки модуля необходимо удалить записи из /proc:

int RemoveProcEntries( void )
{
    remove_proc_entry( "version", MainProcEntry );
    remove_proc_entry( EntryPoint, NULL );

    return 0;
}

Создание и удаление информации о каждой смонтированной mumufs производится аналогичным образом.

Реализацию функций чтения из файла рассмотрим на примере функции чтения версии:

static int ReadVersion( char *  Page, char **  Start, 
                        off_t  Offset, int  Count, 
                        int *  Eof, void *  Data )
{
    return snprintf( Page, Count - 1, "%s
", Version ) + 1;
}

Интерес также представляет функция чтения информации о точке монтирования. В ней по полям структуры vfsmount, указатель на которую был сохранен в момент монтирования mumufs, определяется и выдается полный путь к точке монтирования. Для этого используется функция d_path():

static int ReadMountPoint( char *  Page, char **  Start, 

                           off_t  Offset, int  Count, 
                           int *  Eof, void *  Data )
{
    char                        buf[ NAME_MAX*2 ];
    struct vfsmount *           mount = ((struct mumu_mount_data *)Data)->Mount;
    struct path                 p;

    if ( mount != NULL )
    {
        /* Fill the path structure fields to get the mount point path */

        p.mnt = mount->mnt_parent;
        p.dentry = mount->mnt_mountpoint;

        return snprintf( Page, Count - 1, "%s
", d_path( &p, buf, 512 ) ) + 1;
    }

    return snprintf( Page, Count - 1, "Unknown
" ) + 1;
}

Создание и удаление директорий, символических ссылок и файлов

Когда обсуждался процесс монтирования файловой системы, упоминалась функция mumufs_get_inode(), в обязанности которой входит создание inode. В момент монтирования нужно было создать inode для корневого директория файловой системы. Структура inode, в числе прочего, имеет поле, которое указывает на набор операций, допустимых для inode. Эти операции будут разными для каждого из типов inode: директорий, обычных файлов и символических ссылок. mumufs готовит структуры допустимых операций следующим образом:

static struct inode_operations              mumufs_file_inode_operations =
{
    .getattr    = simple_getattr,
    .setattr    = mumufs_inode_setattr,
};

static struct inode_operations              mumufs_dir_inode_operations =
{
    .create     = mumufs_create,
    .lookup     = simple_lookup,
    .link       = simple_link,
    .unlink     = mumufs_inode_unlink,
    .symlink    = mumufs_symlink,
    .mkdir      = mumufs_mkdir,
    .rmdir      = simple_rmdir,
    .mknod      = mumufs_mknod,
    .rename     = simple_rename,
};

Функции, начинающиеся с simple_ нет необходимости реализовывать – они взяты из VFS, их реализация годится для mumufs. А для символических ссылок используется целиком готовая структура inode_operations.

Используя описанные структуры, можно реализовать mumufs_get_inode() (обработка ошибок опущена для краткости):

static struct inode *  mumufs_get_inode( struct super_block *  sb,
                                         int                   mode,
                                         dev_t                 dev )
{
    if ( S_ISREG( mode ) || S_ISDIR( mode ) || S_ISLNK( mode ) )
    {
        struct inode *      inode = new_inode( sb );
        if ( inode != NULL )
        {
            inode->i_mode = mode;
            inode->i_uid = current->fsuid;
            inode->i_gid = current->fsgid;
            inode->i_mapping->a_ops = &mumufs_aops;
            inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
            inode->i_private = NULL;

            switch ( mode & S_IFMT )
            {
                case S_IFREG:

                    {
                        int  Records = atomic_read( & ((struct mumu_mount_data *)

                                                       (sb->s_fs_info))->NumberOfEntries );
                        if ( Records >= ((struct mumu_mount_data *)
                                         (sb->s_fs_info))->MaxEntries )
                        {
                            iput( inode );
                            return NULL;
                        }
                    }

                    inode->i_op = & mumufs_file_inode_operations;
                    inode->i_fop = & mumufs_file_operations;

                        /* Alloc the node storage */

                    inode->i_private = AllocateStorage();

                        /* Increment # of entries */
                    atomic_inc( & ((struct mumu_mount_data *)
                                   (sb->s_fs_info))->NumberOfEntries );
                    break;
                case S_IFDIR:
                    inode->i_op = & mumufs_dir_inode_operations;
                    inode->i_fop = & simple_dir_operations;
                    /* dir inodes start off with i_nlink == 2 (for "." entry) */

                    inode->i_nlink++;
                    break;
                case S_IFLNK:
                    inode->i_op = & page_symlink_inode_operations;
                    break;
                default:
                    /* It is impossible - we have 'if' at the beginning */

                    break;
            }
        }
        return inode;
    }

    return NULL;
}

Функция проверяет для какого элемента был запрошен inode и затем вызывает new_inode(), предоставляемую VFS. Далее, в зависимости от типа элемента заполняются поля созданного inode. Для inode, созданного для файла, дополнительно проверяется, не превышен ли лимит на количество блоков данных, и если все в порядке, то размещается еще один блок с помощью AllocateStorage() и увеличивается счетчик блоков данных.

Еще одно важное поле в структуре inode называется i_fop. Оно содержит указатель на структуру, описывающую файловые операции. Для символических ссылок и директорий годятся готовые структуры VFS, а для обычных файлов потребуется своя:

static struct file_operations               mumufs_file_operations =
{
    .open       = mumufs_file_open,
    .read       = mumufs_file_read,
    .write      = mumufs_file_write,
    .release    = mumufs_file_release,
    .llseek     = mumufs_no_llseek,

    .poll       = mumufs_file_poll,     /* select & poll support */
};

Указанные здесь функции и будут использоваться для всех операций с файлами, то есть в случае mumufs с блоками данных.

Открытие и закрытие файлов

За открытие и закрытие обычных файлов отвечают функции mumufs_file_open() и mumufs_file_release() соответственно. Здесь важным моментом является то, что ядро подготавливает структуру file, которая соответствует файловому дескриптору в пространстве пользователя. Указатель на эту структуру передается во все функции, реализующие операции с файлами. В структуре file есть поле private_data, которое файловая система может использовать для своих специфических нужд. Mumufs надо хранить версию последнего прочитанного блока и это поле является хорошим кандидатом. В момент открытия файла его нужно обнулить.

int     mumufs_file_open( struct inode *  iNode, struct file *  File )
{
    int     RetVal = generic_file_open( iNode, File );
    if ( RetVal != 0 )
    {
        return RetVal;
    }

        /*
         * private_data stores the last read version of a block.
         * 0 - nothing has been read yet
         */

    File->private_data = 0;
    return 0;
}

В момент закрытия файла ничего делать не требуется. Функция приведена, скорее, с демонстрационными целями.

int     mumufs_file_release( struct inode *  iNode, struct file *  File )
{
    struct Storage *        storage = (struct Storage *)(iNode->i_private);

    if ( storage == NULL )
    {
        /* It is basically a problem however there is nothing to do */

    }
    return 0;
}

Запись в файл

Реализация функции записи в файл не представляет сложности. Она должна проверить, что размер записываемого блока не превышает лимит, скопировать данные из пространства пользователя в пространство ядра, взять блокировку, заменить указатель, увеличить номер версии блока, отпустить блокировку и наконец уведомить заблокированных читателей о том, что новые данные доступны. Все действия строго последовательны (контроль ошибок опущен для краткости):


ssize_t mumufs_file_write( struct file *  File, const char *  Buffer, 
                           size_t  Size, loff_t *  Offset )
{
    struct inode *              node = File->f_dentry->d_inode;
    struct Storage *            storage = (struct Storage *)(node->i_private);
    struct mumu_mount_data *    mount_data = (struct mumu_mount_data *)

                                                 (File->f_dentry->d_sb->s_fs_info);
    unsigned char *             KernelBuffer = NULL;

        /* I support block oriented read/write so the offset must be 0 */
    if ( *Offset != 0 )                     return -EINVAL;
    if ( Size > mount_data->MaxBlockSize )  return -EINVAL;

        /* Buffer is required because the copying could be interrupted */

    KernelBuffer = kmalloc( Size, GFP_KERNEL );
    if ( KernelBuffer == NULL )             return -ENOSPC;
    if ( copy_from_user( KernelBuffer, Buffer, Size ) != 0 )
    {
        kfree( KernelBuffer );
        return -EFAULT;
    }

    down_write( & storage->BufferLock );

    if ( storage->Buffer != NULL )
    {
        kfree( storage->Buffer );
    }
    storage->Buffer = KernelBuffer;
    storage->BufferSize = Size;

        /* Update the block version */

    if ( ++(storage->BufferVersion) == 0 )
    {
        ++(storage->BufferVersion);
    }

    node->i_size = Size;            /* File size update    */
    node->i_mtime = CURRENT_TIME;   /* Modification time   */
    node->i_bytes = Size;           /* Used bytes          */

    up_write( & storage->BufferLock );

        /* Wake up all the reades */
    wake_up_interruptible( & storage->BlockedReaders );
    return Size;
}

Помимо описанных выше действий функция обновляет информацию о текущем размере файла и дату его модификации.

Чтение из файла

Реализация чтения из файла немного сложнее. Здесь необходимо учитывать то, в каком режиме был открыт файл. Если был использован режим с блокированием, то в случае отсутствия новых данных процесс должен быть заблокирован. Если же был использован режим без блокирования, то в таком случае процесс блокировать не надо, а вместо этого возвратить признак отсутствия данных –EAGAIN.

ssize_t mumufs_file_read( struct file *  File, char *  Buffer, size_t  Size, loff_t *  Offset )
{
    struct inode *              node = File->f_dentry->d_inode;
    struct Storage *            storage = (struct Storage *)(node->i_private);

        /* I support block oriented read/write so the offset must be 0 */

    if ( *Offset != 0 )                     return -EINVAL;
    if ( Size < storage->BufferSize )       return -EINVAL;

  Loop:
    down_read( & storage->BufferLock );
    if ( (storage->BufferVersion != 0 ) &&       /* There are data available     */

         (File->private_data != 
             (void*)(storage->BufferVersion)) )  /* New data block is available  */
    {
        int     BytesCopied = storage->BufferSize;

        if ( copy_to_user( Buffer, storage->Buffer, BytesCopied ) != 0 )
        {
            BytesCopied = -EINVAL;
        }
        else

        {
                /* Save the version of the read block */
            File->private_data = (void*)(storage->BufferVersion);
            node->i_atime = CURRENT_TIME;
        }

        up_read( & storage->BufferLock );
        return BytesCopied;
    }


        /*
         * There is nothing to read because: - 0 bytes available
         *                                   - no "fresh" data
         */

    if ( File->f_flags & O_NONBLOCK )   /* Not required to block */
    {
        up_read( & storage->BufferLock );
        return -EAGAIN;
    }

        /* It is blocking I/O                                                 */

        /* I think it is safe to compare buffer version without taking a lock */
    up_read( & storage->BufferLock );
    if ( wait_event_interruptible( storage->BlockedReaders,
                        ((void*)(storage->BufferVersion) != File->private_data) ) != 0 )
    {
        return -ERESTARTSYS;
    }
    goto Loop;
}

В самом начале функция проверяет, что предоставленный пользователем буфер имеет достаточный размер, чтобы туда поместился весь имеющийся блок данных. Затем она берет блокировку на чтение и проверяет наличие новых данных. Если таковые имеются, то они копируются в пространство пользователя, блокировка отпускается и на этом процесс заканчивается. Если же данных нет, то проверяется режим открытия файла, в зависимости от которого процесс помещается в очередь ожидания или нет. Именно эту очередь использует функция записи для нотификации о доступности новых данных.

Select и Poll

Последнее, что нужно сделать, это добавить поддержку операций select и poll. Соответствующая функция должна вернуть маску, которая говорит о том, какую операцию операцию с файлом сможет выполнить процесс без блокирования.

Неблокируемую запись в mumufs можно выполнить в любой момент, а чтение только если имеются новые данные. Перед тем, как вернуть маску, необходимо зарегестрировать очередь ожидания для системных вызовов select и poll с помощью вызова poll_wait().

unsigned int  mumufs_file_poll( struct file *  File, 

                                struct poll_table_struct *  PollTable )
{
    unsigned int        mask = POLLOUT | POLLWRNORM;    /* Writing is always welcome */
    struct inode *      node = File->f_dentry->d_inode;
    struct Storage *    storage = (struct Storage *)(node->i_private);

    poll_wait( File, & storage->BlockedReaders, PollTable );

    down_read( & storage->BufferLock );
    if ( (storage->BufferVersion != 0 ) &&          /* There are data in the block       */

         (File->private_data != 
               (void*)(storage->BufferVersion)) )   /* New block is available            */
    {
        mask |= POLLIN | POLLRDNORM;
    }
    up_read( & storage->BufferLock );

    return mask;
}

Тестирование производительности

Тестирование производительности mumufs имеет смысл проводить в сравнении с производительностью какого-либо другого механизма, обеспечивающего такую же функциональность и уже имеющегося в системе. Подходящих кандидатов два – разделяемая память и очередь сообщений. Разделяемая память на первый взгляд кажется ближе к mumufs, потому что не накладывает ограничений на количество поставщиков и потребителей и хранит только одну копию данных. Однако она не предоставляет встроенного механизма обеспечения синхронизации доступа. Это означает, что клиентский код должен самостоятельно реализовать синхронизацию доступа. Это, в свою очередь, привносит дополнительную сложность в клиентский код с повышением вероятности появления ошибок. Такая синхронизация, к тому же, будет исключительно добровольной, что также может сказаться на надежности программного обеспечения. Исходя из этого очереди сообщений оказываются ближе к mumufs с точки зрения похожести клиентского кода и надежности синхронизации. Строго говоря, код с использованием mumufs для обмена данными будет проще, однако ничего ближе, чем очереди сообщений нет, поэтому сравнение будет проводиться именно с ними.

Очереди сообщений плохо приспособлены для организации обмена с множественными поставщиками и множественными потребителями данных – для каждой пары нужно создавать отдельную очередь, что приводит к сложному тестовому коду. Поэтому тестовые программы написаны для модели с одним поставщиком и переменным количеством потребителей. Здесь, очевидно, очереди сообщений проигрывают по простоте использования mumufs. Однако такое упрощение позволяет сохранить простоту и краткость тестов, а по результатам упрощенной модели сделать предположения и о полной модели.

Далее приводятся графики замера времени, потребовавшегося поставщику данных на отсылку (для очереди сообщений) или запись (для mumufs) и времени, прошедшего с начала отсылки/записи первого пакета до момента приема/чтения последнего пакета всеми потребителями. Время отложено по оси ординат. Соответственно, чем меньше затраченное время, тем быстрее работает механизм обмена данными. Количество пакетов во всех экспериментах одинаково и равно 100000. Количество потребителей варьируется от 1 до 64, оно отложено по оси абсцисс. Каждый эксперимент проводился 3 раза, а полученные результаты затем усреднялись.

Рисунок 2. Сравнительная производительность с пакетом 16 байт

Рисунок 3. Сравнительная производительность с пакетом 512 байт

Рисунок 4. Сравнительная производительность с пакетом 1024 байт

Рисунок 5. Сравнительная производительность с пакетом 2048 байт

Рисунок 6. Сравнительная производительность с пакетом 4096 байт

Рисунок 7. Сравнительная производительность с пакетом 8192 байта

График для размера пакета 16384 байта приводится только для mumufs. Для очереди сообщение системное ограничение максимального размера блока данных по умолчанию установлено в значение 8192, поэтому для нее тестовые запуски не проводились. Цель графика для 16384 байт для mumufs посмотреть как она себя ведет с ростом размера пакета.

Рисунок 7. Производительность mumufs с пакетом 16384 байта

Можно заметить, что в схеме с одним поставщиком и одним потребителем mumufs проигрывает очереди сообщений. При этом проигрыш может достигать порядка 40%. Однако с ростом количества потребителей mumufs начинает выигрывать. Выигрыш может достигать порядка 120%. С ростом размера пакета затраченное время растет, однако несколько медленнее, чем размер пакета.

С точки зрения потребления памяти mumufs в пике потребляет меньше памяти, чем очередь сообщений. В пиковой ситуации очередь сообщений будет хранить все непрочитанные сообщения, помноженные на количество очередей. А mumufs всегда будет хранить только одну копию блока данных.

У mumufs есть резерв повышения производительности за счет расхода памяти. Если для каждого блока сразу выделять два буфера максимального размера одного блока, то появляется возможность избежать дорогих операций выделения и освобождения памяти при каждой операции записи и уменьшить количество столкновений на блокировках. Однако подобная реализация – дело будущего.

Последнее, что стоит добавить, это аппаратура и операционная система, которые использовались для разработки и сравнения. Компьютер был собран на основе процессора Intel Atom с 2 гигабайтами ОЗУ. Этой аппаратурой управлял дистрибутив Fedora 10, ядро 2.6.27. Подробнее об использованной аппаратуре можно посмотреть [5].

Литература

  1. Сергей Сацкий. Mumufs: виртуальная файловая система для поддержки IPC типа много-ко-много. http://satsky.spb.ru/articles/mumufs/mumufsArticle.php
  2. Клаудия Зальзберг Родригес, Гордон Фишер, Стивен Смолски. Linux. Азбука ядра. Кудиц пресс, 2007
  3. Роберт Лав. Разработка ядра Linux. Второе издание. Вильямс, 2006
  4. Дэниел Бовет, Марко Чезати. Ядро Linux. О Рейли, 2006
  5. Сергей Сацкий. Дешевый домашний Linux сервер на mini ITX плате. http://satsky.spb.ru/articles/LinuxOnMiniITX/LinuxOnMiniITX.php
  6. LWN: Creating Linux virtual filesystems. 2002. http://lwn.net/Articles/13325/
  7. LWN: Creating Linux virtual filesystems. 2003. http://lwn.net/Articles/57369/
  8. LWN: lwnfs simpler source code. 2003. http://lwn.net/Articles/57373/
  9. Исходный код mumufs, соответствующий статье. http://satsky.spb.ru/articles/mumufs/mumufs-0.1.0.tar.bz2


Verbatim copying and distribution of this entire article is permitted in any medium, provided this notice is preserved.

Разрешается копирование и распространение этой статьи любым способом без внесения изменений, при условии, что это разрешение сохраняется.
Last Updated: May 14, 2009