Внутреннее устройство ядра Linux 2.4

       

Кеш Inode и взаимодействие с Dcache


Для поддержки различных файловых систем Linux предоставляет специальный интерфейс уровня ядра, который называется VFS (Virtual Filesystem Switch). Он подобен интерфейсу vnode/vfs, имеющемуся в производных от SVR4 (изначально пришедшему из BSD и реализаций Sun)

Реализация inode cache для Linux находится в единственном файле fs/inode.c, длиной в 977 строк (Следует понимать, что размер файла может колебаться от версии к версии, так например в ядре 2.4.18, длина этого файла составляет 1323 строки прим. перев.). Самое интересное, что за последние 5 - 7 лет этот файл претерпел незначительные изменения, в нем до сих пор можно найти участки кода, дошедшие до наших дней с версии, скажем, 1.3.42

Inode cache в Linux представляет из себя:

  • Глобальный хеш-массив inode_hashtable, в котором каждый inode хешируется по значению указателя на суперблок и 32-битному номеру inode. При отсутсвии суперблока (inode->i_sb == NULL), вместо хеш-массива inode добавляется к двусвязному списку anon_hash_chain. Примером таких анонимных inodes могут служить сокеты, созданные вызовом функции net/socket.c:sock_alloc(), которая вызывает fs/inode.c:get_empty_inode().
  • Глобальный список inode_in_use, который содержит допустимые inodes (i_count>0 и i_nlink>0). Inodes вновь созданные вызовом функций get_empty_inode() и get_new_inode() добавляются в список inode_in_use
  • Глобальный список inode_unused, который содержит допустимые inode с i_count = 0.
  • Список для каждого суперблока (sb->s_dirty) , который содержит inodes с i_count>0, i_nlink>0 и i_state & I_DIRTY. Когда inode помечается как "грязный" (здесь и далее под термином "грязный" подразумевается "измененный" прим. перев.), он добавляется к списку sb->s_dirty при условии, что он (inode) хеширован. Поддержка такого списка позволяет уменьшить накладные расходы на синхронизацию.
  • Inode cache суть есть - SLAB cache, который называется inode_cachep. Объекты inode могут создаваться и освобождаться, вставляться и изыматься из SLAB cache

  • Через поле inode->i_list с inode вставляется в список определенного типа, через поле inode->i_hash

    - в хеш-массив. Каждый inode может входить в хеш-массив и в один и только в один список типа (in_use, unused или dirty).

    Списки эти защищаются блокировкой (spinlock) inode_lock.

    Подсистема inode cache инициализируется при вызове функции inode_init() из init/main.c:start_kernel(). Эта функция имеет один входной параметр - число страниц физической памяти в системе. В соответсвии с этим параметром inode cache конфигуририруется под существующий объем памяти, т.е. при большем объеме памяти создается больший хеш-массив.



    Единственная информация о inode cache, доступная пользователю - это количество неиспользованных inodes из inodes_stat.nr_unused. Получить ее можно из файлов /proc/sys/fs/inode-nr и /proc/sys/fs/inode-state.

    Можно исследовать один из списков с помощью gdb:

    (gdb) printf "%d\n", (unsigned long)(&((struct inode *)0)->i_list) 8 (gdb) p inode_unused $34 = 0xdfa992a8 (gdb) p (struct list_head)inode_unused $35 = {next = 0xdfa992a8, prev = 0xdfcdd5a8} (gdb) p ((struct list_head)inode_unused).prev $36 = (struct list_head *) 0xdfcdd5a8 (gdb) p (((struct list_head)inode_unused).prev)->prev $37 = (struct list_head *) 0xdfb5a2e8 (gdb) set $i = (struct inode *)0xdfb5a2e0 (gdb) p $i->i_ino $38 = 0x3bec7 (gdb) p $i->i_count $39 = {counter = 0x0}

    Заметьте, что от адреса 0xdfb5a2e8 отнимается число 8, чтобы получить адрес struct inode (0xdfb5a2e0), согласно определению макроса list_entry() из include/linux/list.h.

    Для более точного понимания принципа работы inode cache, давайте рассмотрим цикл жизни обычного файла в файловой системе ext2 с момента его открытия и до закрытия.

    fd = open("file", O_RDONLY); close(fd);

    Системный вызов open(2) реализован в виде функции fs/open.c:sys_open, но основную работу выполняет функция fs/open.c:filp_open(), которая разбита на две части:

  • open_namei(): заполняет структуру nameidata, содержащую структуры dentry и vfsmount.




  • dentry_open(): с учетом dentry и vfsmount, размещает новую struct file и связывает их между собой; вызывает метод f_op->open() который был установлен в inode->i_fop при чтении inode в open_namei() (поставляет inode через dentry->d_inode).


  • Функция open_namei() взаимодействует с dentry cache через path_walk(), которая, в свою очередь, вызывает real_lookup(), откуда вызывается метод inode_operations->lookup(). Назначение последнего - найти вход в родительский каталог и получить соответствующий inode вызовом iget(sb, ino) При считывании inode, значение dentry присваивается посредством d_add(dentry, inode). Следует отметить, что для UNIX-подобных файловых систем, поддерживающих концепцию дискового номера inode, в ходе выполнения метода lookup(). производится преобразование порядка следования байт числа (endianness) в формат CPU, например, если номер inode хранится в 32-битном формате с обратным порядком следования байт (little-endian), то выполняются следующие действия:

    (Считаю своим долгом подробнее остановиться на понятии endianness. Под этим термином понимается порядок хранения байт в машинном слове (или двойном слове). Порядок может быть "прямым" (т.е. 32-битное число хранится так 0x12345678) и тогда говорят "big endianness" (на отечественном жаргоне это звучит как "большой конец", т.е. младший байт лежит в старшем адресе) или "обратным" (т.е. 32-битное число хранится так 0x78563412 - такой порядок следования байт принят в архитектуре Intel x86) и тогда говорят "little endianness" (на отечественном жаргоне это звучит как "маленький конец", т.е. младший байт лежит в младшем адресе). прим. перев.)

    unsigned long ino = le32_to_cpu(de->inode); inode = iget(sb, ino); d_add(dentry, inode);

    Таким образом, при открытии файла вызывается iget(sb, ino), которая, фактически, называется iget4(sb, ino, NULL, NULL), эта функция:

  • Пытается найти inode в хеш-таблице по номерам суперблока и inode. Поиск выполняется под блокировкой inode_lock. Если inode найден, то увеличивается его счетчик ссылок (i_count); если счетчик перед инкрементом был равен нулю и inode не "грязный", то он удаляется из любого списка (inode->i_list), в котором он находится (это конечно же список inode_unused) и вставляется в список inode_in_use; в завершение, уменьшается счетчик inodes_stat.nr_unused.




  • Если inode на текущий момент заблокирован, то выполняется ожидание до тех пор, пока inode не будет разблокирован, таким образом, iget4() гарантирует возврат незаблокированного inode.


  • Если поиск по хеш-таблице не увенчался успехом, то вызывается функция get_new_inode(), которой передается указатель на место в хеш-таблице, куда должен быть вставлен inode.


  • get_new_inode() распределяет память под новый inode в SLAB кэше inode_cachep, но эта операция может устанавливать блокировку (в случае GFP_KERNEL), поэтому освобождается блокировка inode_lock. Поскольку блокировка была сброшена то производится повторный поиск в хеш-таблице, и если на этот раз inode найден, то он возвращается в качестве результата (при этом счетчик ссылок увеличивается вызовом __iget), а новый, только что распределенный inode уничтожается. Если же inode не найден в хеш-таблице, то вновь созданный inode инициализируется необходимыми значениями и вызывается метод sb->s_op->read_inode(), чтобы инициализировать остальную часть inode Во время чтения метдом s_op->read_inode(), inode блокируется (i_state = I_LOCK), после возврата из s_op->read_inode() блокировка снимается и активируются все ожидающие его процессы.


  • Теперь рассмотрим действия, производимые при закрытии файлового дескриптора. Системный вызов close(2) реализуется функцией fs/open.c:sys_close(), которая вызывает do_close(fd, 1). Функция do_close(fd, 1)

    записывает NULL на место дескриптора файла в таблице дескрипторов процесса и вызывает функцию filp_close(), которая и выполняет большую часть действий. Вызывает интерес функция fput(), которая проверяет была ли это последняя ссылка на файл и если да, то через fs/file_table.c:_fput()

    вызывается __fput(), которая взаимодействует с dcache (и таким образом с inode cache - не забывайте, что dcache является "хозяином" inode cache!). Функция fs/dcache.c:dput() вызывает dentry_iput(), которая приводит нас обратно в inode cache через iput(inode). Разберем fs/inode.c:iput(inode) подробнее:



  • Если входной параметр NULL, то абсолютно ничего не делается и управление возвращается обратно.


  • Если входнй параметр определен, то вызывается специфичный для файловой системы метод sb->s_op->put_inode()

    без захвата блокировки (так что он может быть блокирован).


  • Устанавливается блокировка (spinlock) и уменьшается i_count. Если это была не последняя ссылка, то просто проверяется - поместится ли количество ссылок в 32-битное поле и если нет - то выводится предупреждение. Отмечу, что поскольку вызов производится под блокировкой inode_lock, то для вывода предупреждения используется функция printk(), которая никогда не блокируется, поэтому ее можно вызывать абсолютно из любого контекста исполнения (даже из обработчика прерываний!).


  • Если ссылка была последней, то выполняются дополнительные действия.


  • Дополнительные действия, выполняемые по закрытию в случае последней ссылки функцией iput(), достаточно сложны, поэтому они рассматриваются отдельно:

  • Если i_nlink == 0 (например файл был удален, пока мы держали его открытым), то inode удаляется из хеш-таблицы и из своего списка. Если имеются какие-либо страницы в кеше страниц, связанные с данным inode, то они удаляются посредством truncate_all_inode_pages(&inode->i_data). Затем, если определен, то вызывается специфичный для файловой системы метод s_op->delete_inode(), который обычно удаляет дисковую копию inode. В случае отсутствия зарегистрированного метода s_op->delete_inode()

    (например ramfs), то вызывается clear_inode(inode), откуда производится вызов s_op->clear_inode(), если этот метод зарегистрирован и inode соответствует блочному устройству. Счетчик ссылок на это устройство уменьшается вызовом bdput(inode->i_bdev).


  • Если i_nlink != 0, то проверяется - есть ли другие inode с тем же самым хеш-ключом (in the same hash bucket) и если нет, и inode не "грязный", то он удаляется из своего списка типа, вставляется в список inode_unused, увеличивая inodes_stat.nr_unused. Если имеются inodes с тем же самым хеш-ключом, то inode удаляется из списка типа и добавляется к списку inode_unused. Если это анонимный inode (NetApp .snapshot) то он удаляется из списка типа и очищается/удаляется полностью.



  • Содержание раздела