Показаны сообщения с ярлыком unix. Показать все сообщения
Показаны сообщения с ярлыком unix. Показать все сообщения

25 нояб. 2015 г.

Файловые блокировки в Linux

Advisory locking


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

У всех блокировок есть два режима: exclusive и shared.

BSD locks (flock)

Самый простой вид блокировок это flock(2). Особенности:
  • входят в POSIX;
  • устанавливают блокировку на файл целиком;
  • привязаны к файловому дескриптору;
  • не обеспечивают атомарное изменение режима блокировки;
  • не поддерживают NFS (в Linux).
Эти блокировки ассоциируются с файловым дескриптором:
  • дубликаты файловых дескриторов созданные с помощью dup(2) ссылаются на одну и ту же блокировку;
  • независимые файловые дескпторы, полученные для одного и того же или разных файлов с помощью open(2) ссылаются на разные блокировки.
Это, в частности, позволяет избежать проблем с потоками (см. следующую секцию).

Однако, flock() не гарантирует атомарного переключения между режимами exclusive и shared. При изменении режима блокировки, flock() сначала отпускает ее, а затем ожидает возможности захватить ее с новым режимом.

POSIX record locks (fcntl)

Более сложный (и медленный) вид блокировок это POSIX record locks (см. секцию "Advisory record locking" в fcntl(2)). Особенности:
  • входят в POSIX;
  • устанавливают блокировку на заданный интервал байт в файле;
  • привязаны к паре [i-node, pid], а не файловому дескриптору;
  • обеспечивают атомарное изменение режима блокировки;
  • поддерживают NFS (в Linux).
 В POSIX также входит функция lockf(3). В Linux она является оберткой над POSIX record locks.

Эти блокировки ассоциируются с парой [i-node, pid]:
  • любые (даже несвязанные) файловые дескрипторы, отктрытые для одного и того же файла одним и тем же процессом, ссылаются на одну и ту же блокировку;
  • как следствие, все потоки одного процесса также разделяют одну и ту же блокировку для каждого файла, т.к. имеют одинаковый pid;
  • вызов close() для любого из открытых для файла дескрипторов снимает блокировку с файла, не зависимо от того, через какой дескриптор она была установленна и остаются ли у процесса другие открытые дескрипторы для этого файла.
Эта особенность затрудняет использование record locks при написании библиотек (т.е. при отсутсвии контроля над всем приложением) и в многопоточной среде.

Если необходима гарантия атомарного переключения блокировки между режимами exclusive и shared, есть только два варианта:
  • использовать open file description locks, которые доступны в современных ядрах Linux;
  • использовать record locks и избежать открытия процессом одного и того же файла блокировки через несколько файловых дескрипторов.

Если требуется атомарное переключение режима блокировки, нет возможности использовать open file description locks, и блокировки нужны для синхронизации не только процессов, но и потоков, можно сделать следующее:
  • централизованно хранить все дескрипторы файлов блокировок, отрытые процессом, и не допускать открытия нескольких дескрипторов для одного файла;
  • ассоциировать с каждым дескриптором rw-mutex, например pthread_rwlock, и счетчик количества потоков, захвативших блокировку одновременно (он будет больше единицы только при захвате в shared mode);
  • для захвата блокировки сначала захватывать мьютекс и увеличивать счетчик; и только если счетчик был нулевой, захватывать и файловую блокировку;
  • для освобождения блокировки уменьшать счетчик и только если он стал нулевым, освобождать и файловую блокировку; затем освобождать мьютекс.

Open file description locks (fcntl)

Этот вид блокировок сочетает в себе достоинства BSD locks и record locks. (см. секцию "Open file description locks (non-POSIX)" в fcntl(2)). Особенности:
  • linux-specific; доступны начиная с ядра 3.15;
  • устанавливают блокировку на заданный интервал байт в файле;
  • привязаны к файловому дескриптору;
  • обеспечивают атомарное изменение режима блокировки.
