标准 I/O 访问

更新时间:
2024-03-14
下载文档

标准 I/O 访问

标准 I/O 又称作同步 I/O,即发起传输和对 I/O 的控制都是用户主动行为,文件或设备必须在用户干预下运行,目前绝大多数应用软件都使用这一类型的 I/O 操作。SylixOS 支持 POSIX 规定的绝大多数同步 I/O 操作,下面我们将对 SylixOS 中文件和目录的操作进行详细介绍。

I/O 文件

open 函数

#include <fcntl.h>
int  open(const char  *cpcName, int     iFlag, ...);

函数 open 原型分析:

  • 此函数成功返回文件描述符,失败返回-1 并设置错误号。
  • 参数 cpcName 是需要打开的文件名。
  • 参数 iFlag 是打开文件标志。
  • 参数 ... 是可变参数。

调用 open 函数可以打开或者创建一个文件,open 函数的最后一个参数写为“...”,ISO C 用这种方法表示余下参数的数量及其类型是可变的,对于 open 函数而言,只有在创建新文件时才会用到此参数。

参数 iFlag 包含多个选项,如下表所示,多个选项之间通常以“或”的方式来构成此参数。

选项名说明
O_RDONLY以只读的方式打开文件
O_WRONLY以只写的方式打开文件
O_RDWR以可读、可写的方式打开文件
O_CREAT如果文件不存在,则创建文件,且 open 函数的第三个参数指定文件权限模式
O_TRUNC如果文件存在,而且如果以只写或读写方式成功打开,则将其长度截断为 0
O_APPEND将读写指针追加到文件的尾端
O_EXCL如果指定了 O_CREAT,而文件存在,则出错。如果文件不存在,则创建
O_NONBLOCK以非阻塞的方式打开文件
O_SYNC使每次 write 等待物理 I/O 操作完成,包括由该 write 操作引起的文件属性更新
O_DSYNC使每次 write 等待物理 I/O 操作完成,但是如果该写操作并不影响读取刚写入的数据,则不许等待文件属性被更新
O_NOCTTY如果 cpcName 引用的是终端设备,则不将该设备分配作为此进程的控制终端
O_NOFOLLOW如果 cpcName 引用的是符号链接,则出错
O_CLOEXEC把 FD_CLOEXEC 标志设置为文件描述符标志
O_LARGEFILE打开大文件标志
O_DIRECTORY如果打开的文件是一个非目录文件,则返回错误并设置错误号为 ENOTDIR

open 函数返回的文件描述符一定是系统中最小的且未使用的描述符数值,这一点被某些应用程序用在标准输入、标准输出或者标准错误上打开新的文件。例如,一个应用程序可以先关闭标准输出(文件描述符 1),然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符 1 上打开。在说明 dup2 时,会了解到有更好的方法来保证在一个给定的文件描述符上打开另一个文件。

SylixOS 的 I/O 系统最大可支持 2TB 的文件,但是受限于某些文件系统设计,例如 FAT 文件系统最大只能支持 4GB 的文件大小。在应用程序中,为了能够显式地指定打开大的文件,在调用 open 函数的时候需要指定 O_LARGEFILE 标志。SylixOS 也提供了下列函数来打开大的文件。

#include <fcntl.h>
int  open64(const char  *cpcName, int  iFlag, ...);

函数 open64 原型分析:

  • 此函数成功返回文件描述符,失败返回-1 并设置错误号。
  • 参数 cpcName 是要打开的文件名。
  • 参数 iFlag 是打开文件标志。
  • 参数 ... 是可变参数。

creat 函数

#include <fcntl.h>
int  creat(const char  *cpcName, int  iMode);

函数 creat 原型分析:

  • 此函数成功返回文件描述符,失败返回-1 并设置错误号。
  • 参数 cpcName 是要创建的文件名。
  • 参数 iMode 是创建文件的模式。

调用 creat 函数可以创建一个文件,此函数等效于下面函数调用:

open(cpcName, O_WRONLY | O_CREAT | O_TRUNC, iMode);

creat 函数的一个不足之处是它以只写的方式打开所创建的文件。如果通过 creat 函数创建一个文件,然后读这个文件,则必须要先调用 creat 函数创建这个文件,再调用 close 关闭这个文件,再以读的方式打开这个文件才行,而这种方式可通过调用 open 函数直接实现:

open(cpcName, O_RDWR | O_CREAT | O_TRUNC, iMode);

close 函数

#include <unistd.h>
int  close(int  iFd);

函数 close 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。

调用 close 函数会将文件描述符的引用计数和文件的总引用计数减一,当文件描述符的引用计数为零时,则删除此文件描述符(介绍 dup 函数将看到这一点),当总引用计数减为零时将关闭这个文件,并且会释放当前进程加在该文件上的所有记录锁。

当一个进程终止时,内核自动关闭它打开的所有文件,许多程序都利用了这一功能不显式地调用 close 函数关闭打开的文件。

read 函数

#include <unistd.h>
ssize_t  read(int      iFd,
              void    *pvBuffer,
              size_t   stMaxBytes);

函数 read 原型分析:

  • 此函数成功返回读取的字节数,失败返回 -1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 输出参数 pvBuffer 是接收缓冲区。
  • 参数 stMaxBytes 是接收缓冲区大小。

调用 read 函数可以从打开的文件中读取数据,有很多情况实际读到的字节数少于要求读的字节数。

  • 读普通文件时,在读到要求字节数之前已到达了文件尾端。
  • 当从终端设备读时,通常一次最多读一行。
  • 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
  • 当被信号中断,而已经读了部分数据时。

通常情况,我们需要通过 read 的返回值来判断读取数据的数量与正确性。

write 函数

#include <unistd.h>
ssize_t  write(int           iFd,
               const void   *pvBuffer,
               size_t        stNBytes);

函数 write 原型分析:

  • 此函数成功返回写的字节数,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 pvBuffer 是要写入文件的数据缓冲区地址。
  • 参数 stNBytes 是写入文件的字节数。

调用函数 write 向打开的文件中写入数据,其返回值通常与参数 stNBytes 数值相同,否则表示出错。write 出错的一个常见原因是磁盘已满,或者超过一个进程的文件长度限制。

对于普通文件,写操作从文件的当前偏移量处开始,如果在打开文件时指定了 O_APPEND 标志,则在每次操作之前,将文件偏移量设置在文件的结尾处。在一次写成功之后,该文件偏移量在文件末尾增加实际写的字节数。

lseek 函数

每一个打开的文件都有一个与其相关联的当前文件偏移量,这通常是一个整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件偏移量开始,并使偏移量增加所读写的字节数。SylixOS 默认的情况,当打开一个文件时,除非指定了 O_APPEND 标志,否则当前文件偏移量总是被设置为 0。

#include <fcntl.h>
off_t   lseek(int      iFd,
              off_t    oftOffset,
              int      iWhence);

函数 lseek 原型分析:

  • 此函数成功返回新的文件偏移量,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 oftOffset 是偏移量。
  • 参数 iWhence 是定位基准。

调用 lseek 函数可以显式地为一个已打开的文件设置偏移量,注意,lseek 调用只是调整内核中与文件描述符相关的文件偏移量记录,并没有引起对任何物理设备的访问。

参数 oftOffset 的意义根据参数 iWhence 的不同而不同,如下表所示:

iWhence 值oftOffset 说明
SEEK_SET将文件的偏移量设置为距文件开始处 oftOffset 个字节。
SEEK_CUR将文件的偏移量设置为当前值加 oftOffset 个字节,oftOffset 可为负。
SEEK_END将文件的偏移量设置为文件长度加 oftOffset 个字节,oftOffset 可为负。

如下图所示 iWhence 参数含义。

这里给出了函数 lseek 调用的一些例子,注释中说明了将当前文件指针移到的具体位置。

lseek(fd, 0, SEEK_SET);                    /*  文件开始处                     */
lseek(fd, 0, SEEK_END);                    /*  文件结尾处                     */
lseek(fd, -1, SEEK_END);                   /*  文件倒数第一个字节处(N-1)        */
lseek(fd, -20, SEEK_CUR);                  /*  文件当前位置之前的20个字节处      */
lseek(fd, 100, SEEK_END);                  /*  文件末尾处扩展100个字节(N+100)   */

