The 人2开 https://www.man7.org/linux/man-pages/man2/open.2.html手册页(链接到 man7.org 上最新的 Linux 手册页)在Bugs使用的部分O_CREAT | O_DIRECTORY
将创建一个常规文件。还有这个讨论 https://austin-group-l.opengroup.narkive.com/9z11WMS5/atomic-mkdir-open#post2.
更重要的是,即使它确实成功了,其他一些进程仍然可以在创建成功后立即访问该目录,甚至在调用返回到您的程序之前。因此,你担心的比赛窗口无论如何都会存在。
常见的模式是在同一目录中创建一个具有足够随机名称的临时目录(以.
从典型的文件和目录列表中省略它)仅可由当前用户访问;然后填充它;然后调整其访问方式;然后将其重命名为最终名称。
这并不会使其他进程无法访问该目录,但这种模式被认为足够安全。
这是执行此操作的示例程序:
#define _POSIX_C_SOURCE 200809L
#define _ATFILE_SOURCE
#define _GNU_SOURCE
#include <stdlib.h>
#include <inttypes.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/random.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#ifndef RENAME_NOREPLACE
#define RENAME_NOREPLACE (1 << 0)
static inline int renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, unsigned int flags)
{
int retval = syscall(SYS_renameat2, olddirfd, oldpath, newdirfd, newpath, flags);
if (!retval)
return 0;
errno = -retval;
return -1;
}
#endif
/* Xorshift64* pseudo-random number generator.
*/
static uint64_t prng_state = 0; /* unseeded */
static uint64_t prng_u64(void)
{
uint64_t state = prng_state;
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
prng_state = state;
return state * UINT64_C(2685821657736338717);
}
static uint64_t prng_randomize(void)
{
uint64_t state;
/* Use Linux-specific getrandom() call. */
{
ssize_t n;
do {
n = getrandom(&state, sizeof state, 0);
} while (n == -1 && errno == EINTR);
if (n == (ssize_t)sizeof state && state != 0) {
prng_state = state;
return state;
}
}
/* Fall back to using time as a seed. */
{
struct timespec now;
size_t rounds = 250;
clock_gettime(CLOCK_REALTIME, &now);
state = (uint64_t)now.tv_sec * UINT64_C(270547637)
^ (uint64_t)now.tv_nsec * UINT64_C(90640031)
^ (uint64_t)getpid() * UINT64_C(4758041);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now);
state ^= (uint64_t)now.tv_sec * UINT64_C(3266177)
^ (uint64_t)now.tv_nsec * UINT64_C(900904331);
clock_gettime(CLOCK_MONOTONIC, &now);
state ^= (uint64_t)now.tv_sec * UINT64_C(24400169)
^ (uint64_t)now.tv_nsec * UINT64_C(1926466307);
/* Make sure state is nonzero */
state += (!state);
/* Mix it a bit, to make it less predictable. */
while (rounds-->0) {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
}
prng_state = state;
return state;
}
}
static const char base64[64] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '-', '_'
};
/* Create a new directory atomically, returning an open descriptor to it.
name must be non-empty, and not contain a slash.
*/
int mkdiratfd(const int atfd, const char *dirpath, const char *name, const mode_t mode)
{
char buf[32];
mode_t curr_umask;
int atdirfd, fd;
/* New directory name cannot be NULL, empty, or contain a slash. */
if (!name || !*name || strchr(name, '/')) {
errno = EINVAL;
return -1;
}
/* If dirpath is NULL or empty, we use "." for it. */
if (!dirpath || !*dirpath)
dirpath = ".";
/* Open a handle to the target directory. */
do {
atdirfd = openat(atfd, dirpath, O_PATH | O_DIRECTORY | O_CLOEXEC);
} while (atdirfd == -1 && errno == EINTR);
if (atdirfd == -1) {
return -1;
}
/* Obtain current umask. */
curr_umask = umask(0); umask(curr_umask);
/* Make sure our PRNG has been seeded. */
if (!prng_state)
prng_randomize();
/* Create a temporary random name for the directory. */
while (1) {
char *ptr = buf;
/* Start with a dot, making it "hidden". */
*(ptr++) = '.';
/* Use 2*10 = 20 random characters (120 bits) */
for (int k = 2; k > 0; k--) {
uint64_t u = prng_u64();
int n = 10;
while (n-->0) {
*(ptr++) = base64[u & 63];
u >>= 6;
}
}
/* Terminate name */
*ptr = '\0';
/* Create the temporary directory with access only to current user. */
if (mkdirat(atdirfd, buf, 0700) == -1) {
const int saved_errno = errno;
if (errno == EINTR || errno == EEXIST)
continue;
/* Actual error. */
close(atdirfd);
errno = saved_errno;
return -1;
}
/* Successfully created. */
break;
}
/* Open the temporary directory. */
do {
fd = openat(atdirfd, buf, O_PATH | O_DIRECTORY | O_CLOEXEC);
} while (fd == -1 && errno == EINTR);
if (fd == -1) {
const int saved_errno = errno;
unlinkat(atdirfd, buf, AT_REMOVEDIR);
close(atdirfd);
errno = saved_errno;
return -1;
}
/*
* Note: Other actions, like file creation, etc.
* should be done at this stage.
*/
/* Update directory owner group here, if necessary. */
/* Update proper access mode. */
if (fchmodat(atdirfd, buf, mode & (~curr_umask), 0) == -1) {
const int saved_errno = errno;
close(fd);
unlinkat(atdirfd, buf, AT_REMOVEDIR);
close(atdirfd);
errno = saved_errno;
return -1;
}
/* Rename directory. */
if (renameat2(atdirfd, buf, atdirfd, name, RENAME_NOREPLACE) == -1) {
const int saved_errno = errno;
close(fd);
unlinkat(atdirfd, buf, AT_REMOVEDIR);
close(atdirfd);
if (saved_errno == EPERM)
errno = EEXIST;
else
errno = saved_errno;
return -1;
}
/* Success. */
close(atdirfd);
return fd;
}
int main(int argc, char *argv[])
{
int fd;
if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
const char *argv0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv0);
fprintf(stderr, " %s NAME\n", argv0);
fprintf(stderr, "\n");
fprintf(stderr, "This program creates directory NAME in the current directory.\n");
fprintf(stderr, "\n");
return EXIT_FAILURE;
}
fd = mkdiratfd(AT_FDCWD, NULL, argv[1], 0755);
if (fd == -1) {
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
请注意,这使用renameat2()
如果 C 库未公开它,则通过原始系统调用。 (它在 2.28 中被添加到 glibc,但从 3.15 开始受到 Linux 内核的支持)。
如果您仍然担心,偏执模式是创建一个临时目录来保存临时目录。打开将成为最终目录的内部目录后,将外部临时目录的模式更改为零,以停止遍历内部树。创建者仍然可以通过开放目录描述符访问内部树。该目录仍然可以重命名,因为它们驻留在同一文件系统上。
我个人不会打扰,因为使用临时名称,并且仅在完成后重命名目录(Linux 中的许多应用程序就是这样做的)就足够安全了。