Другими словами, эти блокировки сочетают в себе достоинства BSD locks (привязка к файловому дескриптору) и POSIX record locks (блокировка интервала, атомарное переключение режима).

Mandatory locking


В Linux также есть ограниченная поддержака обязательных блокировок:
  • обязательные блокировки задействуются только если они включены при монтировании раздела и для блокировки используются POSIX record locks;
  • после захвата exclusive или shared блокировки любые системные вызовы, изменяющие файл (например write(), truncate()) будут блокированы до тех пор, пока блокировка не будет отпущена;
  • после захвата exclusive блокировки, также буду блокироваться любые системные вызовы, читающие из файла.
Однако, реализация в Linux считается ненадежной, см. сецию "Mandatory locking" в fcntl(2):
  • возможны гонки при одновременном захвате блокировки и параллельном вызове write() или read();
  • возможны гонки при одновременном использовании с mmap().
Также, обязательные блокировки не решают проблемы, описанные в секции "Удаление и переименование" ниже.

Замечания

 

Обработка падений

Приятной особенностью всех видов блокировок (и flock() и fcntl()) является то, что блокировка автоматически снимается при завершении процесса, в том числе аварийном.

Другими словами, наличие блокировки гарантирует, что существует процесс, удерживающий ее.

Удаление и переименование

Ни рекоммендательные, ни обязательные блокировки не учитываются, когда выполняются вызовы unlink() или rename().

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

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

Производительность

Если имеет значение производительность, вместо файловых блокировок можно использовать POSIX semaphores. В glibc они реализованы поверх futex-ов (однако имеют оверхед в одну страницу памяти на семафор).

Ссылки

18 июл. 2015 г.

Unix: Обработка отсутствия читателей FIFO, потоко-безопасная обработка SIGPIPE

Ссылки

 

Открытие при отсутствии читателей


open(O_RDWR) 
open() не блокируется и сразу открывает FIFO, так как вызывающий процесс становится читателем.

open(O_WRONLY | O_NONBLOCK)
open() не блокируется и сразу возвращает ошибку ENXIO.

open(O_WRONLY)
open() блокируется до тех пор, пока не появится читатель.

 

Запись при отсутствии читателей (обработка SIGPIPE)


write() в FIFO без читателей генерирует SIGPIPE независимо от флага O_NONBLOCK. Обработка SIGPIPE может вызвать определенные затруднения. Ниже перечислены решения проблемы.

1. Использовать сокеты вместо FIFO


Если использовать сокеты (например UNIX domain sockets), то генерацию SIGPIPE можно отключить, если:
  • вместо write() использовать send(), и
  • передавать в send() флаг MSG_NOSIGNAL (Linux) или установить в setsockopt() опцию SO_NOSIGPIPE (BSD).

Замечание: опция SO_NOSIGPIPE работает для send(), но не для write().

2. Использовать режим открытия O_RDWR


При использовании флага O_RDWR, SIGPIPE никогда не будет генерироваться.

В этом случае пропадает возможность получить уведомление о завершении читателя.

3. Игнорировать или перехватывать SIGPIPE


Если SIGPIPE игнорируется или перехватывается, write() будет возвращать ошибку EPIPE.

В этом случае возникает проблема потоко-безопасности. Т.к. игнорирование или перехват сигналов устанавливаются только для всего процесса, нет потоко-безопасного способа временно изменить диспозицию сигнала.

4. Использовать select()/poll()


Если вместо блокирующего write() использовать select() или poll(), то об отсутствии читателей можно узнать, не вызывая write():
  • select() добавит дескриптор в exceptfds;
  • poll() взведет для дескриптора флаг POLLERR.

В этом случае можно не вызывать write() и избежать генерации SIGPIPE. Такой подход может работать, однако здесь присутствует гонка: читатель может завершиться за небольшой промежуток времени между вызовами select()/poll() и write(), и тогда SIGPIPE все равно будет сгенерирован.