如果程序的文件偏移量已经跨越了文件结尾,再执行 I/O 操作时,read 函数调用将返回 0,表示文件结尾;但是 write 函数可以在文件结尾后的任意位置写入数据。

从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看,文件空洞中是存在字节的,读取空洞将返回以 0 填充的缓冲区。

空洞的存在意味着一个文件名义上的大小可能要比其占用的磁盘存储总量要大(有时会大很多),当然,具体的处理方式与文件系统的实现有关(TpsFs 文件系统中空洞将使文件变大)。

下面的实例展示了 lseek 函数的使用,该程序创建一个新的文件,通过调用 lseek 函数使文件产生空洞,然后在文件尾处写入一些数据,这样程序可以读取文件空洞部分,且是不可见字符,程序打印“\0”代表不可见字符。

#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <unistd.h>
#define FILE_MODE      (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)
int main (int argc, char *argv[])
{
    int      fd, i;
    off_t    ret;
    ssize_t  size;
    char    *buf1 = "sylixos";
    char     buf2[16];

    fd = open("./file", O_RDWR | O_CREAT | O_TRUNC, FILE_MODE);
    if (fd < 0) {
        fprintf(stderr, "Create file failed.\n");
        return -1;
    }

    write(fd, buf1, 7);
    ret = lseek(fd, 1000, SEEK_SET);
    if (ret < 0) {
        fprintf(stderr, "lseek failed.\n");
        close(fd);
        return -1;
    }

    write(fd, buf1, 7);
    lseek(fd, -7, SEEK_END);
    size = read(fd, buf2, 7);
    if (size < 0) {
        fprintf(stderr, "read error.\n");
        close(fd);
        return -1;
    }

    for (i = 0; i < size; i++) {
        if (!isprint(buf2[i])) {
            fprintf(stdout, "\\0");
        } else {
            fprintf(stdout, "%c", buf2[i]);
        }
    }

    fprintf(stdout, "\n");
    lseek(fd, -14, SEEK_END);
    size = read(fd, buf2, 7);
    if (size < 0) {
        fprintf(stderr, "read error.\n");
        close(fd);
        return -1;
    }

    for (i = 0; i < size; i++) {
        if (!isprint(buf2[i])) {
            fprintf(stdout, "\\0");
        } else {
            fprintf(stdout, "%c", buf2[i]);
        }
    }
    fprintf(stdout, "\n");
    close(fd);
    return 0;
}

在 SylixOS Shell 下运行这段程序,程序结果显示正确读取了写入的数据和文件空洞部分的内容。

# ./Function_lseek
sylixos
\0\0\0\0\0\0\0

pread 和 pwrite 函数

在“I/O系统结构”中介绍了 SylixOS 中多个进程可以读取同一个文件,每一个进程都有它自己的文件结构,其中也有它自己的当前文件偏移量。但是,在非 NEW_1 型文件系统中(没有唯一的文件节点存在),当多个进程写同一文件时,则可能产生预想不到的结果。为了说明如何避免这种情况,需要了解原子操作的概念。

考虑下面代码,在进程中打开一个文件向其中追加数据。

……
    ret = lseek(fd, 0, SEEK_END);
    if (ret < 0) {
        fprintf(stderr, "Lseek error.\n");
    }

    ret = write(fd, buf, 10);
    if (ret != 10) {
        fprintf(stderr, "Write data error.\n")
    }
……

这段代码在单进程的情况是没有问题的,事实也证明了这一点,但是如果有多个进程时,使用这种方法追加数据到文件将会产生问题。

假如有两个独立的进程 1 和进程 2 同时对一个文件进行追加写操作,每个进程打开文件时都没有使用 O_APPEND 标志,此时各数据结构的关系如下图所示,每个进程都有它自己的文件结构和文件当前偏移量,但是共享了一个文件节点。假如进程 1 调用了 lseek 函数将文件当前偏移量设置到了文件尾,此时进程 2 运行,也调用了 lseek 函数,也将文件当前偏移量设置到了文件尾。接着进程 2 调用 write 函数将进程 2 的文件偏移量推后了 10 个字节,此时文件变长,内核将文件节点中的文件长度也增加了 10 个字节。而后,内核切换进程 1 运行,调用 write 函数,此时进程 1 就从自己的当前偏移量开始写,这样就覆盖了进程 2 刚才写入的数据。

从上面的过程可以看出,问题出在“先定位到文件尾,再写文件”上,这个过程使用了两个函数来完成,这样就造成了一个非原子性的操作,因为在两个函数之间可能会造成进程的切换。所以,我们可以得出,如果这个过程是在一个函数中完成(形成一个原子性的操作)问题就可以解决。

SylixOS 为这样操作提供了一个原子性的操作方法,在打开文件时设置 O_APPEND 标志,这样内核在每次写操作时,都会将当前偏移量设置为文件尾,也就不用每次写之前再调用 lseek 函数。

SylixOS 提供了一种原子性的定位并执行 I/O 操作的函数:pread、pwrite。

#include <unistd.h>
ssize_t  pread(int         iFd,
               void       *pvBuffer,
               size_t      stMaxBytes,
               off_t       oftPos);
ssize_t  pwrite(int             iFd,
                const void     *pvBuffer,
                size_t          stNBytes,
                off_t           oftPos);

函数 pread 原型分析:

  • 此函数成功返回读的字节数,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 输出参数 pvBuffer 是接收缓冲区。
  • 参数 stMaxBytes 是缓冲区大小。
  • 参数 oftPos 指定读的位置。

函数 pwrite 原型分析:

  • 此函数成功返回写的字节数,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 pvBuffer 是数据缓冲区。
  • 参数 stNBytes 是写的字节数。
  • 参数 oftPos 指定写的位置。

调用 pread 函数相当于先调用 lseek 函数后调用 read 函数,但是 pread 函数与这种顺序有下列重要区别:

  • 调用 pread 函数时,无法中断其定位和读操作(原子操作过程)。
  • 不更新当前文件偏移量。

调用 pwrite 函数相当于先调用 lseek 函数后调用 write 函数,但也与上述有类似的区别。

一般而言,原子操作指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

为了能够读写更大的文件(通常大于 4GB),SylixOS 提供了下面一组函数。

#include <unistd.h>
ssize_t  pread64(INT        iFd,
                 PVOID      pvBuffer,
                 size_t     stMaxBytes,
                 off64_t    oftPos);
ssize_t  pwrite64(INT       iFd,
                  CPVOID    pvBuffer,
                  size_t    stNBytes,
                  off_t64   oftPos);

函数 pread64 原型分析:

  • 此函数成功返回读的字节数,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 输出参数 pvBuffer 是接收缓冲区。
  • 参数 stMaxBytes 是缓冲区大小。
  • 参数 oftPos 指定读的位置。

函数 pwrite64 原型分析:

  • 此函数成功返回写的字节数,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 pvBuffer 是数据缓冲区。
  • 参数 stNBytes 是写的字节数。
  • 参数 oftPos 指定写的位置。

下面的实例展示了 pwrite 函数和 pread 函数的用法,该程序创建一个新文件,通过调用 pwrite 函数向文件指定的偏移量处写入数据,然后调用 read 函数验证文件的当前偏移量,调用 pread 函数也验证文件产生了空洞。

#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <unistd.h>

#define FILE_MODE     (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)

