Программа C в Docker: fwrite(3) и write(2) не могут изменить файлы в Windows, но не в MacOS

Я пишу гостевую ОС поверх Linux (дистрибутив Ubuntu) в контейнере Docker. Файловая система реализована в виде одного файла, находящегося внутри хост-ОС, поэтому каждый раз, когда файл изменяется в файловой системе гостевой ОС, файл в хост-ОС должен быть открыт, правильные блоки должны быть перезаписаны, а файл должен быть быть закрытым.

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

/**
 * Recursive procedure to write n bytes from buf to the
 * block specified by block_num. Also updates FAT to
 * reflect changes.
 * 
 * @param block_num identifier for block to begin writing
 * @param buf buffer to write from
 * @param n number of bytes to write
 * @param offset number of bytes to start writing from as
 *               measured from start of file
 * 
 * @returns number of bytes written
*/
int write_bytes(int block_num, const char *buf, int n, int offset) {
    BlockTuple red_tup = reduce_block_offset(block_num, offset);
    block_num = red_tup.block;
    offset = red_tup.offset;
    FILE *fp = fopen(fat->fname, "r+");

    int bytes_to_write = min(n, fat->block_size - offset);
    int write_n = max(bytes_to_write, 0);
    fseek(fp, get_block_start(block_num) + offset, SEEK_SET);
    fwrite(buf, 1, write_n, fp); // This line is returning 48 bytes written
    fclose(fp);

    // Check if there are bits remaining
    int bytes_left = n - write_n;
    if (bytes_left > 0) {
        // Recursively write on next block
        int next_block = get_free_block();
        set_fat_entry(block_num, next_block); // point block to next block
        set_fat_entry(next_block, 0xFFFF);
        return write_bytes(next_block, buf + write_n, bytes_left, max(0, offset - fat->block_size)) + write_n;
    } else {
        set_fat_entry(block_num, 0xFFFF); // mark file as terminated
        return write_n;
    }
}

Проблема в том, что fwrite(3) сообщает о записанных 48 байтах (когда n передается как 48), но шестнадцатеричный дамп файла в хост-ОС показывает, что байты не были изменены:

00000000  00 01 ff ff ff ff 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00008000

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