5. Использовать синхронную обработку сигналов


Наконец, существует потоко-безопасный способ обработать SIGPIPE, используя функции для синхронной обработки сигналов. Этот способ требует больше всего кода. Он подробно описан здесь.

Схема работы следующая:
  1. Перед вызовом write() блокируем SIGPIPE для текущего потока с помощью pthread_sigmask(). Нужно заметить, что ядро всегда доставляет SIGPIPE тому потоку, который вызвал write (см. signal(7)).
  2. После блокировки сигнала с помощью sigpending() проверяем, не ожидает ли SIGPIPE обработки еще до вызова write().
  3. Вызываем write(). Если читатель отсутствует, SIGPIPE будет добавлен в маску сигналов, ожидающих обработки, для текущего потока, а write() вернет ошибку EPIPE.
  4. Если write() вернул EPIPE, удаляем сигнал с помощью неблокирующего вызов sigtimedwait(), но только если сигнал не ожидал обработки до вызова write() (в противном случае наш код "съедал" бы чужой сигнал, т.к. один и тот же сигнал, пришедший несколько раз, не ставится в очередь, а доставляется только один раз).
  5. Восстанавливаем изначальную маску сигналов с помощью pthread_sigmask().
Пример реализации

#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <sys/signal.h>

ssize_t safe_write(int fd, const void* buf, size_t bufsz)
{
    sigset_t sig_block, sig_restore, sig_pending;

    sigemptyset(&sig_block);
    sigaddset(&sig_block, SIGPIPE);

    /* Block SIGPIPE for this thread.
     *
     * This works since kernel sends SIGPIPE to the thread that called write(),
     * not to the whole process.
     */
    if (pthread_sigmask(SIG_BLOCK, &sig_block, &sig_restore) != 0) {
        return -1;
    }

    /* Check if SIGPIPE is already pending.
     */
    int sigpipe_pending = -1;
    if (sigpending(&sig_pending) != -1) {
        sigpipe_pending = sigismember(&sig_pending, SIGPIPE);
    }

    if (sigpipe_pending == -1) {
        pthread_sigmask(SIG_SETMASK, &sig_restore, NULL);
        return -1;
    }

    ssize_t ret;
    while ((ret = write(fd, buf, bufsz)) == -1) {
        if (errno != EINTR)
            break;
    }

    /* Fetch generated SIGPIPE if write() failed with EPIPE.
     *
     * However, if SIGPIPE was already pending before calling write(), it was also
     * generated and blocked by caller, and caller may expect that it can fetch it
     * later. Since signals are not queued, we don't fetch it in this case.
     */
    if (ret == -1 && errno == EPIPE && sigpipe_pending == 0) {
        struct timespec ts;
        ts.tv_sec = 0;
        ts.tv_nsec = 0;

        int sig;
        while ((sig = sigtimedwait(&sig_block, 0, &ts)) == -1) {
            if (errno != EINTR)
                break;
        }
    }

    pthread_sigmask(SIG_SETMASK, &sig_restore, NULL);
    return ret;
}

7 июл. 2013 г.

Unix: job control в подсистеме терминалов

Ссылки


Что такое терминал?


Ядро предоставляет программам абстракцию терминала, которая включает в себя:
  • Ввод/вывод
    Терминал является обычным файлом. Простым программам достаточно уметь читать и записывать поток байтов.

  • Line discipline
    Терминал имеет несколько режимов обработки вводимых символов, которые называются line discipline.

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

    Как работает этот режим можно увидеть, набрав команду cat.

  • Job control
    Терминал хранит информацию о группах процессов, которые к нему привязаны, а процессы хранят информацию о своем терминале.

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

    Job control вступает в силу, когда вы запускаете фоновую задачу, вроде gedit &, нажимаете ^C или ^Z, либо используете команды на подобие fg, bg и kill %1.