int main (int argc, char *argv[])
{
    int             fd, i;
    ssize_t         size;
    char           *buf1 = "sylixos";
    char            buf2[16];

    fd = open("./file", O_RDWR | O_CREAT | O_TRUNC, FILE_MODE);
    if (fd < 0) {
        fprintf(stderr, "Create file failed.\n");
        return (-1);
    }

    size = pwrite(fd, buf1, 7, 100);
    if (size != 7) {
        fprintf(stderr, "pwrite error.\n");
        close(fd);
        return -1;
    }

    size = read(fd, buf2, 7);
    if (size < 0) {
        fprintf(stderr, "read error.\n");
        close(fd);
        return  (-1);
    }

    for (i = 0; i < size; i++) {
        if (!isprint(buf2[i])) {
            fprintf(stdout, "\\0");
        } else {
            fprintf(stdout, "%c", buf2[i]);
        }
    }

    fprintf(stdout, "\n");
    size = pread(fd, buf2, 7, 100);
    if (size < 0) {
        fprintf(stderr, "pread error.\n");
        close(fd);
        return  (-1);
    }

    for (i = 0; i < size; i++) {
        if (!isprint(buf2[i])) {
            fprintf(stdout, "\\0");
        } else {
            fprintf(stdout, "%c", buf2[i]);
        }
    }

    fprintf(stdout, "\n");
    close(fd);
    return  (0);
}

在 SylixOS Shell 下运行这段程序:

# ./Functions_pwrite_pread
\0\0\0\0\0\0\0
sylixos

打印结果显示调用 pwrite 函数后文件产生了空洞,并且文件当前偏移量没有改变,这也证实了前面所说的 pwrite 函数相当于先调用函数 lseek 再调用函数 write,然而不同的是 pwrite 是一个原子操作。

dup 和 dup2 函数

#include <unistd.h>
int  dup(int  iFd);
int  dup2(int  iFd1, int  iFd2);

函数 dup 原型分析:

  • 此函数成功返回新文件描述符,失败返回-1 并设置错误号。
  • 参数 iFd 是原始文件描述符。

函数 dup2 原型分析:

  • 此函数成功返回 iFd2 文件描述符,失败返回-1 并设置错误号。
  • 参数 iFd1 是文件描述符 1。
  • 参数 iFd2 是文件描述符 2。

调用 dup 函数和 dup2 函数可以复制一个现有的文件描述符,由 dup 函数返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于 dup2 函数可以用参数 iFd2 指定新文件描述符的值。如果 iFd2 已经打开,则先将其关闭, iFd2 的 FD_CLOEXEC 文件描述符标志将被清除,这样 iFd2 在进程调用 exec 函数(见进程管理)时是打开状态。注意,SylixOS 内核目前并不支持 iFd1 等于 iFd2 的情况。

dup 函数返回的文件描述符与参数 iFd 共享同一个文件结构项(文件表项),相同地,dup2 函数的文件描述符 iFd1 和 iFd2 也共享同一个文件结构项,如下图所示(fd3 和 fd4 共享同一个文件结构项)。

下图中,进程中调用了:

fd = dup(3);

假设文件描述符 3 已被占用(这是很有可能的),此时我们调用 dup 函数将可能使用文件描述符 4,因为两个文件描述符指向同一个文件结构(文件表项),所以,它们共享同一文件属性标志(读、写、追加等)以及同一文件当前指针(文件偏移量)。

每个文件都有它自己的一套文件描述符标志,新的文件描述符标志(FD_CLOEXEC)总是由 dup 函数清除。

复制描述符的另一种方法是使用 fcntl 函数,之后的小节将介绍该函数,实际上,调用

dup(fd);

等效于

fcntl(fd, F_DUPFD, 0);

而调用

dup2(fd, fd2);

等效于

fcntl(fd, F_DUP2FD, fd2);

或者

close(fd2);
fcntl(fd, F_DUPFD, fd2);

前面介绍过 SylixOS 每个进程都有自己的一个文件描述符表,同时内核也存在一个全局的文件描述符表。那么如果在进程中打开一个文件,内核是看不到这个文件描述符的,但是有一些情况需要内核操作进程打开的文件描述符(例如:日志系统中向应用程序指定的文件中写入数据)。SylixOS 提供了下面函数以实现进程文件描述符向内核空间的复制。

#include <unistd.h>
int  dup2kernel(int fd);

函数 dup2kernel 原型分析:

  • 此函数成功返回内核文件描述符,失败返回-1 并设置错误号。
  • 参数 fd 是进程文件描述符。

下面程序展示了 dup 函数的使用方法,程序创建新的文件,并调用 dup 函数复制一个新的文件描述符,然后在新的文件描述符上对创建的文件进行操作。

#include <stdio.h>
#include <fcntl.h>

int main (int argc, char *argv[])
{
    int      fd, newfd;
    char     buf[64] = {0};

    fd = open("./file", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd < 0) {
        fprintf(stderr, "open file: %s failed.\n", "./file");
        return -1;
    }

    newfd = dup(fd);
    if (newfd < 0) {
        fprintf(stderr, "dup error.\n");
        close(fd);
        return -1;
    }

    write(newfd, "sylixos", 7);
    lseek(newfd, 0, SEEK_SET);
    read(fd, buf, 7);
    fprintf(stdout, "buf: %s\n", buf);
    return 0;
}

在 SylixOS Shell 运行这段程序,运行结果说明 lseek 操作 newfd 等效于操作 fd。

# ./Function_dup
buf: sylixos

sync、fsync 和 fdatasync 函数

SylixOS 在内核中设有磁盘高速缓存,大多数磁盘 I/O 都通过缓冲区进行。当我们向文件中写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,恰当的时候再写入磁盘(由线程“t_diskcache”完成),这种方式被称为延迟写。

通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,SylixOS 提供了 sync、fsync、和 fdatasync 三个函数。

#include <fcntl.h>
void  sync(void);
int   fsync(int  iFd);
int   fdatasync(int  iFd);

函数 fsync 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。

函数 fdatasync 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。

sync 函数是将系统中所有修改过的磁盘高速缓冲排入写队列,然后等待实际写磁盘操作结束后返回。

fsync 函数只对由文件描述符 iFd 指定的一个文件起作用。

fdatasync 函数类似于 fsync 函数,但它只影响文件的数据部分,除数据外,fsync 还会同步更新文件的属性。

fcntl 函数

#include <fcntl.h>
int  fcntl(int  iFd, int  iCmd, ...);

函数 fcntl 原型分析:

  • 此函数成功时根据参数 iCmd 的不同而返回不同的值,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 iCmd 是命令。
  • 参数 ... 是命令参数。

调用 fcntl 函数可以改变已经打开文件的属性,在本节的实例中,第 3 个参数总是一个整数,但是在说明记录锁时,第 3 个参数则是指向一个结构的指针。

SylixOS fcntl 函数支持以下 4 种功能:

  • 复制一个已有的文件描述符(iCmd = F_DUPFD、F_DUPFD_CLOEXEC、F_DUP2FD、F_DUP2FD_CLOEXEC)。
  • 获取/设置文件描述符标志(iCmd = F_GETFD、F_SETFD)。
  • 获取/设置文件属性标志(iCmd = F_GETFL、F_SETFL)。
  • 获取/设置文件记录锁(iCmd = F_GETLK、F_SETLK、F_SETLKW)。

下表介绍了前 3 种命令的功能,记录锁功能将在文件记录锁小节作详细介绍。

命令说明
F_DUPFD复制文件描述符,等效于 dup 和 dup2 函数
F_DUPFD_CLOEXEC复制文件描述符,且设置文件描述符标志
F_DUP2FD复制文件描述符,等效于 dup2 函数
F_DUP2FD_CLOEXEC复制文件描述符,且设置文件描述符标志
F_GETFD获得文件描述符标志(FD_CLOEXEC),作为返回值返回
F_SETFD设置文件描述符标志(FD_CLOEXEC)
F_GETFL获得文件属性标志,作为返回值返回
F_SETFL设置文件属性标志

