Реализация 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 миллиардов файлов.

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