выбор(2) по FIFO на macOS

Вопрос или проблема

На Linux входящая программа возвращает из select и завершается:

$ gcc -Wall -Wextra select_test.c -o select_test
$ ./select_test
чтение из конца чтения
закрытие конца записи
первое чтение вернуло 0
второе чтение вернуло 0
выбор с помощью дескриптора чтения в fdset
select вернул

На OS X select блокируется навсегда, и программа не завершается. Поведение Linux соответствует моему ожиданию и, похоже, соответствует следующему трактату из справочной страницы POSIX по select:

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

Поскольку read(2) на конце чтения fifo всегда будет возвращать EOF, я считаю, что он всегда должен считаться готовым по версии select.

Известно ли поведение macOS в этом отношении или ожидаемо? Есть ли что-то еще в этом примере, что приводит к различиям в поведении?

Дополнительно замечу, что если я уберу вызовы read, то select на macOS вернется. Это и некоторые другие эксперименты, кажется, указывают на то, что после чтения EOF из файла, он больше не будет отмечен как готовый, если позже вызывается select.

Пример программы

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define FILENAME "select_test_tmp.fifo"

int main() 
{
    pid_t pid;
    int r_fd, w_fd;
    unsigned char buffer[10];
    fd_set readfds;

    mkfifo(FILENAME, S_IRWXU);

    pid = fork();
    if (pid == -1) 
    {
        perror("fork");
        exit(1);
    }

    if (pid == 0) 
    {
        w_fd = open(FILENAME, O_WRONLY);

        if (w_fd == -1) 
        {
            perror("open");
            exit(1);
        }

        printf("закрытие конца записи\n");
        close(w_fd);
        exit(0);
    }

    r_fd = open(FILENAME, O_RDONLY);
    if (r_fd == -1) 
    {
        perror("open");
        exit(1);
    }

    printf("чтение из конца чтения\n");

    if (read(r_fd, &buffer, 10) == 0) 
    {
        printf("первое чтение вернуло 0\n");
    } 
    else 
    {
        printf("первое чтение вернуло ненулевое значение\n");
    }

    if (read(r_fd, &buffer, 10) == 0) 
    {
        printf("второе чтение вернуло 0\n");
    } 
    else 
    {
        printf("второе чтение вернуло ненулевое значение\n");
    }

    FD_ZERO(&readfds);
    FD_SET(r_fd, &readfds);

    printf("выбор с помощью дескриптора чтения в fdset\n");
    if (select(r_fd + 1, &readfds, NULL, NULL, NULL) == -1) 
    {
        perror("select");
        exit(1);
    }

    printf("select вернул\n");
    unlink(FILENAME);
    exit(0);
}

На Macintosh трубы обрабатываются так же, как сокеты, через функцию read. Это поведение вызвано тем, что вы пытаетесь read файл select_test_tmp.fifo, и он блокируется всякий раз, когда у вас пустой ввод. EOF по умолчанию записывается в трубу после каждой операции записи.

Один из способов проверить это — запустить cat select_test_tmp.fifo из командной строки. Он будет ждать, пока не получит ввод, прежде чем вернуться — если вы не остановите его первым.

Ваши гипотезы неверны.

Ожидаемое поведение таково: как только все записывающие процессы закроют конец записи fifo, который они удерживают, конец чтения предоставляет однократный вызов read, который возвращает 0 байт. Вы не должны вызывать дополнительные read после этого: конец чтения должен быть закрыт и снова открыт для этого, или перегенерирован, если это труба.

Если вы будете следовать этому правилу, вы получите согласованное поведение на всех платформах.

Вот пример программы, вдохновленный вашей, которая ведет себя правильно на обеих платформах:

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FILENAME "select_test_tmp.fifo"

int main()
{
    pid_t pid;
    int r_fd, w_fd;
    unsigned char buffer[8192];
    fd_set master_set, working_set;
    char const * message = "Привет\n";
    mkfifo(FILENAME, S_IRWXU);

    pid = fork();
    if (pid < 0) exit(1);

    if (pid == 0) {
        w_fd = open(FILENAME, O_WRONLY);
        if (w_fd == -1) exit(1);
        write(w_fd, message, strlen(message));
        close(w_fd);
        exit(0);
    }

    r_fd = open(FILENAME, O_RDONLY);
    if (r_fd < 0) exit(1);

    FD_ZERO(&master_set);
    FD_SET(r_fd, &master_set);
    int finished = 0;
    while (!finished) {
      memcpy(&working_set, &master_set, sizeof(master_set));
      int rc = select(r_fd + 1, &working_set, NULL, NULL, NULL);
      if (rc < 1) exit(1); // Нет таймаута, поэтому rc == 0 тоже является ошибкой
      for (int fd = 0; fd < r_fd +1; ++fd) {
        if (FD_ISSET(fd, &working_set)) {
          if (fd == r_fd) { // Наша fifo
            // Читаем данные и выводим их на stdout
            ssize_t nb_bytes = read(r_fd, buffer, 8192);
            if (nb_bytes < 0) {
              exit(1);
            }
            else if (0 == nb_bytes) {
              finished = 1;
            }
            else {
              write(1,buffer,nb_bytes);
            }
          }
        }
      }
    }
    unlink(FILENAME);
    exit(0);
}

