Привет. В процессе собеседований на Linux-администратора испытуемым задается большое количество интересных вопросов. В большинстве своём это отличные векторы для изучения системы и мне показалось хорошей идеей разобрать часть из них. Сегодня поговорим про вопросы, связанные с линками в Linux.
1. Что такое hard-link, чем он отличается от sym-link?
2. Как найти все хард-линки, ведущие в одно и тоже место?
К любому из этих вопросов можно задать усложняющие - простых ответов вида "ln и find" вам может не хватить, поэтому давайте попробуем копнуть глубже.
Начнем с того, что Linux работает с файловыми системами не напрямую, а использует для этого VFS(Virtual File System) - абстракцию, которая позволяет собрать в одном корне партиции с разными файловыми системами. Секрет заключается в том, что интерфейс VFS предоставляет для драйверов файловых систем механизм взаимодействия, который описывает типичные действия с файловой системой(например - создание файла). Схема взаимодействия выглядит так:
Если заглянуть в исходный код ядра - вы увидите, что на уровне VFS есть некоторое количество структур, которыми описываются сущности ФС - в контексте этого поста нас интересует две из них. Первая - dentry, которая позволяет организовать древовидную структуру файловой системы Linux. Давайте посмотрим на её код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
struct dentry { /* RCU lookup touched fields */ unsigned int d_flags; /* protected by d_lock */ seqcount_t d_seq; /* per dentry seqlock */ struct hlist_bl_node d_hash; /* lookup hash list */ struct dentry *d_parent; /* parent directory */ struct qstr d_name; struct inode *d_inode; /* Where the name belongs to - NULL is * negative */ unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ /* Ref lookup also touches following */ struct lockref d_lockref; /* per-dentry lock and refcount */ const struct dentry_operations *d_op; struct super_block *d_sb; /* The root of the dentry tree */ unsigned long d_time; /* used by d_revalidate */ void *d_fsdata; /* fs-specific data */ struct list_head d_lru; /* LRU list */ struct list_head d_child; /* child of parent list */ struct list_head d_subdirs; /* our children */ /* * d_alias and d_rcu can share memory */ union { struct hlist_node d_alias; /* inode alias list */ struct rcu_head d_rcu; } d_u; };; |
Из интересного здесь стоит отметить следующие элементы: d_parent, d_name, d_inode и d_child. Давайте рассмотрим, как это работает.
Например, мы пытаемся обратиться к /bin/bash. При обращении путь будет разбит на 3 элемента: [ / , bin, bash ] (в первом случае "/" означает корень, представляя отдельную сущность, в остальных - просто разделитель). Каждый из них будет представлен отдельной dentry-записью, имеющей имя d_name, указатель на связанную иноду, а так же - связи вверх и вниз по дереву через элементы d_parent и d_child. Ядро будет скользить вниз по указателям дабы добраться до конечного элемента и посмотреть на его иноду(на самом деле - на попутные иноды из дерева оно тоже будет смотреть, дабы убедиться, что у вас есть права на этот и следующие элементы дерева).
Что касается второй, интересующей нас, структуры, то это Inode(index node) - конструкция, в которой описывается практически всё, что необходимо знать ядру о файле. Листинг кода, описывающий её, можно увидеть ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
struct inode { struct hlist_node i_hash; /* хешированный список */ struct list_head i_list; /* связанный список индексов */ struct list_head i_dentry; /* связанный список объектов dentry */ unsigned long i_ino; /* номер индекса */ atomic_t i_count; /* счетчик ссылок */ umode_t i_mode; /* права доступа */ unsigned int i_nlink; /* количество жестких ссылок */ uid_t i_uid; /* идентификатор пользователя-владельца */ gid_t i_gid; /* идентификатор группы-владельца */ kdev_t i_rdev; /* связанное устройство */ loff_t i_size; /* размер файла в байтах */ struct timespec i_atime; /* время последнего доступа к файлу */ struct timespec i_mtime; /* время последнего изменения файла */ struct timespec i_ctime; /* время изменения индекса */ unsigned int i_blkbits; /* размер блока в битах */ unsigned long i_blksize; /* размер блока в байтах */ unsigned long i_version; /* номер версии */ unsigned long i_blocks; /* размер файла в блоках */ unsigned short i_bytes; /* количество использованных байтов */ spinlock_t i_lock; /* блокировка для защиты полей */ struct rw_semaphore i_alloc_sem /* вложенные блокировки при захваченной i_sem */ struct semaphore i_sem; /* семафор индекса */ struct inode_operations *i_op; /* таблица операций с индексом */ struct file_operations *i_fop; /* файловые операции */ struct super_block *i_sb; /* связанный суперблок */ struct file_lock *i_flock; /* список блокировок файлов */ struct address_space *i_mapping; /* соответствующее адресное пространство */ struct address_space i_data; /* адресное пространство устройства */ struct dquot *i_dquot[MAXQUOTAS]; /* дисковые квоты для индекса */ struct list_head i_devices; /* список блочных устройств */ struct pipe_inode_info *i_pipe; /* информация конвейера */ struct block_device *i_bdev; /* драйвер блочного устройства */ unsigned long i_dnotify_mask; /* события каталога */ struct dnotify_struct *i_dnotify; /* информация о событиях каталога */ unsigned long i_state; /* флаги состояния */ unsigned long dirtied_when /* время первого изменения */ unsigned int i_flags; /* флаги файловой системы */ unsigned char i_sock; /* сокет или нет? */ atomic_t i_writecount; /* счетчик использования для записи */ void *i_security; /* модуль безопасности */ __u32 i_generation; /* номер версии индекса */ union { void *generic_ip; /* специфическая информация файловой системы */ } u; }; |
Здесь есть множество полей, но в контексте рассмотрения линков нас интересует поле i_dentry, которое является списком объектов типа dentry(directory entry) и отвечает за именование элементов.
Таким образом мы видим, что у каждой иноды может быть более одного имени. Именно так и работают хард-линки: создавая хардлинк мы просто добавляем элементы в массив i_dentry(в свою очередь, у созданного объекта dentry в соответствующем поле будет указатель на эту же иноду). Ядро будет считать иноду востребованной до тех пор, пока к ней хоть что-нибудь ведёт(случай, расширяющий этот кейс, мы рассмотрим отдельно).
Обратите внимание на то, что права доступа описываются на уровне иноды, а значит - права будут общими для всех хард-линков, ведущих на одну иноду. Так же в структуре иноды есть указатель на связанное блочное устройство - по этой причине хард-линки работают только в рамках одной точки монтирования.
Теперь давайте поговорим про символические ссылки(symlink). В книге Роберта Лава к этому нашлись весьма косвенные отсылки, поэтому я решил поискать хвосты самостоятельно. Если мы открутим пост в самое начало - увидим, что у структуры dentry есть поле для флагов - d_flags. В том же файле(include/linux/dcache.h) найдется такое:
1 2 3 4 |
static inline bool d_is_symlink(const struct dentry *dentry) { return __d_entry_type(dentry) == DCACHE_SYMLINK_TYPE; } |
такое:
1 2 3 4 |
static inline unsigned __d_entry_type(const struct dentry *dentry) { return dentry->d_flags & DCACHE_ENTRY_TYPE; } |
и такое:
1 |
#define DCACHE_SYMLINK_TYPE 0x00300000 /* Symlink */ |
То есть ядро узнает о том, что попало на симлинк ещё на этапе прохождения по дереву, из специального флага в dentry(к слову, через похожий флаг оно узнает о том, что это mountpoint и дальше начинается другая ФС). Но как оно узнает, куда ведёт ссылка? Очевидно - из внутренностей т.к. симлинка - полноценный объект ФС со своим инодом:
1 2 3 4 |
root@mixermsk:~/links# stat sl|head -n 3 File: ‘sl’ -> ‘/dev/null’ Size: 9 Blocks: 0 IO Block: 4096 symbolic link Device: fc01h/64513d Inode: 934556 Links: 1 |
Теперь зайдем с другой стороны и увидим, что символические ссылки делаются через специальный системный вызов symlink:
1 2 3 4 |
# strace ln -s /dev/null sl 2>&1|grep sl execve("/bin/ln", ["ln", "-s", "/dev/null", "sl"], [/* 22 vars */]) = 0 stat("/tmp/sl", 0x7fff9a5d0d00) = -1 ENOENT (No such file or directory) symlink("/dev/null", "sl") = 0 |
В исходниках ядра(fs/namei.c) находим объявление этого syscall`a:
1 2 3 4 |
SYSCALL_DEFINE2(symlink, const char __user *, oldname, const char __user *, newname) { return sys_symlinkat(oldname, AT_FDCWD, newname); } |
Идем смотреть код указанной функции, из неё перебираемся в sys_symlinkat(), потом - в vfs_symlink(), а там находим:
1 |
error = dir->i_op->symlink(dir, dentry, oldname); |
Эту штуку стоит читать так: из иноды директории сходи в структуру i_op и там дерни метод symlink. Дело в том, что i_ops - это под-структура, которая являет собой список указателей на функции для действий с инодами. В этом месте нам пора выбрать свою ФС и перебраться в её драйвер - более подробных особенностей реализации VFS не описывает.
Я выбрал ext4 и решил поверить докам на ядро, а там сказано следующее:
Depending on the type of file an inode describes, the 60 bytes of storage in inode.i_block can be used in different ways. In general, regular files and directories will use it for file block indexing information, and special files will use it for special purposes.
The target of a symbolic link will be stored in this field if the target string is less than 60 bytes long. Otherwise, either extents or block maps will be used to allocate data blocks to store the link target.
Иными словами, если у нас короткий путь в точку назначения(до 60 символов) - пишем в поле i_block самой иноды(которое обычно используется для перечисления привязанных блоков с данными), в противном случае - привязываем блок с данными и пишем в него.
Давайте убедимся, что всё так и попробуем почитать блок данных для имеющейся у нас симлинки(номер её иноды мы взяли выше - это 934556):
1 2 3 4 5 6 7 8 9 10 11 |
root@mixermsk:~/links# debugfs -R 'cat <934556>' /dev/vda1 debugfs 1.42.9 (4-Feb-2014) cat: Invalid argument while reading ext2 file root@mixermsk:~/links# stat sl|head -n 3 File: ‘sl’ -> ‘/dev/null’ Size: 9 Blocks: 0 IO Block: 4096 symbolic link Device: fc01h/64513d Inode: 934556 Links: 1 root@mixermsk:~/links# debugfs -R 'stat <934556>' /dev/vda1|grep link debugfs 1.42.9 (4-Feb-2014) Inode: 934556 Type: symlink Mode: 0777 Flags: 0x0 Fast_link_dest: /dev/null |
Не получилось, что логично - привязанного блока данных нет, но когда мы посмотрели в структуру иноды - увидели, что секция Fast_link_dest в теле самой иноды, которая хранит нужные нам данные.
Теперь сгенерим ссылку такой длины, чтобы её путь не влез в i_block и попробуем снова:
1 2 3 4 5 6 7 |
root@mixermsk:~/links# stat sl2|head -n 3 File: ‘sl2’ -> ‘/dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev../dev/../dev/../dev/../null’ Size: 123 Blocks: 8 IO Block: 4096 symbolic link Device: fc01h/64513d Inode: 934557 Links: 1 root@mixermsk:~/links# debugfs -R 'cat <934557>' /dev/vda1 debugfs 1.42.9 (4-Feb-2014) /dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev/../dev../dev/../dev/../dev/../null |
В этом случае ссылка оказалась записанной в блок данных. На этом, полагаю, вопрос про различия между ссылками можно считать закрытым.
Напоследок давайте посмотрим, как искать все хардлинки, ведущие к выбранной иноде. Возьмём, например, /bin/ls , по счастливой случайности у меня к нему был прибит дополнительный хардлинк.
1 2 3 4 5 6 7 8 9 |
root@mixermsk:/# stat /bin/ls File: ‘/bin/ls’ Size: 110080 Blocks: 216 IO Block: 4096 regular file Device: fc01h/64513d Inode: 871 Links: 2 Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2018-03-22 23:13:40.987385614 +0300 Modify: 2014-03-24 11:35:39.000000000 +0400 Change: 2018-03-22 23:13:40.779384159 +0300 Birth: - |
Видим, что ссылок две, а номер иноды - 871. Возьмём find, натравим его на корень и будем искать, у него как раз есть ключик на эту тему:
-samefile name
File refers to the same inode as name. When -L is in effect, this can include symbolic links.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
root@mixermsk:/# find -xdev -samefile /bin/ls ./root/.debug/bin/ls/64d095bc6589dd4bfbf1c6d62ae985385965461b/elf ./bin/ls root@mixermsk:/# stat /root/.debug/bin/ls/64d095bc6589dd4bfbf1c6d62ae985385965461b/elf File: ‘/root/.debug/bin/ls/64d095bc6589dd4bfbf1c6d62ae985385965461b/elf’ Size: 110080 Blocks: 216 IO Block: 4096 regular file Device: fc01h/64513d Inode: 871 Links: 2 Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2018-03-22 23:13:40.987385614 +0300 Modify: 2014-03-24 11:35:39.000000000 +0400 Change: 2018-03-22 23:13:40.779384159 +0300 Birth: - |
Если запустить find через strace - станет понятно, что никакой магии не происходит, он просто ходит и делает на все файлы newfstatat, и если в ответе на вызов видит такой же номер иноды - выводит имя файла на экран.
На этом у меня всё, спасибо за внимание.