Реализация mkstemp() для Windows для записи временных файлов
Я хочу создать временные файлы по указанному пути временного каталога в Windows через C++. mktemp()
выполняет необходимую работу, но создает только 26 уникальных файлов. mkstemp()
нормально работает в Linux, но его нет в Windows. Поэтому, пожалуйста, помогите мне использовать mkstemp()
функциональность в винде или предложить альтернативу?
2 ответа
Я стряхнул пыль со своей старой библиотеки RIG (Reusable Interface Glue), потому что много лет назад я писал уровни абстракции ОС; вот производительная, не зависящая от ОС реализация mkostemps(), которая похожа на mkstemp(), но с необязательным суффиксом имени файла и необязательными дополнительными открытыми флагами, т.е.
inline int mkstemp(char *pathTmpl) {return rig::mkostemps(pathTmpl, 0, 0);}
Реализация mkstemp() в Linux обычно заменяет 6 символов «шаблона» X буквенно-цифровыми символами, чувствительными к регистру (т. е. 2 * 26 + 10 = 62 значения), и такие реализации иногда используются и в Windows. Однако, хотя имена файлов Windows в настоящее время сохраняют регистр, уникальные имена обычно нечувствительны к регистру, поэтому такие алгоритмы расточительно пытаются дублировать имена файлов (различающиеся только в верхнем/нижнем регистре). Мой подход использует az и 0-9 для 36**6 возможных имен файлов (что составляет 2**31 плюс около 29 миллионов, т.е. почти 2,2 миллиарда возможных имен файлов).
Около 20 лет назад я придумал метод генерации детерминированной (хотя и несколько случайной) последовательности чисел, которая никогда не повторяется до тех пор, пока не будут выведены все числа в диапазоне. Спустя годы я наткнулся на аналогичный код в генераторах псевдослучайных чисел, где его назвали последовательностью Вейля. Поэтому я назвал свои последовательности Вейля, расширенные с помощью XOR, последовательностями X-Weyl. Основная идея состоит в том, чтобы неоднократно добавлять нечетное число к индексу в диапазон размера степени 2 с циклическим преобразованием (т. е. по модулю) и выполнять XOR выходных данных с другой случайно выбранной константой, чтобы выходные данные выглядели менее предсказуемыми.
В следующем коде используются две последовательности X-Weyl: одна для перебора диапазона [0, 2**31) для генерации случайных имен файлов, а другая для перебора [0, 5] — фактически от 0 до 7, с пропущенными 6 и 7. — «случайным» образом изменить порядок символов в имени файла. Я использую std::random_device для получения (надеюсь) случайной энтропии для «затравки» последовательностей X-Вейля; это хорошо для Windows и большинства систем Linux, но на тот случай, если он когда-либо будет построен в системе с детерминированным выводом random_device, я добавил вызов time(), чтобы хотя бы обеспечить уникальные условия запуска при повторном запуске.
// Excerted from RIG - Reusable Interface Glue, Copyright (c) 1995 - 2023, GTB.
// Portions below are freely redistributable, with no warranties.
#include <cinttypes>
#include <climits>
#include <cstring>
#include <errno.h>
#include <time.h>
#include <fcntl.h>
#if defined(_WIN32)
#include <io.h>
#define TMP_OPEN_MODE_ (_S_IREAD | _S_IWRITE)
#else
#include <unistd.h>
#define TMP_OPEN_MODE_ (S_IRUSR | S_IWUSR)
#endif
#include <random>
// Number of elements in an array:
#define rig_COUNT_OF(_array) (sizeof(_array) / sizeof((_array)[0]))
// Create a bit mask of the specified number of (least-significant) bits. Must supply a positive
// (non-zero) number of bits, and gives less-efficient code if not a constant.
#define rig_BIT_MASK_EX(_type, _count) \
((_type)((_type)((((_type)1 << ((_count) - 1)) - 1) << 1) | (_type)0x1))
namespace rig
{
// A Weyl sequencer with an XOR mask to optionally "twist" the ordering to be less linear:
// A Weyl sequence produces every unique value in 2 to the N exactly once before repeating.
template <typename UTYPE_, unsigned N_BITS_ = CHAR_BIT * sizeof(UTYPE_)> class XWeylSequence
{
UTYPE_ prevVal_, delta_, xor_;
static UTYPE_ Val_(UTYPE_ val) {return val & rig_BIT_MASK_EX(UTYPE_, N_BITS_);}
public:
XWeylSequence(UTYPE_ seedValue, UTYPE_ sequenceStep, UTYPE_ xorMask = 0) :
prevVal_(Val_(seedValue)), delta_(Val_(sequenceStep) | 0x1), xor_(Val_(xorMask)) {}
inline void SetSeed(UTYPE_ seedValue) {prevVal_ = Val_(seedValue);}
inline UTYPE_ Next() {return Val_((prevVal_ += delta_)) ^ xor_;}
};
class RandomSeed
{
std::random_device rng_;
unsigned int xtra_; // In case random_device is only pseudo-random
inline uint32_t Entropy_() {return (uint32_t)(rng_() | xtra_);}
public:
RandomSeed() : xtra_((unsigned int)time(nullptr)) {}
inline uint32_t operator()()
{return Entropy_() ^ ((sizeof(unsigned) < 4) ? Entropy_() << (CHAR_BIT * 2) : 0);}
};
int mkostemps(char *pathTmpl, int sfxLen = 0, int oflags = 0)
{ // Validate input parameters:
static const char XPatt[] = "XXXXXX";
char *tmplP = &pathTmpl[pathTmpl ? strlen(pathTmpl) : 0] - sfxLen - (sizeof(XPatt) - 1);
if ((sfxLen < 0) || (tmplP < pathTmpl) || memcmp(tmplP, XPatt, sizeof(XPatt) - 1))
{
errno = EINVAL;
return -1;
}
// Each X is replaced with one of 36 values (case-INSENSITIVE alphanumeric characters),
// giving 36**6 possible values (slightly more than 2**31).
static const char encodingSet[36 + 1] = "abcdefghijkLmnopqrstuvwxyz0123456789";
RandomSeed rng;
uint32_t r[4];
for (unsigned int idx = 0; idx < rig_COUNT_OF(r); ++idx)
r[idx] = rng();
XWeylSequence<uint32_t, 31> seq(r[3], r[2], r[1]); // Step thru 2**31 values
XWeylSequence<uint8_t, 3> out(r[0], r[0] >> (6 - 1), r[0] >> 3); //Step thru out indices
uint32_t baseOffs = r[0] >> 8; // Capture most of the gap between 36**6 and 2**31
unsigned long tryLimit = std::max(
#ifdef TMP_MAX
(unsigned long)TMP_MAX +
#endif
0ul, 1ul << 18); // (Linux tries < 2**18 times)
do // Follow a randomly-selected X-Weyl sequence until it produces a unique filename:
{
uint32_t rv = seq.Next() + baseOffs;
for (unsigned cnt = 8; cnt; --cnt)
{ // Randomly-selected order of output indices:
unsigned idx = out.Next();
if (idx < 6) // Operating on [0-7], but only [0-5] are valid indices
{
tmplP[idx] = encodingSet[rv % 36];
rv /= 36;
}
}
// Try to create the file:
int fd = open(pathTmpl, oflags | O_CREAT | O_EXCL | O_RDWR, TMP_OPEN_MODE_);
if ((fd >= 0) || (errno != EEXIST))
return fd;
} while (--tryLimit); // Retry so long as error EEXIST is returned, until limit reached
return -1;
}
} // end namespace rig
Я просто потратил немного времени на инструментирование кода, потому что мне было любопытно, насколько он хорош. Я быстро понял, что тестирование миллиардов имен файлов с реальными файлами на диске происходит непомерно медленно из-за поиска существующих имен файлов операционной системой. В целях инструментирования я запустил его с растровым изображением размером 512 МБ, маркирующим используемые имена файлов, чтобы я мог видеть, как быстро этот код находит уникальные имена; Я запускал его до тех пор, пока он не отказался от 86% возможных вариантов имени файла, использованных через 5 часов. [Обратите внимание, что при использовании реальных файлов я достиг 210 миллионов файлов только после ночной работы, поэтому потребовалось бы непрерывное создание файлов в течение более недели, чтобы фактически исчерпать запас уникальных имен файлов, при условии, что ОС не замедлит сканирование, как размер каталога увеличился.]
На отметке в 210 миллионов файлов гистограмма того, сколько попыток потребовалось для создания уникального имени файла, выглядела следующим образом:
After 210501632 temp files created:
1: 197340597
2: 12211367
3: 873750
4: 69692
5: 5643
6: 542
7: 36
8: 5
Таким образом, после успешного завершения работы в 99,5% случаев уникальное имя файла можно сгенерировать всего за две попытки. Создано около миллиарда имен файлов:
After 1000079360 temp files created:
1: 650706306
2: 206972880
3: 79540728
4: 33792156
5: 14654019
6: 7133227
7: 3483588
8: 1776113
9: 912556
10: 494348
11: 269506
12: 149952
13: 81034
14: 46228
15: 25989
16: 15457
17: 9362
18: 5900
19: 3532
20: 2212
21: 1351
22: 887
23: 566
24: 355
25: 258
26: 195
27: 143
28: 97
29: 63
30: 52
31: 49
32: 41
33: 38
34: 22
35: 22
36: 16
37: 14
38: 16
39: 11
40: 9
41: 10
42: 7
43: 8
44: 1
45: 7
46: 3
48: 1
49: 2
50: 3
51: 1
52: 2
53: 2
54: 1
55: 4
56: 1
57: 1
60: 1
61: 1
62: 1
64: 1
66: 1
83: 1
85: 1
125: 1
Таким образом, ~99% уникальных имен файлов потребовало не более 5 попыток с миллиардом уникальных имен файлов! Даже для длинного хвоста (когда однажды потребовалось даже 85 попыток, а в другой раз 125) генерация имени файла по-прежнему происходит молниеносно (в отладочной сборке я заметил около 130000 уникальных имен в секунду); практически все накладные расходы — это просто вызов open() для попытки создать уникальный файл.
_mktemp
(имя MSVC) заменит X
с буквой, поэтому вы можете получить только 26 разных имен. Есть также _tempnam
который использует числа вместо. Он должен поддерживать до 4 миллиардов файлов.