Механизм vs политика


Терминал – это один из редких примеров, когда в ядре UNIX реализована политика, а не механизм. Это обусловлено тем, что терминал позволяет незаметно для программы заменить взаимодействие с другой программой на взаимодействие с пользователем {1}.

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

Вот что получается в результате:
  1. Ядро реализует простой режим редактирования для взаимодействия с простыми программами; сложные программы, вроде vim, readline и curses, отключают его и реализуют обработку и отображение символов сами.
  2. Ядро предоставляет системные вызовы для управления группами процессов и реализует взаимодействие с этими группами драйвера терминала и line descipline с помощью сигналов; оболочки, вроде bash и zsh, создают группы процессов и предоставляют команды для переключения между этими группами.
Благодаря этому пользователю одинаково просто работать с любыми текстово-ориентированными программами, а для большинства программ взаимодействие с пользователем прозрачно.

В этом посте рассматривается второй пункт.

Параметры процессов и терминалов


Параметры процесса


Каждый процесс имеет набор параметров, которые используются для реализации job control.
  • Controlling tty
    Терминал, к которому привязан процесс.

  • pid (process id)
    Идентификатор процесса.

  • pgid (process group id)
    Идентификатор группы процессов. Равен pid лидера группы процессов (первый процесс в группе).

    Для каждого нового pipeline шелл создает новую группу и все процессы и их потомки, запущенные в этом pipeline, принадлежат этой группе. Вместе эти процессы называются "задачей" ("job").

    Группы процессов нужны для того, чтобы можно было выделить "активную" группу процессов ("foreground process group") для данного терминала.

  • sid (session id)
    Идентификатор сессии. Равен pid лидера сессии (первый процесс в сессии).

    Для каждого нового входа в систему програма login(1), которую обычно вызывает getty, создает новую сессию; шелл и его потомки принадлежат этой сессии.

    Сессии нужны для того, чтобы можно было найти все процесы, использующие данный терминал. У каждого терминала есть не более одной ассоциированной с ним сессии, она называется "controlling process group".

Параметры терминала