Ответ или решение

Проблема работы select() с FIFO на macOS

Введение в проблему

Ваша проблема заключается в том, что вызов функции select() на FIFO (именованном канале) в macOS блокируется навсегда после того, как завершается запись в канал и происходит чтение EOF (конца файла). Это поведение отличается от ожидаемого на Linux, где программа корректно завершает работу после завершения чтения из FIFO. Давайте рассмотрим, почему происходит такое различие между платформами и как можно исправить эту ситуацию.

Фундаментальная разница в поведении

Основная причина, по которой select() блокируется в macOS, заключается в различиях в реализации обработки FIFO между Linux и macOS. По спецификации POSIX файловые дескрипторы должны считаться "готовыми" для чтения, даже если последний вызов read() вернул 0 (EOF). На практике, однако, поведение может отличаться в зависимости от операционной системы.

На macOS, после того как все дескрипторы записи были закрыты и EOF был прочитан, последующий вызов select() с дескриптором FIFO не сработает и будет ждать, в то время как Linux позволяет обрабатывать этот случай и возвращает управление.

Решение проблемы

Чтобы избежать блокировки select() в macOS, необходимо учитывать, что после чтения EOF дальнейшие попытки чтения должны обрабатываться корректно. Стандартная практика в таких сценариях состоит в том, чтобы завершать чтение из FIFO после первого получения EOF или аналогичным образом управлять логикой.

Рассмотрим исправленный пример, который показывает, как правильно обрабатывать ситуацию при помощи select() на обоих платформах:

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FILENAME "select_test_tmp.fifo"
#define BUFFER_SIZE 8192

int main() {
    pid_t pid;
    int r_fd, w_fd;
    unsigned char buffer[BUFFER_SIZE];
    fd_set master_set, working_set;
    const char *message = "Hello there\n";

    // Создаем FIFO
    mkfifo(FILENAME, S_IRWXU);

    // Создаем дочерний процесс для записи в FIFO
    pid = fork();
    if (pid < 0) exit(1);

    if (pid == 0) {
        w_fd = open(FILENAME, O_WRONLY);
        if (w_fd == -1) exit(1);
        write(w_fd, message, strlen(message));
        close(w_fd);
        exit(0);
    }

    // Открываем FIFO для чтения
    r_fd = open(FILENAME, O_RDONLY);
    if (r_fd < 0) exit(1);

    // Подготовка наборов дескрипторов для select
    FD_ZERO(&master_set);
    FD_SET(r_fd, &master_set);
    int finished = 0;

    while (!finished) {
        memcpy(&working_set, &master_set, sizeof(master_set));
        int rc = select(r_fd + 1, &working_set, NULL, NULL, NULL);
        if (rc < 1) exit(1); // Общая ошибка при select

        // Обработка события
        for (int fd = 0; fd <= r_fd; ++fd) {
            if (FD_ISSET(fd, &working_set)) {
                if (fd == r_fd) { // Наша FIFO
                    ssize_t nb_bytes = read(r_fd, buffer, BUFFER_SIZE);
                    if (nb_bytes < 0) {
                        exit(1);
                    } else if (nb_bytes == 0) {
                        finished = 1; // Закрываемся, если пришел EOF
                    } else {
                        write(1, buffer, nb_bytes); // Печатаем данные
                    }
                }
            }
        }
    }

    // Удаляем созданный FIFO
    unlink(FILENAME);
    exit(0);
}

Заключение

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

SEO-оптимизация

Ключевые слова: select(), FIFO, macOS, Linux, системные вызовы, производительность, программирование на C.

Убедитесь, что ваша программа тестировалась на нескольких платформах, чтобы обеспечить высокую степень переносимости.

Оцените материал
Добавить комментарий

Капча загружается...