下面程序展示了通过调用 fcntl 函数来获得文件的属性标志,用户输入不同的文件打开属性标志来验证 fcntl 函数获得的属性标志也不同。

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
int main (int argc, char *argv[])
{
    int  fd;
    int  flags, inflags = 0;

    if (argc < 3) {
        fprintf(stderr, "Don't find parse files.\n");
        return (-1);
    }
    if (!strcmp(argv[2], "O_RDONLY")) {
        inflags = O_RDONLY;
    }
    if (!strcmp(argv[2], "O_WRONLY")) {
        inflags = O_WRONLY;
    }
    if (!strcmp(argv[2], "O_RDWR")) {
        inflags = O_RDWR;
    }

    fd = open(argv[1], inflags);
    if (fd < 0) {
        fprintf(stderr, "open file: %s failed.\n", argv[1]);
        return  (-1);
    }

    flags = fcntl(fd, F_GETFL, 0);
    switch (flags & O_ACCMODE) {
    case O_RDONLY:
        fprintf(stdout, "file: %s read only!\n", argv[1]);
        break;
    case O_WRONLY:
        fprintf(stdout, "file: %s write only!\n", argv[1]);
        break;
    case O_RDWR:
        fprintf(stdout, "file: %s read write.\n", argv[1]);
        break;
    default:
        fprintf(stdout, "file: %s flags: %x\n", argv[1], flags);
        break;
    }
    close(fd);
    return (0);
}

在 SylixOS Shell 下运行程序,从程序运行结果看,打开文件的属性标志不同,fcntl 函数获得的文件属性标志也跟着变化。

# touch test.file
# ./Function_fcntl test.file O_RDONLY
file: test.file read only!
# ./Function_fcntl test.file O_RDWR
file: test.file read write.

ioctl 函数

#include <fcntl.h>
int  ioctl(int   iFd, int   iFunction,...);

函数 ioctl 原型分析:

  • 此函数成功返回 0,失败返回-1。
  • 参数 iFd 是文件描述符。
  • 参数 iFunction 是功能。
  • 参数 ... 是功能参数。

对于 I/O 操作,ioctl 函数可以看成一个“百宝箱”,一些 I/O 函数做不了的事情,可以用 ioctl 函数来完成,在终端 I/O 中用了大量的 ioctl 操作。

每个设备驱动程序可以定义它自己专用的一组 ioctl 命令,系统则为不同种类的设备提供了通用的 ioctl 命令。

文件和目录

在上一节中我们讨论了文件的基本操作:打开文件、读文件、写文件等。这一节我们来介绍文件的其他特性,以及这些特性的修改方法,最后介绍 SylixOS 中的符号链接。

stat、lstat 和 fstat 函数

#include <sys/stat.h>
int  stat(const char  *pcName, struct stat *pstat);
int  lstat(const char  *pcName, struct stat *pstat);
int  fstat(int  iFd, struct stat *pstat);

函数 stat 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcName 是文件名。
  • 输出参数 pstat 返回文件状态信息。

函数 lstat 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcName 是文件名。
  • 输出参数 pstat 返回文件状态信息。

函数 fstat 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 输出参数 pstat 返回文件状态信息。

调用 stat 函数将通过参数 pstat 返回 pcName 文件的状态信息,调用 fstat 函数将获得已在描述符 iFd 上打开文件的有关信息,lstat 函数类似 stat 函数,但是当传入的文件名是符号链接名字时,lstat 函数将获得符号链接的相关信息,而不是符号链接所指实际文件的信息(之后的小节将详细说明符号链接)。

参数 pstat 是需要用户提供的一个状态缓冲区,该指针指向 stat 结构体类型缓冲区,这个结构体如下所示。

struct stat {
    dev_t         st_dev;                       /* device                           */
    ino_t         st_ino;                       /* inode                            */
    mode_t        st_mode;                      /* protection                      */
    nlink_t       st_nlink;                     /* number of hard links           */
    uid_t         st_uid;                       /* user ID of owner               */
    gid_t         st_gid;                        /* group ID of owner              */
    dev_t         st_rdev;                      /* device type (if inode device)*/
    off_t         st_size;                      /* total size, in bytes           */
    time_t        st_atime;                     /* time of last access            */
    time_t        st_mtime;                     /* time of last modification     */
    time_t        st_ctime;                     /* time of last create            */
    blksize_t     st_blksize;                  /* blocksize for filesystem I/O */
blkcnt_t      st_blocks;                   /* number of blocks allocated    */
    ...
};

在 stat 结构体中基本上都是系统的基本数据类型,SylixOS 中 stat 函数用得最多的地方就是 ll 命令,此命令可以获得以下一些文件信息:

# ll
-rw-r--r-- root     root     Tue Jul 07 10:22:28 2015      0 B, test.file
-rwxr-xr-x root     root     Tue Jul 07 10:18:51 2015    233KB, app

接下来,我们重点介绍一下文件模式(st_mode 信息),在“文件类型”小节中我们介绍过文件类型,下表列出了这些类型在 st_mode 中对应的位信息。

st_mode 位说明
S_IFIFOFIFO 文件
S_IFCHR字符设备文件
S_IFDIR目录文件
S_IFBLK块设备文件
S_IFREG普通文件
S_IFLNK符号链接文件
S_IFSOCK套接字文件

所有的这些文件类型都有访问权限,每个文件有 9 个访问权限位,正如 ll 命令输出的第一列那样。可将这些访问权限位分成 3 类,如下表所示。

st_mode 位说明
S_IRUSR
S_IWUSR
S_IXUSR
用户读
用户写
用户执行
S_IRGRP
S_IWGRP
S_IXGRP
组读
组写
组执行
S_IROTH
S_IWOTH
S_IXOTH
其他读
其他写
其他执行

表所示的各组中,术语“用户”指的是文件所有者(owner),“组”指的是所有者所在组,“其他”指的是不属于这个组的其他用户, chmod 命令可以修改这 9 个权限位,需要注意的是,Linux 等系统中 chmod 命令修改权限可用 u 表示用户,用 g 表示组,用 o 表示其他,SylixOS 则直接使用数字进行表示,例如:755 表示-rwxr-xr-x。

当我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录应具有执行权限。例如,为了打开文件/apps/app/test.c 需要对目录/apps、/apps/app 具有执行权限位。

对于一个文件的读权限位决定了我们是否能够打开现有文件进行读操作。这与 open 函数的 O_RDONLY 和 O_RDWR 标志有关,当然写的情况也类似。

access 函数

#include <unistd.h>
int  access(const char  *pcName, int  iMode);

函数 access 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcName 是文件名。
  • 参数 iMode 是访问模式。

access 函数按文件所有者对文件的访问权限进行测试,access 函数测试模式如下表所示。

iMode 位说明
R_OK文件可读
W_OK文件可写
X_OK文件可执行
F_OK文件存在

下面程序展示了 access 函数的使用方法,程序从 Shell 接口读取用户提供的文件,判断此文件是否可写,并判断是否能够成功打开。

#include <stdio.h>
#include <fcntl.h>
int main (int argc, char *argv[])
{
    int  fd;

    if (argc != 2) {
        fprintf(stderr, "%s [filename].\n", argv[0]);
        return -1;
    }
    if (access(argv[1], W_OK) < 0) {
        fprintf(stdout, "%s can't write.\n", argv[1]);
    } else {
        fprintf(stdout, "%s can write.\n", argv[1]);
    }
    if ((fd = open(argv[1], O_WRONLY)) < 0) {
        fprintf(stdout, "open file failed.\n");
    } else {
        fprintf(stdout, "open file success.\n");
        close(fd);
    }
    return 0;
}

在 SylixOS Shell 下运行这段程序:

# touch a.c
# touch b.c
# chmod 000 a.c
# ll
-r-------- root     root     Mon Dec 28 14:00:04 2020      0 B, a.c
-rw-r--r-- root     root     Mon Dec 28 14:00:07 2020      0 B, b.c
# ./Function_access b.c
b.c can write.
open file success.
# ./Function_access a.c
a.c can't write.
open file failed.

从上面的例子可以看出,通过设置文件的访问权限后,open 函数不能正常打开此文件。

umask 函数

#include <sys/stat.h>
mode_t  umask(mode_t modeMask);

函数 umask 原型分析:

  • 此函数返回之前的屏蔽字。
  • 参数 modeMask 是新的屏蔽字。

umask 函数为当前进程设置文件创建屏蔽字,并返回之前的值,这是一个没有错误值返回的函数。

其中,参数 modeMask 是由下表访问权限位列出的常量中的若干个按位“或”构成的。

