第2章 标准I/O库

前面的章节介绍的是Linux的系统调用。本章将从O库开始讲解Linux环境编程中不可或缺的C库。在学习和分析标准I/O库的同时,与Linux的I/O系统调用进行比较,可以加深对两者的认识和理解。

2.1 stdin、stdout和stderr

当Linux新建一个进程时,会自动创建3个文件描述符0、1和2,分别对应标准输入、标准输出和错误输出。C库中与文件描述符对应的是文件指针,与文件描述符0、1和2类似,我们可以直接使用文件指针stdinstdoutstderr。那么这是否意味着stdinstdoutstderr是“自动打开”的文件指针呢?

查看C库头文件stdio.h中的源码:

typedef struct _IO_FILE FILE;
/* Standard streams.  */
extern struct _IO_FILE *stdin;      /* Standard input stream.  */
extern struct _IO_FILE *stdout;     /* Standard output stream.  */
extern struct _IO_FILE *stderr;     /* Standard error output stream.  */
#ifdef __STDC__
/* C89/C99 say they're macros.  Make them happy.  */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#endif

从上面的源码可以看出,stdinstdoutstderr确实是文件指针。而C标准要求stdinstdoutstderr是宏定义,所以在C库的代码中又定义了同名宏。

那么stdinstdoutstderr又是如何定义的呢?定义代码如下:

_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

继续查看_IO_2_1_stdin_等的定义,代码如下:

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

DEF_STDFILE是一个宏定义,用于初始化C库中的FILE结构。这里_IO_2_1_stdin_IO_2_1_stdout_IO_2_1_stderr这三个FILE结构分别用于文件描述符0、1和2的初始化,这样C库的文件指针就与系统的文件描述符互相关联起来了。大家注意最后的标志位,stdin是不可写的,stdout是不可读的,而stderr不仅不可读,且没有缓存。

通过上面的分析,可以得到一个结论:stdinstdoutstderr都是FILE类型的文件指针,是由C库静态定义的,直接与文件描述符0、1和2相关联,所以应用程序可以直接使用它们。

2.2 I/O缓存引出的趣题

C库的I/O接口对文件I/O进行了封装,为了提高性能,其引入了缓存机制,共有三种缓存机制:全缓存行缓存无缓存

  • 全缓存一般用于访问真正的磁盘文件。C库会为文件访问申请一块内存,只有当文件内容将缓存填满或执行冲刷函数flush时,C库才会将缓存内容写入内核中。
  • 行缓存一般用于访问终端。当遇到一个换行符时,就会引发真正的I/O操作。需要注意的是,C库的行缓存也是固定大小的。因此,当缓存已满,即使没有换行符时也会引发I/O操作。
  • 无缓存,顾名思义,C库没有进行任何的缓存。任何C库的I/O调用都会引发实际的I/O操作。

C库提供了接口,用于修改默认的缓存行为,相关代码如下:

#include <stdio.h>
void setbuf(FILE *stream, char *buf);
void setbuffer(FILE *stream, char *buf, size_t size);
void setlinebuf(FILE *stream);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

下面看一个跟C库缓存相关的趣题。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    printf("Hello ");
    if (0 == fork()) {
        printf("child\n");
        return 0;
    }
    printf("parent\n");
    return 0;
}

其输出结果是什么?正确的结果是:

Hello parent
Hello child

或者:

Hello child
Hello parent

之所以是这样的结果,就是因为背后的行缓存。执行printf("Hello")时,因为printf是向标准输出打印的,因此使用的是行缓存。字符串Hello没有换行符,所以并没有真正的I/O输出。当执行fork时,子进程会完全复制父进程的内存空间,因此字符串Hello也存在于子进程的行缓存中。故而最后的输出结果中,无论是父进程还是子进程都有Hello字符串。

2.3 fopen和open标志位对比

C库的fopen用于打开文件,其内部实现必然要使用open系统调用。那么fopen的各个标志位又对应open的哪些标志位呢?请看表2-1。

表2-1 fopen标志位和open标志位对应表

表格说明

