null

Интересные программные трюки

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

SIGCHLD и состояние гонок с select()

Эту интересность я обнаружил в ssh-сервере Solaris, когда дебажил панику, возникшую у одного из наших заказчиков. Паника возникла в процессе sshd в подсистеме ядра, отвечающего за пайпы (fifofs), и естественным было желание узнать, для чего же sshd использует пайпы. В процессе поиска по xref'у исходников Solaris (теперь уже Illumos), нашелся некая пара каналов notify_pipe: http://src.illumos.org/source/xref/illumos-gate/usr/src/cmd/ssh/sshd/serverloop.c#108 с не совсем очевидным предназначением: читатель - основной поток и вызов select(), а писатель - обработчик сигнала SIGCHLD.

Причина этого - состояние гонок, которое возникает между сигналом и вызовом. Представим себе такую последовательность событий:

    Основной поток sshd проверяет флаг child_terminated
            Дочерний процесс завершается, и возникает сигнал SIGCHLD
            Обработчик сигнала SIGCHLD устанавливает сигнал child_terminated
    Основной поток sshd вызывает select() для сетевого сокета


После этого ожидание может продлится сколь угодно долго (в зависимости от активности сети и установленного в select таймаута). Такая проблема описана например здесь: http://evbergen.home.xs4all.nl/unix-signals.html

Решение, используемое разработчиками sshd просто и изящно: т.к. select принимает на входе файловые дескрипторы - надо просто что-то записать в такой дескриптор в обработчике SIGCHLD, используя пайп. Тогда последовательность будет уже другой:

    Основной поток sshd проверяет флаг child_terminated
            Дочерний процесс завершается, и возникает сигнал SIGCHLD
            Обработчик сигнала SIGCHLD устанавливает сигнал child_terminated
            Обработчик сигнала SIGCHLD записывает в пайп байт
    Основной поток sshd вызывает select() для сетевого сокета и пайпа
        select() немедленно завершается, т.к. в пайпе есть готовый для чтения байт

Самый простой механизм IPC

Полагаю, истерия вокруг обновления временных зон в России коснулась и вас. Solaris 10 начиная с версии Update 9 позволяет избежать перезагрузки (если вы не меняли временную зону), просто запустив команду /usr/sbin/tzreload. Естественно, я заинтересовался как она работает.

По идее эта команда должна как-то уведомлять все процессы в системе - для этого можно предусмотреть отдельный сигнал или завести семафор Unix c общей областью памяти. Оба этих подхода однако черезчур сложны. И все оказалось гораздо прозаичнее: tzreload всего лишь пишет 4 байта в файл /var/run/tzsynс (это касается только Solaris 10, в Solaris 11 ее вызов без опций перезагружает сервис svc:/system/timezone:default):
# od -c /var/run/tzsync
0000000  \0  \0  \0 001  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
...
# /usr/sbin/tzreload
# od -c /var/run/tzsync
0000000  \0  \0  \0 002  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
...


Но ведь этот байт как-то должен перечитаться процессом, а для этого надо файл открыть, вызвать read() - подумал я. Реализация libc в Solaris куда более изящна: файл tzsync просто всегда отображается в пространство пользовательского процесса:
# truss -t open,mmap date
...
open("/var/run/tzsync", O_RDONLY)               = 3
mmap(0x00000000, 4, PROT_READ, MAP_SHARED, 3, 0) = 0xFF380000


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

    #define RELOAD_INFO()   (zoneinfo_seqno != *zoneinfo_seqadr)

http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/localtime.c#324

Слева zoneinfo_seqno - число, сохраненное внутри libc, а zoneinfo_seqadr - указатель на отображенное из tzsync значение. Соответственно, когда второе меняется извне (вызов tzreload), условие срабатывает и приложение перечитывает файлы временных зон.

Этот подход требует всего лишь одной страницы оперативной памяти (8к на SPARC и 4k на x86), зато какой эффект!

Самая большая хеш таблица

Эта находка появилась в результате обсуждения на ГовноКоде: http://govnokod.ru/16854#comment251372

Производительность поиска по ключу в хеш таблице зависит от количества букетов (buckets), которая она имеет - чем меньше букетов, тем больше вероятность коллизии и необходимости прохода связного списка. А что если сделать длину хеш-ключа - 32 бита? На первый взгляд тут есть следующие нюансы:

  • В самом простом случае нам нужно оперативной 32 Гб памяти (на 64-х битной системе, 32-х битные для таких объемов рассматривать бессмысленно).
  • Однако, современные системы обычно выделяют память лениво, можно сделать mmap на 32 гига указателей, а потом его заполнить. В худшем случае это потребует N * PAGESIZE + C, где C - некий предварительно выделяемый объем памяти, а N - количество записей в хеш таблице.
  • Однако, если запросы к хеш таблице приходят извне, любое обращение к несуществующему в таблице элементу возможно приведет к страничному сбою и выделению новой страницы. Со временем 32 Гб должны все равно быть потреблены, что означает потенциальный DoS системы.


Мне стало интересно, как быстро будет кончаться память в таком случае и я написал небольшой модуль к нашему нагрузчику TSLoad --> ссылка. После его запуска на тестовой системе с Linux я отправился обедать, и, каково же было мое удивление, когда после возвращения, система продолжала работать, а количество выделенной процессом нагрузчика памяти практически не изменилось.

Поковырявшись в исходниках Linux, я обнаружил такую функцию:

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
        unsigned long address, pte_t *page_table, pmd_t *pmd,
        unsigned int flags)
{
...

    /* Use the zero-page for reads */
    if (!(flags & FAULT_FLAG_WRITE)) {
        entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),
                        vma->vm_page_prot));
        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (!pte_none(*page_table))
            goto unlock;
        goto setpte;
    }



Для всех неверных обращений к нашей большой хеш таблице будет использоваться одна и та же страница. Все, что нужно - это большой swap-файл, чтобы Linux позволил выполнить mmap(). Интересно, можно ли обратиться в Книгу Рекордов Гиннеса с самой большой хеш таблицей?

Надо сказать этот пример показывает и то, насколько недооценены возможности виртуальной памяти. Oracle например в своих новых процесорах SPARC M7 добавил функцию VA Masking, позволяющую использовать часть виртуального адреса для хранения метаданных: https://blogs.oracle.com/rajadurai/entry/sparc_m7_chip_32_cores

Модуль для нагрузчика TSLoad: jt.tar.gz

К списку статей

 

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

Основная сфера моей деятельности связана с поддержкой Solaris и оборудования Sun/Oracle, хотя в последнее время к ним прибавились технологии виртуализации (линейка Citrix Xen) и всякое разное от IBM - от xSeries до Power. Учусь на кафедре Вычислительной Техники НИУ ИТМО.

See you...out there!

http://www.facebook.com/profile.php?id=100001947776045
https://twitter.com/AnnoyingBugs