在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字(在介绍 open 函数、creat 函数时,这两个函数都有一个模式参数,它指定了新文件的访问权限),在文件屏蔽字中为 1 的位,文件中相应的访问权限位一定被关闭,需要注意的是,SylixOS 内核在创建新文件时所有者的读权限不会被屏蔽(这样保证了文件的所有者能够正常读文件)。

st_mode 位说明
S_IRUSR
S_IWUSR
S_IXUSR
用户读
用户写
用户执行
S_IRGRP
S_IWGRP
S_IXGRP
组读
组写
组执行
S_IROTH
S_IWOTH
S_IXOTH
其他读
其他写
其他执行

下面程序展示了 umask 函数的使用方法,程序创建两个文件,创建第一个时,umask 值为 0,也不屏蔽任何权限位,将按内核默认的权限模式创建文件,创建第二个时,umask 值禁止了组和其他的读、写权限。

#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>

#define FILE_MODE  (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)

int main (int argc, char *argv[])
{
    umask(0);
    if (creat("./a.c", FILE_MODE) < 0) {
        fprintf(stderr, "create file failed.\n");
        return -1;
    }

    umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
    if (creat("./b.c", FILE_MODE) < 0) {
        fprintf(stderr, "create file failed.\n");
        return -1;
    }

    return 0;
}

在 SylixOS Shell 下运行这段程序,从运行结果中可以发现文件权限位受进程屏蔽字的影响。

# ./Function_umask
# ll
-rw-rw-rw- root     root     Mon Dec 28 14:10:22 2020      0 B, a.c
-rw------- root     root     Mon Dec 28 14:10:22 2020      0 B, b.c

fchmod、chmod 函数

#include <sys/stat.h>
int  fchmod(int  iFd, int  iMode);
int  chmod(const char  *pcName, int  iMode);

函数 fchmod 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 iMode 是要设置的模式。

函数 chmod 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcName 是文件名。
  • 参数 iMode 是要设置的模式。

调用 chmod 函数和 fchmod 函数可以改变现有文件的访问权限。chmod 函数在指定的文件上进行操作,fchmod 函数对已打开的文件进行操作。

下面程序展示了 chmod 函数的使用,程序实现了两个操作,一个操作是在文件原来的访问权限基础上去掉某个权限,另一个操作是设定某些访问权限位。

#include <stdio.h>
#include <sys/stat.h>
int main (int argc, char *argv[])
{
    struct stat  newstat;

    if (stat("./a.c", &newstat) < 0) {
        fprintf(stderr, "stat error.\n");
        return -1;
    }
    if (chmod("./a.c", (newstat.st_mode & ~S_IRGRP)) < 0) {
        fprintf(stderr, "drop mode error.\n");
        return -1;
    }
    if (chmod("./b.c", S_IRUSR | S_IWGRP) < 0) {
        fprintf(stderr, "set mode error.\n");
    }
    return 0;
}

在 SylixOS Shell 下运行这段程序,从程序运行结果可以看出 chmod 函数正确地设置了相应的访问权限位。

# ll
-rw-rw-rw- root     root     Mon Dec 28 14:10:22 2020      0 B, a.c
-rw------- root     root     Mon Dec 28 14:10:22 2020      0 B, b.c
# ./Function_chmod
# ll
-rw--w-rw- root     root     Mon Dec 28 14:10:22 2020      0 B, a.c
-r---w---- root     root     Mon Dec 28 14:10:22 2020      0 B, b.c

unlink、remove 函数

#include <unistd.h>
int  unlink(const char  *pcName);

函数 unlink 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcName 是要删除的文件名。

调用 unlink 函数可以删除一个文件。删除一个文件需要满足一定的条件,当文件引用计数达到 0 时,文件才可以被删除,当有进程打开了该文件,其不能被删除。删除一个文件时,内核首先检查打开该文件的进程个数,如果个数达到了 0,内核再去检查其引用计数,如果也是 0,那么就删除该文件。

如果参数 pcName 是一个符号链接的名字,此符号链接将被删除,而不是删除该符号链接所引用的文件。

也可以调用 ANSI C 中的 remove 函数删除一个文件。

#include <stdio.h>
int  remove(const char *file);

函数 remove 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 file 是要删除的文件名。

rename 函数

#include <stdio.h>
int  rename(const char  *pcOldName, const char  *pcNewName);

函数 rename 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcOldName 是旧文件名。
  • 参数 pcNewName 是改后的文件名。

文件或者目录可以用 rename 函数进行重命名。根据参数 pcOldName 的不同,有下列几种情况需要说明:

  • 如果 pcOldName 指向一个非目录文件,则有以下两种情况:
    • 如果 pcNewName 已存在,则 pcNewName 不能是一个目录文件。
    • 如果 pcNewName 已存在,且不是目录文件,则先将其删除,然后重命名 pcOldName。
  • 如果 pcOldName 指向一个目录文件,则有以下两种情况:
    • 如果 pcNewName 已存在,则 pcNewName 必须是一个空目录。
    • 如果 pcNewName 已存在,则先将其删除,然后重命名 pcOldName。
  • 如果 pcOldName、 pcNewName 是同一个文件,则函数安静的返回不做任何修改。

注意:
SylixOS 中,如果 pcOldName 是一个符号链接名,调用 rename 函数将修改符号链接所指向的真正文件的名字,这一点需要特别注意。

opendir、closedir 函数

#include <dirent.h>
DIR *opendir(const char *pathname);
int  closedir(DIR *dir);

函数 opendir 原型分析:

  • 此函数成功返回目录指针,失败返回 NULL 并设置错误号。
  • 参数 pathname 是目录名。

函数 closedir 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 dir 是目录指针。

调用 opendir 函数将打开 pathname 指向的目录并返回 DIR 类型的目录指针,这个目录指针指向目录的开始位置。需要注意的是,opendir 函数将以只读的方式打开目录,这意味着,打开的目录必须是存在的,否则将返回 NULL 并设置 errno 为 ENOENT。如果 pathname 不是一个有效目录文件,则返回 NULL 并设置 errno 为 ENOTDIR。

调用 closedir 函数将关闭 dir 指向的目录(dir 由 opendir 函数返回)。

readdir、readdir_r 函数

#include <dirent.h>
struct dirent  *readdir(DIR   *dir);
int  readdir_r(DIR             *pdir, 
               struct dirent   *pdirentEntry,
               struct dirent  **ppdirentResult);

函数 readdir 原型分析:

  • 此函数成功返回目录信息指针,失败返回 NULL 并设置错误号。
  • 参数 dir 是已打开的目录指针。

函数 readdir_r 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pdir 是已打开的目录指针。
  • 输出参数 pdirentEntry 返回目录信息。

调用 readdir 函数将返回指定目录的目录信息,readdir 函数是不可重入的。readdir_r 函数是 readdir 函数的可重入实现,pdirentEntry 指向用户缓冲区用于存放目录信息,如果读到目录末尾,则 ppdirentResult 等于 NULL。

读取的目录信息存放在 dirent 结构体中,如程序清单如下所示:

struct dirent {
    char            d_name[NAME_MAX + 1];   /*  文件名                      */
    unsigned char   d_type;                 /*  文件类型 (可能为 DT_UNKNOWN) */
    char            d_shortname[13];        /*  fat短文件名 (可能不存在)      */
    ……
};

d_name 成员保存了目录中文件的名字,d_type 指示了该文件的类型如下表所示。

宏名文件类型
S_ISDIR(mode)目录文件
S_ISCHR(mode)字符设备文件
S_ISBLK(mode)块设备文件
S_ISREG(mode)普通文件
S_ISLNK(mode)符号链接文件
S_ISFIFO(mode)管道或命名管道
S_ISSOCK(mode)套接字文件

通过下面的宏可实现文件类型和文件类型模式位(如下表所示)的互转。

st_mode 位说明
S_IFIFOFIFO 文件
S_IFCHR字符设备文件
S_IFDIR目录文件
S_IFBLK块设备文件
S_IFREG普通文件
S_IFLNK符号链接文件
S_IFSOCK套接字文件
#include <dirent.h>
unsigned char IFTODT(mode_t mode);
mode_t DTTOIF(unsigned char dtype);