原书有表2-1展示常用标志位的对应关系,此处省略具体表格内容,保留文字描述。

表2-1是fopen常用的标志位,实际上fopen还有更多的标志位,这也是很多书籍没有涉及的,具体见表2-2。

表2-2 更多的fopen和open标志位对应

表格说明

原书有表2-2展示更多标志位的对应关系,此处省略具体表格内容,保留文字描述。

下面进入glibc的源码,查看函数_IO_new_file_fopen来验证上面的结论。

_IO_FILE *
_IO_new_file_fopen (fp, filename, mode, is32not64)
     _IO_FILE *fp;
     const char *filename;
     const char *mode;
     int is32not64;
{
  int oflags = 0, omode;
  int read_write;
  int oprot = 0666;
  int i;
  _IO_FILE *result;
#ifdef _LIBC
  const char *cs;
  const char *last_recognized;
#endif
  if (_IO_file_is_open (fp))
    return 0;
  switch (*mode)
    {
    case 'r':
      omode = O_RDONLY;
      read_write = _IO_NO_WRITES;
      break;
    case 'w':
      omode = O_WRONLY;
      oflags = O_CREAT|O_TRUNC;
      read_write = _IO_NO_READS;
      break;
    case 'a':
      omode = O_WRONLY;
      oflags = O_CREAT|O_APPEND;
      read_write = _IO_NO_READS|_IO_IS_APPENDING;
      break;
    default:
      __set_errno (EINVAL);
      return NULL;
    }
#ifdef _LIBC
  last_recognized = mode;
#endif
  for (i = 1; i < 7; ++i)
    {
      switch (*++mode)
    {
    case '\0':
      break;
    case '+':
      omode = O_RDWR;
      read_write &= _IO_IS_APPENDING;
#ifdef _LIBC
      last_recognized = mode;
#endif
      continue;
    case 'x':
      oflags |= O_EXCL;
#ifdef _LIBC
      last_recognized = mode;
#endif
      continue;
    case 'b':
#ifdef _LIBC
      last_recognized = mode;
#endif
      continue;
    case 'm':
      fp->_flags2 |= _IO_FLAGS2_MMAP;
      continue;
    case 'c':
      fp->_flags2 |= _IO_FLAGS2_NOTCANCEL;
      continue;
    case 'e':
#ifdef O_CLOEXEC
      oflags |= O_CLOEXEC;
#endif
      fp->_flags2 |= _IO_FLAGS2_CLOEXEC;
      continue;
    default:
      /* Ignore.  */
      continue;
    }
      break;
    }
  result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
            is32not64);
}

上面的源代码非常简单,很容易理解。每个mode都是switch语句的一个caseoflags就是要传给open的标志位,这就验证了前文的结论。

2.4 fdopen与fileno

Linux提供了文件描述符,而C库又提供了文件流。在平时的工作中,有时候需要在两者之间进行切换,因此C库提供了两个API:

#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
int fileno(FILE *stream);

fdopen用于从文件描述符fd生成一个文件流FILE,而fileno则用于从文件流FILE得到对应的文件描述符。

查看fdopen的实现,其基本工作是创建一个新的文件流FILE,并建立文件流FILE与描述符的对应关系。我们以fileno的简单实现,来了解文件流FILE与文件描述符fd的关系。——因为该函数代码较长,在此就不罗列C库的代码了。代码如下:

int fileno (_IO_FILE* fp)
{
    CHECK_FILE (fp, EOF);
    if (!(fp->_flags & _IO_IS_FILEBUF) || _IO_fileno (fp) < 0)
    {
         __set_errno (EBADF);
         return -1;
    }
    return _IO_fileno (fp);
}
#define _IO_fileno(FP) ((FP)->_fileno)

fileno的实现基本上就可以得知文件流与文件描述符的对应关系。文件流FILE保存了文件描述符的值。当从文件流转换到文件描述符时,可以直接通过当前FILE保存的值_fileno得到fd。而从文件描述符转换到文件流时,C库返回的都是一个重新申请的文件流FILE,且这个FILE_fileno保存了文件描述符。

因此无论是fdopen还是fileno,关闭文件时,都要使用fclose来关闭文件,而不是用close。因为只有采用此方式,fclose作为C库函数,才会释放文件流FILE占用的内存。

2.5 同时读写的痛苦

前面介绍过内核的文件描述符实现。在内核中,每一个文件描述符fd都对应了一个文件管理结构struct file——用于维护该文件描述符的信息,如偏移量等。在第1章对readwrite的源码分析中,可以发现每一次系统调用的readwrite成功返回后,文件的偏移量都会被更新。

因此,如果程序对同一个文件描述符进行读写操作的话,肯定会得到非期望的结果,示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    char buf[20];
    int ret;
    FILE *fp = fopen("./tmp.txt", "w+");
    if (!fp) {
        printf("Fail to open file\n");
        return -1;
    }
    ret = fwrite("123", sizeof("123"), 1, fp);
    printf("we write %d member\n", ret);
    memset(buf, 0, sizeof(buf));
    ret = fread(buf, 1, 1, fp);
    printf("We read %s, ret is %d\n", buf, ret);
    fwrite("456", sizeof("456"), 1, fp);
    fclose(fp);
    return 0;
}

上面的代码中,利用fopen的读写模式打开了一个文件流,先写入一个字符串"123",然后读取一个字节,再写入一个字符串"456"

大家想想输出结果会是什么呢?fread读取的字符又会是什么呢?是否为'1'呢?请看下面的结果:

[fgao@ubuntu chapter2]#./a.out
we write 1 member
We read , ret is 0

为什么fread什么都没有读取到,返回值是0呢?这是因为上面的代码中,fwritefread操作的是同一个文件指针fp,也就是对应的是同一个文件描述符。第一次fwrite后,在tmp.txt中写入了字符串"123",同时文件偏移为3,也就是到了文件尾。进行fread操作时,既然操作的是同一个文件描述符,自然会共享同一个文件偏移,那么,从文件尾自然读取不到任何数据。

2.6 ferror的返回值

ferror用于告诉用户C库的文件流FILE是否有错误发生。当有错误发生时,ferror返回非零值,反之则返回0。那么ferror是否会返回不同的错误呢?让我们来看看ferror的源码。

weak_alias (_IO_ferror, ferror)
int _IO_ferror (fp)
     _IO_FILE* fp;
{
    int result;
    /* 检查文件流的有效性,失败则返回EOF */
    CHECK_FILE (fp, EOF);
    _IO_flockfile (fp);
    result = _IO_ferror_unlocked (fp);
    _IO_funlockfile (fp);
    return result;
}

进入_IO_ferror_unlocked,代码如下:

#define _IO_ferror_unlocked(__fp) (((__fp)->_flags & _IO_ERR_SEEN) != 0)
#define _IO_ERR_SEEN 0x20

从源码上可以看出ferror有两个返回值:

  • 当文件流FILE *fp非法时,返回EOF(-1)。
  • 当文件流FILE *fp前面的操作发生错误时,返回1。

并且由于文件流的错误只是使用一个标志位_IO_ERR_SEEN来表示的,因此ferror的返回值就不可能针对不同的错误返回不同的值了。

2.7 clearerr的用途

2.6节中的ferror用于检测文件流是否有错误发生,而clearerr用于清除文件流的文件结束位和错误位。

查看clearerr的实现,代码如下:

#define clearerr_unlocked(x) clearerr (x)
void
clearerr_unlocked (fp)
     FILE *fp;
{
    CHECK_FILE (fp, /*nothing*/);
    _IO_clearerr (fp);
}
#define _IO_clearerr(FP) ((FP)->_flags &= ~(_IO_ERR_SEEN|_IO_EOF_SEEN))

可见,clearerr可以清除文件流中的文件结尾标志和错误标志。

但是清除错误标志又有什么用处呢?按照某些资料上的描述,当文件流读到文件尾时,文件流会被设置上EOF标志。如果不使用clearerr清除EOF标志,即使有新的数据,也无法读取成功。

让我们写个程序来验证一下:

#include <stdlib.h>
#include <stdio.h>
int main(void)
{
    FILE *fp = fopen("./tmp.txt", "r");
    if (!fp) {
        printf("Fail to fopen\n");
        return -1;
    }
    while (1) {
        int c = getc(fp);
        if (feof(fp)) {
            printf("reach feof\n");
        }
    }
    return 0;
}

为了满足前面所说的测试情况,我们使用gdb来控制程序,代码如下:

31                      int c = getc(fp);
(gdb)
33                      if (feof(fp)) {
(gdb) n
34                      printf("reach feof\n");

现在,文件流fp已经读到了文件尾,被设置上了EOF标志。接下来向tmp.txt追加一个字母'a'

[f我们可以发现虽然此时文件流`fp`仍然是被设置了`EOF`标志,但是依然能够成功读取数据.这与某些资料的描述不符,这就应对了那句老话“尽信书不如无书”,对于一些资料的结论,不要完全相信,而是要通过自己的实践来验证.

下面回到glibc的源码,查看`_IO_getc`,从代码中了解为什么是这样的结果.

```c
int
_IO_getc (fp)
     FILE *fp;
{
  int result;
  /* 检查fp */
  CHECK_FILE (fp, EOF);
  _IO_acquire_lock (fp);
  result = _IO_getc_unlocked (fp);
  _IO_release_lock (fp);
  return result;
}
/*只有定义了IO_DEBUG,CHECK_FILE才会检查_IO_file_flags标志,当其不为0时,则返回错误值。对于fgetc即为EOF */
#ifdef IO_DEBUG
# define CHECK_FILE(FILE, RET) \
    if ((FILE) == NULL) { MAYBE_SET_EINVAL; return RET; } \
    else { COERCE_FILE(FILE); \
          if (((FILE)->_IO_file_flags & _IO_MAGIC_MASK) != _IO_MAGIC) \
      { MAYBE_SET_EINVAL; return RET; }}
#else
# define CHECK_FILE(FILE, RET) COERCE_FILE (FILE)
#endif

从glibc的源码中可以发现,文件流FILE的错误标志位只有在打开IO_DEBUG的情况下才会对后面的I/O调用产生影响:在有错误标志位的时候,后面的I/O调用都会直接返回EOF.而一般情况下,IO_DEBUG这个宏是没有定义的.

2.8 小心fgetc和getc

fgetcgetc是两个定义得很不友好的函数,其函数名中的getc很容易让使用者误以为其返回值是char字符.实际上两个函数的接口定义如下:

#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);

两者的返回值都是int类型.为什么要用int类型作为返回值呢?因为当文件流读到文件尾时,需要返回EOF值.C99标准中规定了EOF为一个int类型的负数常量,并没有规定具体的值.在glibc中,EOF被定义为-1且char为有符号数.但是不能排除某些实现将EOF定义为其他负值,甚至可能因为不遵守C99标准,EOF的值有可能超过char的表示范围.因此,为了代码的健壮性和可移植性,在使用fgetcgetc时,应使用int类型的变量保存其返回值.

2.9 注意fread和fwrite的返回值

freadfwrite的声明代码如下:

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

这两个函数原型很容易让人产生误解.当看到返回值类型为size_t时,人们很有可能理解为freadfwrite会返回成功读取或写入的字节数,然而实际上其返回的是成功读取或写入的个数,即有多少个size大小的对象被成功读取或写入了.而参数nmemb则用于指示freadfwrite要执行的对象个数.

看看下面的示例代码:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
    const char str[] = "123456789";
    FILE *fp = fopen("tmp.txt", "w");
    size_t size = fwrite(str, strlen(str), 1, fp);
    printf("size is %d\n", size);
    fclose(fp);
    return 0;
}

这段代码的输出为:

size is 1

结果并不是写入的字符串长度9,而是返回写入的对象个数1.其原因是参数ptr指示的是要写入对象的地址,size为每个对象的字节数,nmemb为有多少个要写入的对象.

将上面的代码稍微变换一下,将fwrite的语句改为:

size_t size = fwrite(str, 1, strlen(str), fp);

这时程序的输出就变为:

size is 9

其原因在于,参数size表示每个对象的字节数是1字节,nmemb表示要写入9个对象,因此返回值就变为9了.

2.10 创建临时文件

在项目中经常会需要生成临时文件,用于保存临时数据,创建管道文件、Unix域socket等.为了不与已有的文件同名,或者避免与其他临时文件相冲突,有些朋友可能会选择利用进程id、时间戳等来生成临时文件名.其实,C库已经提供了生成临时文件的接口.下面对生成临时文件的各种方法进行分析对比.

先来看看tmpnam方式,代码如下:

#include <stdio.h>
char *tmpnam(char *s);

tmpnam会返回一个目前系统不存在的临时文件名.当s为NULL时,返回的文件名保存在一个静态的缓存中,因此再次调用tmpnam时,新生成的文件名会覆盖上一次的结果.当s不为NULL时,生成的临时文件名会保存在s中,因此要求s至少要有C库规定的L_tmpnam大小.C库同时还规定tmpnam产生的临时文件的路径以P_tmpdir开头——glibc中P_tmpdir定义为/tmp.

从上面的描述中可以清楚地发现tmpnam的缺点:

  • s为NULL时,tmpnam不是线程安全的.
  • tmpnam生成的临时文件名,必须位于固定的路径下(/tmp).
  • 使用tmpnam创建临时文件不是一个原子行为,需要先生成临时文件名,然后调用其他I/O函数创建文件.这有可能会导致在创建文件时,该文件已经存在.

再来看看tmpfile方式:

#include <stdio.h>
FILE *tmpfile(void);

tmpfile返回一个以读写模式打开的、唯一的临时文件流指针.当文件指针关闭或程序正常结束时,该临时文件会被自动删除.

tmpfile直接返回临时的文件流指针——这个自然避免了tmpnam中潜在的线程安全问题,同时还避免了将生成文件名和创建文件分为两个步骤来执行的行为.那么tmpfile是否真的实现了原子地创建临时文件?让我们看一下tmpfile的实现,代码如下:

FILE *
tmpfile (void)
{
  char buf[FILENAME_MAX];
  int fd;
  FILE *f;
  if (__path_search (buf, FILENAME_MAX, NULL, "tmpf", 0))
    return NULL;
  int flags = 0;
#ifdef FLAGS
  flags = FLAGS;
#endif
  fd = __gen_tempname (buf, 0, flags, __GT_FILE);
  if (fd < 0)
    return NULL;
  /* Note that this relies on the UNIX semantics that
     a file is not really removed until it is closed.  */
  (void) __unlink (buf);
  if ((f = __fdopen (fd, "w+b")) == NULL)
    __close (fd);
  return f;
}

乍一看,tmpfile是通过__path_search先产生临时文件名,然后再创建该文件,最后通过文件句柄生成文件流指针.这样的过程看上去好像并不是原子的.下面,让我们深入到__gen_tempname中一探究竟.

    case __GT_FILE:
      fd = __open (tmpl,
               (flags & ~O_ACCMODE)
               | O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
      break;

在创建临时文件时,C库使用了open函数的O_CREATO_EXCL标志组合,这点保证了文件的原子性创建,从而使tmpfile创建临时文件的行为是原子的.但tmpfile也有一个缺点,与tmpnam相同,这个临时文件只能生成在固定的路径下(/tmp),并且其有可能因为文件名称冲突而失败返回NULL.

那么,有没有可以给临时文件指定目录的方法呢?下面请看mkstemp,代码如下:

#include <stdlib.h>
int mkstemp(char *template);

mkstemp会根据template创建并打开一个独一无二的临时文件.template的最后6个字符必须是“XXXXXX”.glibc库会生成一个独一无二的后缀来替换“XXXXXX”,因此要求template必须是可修改的.

mkstemp执行成功后会返回创建的临时文件的文件描述符,失败时则返回-1.下面看一下mkstemp的实现.

int #mkstemp (template)
     char *template;
{
  return __gen_tempname (template, 0, 0, __GT_FILE);
}

进入__gen_tempname后:

int #__gen_tempname (char *tmpl, int suffixlen, int flags, int kind)
{
    int len;
    char *XXXXXX;
    static uint64_t value;
    uint64_t random_time_bits;
    unsigned int count;
    int fd = -1;
    int save_errno = errno;
    struct_stat64 st;
#define ATTEMPTS_MIN (62 * 62 * 62)
    /* The number of times to attempt to generate a temporary file.  To
     conform to POSIX, this must be no smaller than TMP_MAX.  */
#if ATTEMPTS_MIN < TMP_MAX
    unsigned int attempts = TMP_MAX;
#else
    unsigned int attempts = ATTEMPTS_MIN;
#endif
  /* 检查template的合法性,检查长度及结尾的XXXXXX字符 */
    len = strlen (tmpl);
    if (len < 6 + suffixlen || memcmp (&tmpl[len - 6 - suffixlen], "XXXXXX", 6))
    {
        __set_errno (EINVAL);
        return -1;
    }
    /* 得到结尾XXXXXX起始位置 */
    XXXXXX = &tmpl[len - 6 - suffixlen];
    /* 得到“随机”数据 */
#ifdef RANDOM_BITS
    RANDOM_BITS (random_time_bits);
#else
#if HAVE_GETTIMEOFDAY || _LIBC
    {
        struct timeval tv;
        __gettimeofday (&tv, NULL);
        random_time_bits = ((uint64_t) tv.tv_usec << 16) ^
            tv.tv_sec;
    }
#else
    random_time_bits = time (NULL);
#endif
#endif
    /* 根据上面的伪随机数和进程pid生成value */
    value += random_time_bits ^ __getpid ();
    /*
    根据value得到唯一的临时文件名,如有重复则加上7777继续。
    最多重复attempts次。
    */
    for (count = 0; count < attempts; value += 7777, ++count)
    {
        uint64_t v = value;
        /*
        letters是26个英文大小写加上10个阿拉伯数字,为62个大小的字符数组。因此使用62作为除数,
        以得到随机字符。
        */
        XXXXXX[0] = letters[v % 62];
        v /= 62;
        XXXXXX[1] = letters[v % 62];
        v /= 62;
        XXXXXX[2] = letters[v % 62];
        v /= 62;
        XXXXXX[3] = letters[v % 62];
        v /= 62;
        XXXXXX[4] = letters[v % 62];
        v /= 62;
        XXXXXX[5] = letters[v % 62];
        switch (kind)
        {    case __GT_FILE:
             /* 这是mkstemp的情况,利用O_CREAT|O_EXCL创建唯一文件 */
             fd = __open (tmpl,
               (flags & ~O_ACCMODE)
               | O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
             break;
        }
        if (fd >= 0)
        {
            /* 成功创建了文件,恢复原来的errno,并返回创建的文件描述符fd */
            __set_errno (save_errno);
            return fd;
        }
        else if (errno != EEXIST) {
        /* 如失败的原因不是因为文件已经存在的时候,则直接返回。 */
            return -1;
        }
    /* 如果是其他原因,则会重新生成新的文件名,并再次尝试重建 */
    }
    /* 将errno设置为EEXIST,即文件已经存在 */
    __set_errno (EEXIST);
    return -1;
}

综上所述,在需要使用临时文件时,不推荐使用tmpnam,而要用tmpfilemkstemp。前者的局限在于不能指定路径,并且在文件名称冲突时会返回失败。后者可以由调用者来指定路径,并且在文件名称冲突时,会自动重新生成并重试。

除了上面介绍的几种方法,Linux环境还提供了这些接口的一些变种:tempnammkostempmkstemps等,分别对其原始形态进行了扩展,详细区别可以直接查看Linux手册。

临时文件生成总结

  • tmpnam:不推荐,存在线程安全问题且非原子操作。
  • tmpfile:自动删除,原子创建,但路径固定为/tmp
  • mkstemp:可指定路径(通过template参数),自动重试,推荐用于需要自定义目录的场景。
  • 其他变种如mkostempmkstemps等可参考手册。