Ориентация главы
Средняя глава о файлах разбирает три высшие способности. Отображение в память (xrtFileMap): файл проецируется в адресное пространство, и указатель есть содержимое файла — произвольный доступ к большим файлам без копирований и системных вызовов. Файловые блокировки (xrtFileLock на весь файл + xrtFileLockRange на диапазон): режимы совместный/монопольный, согласующие чтение и запись нескольких процессов в один файл — две ступени зернистости, от «взаимоисключения на весь файл» до «тонкого зерна байтовых диапазонов». Атомарная доставка (xrtFileTemp + переименование): временный файл записан + атомарное переименование — читатель всегда видит целый файл, состояние «наполовину записано» исчезает структурно. Каждая из трёх решает одну настоящую проблему параллельности или производительности.
46.1引入
Три сцены. Сцена первая: файлу индексов в 2 ГБ нужен произвольный доступ — цикл Read/Seek делает каждое чтение системным вызовом (ядро копирует в пользовательский буфер), и на горячем пути с миллионом обращений расходы на syscall становятся главной статьёй; отобразив файл в память, вы обращаетесь обычным разыменованием указателя — ядро подгружает страницы лениво, ноль копий. Сцена вторая: два процесса обновляют один файл данных — без согласования они перезаписывают друг друга; блокировка на весь файл слишком груба (в сценариях «много чтения, мало записи» читатели полностью сериализуются), а диапазонные блокировки дают читателям совместный доступ, писателю — монопольный, и непересекающиеся диапазоны работают параллельно. Сцена третья: во время обновления файла конфигурации читатель как раз читает — он видит наполовину записанное, и разбор конфигурации рушится; запись во временный файл с последующим атомарным переименованием даёт читателю либо старый, либо новый файл — промежуточное состояние не увидит никто.
46.2概念
Отображение в память: указатель есть файл (арена произвольного доступа)
Выгода отображения расщепляется по режиму доступа: произвольный доступ — каждое обращение это страничный сбой (первый раз) или чтение памяти (дальше); после оседания горячей зоны скорость приближается к памяти; последовательный поток — у отображения нет преимущества предзагрузки, поблочный Read проще и управляемее. Так что «отображение быстрее» — полуправда: быстро для произвольного, без преимущества для последовательного; выбор начинается с режима доступа (продолжение трёх хозяев главы 45).
Диапазонные блокировки: согласование с зерном до байта
Две ступени зернистости: xrtFileLock(句柄, 共享/独占, 是否等待) (дескриптор, совместный/монопольный, ждать ли) — блокировка на весь файл (стандартная поза стража единственного экземпляра и взаимоисключения очередей задач); xrtFileLockRange(句柄, 模式, 偏移, 长度, 是否等待) (дескриптор, режим, смещение, длина, ждать ли) — блокировка байтового диапазона (согласование на уровне записей); Unlock/UnlockRange освобождают зеркально, и параметры снятия диапазонной блокировки обязаны в точности совпадать с параметрами установки. Выбор между ступенями смотрит на форму данных: целофайловая блокировка подходит взаимоисключению «файл как единица» (классическое употребление стража единственного экземпляра: держится, пока процесс жив, и освобождается при выходе); диапазонная — параллелизму на уровне записей (читатель берёт совместную блокировку на диапазон A, писатель — монопольную на диапазон B; без пересечения работа идёт параллельно). Дисциплина семантики блокировок: блокировка — протокол согласования, а не магия — смысл она имеет, только когда все стороны доступа соблюдают один протокол (процесс, пишущий без блокировки, не защищён ничьей блокировкой); диапазон блокировки выравнивается с границами структуры данных (блокировки на уровне записей — по диапазонам записей).
Атомарная доставка: временный файл + атомарное переименование (уничтожитель промежуточных состояний)
Промышленная полная форма «трёхшагового завершения» главы 45: (1) запись во временный файл (в том же каталоге — переименование через файловые системы провалится); (2) Flush-семантика гарантирует попадание данных на диск; (3) атомарное переименование в окончательное имя — rename внутри одной файловой системы атомарен, и читатель в любой момент видит целый файл. Помимо образца lock, образцы temp и dir_temp демонстрируют согласование именования и зачистки временных файлов (xrtFileTemp создаёт временный файл с уникальным именем (каталог/префикс параметризуются), по завершении он удаляется или переименовывается).
Комбинации тройки (смешивать по сценарию)
Отображение плюс блокировки: пишущая сторона отображения согласуется диапазонными блокировками — каждый писатель блокирует свой диапазон записей и пишет через отображение. Атомарная доставка плюс блокировки: часто обновляемые файлы используют «версионный файл + атомарное переключение» (записали временный v2, rename на главное имя), не держа блокировку долго. Тройка не предписывает обязательных сочетаний — берите по сценариям параллельности, и именно поэтому каждая из них живёт в собственном разделе.
46.3示例
Полная программа: чтение через отображение
Из образца репозитория examples/file/map/main.c:
#include <stdio.h> #include <xrt.h> */ /* 展示使用只读映射检查文件内容。 */ int main(void) { static const char sPath[] = "xrt-file-map-example.tmp"; xfile File; xfilemap Map; int iResult = 1; File = xrtOpen(sPath, XFILE_READ | XFILE_WRITE | XFILE_CREATE | XFILE_TRUNCATE); if ( (File == NULL) || !xrtWriteFull(File, "mapped", 6u, NULL) ) { (void)xrtClose(File); return 1; } Map = xrtFileMap(File, 0u, 0u, XFILE_MAP_READ); if ( Map != NULL ) { printf("%.*s\n", (int)xrtFileMapSize(Map), (const char*)xrtFileMapData(Map)); if ( xrtFileUnmap(Map) && xrtClose(File) && xrtFileDelete(sPath) ) { iResult = 0; } } return iResult; }
Что только что произошло. (1) Файл сперва записывается, затем отображается — параметры (file, offset=0, size=0, READ) с size 0 означают «отобразить до конца файла»; отображение не создаёт файл, оно отображает текущие байты файла. (2) После установления отображения содержимое читается через указатель и выводится mapped — это чтение обошлось без вызова Read, чистый доступ к памяти. (3) xrtFileMapData/xrtFileMapSize дают указатель и длину представления, xrtFileUnmap снимает отображение — дисциплина порядка: сперва Unmap, потом Close (порядок не обратим). Дополнительный дивиденд отображения только для чтения: несколько процессов делят одни и те же физические страницы — способ загрузки словарей/индексов/ресурсных файлов. Записи отображения с флагом записи синхронизируются в файл — грязные страницы ядро сбрасывает по своему выбору; явная потребность в долговечности идёт с flush-семантикой.
Полная программа: файловые блокировки
Из examples/file/lock/main.c:
#include <stdio.h> #include <xrt.h> */ /* 展示对整个文件使用非阻塞排他锁。 */ int main(void) { static const char sPath[] = "xrt-file-lock-example.tmp"; xfile File = xrtOpen(sPath, XFILE_READ | XFILE_WRITE | XFILE_CREATE); if ( File == NULL ) { return 1; } if ( !xrtFileLock(File, XFILE_LOCK_EXCLUSIVE, false) ) { (void)xrtClose(File); return 1; } printf("locked\n"); if ( !xrtFileUnlock(File) || !xrtClose(File) ) { return 1; } return xrtFileDelete(sPath) ? 0 : 1; }
Что только что произошло. (1) На целевой диапазон берётся монопольная блокировка — при успехе печатается locked; пока она держится, запросы других процессов на тот же диапазон будут ждать или отказывать (по флагу). (2) Образец — однопроцессная демонстрация формы API; настоящее межпроцессное согласование требует, чтобы все участники ходили через протокол блокировок, — практическая сторона самого понятия «протокол согласования». (3) Unlock освобождает — жизненный цикл блокировки можно повесить и на закрытие дескриптора (автоосвобождение при выходе процесса), но явный Unlock — дисциплинарное написание. Образцы link и report оставлены продвинутым чтением: операции с симлинками и отчёт о состоянии файла (размер/времена/атрибуты) принадлежат семейству файловых метаданных и дополняют тройку этой главы.
46.4契约
- Дисциплина отображения: сперва установить, потом пользоваться; сперва Unmap, потом Close; произвольный доступ — его арена, последовательный поток — Read.
- Протокол блокировок: защита существует, только когда все стороны соблюдают один протокол; диапазоны блокировок выровнены с границами данных; явный Unlock.
- Атомарная доставка: временный файл в том же каталоге, после записи — на диск, rename переключает атомарно; состояние «наполовину записано» исчезает структурно.
- Временные файлы: уникальное имя (
xrtFileTemp); на пути успеха — переименование, на пути отказа — удаление; ни один финал не оставляет мусора. - Свобода комбинаций: тройка сочетается по сценариям параллельности; обязательных пар нет.
От примеров к инженерии: три хозяина тройки
Загрузка индексов и ресурсов (арена отображения): словари, большие файлы конфигурации только для чтения, индексы баз данных — отображение на старте, доступ только для чтения на всём пути, Unmap на выходе; деля несколькими процессами одни физические страницы, «память — по копии на процесс» превращается в «представление — по одному на процесс».Межпроцессное согласование (арена блокировок): страж единственного экземпляра — целофайловой блокировкой (обслуживает только тот, кто её взял; второй экземпляр выходит); файлы очередей задач и книг учёта обновляются на уровне записей диапазонными блокировками — протокол блокировок фиксируется в «инструкции по применению» файла (какие диапазоны полей, какой режим, потолок длительности удержания).Доставка конфигурации и состояния (арена атомарной доставки): все файлы, которые «читатель может прочесть в любой момент», — конфигурации, манифесты, контрольные точки. Хозяева и сочетаются: файл книги учёта = диапазонные блокировки (обновление записей) + атомарная доставка (ротация целиком).
Отказоустойчивость: слепое пятно атомарной доставки
Атомарная доставка обещает «читатель не видит промежуточных состояний», но одно слепое пятно надо признать: успешный rename не означает, что данные уже на диске — rename атомарно переключает запись каталога, а блоки данных могут ещё сидеть в страничном кеше; обрыв питания в этот момент — и каталог указывает на новое имя с неполными данными. Для сценариев с высокими требованиями к отказоустойчивости (книги учёта, контрольные точки) полная цепь — «временный файл → семантика fsync (данные на диск) → rename → fsync каталога (современные файловые системы)»: файловый слой XRT предоставляет соответствующие возможности на flush-семантике, а fsync каталога после rename — деталь хостовой файловой системы (в большинстве сцен опускается, в сценариях уровня книги учёта добавляется). Запишите эту цепь в свой «список долговечности»: промежуточные состояния уничтожает rename, окно обрыва питания уничтожает fsync — это два гаранта на разных слоях, и без любого остаётся окно.
Напоминание о выборе: когда блокировки не нужны
Файловые блокировки — не единственный способ согласования параллельности; перед выбором задайте три вопроса. Несколько процессов на одной машине? Да → файловые блокировки уместны; между машинами → блокировки бессильны (семантика блокировок на сетевых файловых системах ненадёжна), берите протокол прикладного уровня. Зернистость доступа? Уровень файла → целофайловая блокировка (проще всего); уровень записи → диапазонная; ещё тоньше → рассмотрите модель единственного писателя (один процесс единолично ведает файлом, остальные ходят через него — сообщения очередью главы 21), обходя блокировки. Частота конфликтов? Редкие обновления → атомарная доставка напрямую заменяет блокировки (переключение версий без блокировок); горячие конфликты → ожидание на блокировках становится заметно, рассмотрите общую память + единственного писателя. Блокировка — один из последних доводов согласования, а не первая реакция; тот же корень, что и встречный вопрос главы 23 «сперва блокировка — и порядок».
Типичные ошибки
Симптом: большой файл сканируется через отображение байт за байтом — производительность не выросла, а то и упала (накладные расходы страничных сбоев + отсутствие предзагрузки), код стал сложнее.
Причина: модель выгоды отображения — «произвольный доступ с оседанием горячей зоны»; у последовательного потока его страничные сбои не дают никакого преимущества перед поблочным Read.
xfilemap Map = xrtFileMap(File, 0, Size, XFILE_MAP_READ); for ( uint64 i = 0; i < Size; i++ ) { Consume(Map.Data[i]); /* последовательный скан 2 ГБ - страничный сбой на каждую страницу, без предзагрузки */ }
/* последовательный поток: вернуться к поблочному Read (потоковый хозяин главы 45) */ char Buffer[65536]; uint64 Done = 0; while ( Done < Size ) { uint64 Chunk = Min(sizeof(Buffer), Size - Done); xrtReadFull(File, Buffer, Chunk); Consume(Buffer, Chunk); Done += Chunk; }
Симптом: при блокировках всё равно гонки — испорченные записи, чтение наполовину старого наполовину нового; эпизодично и плохо воспроизводится.
Причина: блокировка — протокол между участниками: один пишущий, обходящий её напрямую, превращает блокировки всех законопослушных в декорацию.
/* процесс A (законопослушный): сперва блокировка, потом запись */ xrtFileLockRange(File, XFILE_LOCK_EXCLUSIVE, Off, Len, true); WriteRecord(File, Off, Data); xrtFileUnlockRange(File, Off, Len); /* процесс B (старый код, без протокола): пишет напрямую */ WriteRecord(File, Off, OtherData); /* блокировка A не сковывает незаблокированного B - гонка осталась */
/* все стороны доступа идут по одному протоколу - B исправлен в согласии с A */ xrtFileLockRange(File, XFILE_LOCK_EXCLUSIVE, Off, Len, true); WriteRecord(File, Off, OtherData); xrtFileUnlockRange(File, Off, Len); /* протокол фиксируется в документации модуля: любая запись в этот файл - только под блокировкой */
Практические задания
Прочитайте один и тот же отрезок через отображение и через ReadFull — байты совпадают; затем измените через отображение (с флагом записи) и прочитайте назад, проверив синхронизацию.
Напишите две программы — «держатель блокировки» и «ждущий»: первый берёт блокировку и спит несколько секунд, второй пытается заблокировать и репортит ожидание/успех — прочувствуйте семантику согласования (ждущий пусть попробует и совместную, и монопольную).
Реализуйте save_config(路径, 内容) (путь, содержимое): запись в одноимённый временный файл уникального имени в том же каталоге → сброс на диск → атомарное переименование; параллельные читатели в цикле читают и разбирают конфигурацию, проверяя, что в любой момент читается только целое легальное содержимое. Критерии приёмки: за 100 обновлений у читателей ноль сбоев разбора; после инъекции отказа (убить процесс до переименования) исходный файл цел; временных файлов не осталось.
Итоги
| Тема | Кратко |
|---|---|
| Отображение | xrtFileMap — указатель есть файл; арена произвольного доступа, последовательному потоку преимущества нет; Unmap прежде Close |
| Две ступени блокировок | Lock на весь файл (взаимоисключение единственного экземпляра) / LockRange на диапазон (параллелизм на уровне записей); протокол связывает все стороны |
| Атомарная доставка | временный файл в том же каталоге (уникальное имя) → сброс на диск → атомарное переименование; промежуточные состояния исчезают структурно |
| Временные файлы | xrtFileTemp(目录, 前缀, ...) (каталог, префикс) уникальное имя; успех — переименование, отказ — удаление - два финала, ноль мусора |
| Комбинации | отображение + блокировки (согласование пишущих) / атомарное переключение (без долгих блокировок) - брать по сценарию |
| Семейство метаданных | образцы link/report: симлинки и отчёт о состоянии файла - попутчики тройки |
| Три хозяина | загрузка индексов и ресурсов (отображение) / межпроцессное согласование (блокировки) / доставка читаемых в любой момент файлов (атомарное переименование) |
| Слепое пятно отказов | промежуточные состояния уничтожает rename, окно обрыва питания - fsync - гаранты двух слоёв; без любого остаётся окно |
| Границы блокировок | между машинами бессильны, при редких обновлениях заменимы атомарной доставкой, при горячих конфликтах — единственный писатель - последний довод, не первая реакция |
📖 Справочники API в этой главе:file (132 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки