我想在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 亿个文件。