下面程序展示了操作目录函数的使用方法。程序打开一个指定的目录(如“/”)并读取目录信息,然后显示目录中文件的名字和类型。

#include <stdio.h>
#include <dirent.h>
#include <string.h>

#define  DIR_PATH   "/"

char  *file_type (char type, char *name, int len)
{
    if (!name) {
        return  (NULL);
    }
    if (S_ISDIR(DTTOIF(type))) {
        strlcpy(name, "directory", len);
        return  (name);
    }
    if (S_ISREG(DTTOIF(type))) {
        strlcpy(name, "regular", len);
        return  (name);
    }
    if (S_ISSOCK(DTTOIF(type))) {
        strlcpy(name, "socket", len);
        return  (name);
    }
    if (S_ISLNK(DTTOIF(type))) {
        strlcpy(name, "link", len);
        return  (name);
    }
    return  (NULL);
}

int main (int argc, char *argv[])
{
    DIR              *dir;
    struct dirent     dirinfo;
    struct dirent    *tempdir;
    int               ret;
    char              name[64];

    dir = opendir(DIR_PATH);
    if (dir == NULL) {
        return  (-1);
    }
    while (((ret = readdir_r(dir, &dirinfo, &tempdir)) == 0) && tempdir) {
        fprintf(stdout, "file: %s type is: %s file\n",
                dirinfo.d_name, file_type(dirinfo.d_type, name, sizeof(name)));
    }
    closedir(dir);
    return  (0);
}

在 SylixOS Shell 下运行程序:

# Display_Directory_Information
file: apps type is: link file
file: bin type is: link file
file: boot type is: link file
file: etc type is: link file
file: home type is: link file
file: lib type is: link file
file: qt type is: link file
file: root type is: link file
file: sbin type is: link file
file: tmp type is: link file
file: usr type is: link file
file: var type is: link file
file: proc type is: directory file
file: media type is: directory file
file: mnt type is: directory file
file: dev type is: directory file

mkdir 和 rmdir 函数

#include <sys/stat.h>
int  mkdir(const char  *dirname, mode_t  mode);
int  rmdir(const char  *pathname);

函数 mkdir 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 dirname 是创建的目录名。
  • 参数 mode 是创建模式。

函数 rmdir 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pathname 是目录名。

调用 mkdir 函数可以创建一个空目录,所指定的目录访问权限由 mode 指定,mode 会根据进程的文件模式屏蔽字修改。

调用函数 rmdir 可以删除一个空目录,底层通过调用 unlink 函数实现。

chdir、fchdir 和 getcwd 函数

每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径)当用户登录到 SylixOS 时,其当前工作目录通常是口令文件“/etc/passwd”中该用户登录项的第 6 个字段——用户的起始目录。

#include <unistd.h>
int    chdir(const char  *pcName);
int    fchdir(int  iFd);
char  *getcwd(char  *pcBuffer, size_t  stByteSize);

函数 chdir 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcName 是新的默认目录。

函数 fchdir 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。

函数 getcwd 原型分析:

  • 此函数成功返回默认目录缓冲区首地址,失败返回 NULL。
  • 输出参数 pcBuffer 是默认目录缓冲区。
  • 参数 stByteSize 是缓冲区大小。

进程调用 chdir 函数或 fchdir 函数可以更改当前工作目录,chdir 函数用参数 pcName 指定当前的工作目录,fchdir 函数用文件描述符 iFd 来指定当前的工作目录。

因为当前工作目录是进程的一个属性,所以,修改本进程的工作目录并不会影响其他进程的工作目录,这一点值得注意。

调用 getcwd 函数可以获得当前默认的工作路径,此函数必须要有一个足够大的缓冲区来存放返回的绝对路径名再加上一个终止 null 字符,否则返回出错。

当一个应用程序需要在文件系统中返回到它工作的出发点时,getcwd 函数是很有用的。在更改工作目录前,先调用 getcwd 函数保存当前工作目录,在完成处理后,可以将之前保存的工作目录作为参数传递给 chdir 函数,返回到文件系统的出发点处。

fchdir 函数提供了一个更为便捷的方法,在更换文件系统中的不同位置前,先调用 open 函数打开当前工作目录,然后保存返回的文件描述符,当希望返回原工作目录时,只需要将保存的文件描述符作为参数传递给 fchdir 函数即可。

符号链接

符号链接是对一个文件的间接指针,任何用户都可以创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构定向到系统中另一个位置。

用 open 函数打开文件时,如果传递给 open 函数的文件名参数是一个符号链接,那么 open 将跟随符号链接打开指定的文件,但是,如果此文件不存在,则 open 函数将返回出错,表示文件不存在,这一点需要注意。

#include <unistd.h>
int      symlink(const char  * pcActualPath, const char  *pcSymPath);
ssize_t  readlink(const char  *pcSymPath, char  *pcBuffer, size_t  iSize);

函数 symlink 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcActualPath 是实际链接的目标文件。
  • 参数 pcSymPath 是新创建的符号链接文件。

函数 readlink 原型分析:

  • 此函数成功返回读取的符号链接内容长度,失败返回-1 并设置错误号。
  • 参数 pcSymPath 是要读取的符号链接名。
  • 输出参数 pcBuffer 是内容缓冲区。
  • 参数 iSize 是缓冲区长度。

SylixOS 可以调用 symlink 函数来创建符号链接,symlink 函数将创建一个指向 pcActualPath 的符号链接 pcSymPath ,并且 pcActualPath 和 pcSymPath 可以不在同一个文件系统中。上面提到 open 函数只能打开符号链接指向的文件,所以需要有一种方法打开该符号链接本身,并读取其中的内容,readlink 提供了这种功能。

注意:
SylixOS 当前不支持硬链接。

文件截断

有时我们需要在文件尾端截去一些数据以缩短文件,将一个文件的长度截断为 0 是一个特例,在打开文件时使用 O_TRUNC 标志可以做到这一点。

#include <unistd.h>
int  ftruncate(int  iFd, off_t  oftLength);
int  truncate(const char  *pcName, off_t  oftLength);

函数 ftruncate 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 oftLength 是文件长度。

函数 truncate 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 pcName 是文件名。
  • 参数 oftLength 是文件长度。

调用 truncate 函数和 ftruncate 函数可以缩减或者扩展文件长度,如果之前的文件长度比 oftLength 长,额外的数据会丢失,如果之前的文件长度比指定的长度小,文件长度将扩展,也就是产生文件空洞。ftruncate 函数对用户已经打开的文件进行操作,传入的是文件描述符。

标准 I/O 库

标准输入、标准输出和标准出错

上面介绍的所有 I/O 函数都是围绕文件描述符的,当打开一个文件时,即返回一个文件描述符,该文件描述符用于后续的 I/O 操作。而对于标准 I/O 库,它们的操作是围绕流进行的,当用标准 I/O 库打开一个文件时,流和文件进行相应的关联,然后返回一个 FILE 类型的文件指针。

对一个进程预定义了 3 个流,并且这 3 个流可以自动地被进程使用,它们分别是:标准输入、标准输出、标准错误。这些流引用的文件与文件描述符 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 所引用的文件相同。

这三个流预定义文件指针是:stdin、stdout、stderr,之前我们的打印函数就用到了这三个指针。

缓冲区

标准 I/O 库提供缓冲的目的是尽可能少地调用 read 函数和 write 函数,它也对每个 I/O 流自动地进行缓冲管理,从而避免应用程序需要考虑这一点所带来的麻烦。标准 I/O 库提供了 3 种类型的缓冲区。

  • 全缓冲,在这种情况下,在填满标准 I/O 缓冲区后才进行实际 I/O 操作。对于驻留在磁盘上的文件通常是由标准 I/O 库进行全缓冲。
  • 行缓冲,当输入和输出遇到换行符时才进行实际的 I/O 操作,标准输入和标准输出,通常使用行缓冲。
  • 无缓冲,标准 I/O 库不对字符进行缓冲存储,标准错误通常是不带缓冲的,这样出错信息可以尽可能快地显示出来。

