os.NewFile(0) вызывает bad file descriptor при io.Copy в тестах периодически

В коде, приведенном ниже, регулярно возникает ошибки

  • bad file descriptor
  • write /tmp/test-file: copy_file_range: bad file descriptor

Обычно они возвращаются при io.Copy()

Но если закомментировать строки, описыващие открытие файлового дескриптора с номером 0 - stdin, то все работает хорошо.

const (
    _numLevels        = 5
    _countersPerLevel = 4096
)

type counter struct {
    resetAt atomic.Int64
    counter atomic.Uint64
}

type counters [_numLevels][_countersPerLevel]counter

func newCounters() *counters {
    return &counters{}
}

func Test(t *testing.T) {
    for n := range 50 {
        t.Run(strconv.Itoa(n), func(t *testing.T) {
            // если закомментировать эти две строчки, то
            // ошибка пропадет
            outFile := os.NewFile(0, "some-file")
            require.NotNil(t, outFile)

            // или если закомментировать эту строку, то 
            // ошибка пропадет
            newCounters()

            require.NoError(t, copyFiles(t))
        })
    }
}

func copyFiles(t *testing.T) error {
    content, err := os.Open("video.webm")
    require.NoError(t, err)
    defer func() {
        require.NoError(t, content.Close())
    }()

    out := os.TempDir() + "/test-file"
    f, err := os.Create(out)
    require.NoError(t, err)

    defer func() {
        require.NoError(t, f.Close())
    }()

    _, err = io.Copy(f, content)
    require.NoError(t, err)

    require.NoError(t, f.Sync())

    return nil
}

Возникает ошибка не регулярно, поэтому есть цикл for n := range 50

Не могу понять в чем тут дело, ведь эти операции - открытие файлового дескрипотора и создание счетчика и копирование файла, вроде как не зависимы друг от друга?

Окружение

  • go: 1.24
  • os: UBUNTU 24.04.2 LTS

Ответы (2 шт):

Автор решения: Stanislav Volodarskiy

Когда outFile выходит из области видимости, соответствующий файл может быть собран сборщиком мусора. Когда сборщик удаляет объект-файл, он закрывает дескриптор файла. После этого новые вызовы os.NewFile будут завершатся ошибками.

То есть, если сборщик мусора "успеет раньше", ваш код завершиться с ошибкой. Иначе всё пройдёт без видимых проблем.

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

Ещё лучше так не делать. Как может быть польза от нескольких файлов на одном дескрипторе?

→ Ссылка
Автор решения: user7860670

"строки, описыващие открытие файлового дескриптора с номером 0" - эти строки делают не то, что вы думаете. Ничего не открывается. Создается новый экземпляр File с передачей владения указанным файловым дескриптором. При явном вызове Close, либо при финализации объекта, дескриптор будет закрыт. Соответственно все другие части программы, по-прежнему использующие дескриптор со значением 0 (хотя бы ваш же код на следующией итерации), будут иметь дело с потенциально невалидным дескриптором.

Вызов os.NewFile корректен только когда передаваемый файловый дескриптор валиден и вы монопольно им владеете. Это имеет смысл например при получении файлового дескриптора со стороны C кода.

→ Ссылка