Каждый терминал ссылается на сессию, связанную с ним, и активную группу процессов.
  • Foreground process group
    Идентификатор pgid группы процессов, которая в данный момент активна.

    Процессам из активной группы разрешено читать и писать в терминал. Если процесс пытается читать или писать из своего управляющего терминала, но при этом не находится в активной группе процессов, драйвер посылает ему сигналы SIGTTIN (при чтении) и SIGTTOU (при записи, если включен флаг TOSTOP у терминала {2}. По-умолчанию они приостанавливают процесс до тех пор, пока кто-нибудь, например команда fg, не пошлет ему сигнал SIGCONT.

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

    NB: При вводе/выводе в терминал, который не является управляющим терминалом процесса, никакие сигналы не генерируются, но для этого обычно требуются права root.

    При нажатии специальных клавиш драйвер line discipline посылает сигналы процессам из активной группы:
    • SIGINT (^C)
    • SIGQUIT (^\)
    • SIGTSTP (^Z)

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

  • Controlling process group
    Идентификатор sid сессии, ассоциированной с данным терминалом.

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

Примеры


Вот примеры команд, в которых все процессы будут помещены в одну группу:
$ ls
$ find / | wc -l   # При нажатии ^C или ^Z сигнал будет послан обоим процессам.
А в этом примере будет создано две группы процессов:
$ vim & cat -n
В первой группе – vim, который будет приостановлен сигналом SIGTTIN до тех пор, пока пользватель не наберет fg. Во второй группе – cat -n, эта группа станет активной группой терминала. Оба процесса будут находится в одной и той же сессии.

Основные функции


Создание группы процессов


Текущий процесс можно переместить в новую группу вызвовом setpgrp() или setpgid(0, 0). После успешного завершения вызова, pgid процесса становится равным его pid; процесс становится лидером новой группы.

Создание сессии


Текущий процесс можно переместить в новую сессию вызовом setsid(). После успешного завершения вызова, sid и pgid процесса становятся раным его pid; процесс становится лидером новой сессии и новой группы и не имеет управляющего терминала.

Изначально процесс не должен быть лидером группы, в противном случае сгенерируется ошибка EPERM. NB: первая команда в pipeline-е обычно является лидером группы.

Вот пример запуска программы в отдельной сессии:
if (fork() == 0) {
  // Здесть гарантированно pgid != pid.
  if (setsid() == -1) {
    perror("setsid");
    exit(1);
  }

  execlp("bash", "bash", NULL);
}
Запустив этот код, вы можете увидеть следующее сообщение:
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
Сообщение означает, что сессия, в которой был запущен bash, не является controling process group для текущего терминала, поэтому полноценное управление процессами будет недоступно.

Если теперь набрать команду cat, а затем нажать ^C, убиты будут и cat, и bash, так как bash не создавал для cat отедльную группу.

Скорее всего вы увидели эту ошибку, так как программа была запущена из шелла, а значит controlling process group терминала уже ссылается на другую сессию, в которую входит этот шелл.

Чтобы этого не произошло, нужно чтобы controlling process group терминала был пуст, что справедливо, когда нет процессов, ассоциированных с данным терминалом. Например, это так в тот момент, когда login(1) запускает bash.

Управление группами, связанными с терминалом


С помощью функций tcgetsid() и tcgetpgrp() можно узнать, соотвественно, controlling process group и foreground process group управляющего терминала текущего процесса.

С помощью функции tcsetpgrp() можно установить foreground process group управляющего терминала текущего процесса.

Функции для установки controlling process group нет. Вместо этого этот параметр автоматически обновляется при возникновении одного из событий во время установки управляющего терминала какого-нибудь процесса (см. следующий раздел):
  • Нет ни одного процесса, управляющий терминал которого равен данному (то есть controlling process group терминала пуст) и процесс устанавливает свой управляющий терминал равным данному.
  • Процесс захватывает терминал, отбирая его у текущей controlling process group.

В обоих случаях controlling process group становится раным sid процесса.

Управляющий терминал процесса

  • Процесс может установить свой управляющий терминал следующими способами:
    • С помощью open()
      Если процесс открывает файл устройства терминала вызовом open() без флага O_NOCTTY и у процесса нет управляющего терминала, этот терминал становится его управляющим терминалом.

    • С помощью ioctl(TIOCSCTTY)
      Если процесс является лидером сессии и не имеет управляющего терминала, он может установить управляющий терминал вызовом ioctl(fd, TIOCSCTTY, arg).

      Если терминал уже имеет непустой controlling process group:
      • вызов ioctl(fd, TIOCSCTTY, 0) (arg != 1) вернет ошибку EPERM;
      • вызов ioctl(fd, TIOCSCTTY, 1) (arg = 1) захватит терминал, пошлет всем процессам из текущей controlling process group сигналы SIGHUP и SIGCONT и заменит ее на sid вызывающего процесса (для этого нужны права root).

      В последнем случае процессы, которые не умрут от SIGHUP, смогут по-прежнему читать и писать в терминал, но не будут участвовать в job control, то есть:
      • им больше не будут доставляться сигналы, связанные с терминалом;
      • они смогут хаотично захватывать введенные символы и замусоривать вывод;
      • им не будут доступны функции вроде tcgetsid() и tcgetpgrp().

  • Процесс может отказаться от управляющего терминала следующими способами:
    • С помощью setsid()
    • С помощью ioctl(TIOCNOTTY)

Примечания


{1} К статье The TTY demystified есть интересный комментарий о причинах реализации line discipline в ядре:
It's not to keep application simple that the line discipline is in the kernel. The unix philosophy is quite the contrary, to keep the KERNEL simple, and deport to user space the handing of complexities that they don't want inside the kernel. (See for example interrupted system calls and signal handling, but that's another topic).

The reason why the line discipline is inside the kernel, is to avoid context switches at the reception of each character (which in the early times of small core memories, would imply swap-outs and swap-ins!). So the line discipline keeps in a kernel buffer a line of input, and since it's simple enough to test for a specific byte and decrement a counter to implement the backspace "editing" (and a few other simple editing functions), it's done there.

The alternative, is to use the raw mode, where the characters are forwarded to the application as soon as they're received, which is needed for more sophisticated editors, like the (at the time) famously known Eight Megabytes And Constantly Swapping editor (emacs). And indeed, since emacs had to use this raw mode, which implies a context switch at the reception of each character typed, it was constantly swapping when the computers hadn't enough memory to keep emacs and all the other programs in core.
{2} Флаг TOSTOP можно включить командой stty tostop.

16 дек. 2012 г.

Unix domain sockets: гарантированное освобождение и повторное использование сокета

Ссылки

Для связи процессов на одном компьютере можно использовать доменные сокеты (IPC-сокеты).

Отличия от интернет-сокетов:
  • Более высокая производительность.
  • Доставка SIGPIPE сразу после обрыва соединения с другого конца.
  • Стандартные права доступа unix у файлов сокетов.
См. также здесь.

Производительность можно измерить с помощью netperf:

$ netperf -t TCP_STREAM      # TCP
$ netperf -t STREAM_STREAM   # unix domain (компилировать netperf с опцией --enable-unixdomain)

Проблема

Файл сокета создается при вызове bind(2). Если файл уже существует, возвращается ошибка EADDRINUSE.

Единственный способ повторного использования файла сокета - вызов unlink(2) перед bind.

Есть две схемы:
  • Вызывать unlink перед завершением сервера. Тогда нет гарантии, что при крахе сервера не останется висячего файла, который пользователю придется удалять вручную.
  • Вызывать unlink при запуске сервера. Тогда нет гарантии, что не будет удален файл другого запущенного экземпляра сервера.

Решение

Именованные доменные сокеты бывают двух типов: filesystem sockets и abstract namespace sockets, см. unix(7).

Filesystem sockets

Простое решение - использовать дополнительный lock-файл для каждого сокета.

Этот файл может быть заблокирован тогда и только тогда, когда в данный момент запущен другой экземпляр сервера. Если блокировку удалось захватить, можно безопасно вызывать unlink() для файла сокета.


 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
const char *socket_path = "/tmp/server-socket";

int server = socket(PF_LOCAL, SOCK_STREAM, 0);
assert(server != -1);

struct sockaddr_un server_addr;
bzero(&server_addr, sizeof(server_addr));

server_addr.sun_family = AF_LOCAL;
strcpy(server_addr.sun_path, socket_path);

char lock_path[256];
sprintf(lock_path, "%s.lock", socket_path);

//
// Open lock file
//
int lock_fd = open(lock_path, O_RDONLY | O_CREAT, 0600);
if (lock_fd == -1) {
    printf("can't open lock file\n");
    exit(1);
}

//
// Acquire lock
//
int ret = flock(lock_fd, LOCK_EX | LOCK_NB);
if (ret != 0) {
    printf("address already in use!\n");
    exit(1);
}

//
// Remove socket file
//
unlink(socket_path);

//
// Create socket file
//
ret = bind(server, (struct sockaddr *)&server_addr, sizeof(server_addr));
assert(ret == 0);

Abstract namespace sockets

Еще более простое решение - использовать abstract namespace (поддерживается только в Linux).

Если в  структуре sockaddr_un в поле sun_path в первый байт имени сокета записать '\0', сокет с этим именем будет создан не в файловой системе, а абстрактном пространстве имен.

Такой сокет будет автоматически освобожден при завершении приложения.