调用下面的函数可以修改缓冲区类型。

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

函数 setbuf 原型分析:

  • 参数 fp 是文件指针。
  • 参数 buf 是缓冲区。

函数 setvbuf 原型分析:

  • 此函数成功返回 0,失败返回非 0 值并设置错误号。
  • 参数 fp 是文件指针。
  • 参数 buf 是缓冲区。
  • 参数 mode 是缓冲类型,如下表所示。
缓冲区类型说明
_IOFBF全缓冲
_IOLBF行缓冲
_IONBF无缓冲
  • 参数 size 是缓冲区大小。

上面的函数要求指定的流已经打开,而且是在执行任何操作之前进行调用,可以使用 setbuf 函数打开或者关闭缓冲区,为了设置一个缓冲区,参数 buf 指向缓冲区的首地址,BUFSIZ 定义了缓冲区的大小(定义在 <stdio.h> 中)。如果要关闭缓冲区,只需将参数 buf 指向 NULL。调用 setvbuf 函数可以指定缓冲类型,如下表所示。

一般情况下,在关闭流的时候,标准 I/O 会自动释放缓冲区。当然调用 fflush 函数可以在任何时候冲洗一个流。

#include <stdio.h>
int  fflush(FILE *fp);

函数 fflush 原型分析:

  • 此函数成功返回 0,失败返回 EOF。
  • 参数 fp 是文件指针。

fflush 函数将指定的流上所有未写的数据传送到内核,SylixOS 目前不支持 fp 为 NULL 的情况。

打开流

#include <stdio.h>
FILE  *fopen(const char *file, const char *mode);
FILE  *freopen(const char *file, const char *mode, FILE *fp);
FILE  *fdopen(int fd, const char  *mode);

函数 fopen 原型分析:

  • 此函数成功返回文件指针,失败返回 NULL 并设置错误号。
  • 参数 file 是要打开的文件名。
  • 参数 mode 是打开模式,如下表所示。

函数 freopen 原型分析:

  • 此函数成功返回文件指针,失败返回 NULL 并设置错误号。
  • 参数 file 是要打开的文件名。
  • 参数 mode 是打开模式,如下表所示。
  • 参数 fp 是文件指针。

函数 fdopen 原型分析:

  • 此函数成功返回文件指针,失败返回 NULL 并设置错误号。
  • 参数 fd 是已经打开的文件描述符。
  • 参数 mode 是打开模式。

上述 3 个函数都可以打开一个标准 I/O 流,fopen 函数打开 file 指定的文件。freopen 函数在一个指定的流上打开一个指定的文件,如果该流已经打开,则先关闭该流。如果该流已经定向,则会清除该定向。fdopen 函数取一个已有的文件描述符,并使一个标准的 I/O 流与该描述符相结合。

操作类型说明open 函数标志
r 或 rb以读的方式打开O_RDONLY
w 或 wb把文件截断为 0 字节长度,或写的方式创建O_WRONLY,O_CREAT,O_TRUNC
a 或 ab追加,在文件尾以写的方式打开,或写的方式创建O_WRONLY,O_CREAT,O_APPEND
r+或 r+b 或 rb+以读和写的方式打开O_RDWR
w+或 w+b 或 wb+把文件截断为 0 字节长度,或以读和写的方式打开O_RDWR,O_CREAT,O_TRUNC
a+或 a+b 或 ab+从文件尾以读和写的方式打开或创建O_RDWR,O_CREAT,O_APPEND

使用字符 b 作为 mode 的一部分,这使得标准 I/O 系统可以区分文本文件和二进制文件,但在 SylixOS 中不区分这两种文件,因此字符 b 在 SylixOS 中无效。

对于 fdopen 函数,mode 参数的意义稍有区别。因为该文件描述符已被打开,所以 fdopen 函数为写而打开的文件并不被截断,另外,标准 I/O 追加写方式也不能用于创建该文件。

当用追加写打开一个文件后,每次写都将数据写到文件的尾端处,如果有多个进程用标准 I/O 追加写方式打开同一个文件,那么来自每个进程的数据都将正确地写到文件中。

调用 fclose 函数关闭一个打开的流。

#include <stdio.h>
int fclose(FILE *fp);

函数 fclose 原型分析:

  • 此函数成功返回 0,失败返回 EOF 并设置错误号。
  • 参数 fp 是文件指针。

在该文件被关闭之前,首先冲洗缓冲区中的输出数据,缓冲区中的输入数据被丢弃。如果标准 I/O 库已经自动分配了缓冲区,则释放此缓冲区。

当一个进程正常终止时,所有带未写缓冲数据的标准 I/O 流都被冲洗,所有打开的标准 I/O 流被关闭。

读写流

一但流被打开,可对不同类型的 I/O 进行读、写操作,调用以下函数可一次读一个字符。

#include <stdio.h>
int  getc(FILE *fp);
int  fgetc(FILE *fp);
int  getchar(void);

函数 getc 原型分析:

  • 此函数成功返回下一个字符,否则返回 EOF。
  • 参数 fp 是文件指针。

函数 fgetc 原型分析:

  • 此函数成功返回下一个字符,否则返回 EOF。
  • 参数 fp 是文件指针。

函数 getchar 原型分析:

  • 此函数成功返回下一个字符,否则返回 EOF。

getchar 函数等同于 getc(stdin)。fgetc 函数与 getc 函数的区别是,getc 函数可以被实现为宏,而 fgetc 函数不能实现为宏。

上述的 3 个输入函数对应以下 3 个输出函数。

#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);

函数 putc 原型分析:

  • 此函数成功返回输入的字符,否则返回 EOF 并设置错误号。
  • 参数 c 是要输入的字符。
  • 参数 fp 是文件指针。

函数 fputc 原型分析:

  • 此函数成功返回输入的字符,否则返回 EOF 并设置错误号。
  • 参数 c 是要输入的字符。
  • 参数 fp 是文件指针。

函数 putchar 原型分析:

  • 此函数成功返回输入的字符,否则返回 EOF 并设置错误号。
  • 参数 c 是要输入的字符。

putchar(c)等同于 putc(c, stdout),putc 函数和 fputc 函数都可以向指定的流输出一个字符,不同的是,putc 函数可被实现为宏,而 fputc 函数不能实现为宏。

下面函数可以从指定的流读取一行字符(行结束符用“\n”表示)。

#include <stdio.h>
char  *fgets(char *buf, int n, FILE *fp);
char  *gets(char *buf);

函数 fgets 原型分析:

  • 此函数成功返回 buf 首地址,否则返回 NULL。
  • 参数 buf 是字符缓冲区。
  • 参数 n 是缓冲区长度。
  • 参数 fp 是文件指针。

函数 gets 原型分析:

  • 此函数成功返回 buf 首地址,否则返回 NULL。
  • 参数 buf 是字符缓冲区。

这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets 函数从标准输入读,而 fgets 函数则从指定的流中读。

fgets 函数必须指定缓冲区的长度,此函数一直读到下一个换行符为止,但是不超过 n-1 个字符,读入的字符被送到缓冲区,该缓冲区以 null 字符结尾。如若该行包括最后一个换行符的字符数超过 n-1,则 fgets 函数只返回一个不完整的行,缓冲区总是以 null 字节结尾,对 fgets 函数的下一次调用会继续读该行。

gets 函数是不推荐使用的,因为调用者不指定缓冲区的长度,这样就可能造成缓冲区溢出。

下面函数可以向指定的流输出一行字符。

#include <stdio.h>
int  fputs(const char *str, FILE *fp);
int  puts(const char *str);

函数 fputs 原型分析:

  • 此函数成功返回非负值,失败返回 EOF 并设置错误号。
  • 参数 str 是要写入的字符串。
  • 参数 fp 是文件指针。

函数 puts 原型分析:

  • 此函数成功返回非负值,失败返回 EOF 并设置错误号。
  • 参数 str 是要写入的字符串。

