用于 Windows 写入临时文件的 mkstemp() 实现

mkstemp() implementation for Windows to write temporary files

本文关键字:mkstemp 实现 临时文件 Windows 用于      更新时间:2023-10-16

我想在Windows到C++的给定临时目录路径中创建临时文件。mktemp()执行所需的工作,但它只创建 26 个唯一文件。mkstemp()在Linux中运行良好,但在Windows中不存在。因此,请帮助我在 Windows 中使用mkstemp()功能或建议替代方案?

我已经掸掉了我的旧RIG(可重用接口胶水(库,因为我多年前曾经编写过操作系统抽象层;这是一个高性能的,与操作系统无关的mkostemps((实现,它类似于mkstemp((,但带有可选的文件名后缀和可选的附加开放标志,即

inline int mkstemp(char *pathTmpl) {return rig::mkostemps(pathTmpl, 0, 0);}

在 Linux 上实现 mkstemp(( 通常将 6 个"模板"X 字符替换为区分大小写的字母数字字符(因此 2 * 26 + 10 = 62 个值(,并且有时在 Windows 上也使用此类实现。但是,尽管Windows文件名现在保留大小写,但唯一名称通常不区分大小写,因此此类算法浪费地尝试重复文件名(仅在大写/小写中有所不同(。我的方法使用 a-z 和 0-9 表示 36**6 个可能的文件名(即 2**31 加上大约 2900 万个,即近 22 亿个可能的文件名(。

大约20年前,我想出了一种技术来生成一个确定性的(虽然看起来有些随机(的数字序列,在输出范围中的每个数字之前,它永远不会重复。多年后,我在伪随机数生成器中偶然发现了类似的代码,在那里它被称为Weyl序列。因此,我将我的异或增强Weyl序列命名为X-Weyl序列。基本思想是将索引中的奇数重复添加到 2 的幂大小范围内,使用环绕(即模(,并使用另一个随机选择的常量对输出进行 XOR 运算,以使输出看起来不太可预测。

以下代码使用 2 个 X-Weyl 序列:一个用于迭代范围 [0, 2**31( 以生成随机文件名,另一个用于迭代 [0, 5](实际上是 0 到 7,跳过 6 和 7(以"随机"重新排序文件名中的字符。我使用 std::random_device 来获得(希望(随机熵来"播种"X-Weyl 序列;这对 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 MB 位图运行它,标记使用的文件名,以便我可以看到此代码找到唯一名称的速度;我运行它,直到它在 86 小时后消耗了 5% 的可能文件名变体。[请注意,由于涉及真实文件,我在一夜之间运行后只达到了 2.1 亿个文件,因此需要连续创建文件一个多星期才能真正耗尽唯一的文件名供应,假设操作系统不会随着目录大小的爆炸而缓慢爬行。

在 2.1 亿个文件标记处,生成唯一文件名所花费的尝试次数的直方图如下所示:

After 210501632 temp files created:
1:  197340597
2:  12211367
3:  873750
4:  69692
5:  5643
6:  542
7:  36
8:  5

因此,在进展顺利之后,99.5% 的时间只需 2 次尝试即可生成唯一的文件名。创建了大约十亿个文件名:

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 次(,文件名生成仍然快如闪电(我在调试版本中注意到大约 130,000 个唯一名称/秒(;实际上,所有的开销都只是尝试创建唯一文件的 open(( 调用。

_mktemp

(MSVC名称(将用字母替换X,这就是为什么您只能获得26个不同的名称。还有_tempnam使用数字代替。它应该支持多达 40 亿个文件。