00000000  00 01 ff ff ff ff 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100  66 31 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |f1..............|
00000110  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000120  00 01 00 00 02 00 01 06  e7 36 75 63 00 00 00 00  |.........6uc....|
00000130  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000200  e0 53 f8 88 c0 0d 37 ca  84 1f 19 b0 6c a8 68 7b  |.S....7.....l.h{|
00000210  57 83 cf 13 f0 42 21 d3  21 e1 da de d4 8a f1 e6  |W....B!.!.......|
00000220  f0 12 98 fb 1c 30 4c 04  b3 16 1d 96 17 ba d7 5a  |.....0L........Z|
00000230  7e f3 8a f5 6a 42 6b ef  58 f6 bc 01 db 0c 02 53  |~...jBk.X......S|
00000240  e5 10 7e f3 4a d5 3f ac  8e 38 82 c3 95 f8 11 8e  |..~.J.?..8......|
00000250  a6 82 eb 3b 24 56 9a 75  44 36 8b 25 60 83 4c 04  |...;$V.uD6.%`.L.|
00000260  07 9e 14 99 9c 9f 87 3c  8a d4 c3 e8 17 60 81 0e  |.......<.....`..|
00000270  bc eb 1d 35 68 fc d5 be  4f 1c 9d 5e 72 57 65 01  |...5h...O..^rWe.|
00000280  b7 43 54 26 d6 6d ba 51  bf 12 8c a1 03 d5 66 b3  |.CT&.m.Q......f.|
00000290  90 0d 60 b8 95 8d 15 bd  53 9a 70 77 4f 7a 04 1e  |..`.....S.pwOz..|
000002a0  9e b2 4c 9a 79 dd de 48  cd fe 1e dc 57 7d d1 7f  |..L.y..H....W}..|
000002b0  3f f5 77 96 fa e7 d7 33  33 48 ce 0a 4d 61 ab 96  |?.w....33H..Ma..|
000002c0  5f c4 88 bf c6 3a 09 37  76 c4 b8 db bc 6a 7d c0  |_....:.7v....j}.|
000002d0  c4 89 68 e7 b4 70 f8 a6  a8 00 9d c4 63 da fb 66  |..h..p......c..f|
000002e0  be d2 cd 68 1c d2 ff bf  00 e9 37 ab 6b 1a 3c f2  |...h......7.k.<.|
000002f0  7b c1 a2 c4 46 ae db 93  b4 4f 64 79 14 2a 1a d4  |{...F....Ody.*..|
00000300  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00008000

48 байтов, о которых я говорю, которые не записываются, - это байты, записанные в блок каталогов, работающий с адреса 00000100-0000012E (байты ниже, которые представляют фактически записываемый файл, ошибки сегментации кода на моем конце, прежде чем достичь этого написать). Стоит отметить, что мой контейнер по-прежнему может форматировать файл файловой системы, поэтому все операции записи не прерываются. Этот фрагмент представляет собой первую запись, которая не сработала.

Мы оба запускаем код в идентичном контейнере Docker. Единственная разница, которую я мог себе представить, это то, что мой компьютер — Windows, а ее — Mac. В чем может быть проблема?

Первое, что я подумал, это то, что был какой-то конфликт с ОС хоста, который заблокировал мою запись, но назначение и печать возвращаемого значения fwrite(3) вернули, что 48 байт действительно были записаны на обеих машинах.

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

В конце концов я решил, что это какая-то проблема с интерфейсом более высокого уровня в <stdio.h>, а не с интерфейсом более низкого уровня в <unistd.h>. Я заменил fopen(3), fwrite(3), flseek(3), fclose(3) каждый их эквивалентами более низкого уровня (write(2) и т. д.), и все равно получилось 48 байтов, записанных без каких-либо фактических изменений в файлах.

Обновлено: Файловая система гостевой ОС может быть отформатирована с учетом пользовательских параметров. Все вышеперечисленные тесты проводились с размером блока 256 байт и общим количеством блоков 128. Я снова попытался выполнить ту же самую последовательность записи с размером блока 1024 байта и общим количеством блоков 16384, и ошибки не было. До сих пор неясно, почему код работает на машине моего партнера для обеих конфигураций формата, а не для моей, но это может сузить его.

Бег strace показывает следующий отрывок вокруг записи:

openat(AT_FDCWD, "minfs", O_RDWR)       = 4
newfstatat(4, "", {st_mode=S_IFREG|0777, st_size=32768, ...}, AT_EMPTY_PATH) = 0
lseek(4, 0, SEEK_SET)                   = 0
read(4, "\0\1\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 256) = 256
write(4, "f1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 48) = 48
close(4) 

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

lseek(4, 0, SEEK_SET)                   = 0
read(4, "\0\1\377\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 320) = 320
write(4, "\0", 1)                       = 1
close(4)

который должен быть по адресу 320, сразу после записи по адресу 256 выше.

Глупый вопрос, но проверили ли вы, что файл, который вы открыли, — это тот, который вы намеревались открыть, и что файл, который вы проверяете, — тот же самый? (И что вы находитесь в правильном рабочем каталоге и все такое.) И вы подтвердили, что ваш счетчик поиска соответствует вашему мнению? strace может быть полезным инструментом.

Nate Eldredge 17.11.2022 05:55

«Самое первое, во что я поверил, это какой-то конфликт с хостовой ОС, которая заблокировала мою запись»: Ну, обычно самое первое, во что вы должны поверить, это «в моем коде есть ошибка, которую я еще не нашел». Как говорится, «select не ломается», и fwrite тоже.

Nate Eldredge 17.11.2022 05:55

Чтобы доказать свои подозрения, напишите небольшие экспериментальные программы и запустите их вместо своей ОС. В какой-то момент дополнительный «уровень» WSL может ввести вас в заблуждение. -- Как говорит Нейт, такие проблемы наверняка есть в вашем коде. Это может быть какое-то неопределенное поведение, проявляющееся в различных средах хост-ОС.

the busybee 17.11.2022 08:18

@NateEldredge Хорошие моменты, и да, оба были проверены (распечатка get_block_start(block_num) + offset показывает правильное значение, и да, имена файлов были проверены). Я признаю, что формулировка была плохой, но под конфликтом я подразумеваю неправильное использование, то есть мы создали набор условий, при которых fwrite не ожидается, что он будет работать должным образом, и поэтому он вернет 0 записанных байтов. strace отличный звонок, отрывок, где нужно выполнить запись, добавлен внизу поста

wyomatthew 17.11.2022 18:38

@thebusybee Неопределенное поведение кажется беспроигрышным вариантом. Отметив, что отладка может только выявить наличие неопределенного поведения, но не доказать его отсутствие, до сих пор ни один инструмент не выявил, откуда берется неопределенное поведение. Запуск программы через valgrind на машине моего партнера вообще не показывает ошибок (нет недопустимых операций чтения/записи и т. д.), но я разработаю некоторые экспериментальные программы, как вы предлагаете.

wyomatthew 17.11.2022 18:47

Я бы сказал, что журнал strace является доказательством того, что запись выполняется в какой-то файл, и поэтому сам fwrite не виноват. Спорим на пять баксов, что окажется, что вы открыли не тот файл, который планировали. Вы действительно проверяли текущий рабочий каталог? (Ищите вызовы chdir в strace или фактически выполняйте getcwd в своем коде.) Кроме того, вы говорите, что находитесь в контейнере Docker — вы уверены, что ищете файл внутри правильного контейнера?

Nate Eldredge 18.11.2022 02:09

Например, если вы передадите достаточно флагов -v в strace, вы увидите полные результаты системного вызова newfstatat (который действительно делает fstat). Сравните номер устройства и инода с нужным файлом (ls -li).

Nate Eldredge 18.11.2022 02:18
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Оказывается, несоответствие произошло из-за неопределенного поведения, связанного с синхронизацией изменений в mmap(2). Был раздел кода, в котором область памяти, отображенная через mmap(2), была изменена, а затем сразу же последовало чтение/запись в файл на хост-ОС, содержащий отображенную область памяти. Похоже, что Mac будет записывать изменения до следующего раздела, в то время как Windows не будет синхронизироваться до тех пор, пока это не произойдет, что приведет к неопределенному поведению.

Проблема была устранена путем вызова msync(2) сразу после изменения отображаемой области из mmap(2) с флагом MS_SYNC, вызывающим сквозную запись.

Ссылки на документацию здесь: mmap(2) , msync(2).

Другие вопросы по теме