fputs 函数将一个以 null 字节终止的字符串写到指定的流,尾端的终止符 null 不写出,puts 函数将一个以 null 字节终止的字符串写到标准输出,终止符 null 不写出,与 fputs 函数不同的是,puts 函数随后又将一个换行符写到标准输出。通常情况,puts 函数也是不推荐使用的。

下面程序展示了标准 I/O 读写函数的使用,程序将一个字符串写到打开的文件中,随后调用 rewind 函数(之后将介绍此函数的用法)将文件当前指针移到文件的开始处,调用 fgets 函数读取文件中的字符串并打印。

#include <stdio.h>
int main (int argc, char *argv[])
{
    FILE    *fp;
    int      ret;
    char    *str = "This is test sylixos string functions example.";
    char     buf[64] = {0};

    fp = fopen("file", "r+");
    if (fp == NULL) {
        fprintf(stderr, "fopen error.\n");
        return  (-1);
    }

    ret = fputs(str, fp);
    if (ret < 0) {
        fprintf(stderr, "fputs write error.\n");
        fclose(fp);
        return  (-1);
    }

    rewind(fp);                                  /*  文件当前偏移到文件开始         */
    if (fgets(buf, sizeof(buf), fp) == NULL) {
        fprintf(stderr, "fgets read fp error.\n");
        fclose(fp);
        return  (-1);
    }

    fprintf(stdout, "buf: %s\n", buf);
    fclose(fp);
    return  (0);
}

在 SylixOS Shell 下运行程序:

# ./Functions_StandardIO
buf: This is test sylixos string functions example.

定位流

#include <stdio.h>
long  ftell(FILE *fp);
int   fseek(FILE *fp, long offset, int whence);
void  rewind(FILE *fp);

函数 ftell 原型分析:

  • 此函数成功返回当前的文件偏移,失败返回-1 并设置错误号。
  • 参数 fp 是文件指针。

函数 fseek 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 fp 是文件指针。
  • 参数 offset 是设定的偏移量。
  • 参数 whence ,如下表所示。

函数 rewind 原型分析:

  • 参数 fp 是文件指针。

对于一个二进制文件,其文件位置指示器是从文件起始位置开始度量,并以字节为单位的。ftell 函数用于二进制文件时,其返回值就是这种字节位置。为了用 fseek 定位一个二进制文件,必须指定一个字节 offset,以及 whence。ISO C 并不要求一个实现对二进制文件支持 SEEK_END,其原因是某些系统要求二进制文件的长度是某个幻数的整数倍,但是在 SylixOS 中,是支持 SEEK_END 的。

对于文本文件,它们的文件当前位置可能不以简单的字节偏移量来度量。这主要也是在非 UNIX 系统中,它们可能以不同的格式存放文本文件。为了定位一个文本文件,whence 一定要是 SEEK_SET,而且 offset 只能有两种值:0(后退到文件起始位置),或是对该文件的 ftell 函数所返回的值。使用 rewind 函数也可将一个流设置到文件的起始位置。

fgetpos 函数和 fsetpos 函数是 ISO C 标准引入的。

iWhence 值oftOffset 说明
SEEK_SET将文件的偏移量设置为距文件开始处 oftOffset 个字节
SEEK_CUR将文件的偏移量设置为当前值加 oftOffset 个字节,oftOffset 可为负
SEEK_END将文件的偏移量设置为文件长度加 oftOffset 个字节,oftOffset 可为负
#include <stdio.h>
int  fgetpos(FILE *fp, fpos_t *pos);
int  fsetpos(FILE *fp, const fpos_t *pos);
  • 此函数成功返回 0,失败返回非 0 值并设置错误号。
  • 参数 fp 是文件指针。
  • 输出参数 pos 是文件偏移位置。

函数 fsetpos 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 fp 是文件指针。
  • 参数 pos 是文件偏移。

fgetspos 函数将文件位置指示器的当前值存入由 pos 指向的对象中,在以后调用 fsetpos 函数时,可以使用此值将流重新定位至该位置。

I/O 格式化

#include <stdio.h>
int  printf(const char *format, ...);
int  fprintf(FILE *fp, const char *format, ...);
int  fdprintf(int fd, const char *format, ...);
int  sprintf(char *buf, const char *format, ...);
int  snprintf(char *buf, size_t n, const char *format, ...);

函数 printf 原型分析:

  • 此函数成功返回输出字符数,失败返回负值。
  • 参数 format 是格式字符串。
  • 参数 ... 是可变参数。

函数 fprintf 原型分析:

  • 此函数成功返回输出字符数,失败返回负值。
  • 参数 fp 是文件指针。
  • 参数 format 是格式字符串。
  • 参数 ... 是可变参数。

函数 fdprintf 原型分析:

  • 此函数成功返回输出字符数,失败返回负值。
  • 参数 fd 是文件描述符。
  • 参数 format 是格式字符串。
  • 参数 ... 是可变参数。

函数 sprintf 原型分析:

  • 此函数成功返回输出字符数,失败返回负值。
  • 参数 buf 是字符缓冲区指针。
  • 参数 format 是格式字符串。
  • 参数 ... 是可变参数。

函数 snprintf 原型分析:

  • 此函数成功返回输出字符数,失败返回负值。
  • 参数 buf 是字符缓冲区指针。
  • 参数 n 是缓冲区长度。
  • 参数 format 是格式字符串。
  • 参数 .. 是可变参数。

printf 函数将按 format 规定的格式打印字符到标准输出流,fprintf 函数将按 format 规定的格式打印字符到 fp 指定的流,fdprintf 函数的参数 fd 是已打开文件的描述符,此函数将格式字符打印到 fd 指定的文件。sprintf 函数和 snprintf 函数将格式字符打印到 buf 指定的缓冲区中,两个函数不同的是 snprintf 函数指定了缓冲区的长度,从而保证了内存的安全性,sprintf 函数不建议使用。

#include <stdio.h>
int  scanf(const char *format, ...);
int  fscanf(FILE *fp, const char *format, ...);
int  sscanf(const char *buf, const char *format, ...);

函数 scanf 原型分析:

  • 此函数成功返回匹配的字符数量,否则返回 EOF。
  • 参数 format 是格式字符串。
  • 参数 ... 是可变参数。

函数 fscanf 原型分析:

  • 此函数成功返回匹配的字符数量,否则返回 EOF。
  • 参数 fp 是文件指针。
  • 参数 format 是格式字符串。
  • 参数 ... 是可变参数。

函数 sscanf 原型分析:

  • 此函数成功返回匹配的字符数量,否则返回 EOF。
  • 参数 buf 是缓冲区指针。
  • 参数 format 是格式字符串。
  • 参数 ... 是可变参数。

scanf 函数扫描标准输入并按 format 格式保存值到相应的内存,内存地址将在可变参数中给出,fscanf 函数和 sscanf 函数功能类似,不同的是,fscanf 函数从 fp 指定的流中扫描字符,而 sscanf 函数从 buf 指定的缓冲区中扫描字符。

下面程序展示了格式化函数的使用,程序使用了 fdopen 函数从已打开的文件描述符获得文件指针。

#include <stdio.h>
#include <fcntl.h>
int main (int argc, char *argv[])
{
    FILE    *fp;
    char     buf[64] = {0};
    char    *content = "Format print function test.";
    char    *temp;
    int      fd;

    fd = open("file", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        fprintf(stderr, "open failed test done.\n");
        return  (-1);
    }
    fp = fdopen(fd, "r+");
    if (fp == NULL) {
        fprintf(stderr, "fdopen failed test done.\n");
        close(fd);
        return  (-1);
    }
    fprintf(fp, "%s", content);
    rewind(fp);
    temp = fgets(buf, sizeof(buf), fp);
    if (temp == NULL) {
        fprintf(stderr, "fgets read error or file end.\n");
        fclose(fp);
        return  (-1);
    }
    fprintf(stdout, "buf: %s\n", buf);
    fclose(fp);
    return  (0);
}

在 SylixOS Shell 下运行该程序:

# ./format_test
buf: Format print function test. functions example.
文档内容是否对您有所帮助?
有帮助
没帮助