文件操作

作者IYATT-yx

4 月 27, 2021

最近更新于 2022-04-18 10:37

玩 Linux 一定听过这样一句话,“Linux 下一切皆文件”。Linux 下对一切资源的管理归根到底都是对文件的管理,所有硬件资源都是作为文件来操作的。

Linux 文件结构

在现代操作系统中,用利用大量的程序和数据,由于内存容量有限,且不能长期保存,于是人们想出了把这些数据以文件的形式放在外存中,需要的时候再将它们调入内存,从此就有了文件系统,它负责管理在外存上的文件,并把存取、共享和保护等手段提供给用户,这样就方便了用户,保证了文件的安全性,并提高了系统资源的利用率。

(1)从系统的角度来看,文件系统是对文件储存器空间进行组织和分配,负责文件的存储并对存入的文件进行保护和检索的系统;从用户的角度看,文件系统的主要功能是实现了对文件的按名存取。

(2)由于要储存大量的文件,但如何对这些文件实施有效的管理呢?人们又引入了目录,通过目录来对文件进行管理。

Linux 文件系统

文件系统是指文件存在的物理空间,Linux 系统中的每个分区都是一个文件系统,都有自己的目录层次结构。Linux 会将这些分属不同分区的、单独的文件系统按一定的方式组合起来形成一个系统的总的目录层次结构。

系统依靠 PCB 来管理文件,而具体到 Linux,它是依靠 index node 来管理文件的。

(1)Linux 是一个安全的操作系统,它是以文件为基础设计的(Linux 下一切皆文件)。Linux 中的文件系统主要是用于管理储存空间的分配、文件访问权限的维护和对文件的各种操作。

(2)Linux 文件主要包括两方面内容:一是文件本身所含的数据;另外就是文件的属性,也称为元数据,包括文件访问权限、所有者、文件大小和创建日期等。

(3)目录也是一种文件,称为目录文件。文件的内容是该目录的目录项,目录项是该目录下的文件和目录相关的信息。当创建一个新目录时,系统会自动创建两个目录项:“.”、“..”。

(4)Linux 采用的是标准目录结构——树形结构,无论操作系统管理几个磁盘分区,这样的目录树只有一个。(有助于对系统文件和不同用户文件进行统一管理)

(5)在 Linux 安装时,安装程序就已经为用户创建了文件系统和完整而固定的目录组成的形式,并指定了每个目录的作用和其中的文件类型。

Linux 目录结构

与 Windows 将硬盘看作 C盘、 D盘 ……几个独立分区不同,Linux 将整个文件系统看作一颗树,这颗树的树根叫做根文件系统,用 / 表示,各个分区通过挂载以文件夹的形式访问。

在根目录的文件夹很多,这里就介绍其中一些常见文件夹的意义。Linux 的目录结构确实比较复杂,但设置合理、层次鲜明。

1.根文件系统

/bin这一目录中存放了供所有用户使用的完成基本维护任务的命令,其中 bin 是 binary 的缩写,表示二进制文件,通常为可执行文件。一些常用的系统命令。如 cp、mv 等都在此目录中。
/boot这里存放的是启动 Linux 时使用的一些核心文件,如操作系统内核,引导程序 Grub 等。
/dev此目录中包含所有的系统设备文件。从该目录可以访问各种系统设备。
/etc该目录中包含系统和应用程序的配置文件。
/etc/passwd该目录中包含系统中的用户描述信息,每行记录一个用户信息。(用户名,“加密”的密码,默认shell等)
/home储存普通用户的个人文件,每个用户的主目录均在 /home 下以自己的用户名命名。
/lib这个目录里存放着系统最基本的共享链接库和内核模块,共享链接库(.so)类似于 Windows 里的 .dll 文件。
/lib6464位系统有这个文件夹,存放64位程序的库。
/lost+found这并不是 Linux 目录结构的组成部分,而是 ext3 文件系统用于保存丢失文件的地方。不恰当的关机操作和磁盘错误均会导致文件丢失,这意味着这些文件被标注为“在使用”,但却并未列于磁盘的数据结构上。在正常情况下,引导程序会运行 fsck 程序,该程序能发现这些丢失的文件。除了 “/” 分区上的这个目录外,在每个分区上均有一个。
/media可移动设备的挂载点,当前的操作系统通常会把 U盘 等设备自动挂载到该文件夹下。
/mnt临时用于挂载文件系统的地方。一般情况下这个目录是空的,而在我们将要挂载分区时再在这个目录下建立目录,将我们要访问的设备挂载在这个目录上,就可以访问到文件了。
/opt多数第三方软件默认安装到此位置,并不是每个系统都会创建这个目录。
/proc它是存在于内存中的虚拟文件系统,里面保存了内核和进程的状态信息,多为文本文件,可以直接查看。
/root这是 root 用户的主目录,与保留给个人用户的 /home 下的目录相似。
/sbin供超级用户使用的可执行文件,里面多是系统管理命令。
/tmp该目录用于保存临时文件,具有 Sticky 特殊权限,所有用户都可以在这个目录中创建和编辑文件,但只有文件拥有者才能删除文件。为了加快临时文件的访问速度,有的用户会把 /tmp 放到内存中。
/usr静态的用户级程序等。
/var动态的程序数据等。

2. /usr 目录结构

/usr 通常是一个庞大的文件夹,其下的结构与根目录相似,但根目录中的文件多是系统级的文件,而 /usr 中的文件是用户级的文件,一般与具体系统无关。

(usr 最早是 user 的缩写, /usr 的作用与现在的 /home 相同。而目前 usr 通常被认为是 User System Resources 的缩写,其中通常是用户级的软件等,与存放系统文件的根目录形成对比。)

应注意,程序的配置文件、动态的数据文件等都不会存放到 /usr 中,所以除了安装、卸载软件外,一般无需修改 /usr 中的内容。在系统正常运行时,/usr 甚至可以被只读挂载。由于这一特性,/usr 常被划分在单独的分区,甚至有时多台计算机可以共享一个 /usr。

(1)/usr/bin 。多数日常应用程序放在该目录中。如果 /usr 被放在单独的分区中,Linux 的单用户模式不能访问 /usr/bin,所以对系统至关重要的程序不应该放在此文件夹中。

(2)/usr/include 。存放 C/C++ 头文件的目录。

(3)/usr/lib 。系统的库文件。

(4)/usr/local 。在新装的系统中这个文件夹是空的,可以用于存放个人安装的软件。安装了本地软件的 /usr/local 里的目录结构和 /usr 相似。

(5)/usr/sbin 。在单用户模式中不用的系统管理程序。

(6)/usr/share 。存放与架构无关的数据,多数软件安装在此。

(7)/usr/src 。存放源代码。

3. /var 目录结构

/var 中包括了一些数据文件,如系统日志等,/var 使得 /usr 被只读挂载成为可能。

(1)/var/cache 。应用程序的缓冲文件。

(2)/var/lib 。应用程序的信息和数据,如数据库的数据都存放在该文件夹中。

(3)/var/local 。/usr/local 中程序的信息和数据。

(4)/var/lock 。锁文件。

(5)/var/log 。日志文件。

(6)/var/opt 。/opt 中程序的信息和数据。

(7)/var/run 。正在执行的程序的信息,如 PID 文件应存放于此。

(8)/var/spool 。存放程序的脱机数据。

(9)/var/tmp 。临时文件。

Linux 文件分类

(1)普通文件。计算机用户和操作系统用于存放数据和程序等信息的文件,一般都长期存放在外存储器(硬盘、U盘等)中,普通文件一般又分为文本文件和二进制文件。

(2)目录文件。Linux 文件系统将文件索引节点号和文件名同时保存在目录中,所以目录文件就是将文件的名称和它的索引节点结合在一起的一张表。目录文件只允许系统进行修改,用户进程可以读取目录文件,但不能对它们修改。

(3)设备文件。Linux 把所有的外设都当作文件来看待,每一种 I/O 设备对应一个设备文件并存放在 /dev 目录中。

(4)管道文件。主要用于在进程间传递数据,管道是进程间传递数据的“媒介”。某进程数据写入管道的一端,另一个进程从管道的另一端读取数据。Linux 对管道的操作与文件操作相同,它把管道当作文件进行处理。管道文件又称为先进先出(FiFO)文件。

(5)链接文件。又称符号链接文件,它提供了共享文件的一种方法。在链接文件中不是通过文件名实现文件共享的,而是通过链接文件中的指向文件的指针来实现对文件的访问的。使用链接文件可以访问普通文件,目录文件和其它文件。

文件属性

  • 文件类型符号

– 普通文件

d 目录文件

l 链接文件

c 字符设备

b 块设备

p 管道文件

s 套接字

  • 文件权限符号

r 可读(4)

w 可写(2)

x 可执行(1)

这个权限可以用八进制数表示的,读、写、执行分别对应4、2、1,例如:

可读,可写,不可执行:4+2+0=6

不可读,不可写,可执行:0+0+1=1

  • 权限级别

文件拥有者(u)

所属用户组(g)

系统里其它用户(o)

  • 举例说明

使用 ls -l 可以查看一部分文件属性

这里就用上图中 test 文件来说明(第一列):

drwxr-xr-x

先拆分 d rwx r-x r-x

开头 d 表明文件类型为 目录

后面三组都是文件权限,分贝对应 u g o 的权限

文件拥有者(u)的权限是 rwx 即 读 写 执行

所属用户组(g)的权限是 r-x 即 读 执行 (短线表示无对应权限),o 的权限和 g 一样就不说了。

另外,上面可以看到 yx yx (第3列和第4列)就分别是文件的 u 和 g,可以使用 chown 修改

第二列代表的是链接数,第五列是文件大小(字节 B),第6列和第7列是最后修改的时间,第8列为文件名(以点开头的文件名即是 Linux 下的隐藏文件,ls 的时候加参数 -a 就能看到)。

目录权限和文件权限有一定的区别,就目录而言, r 表示允许列出该目录下的文件,w 表示允许生成和删除该目录下的文件,x 表示允许访问该目录。

系统调用

所谓的系统调用,是指操作系统提供给用户程序调用的一组特殊接口,用户程序可以通过这些接口来获得操作系统内核提供的服务。例如,用户可以通过进程控制相关的系统调用来创建进程、实现进程调度以及进程管理等。

在 C 语言中,操作系统的系统调用通常是通过函数调用的形式来完成的,这是因为这些函数封装了系统调用的细节,将系统调用的入口、参数和返回值用 C 语言的函数调用过程实现。在 Linux 系统中,系统调用函数定义在 Glibc 中。

(1)系统调用函数通常在成功后返回 0 值,不成功返回非 0 值。如果要检查失败的原因,则要判断全局变量 errno 的值,errno 中包含错误代码。(实际使用不需要自己去判断,当返回值为 -1 时就调用 perror 函数,则可以打印出对应的错误原因(perror 内部实现了判断 errno 值匹配对应的错误信息的功能))

(2)许多系统调用的返回数据通常通过引用参数传递,这时需要在函数参数中传递缓冲区地址,而返回的数据就保存在该缓冲区中。

(3)不能认为系统调用函数比其它函数的执行效率高。要注意,系统调用是一个十分耗时的过程。

为了对系统提供保护,Linux 系统定义了内核模式和用户模式。内核模式可以执行一些特权指令并且可以进入用户模式,而用户模式则不能。内核模式与用户模式分别使用自己的堆栈,在发生模式切换的时候要同时进行堆栈的切换。

同样,Linux 将程序的运行空间也分为内核空间和用户空间,它们分别运行在不同的级别上,在逻辑上是相互隔离的。系统调用规定用户进程进入内核空间的具体位置,在进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完毕后再返回到用户空间。

系统调用对于内核来说就相当于函数,关键问题是从用户模式到内核模式的转换、堆栈的切换以及参数的传递。

Linux 的系统调用按照功能大致可以分为:进程控制、进程间通信、文件系统控制、系统控制、储存控制、网络管理、套接字控制和用户管理等几类。

Linux 文件描述符

当某个程序打开文件时,操作系统返回相应的文件描述符,程序为了处理该文件必须引用该文件描述符。所谓的文件描述符,是一个低级的正整数。通常,当一个进程启动时,都会打开三个文件(标准输入、标准输出和标准错误)。这3个文件所对应的文件描述符分别是0、1 和 2,对应的宏为 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO(建议使用宏)。可以用不同的文件描述符改写默认的设置并重定向进程的 I/O 到不同的文件。

要访问文件,而且使用的是系统调用的 write、read、open 和 close,就必须用到文件描述符(每个进程的前3个默认已占用,打开文件时文件描述符从3开始)。如果是使用的 C 库的 fwrite、fread、fopen 和 fclose,则不用文件描述符,而是使用 FILE 指针(与 Linux 平台无关)。

对 Linux 而言,所有对设备和文件的操作都使用文件描述符来进行。文件描述符是一个非负的整数,它是一个索引值,并指向内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新的文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。

不带缓冲的 I/O 操作

不带缓冲的 I/O 操作,主要用到6个函数(create、open、read、write、lseek 和 close)。这里的不带缓冲是指每一个函数都只调用系统中的一个函数,这些函数虽然不是 ISO C 的组成部分,但却是 POSIX 的组成部分。

文件创建、打开与关闭

int creat(const char *path, mode_t mode); 创建文件

int open(const char *path, int oflag, …); 打开文件

oflag(前3个互斥,可与后面的“与”):

  • O_RDONLY 只读。
  • O_WRONLY 只写。
  • O_RDWR 读写。
  • O_CREAT 不存在则创建。
  • O_EXCL 和 O_CREAT 一起被设置时,如果文件不存在则创建否则失败,且要打开的文件为符号链接也会失败。
  • O_NOCTTY 如果要打开的文件为终端设备,则不会将该终端作为进程控制终端。
  • O_TRUNC 如果文件存在且可写方式打开,将会清空原文件
  • O_APPEND 读写文件从尾部开始(追加常用)。
  • O_NONBLOCK 非阻塞,无论有无数据可读,都不等待(普通文件默认不阻塞,终端、管道和套接字文件等默认阻塞)。
  • O_SYNC 以同步方式打开文件。
  • O_NOFOLLOW 如果打开符号链接则失败。
  • O_DIRECTORY 打开的不是目录则失败。

mode(实际创建出的文件权限为 mode & (~umask) )(我一般还是喜欢直接用八进制权限值,更为省事):

标志权限值解释
S_IRWXU00700文件所有者具有读、写和执行权限
S_IRUSR00400文件所有者具有读权限
S_IWUSR00200文件所有者具有写权限
S_IXUSR00100文件所有者具有执行权限
S_IRWXG00070文件用户组具有读、写和执行权限
S_IRGRP00040文件用户组具有读权限
S_IWGRP00020文件用户组具有写权限
S_IXGRP00010文件用户组具有执行权限
S_IRWXO00007其他用户具有读、写和执行权限
S_IROTH00004其他用户具有读权限
S_IWOTH00002其他用户具有写权限
S_IXOTH00001其他用户具有执行权限

int close(int fildes); 用来关闭 open 打开的文件

/**
 * @author IYATT-yx
 * @date 2021-4-28
 * @brief creat 函数的使用
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/**
 * @brief 指定文件名即可创建文件
 * @param fileName 文件名
 * @return 成功返回 0; 失败返回 -1
 */
int createFile(char *fileName)
{
    if (creat(fileName, 0755) < 0)
    {
        printf("创建文件 %s 失败!\n", fileName);
        return -1;
    }
    else
    {
        printf("创建文件 %s 成功!\n", fileName);
    }
    return 0;
}

int main(int argc, char **argv)
{
    if (argc < 2)
    {
        printf("请输入文件名,再试一次!\n");
        exit(EXIT_FAILURE);
    }
    
    for (int i = 1; i < argc; ++i)
    {
        if (createFile(argv[i]) == -1)
        {
            perror("createFile");
            exit(EXIT_FAILURE);
        }
    }
}
/**
 * @author IYATT-yx
 * @date 2021-4-28
 * @brief open 和 close 函数的使用
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    if (argc < 2)
    {
        printf("请输入文件名!\n");
        exit(EXIT_FAILURE);
    }

    int fd = open(argv[1], O_CREAT | O_RDWR, 0755);
    if (fd == -1)
    {
        perror("打开文件失败");
        exit(EXIT_FAILURE);
    }
    else
    {
        printf("打开文件 %s 成功,文件描述符为 %d\n", argv[1], fd);
    }
    close(fd);
}

read、write 和 lseek

ssize_t read(int fildes, void *buf, size_t nbyte); 从已打开的文件中读取数据

ssize_t write(int fildes, const void *buf, size_t nbyte); 将数据写入已打开的文件中

off_t lseek(int fildes, off_t offset, int whence); 移动文件的读写位置偏移量。每一个已打开的文件都有一个偏移量,在打开文件时通常其偏移量为0,即文件开头位置,直接写入会覆盖原文件的内容。如果是以追加的方式打开文件(O_APPEND),则偏移到文件尾。

// 读写位置移动到文件开头
lseek(int files, 0,SEEK_SET);
// 读写位置移动到文件末尾
lseek(int files, 0, SEEEK_END);
// 获取文件当前的读写位置
lseek(int files, 0, SEEK_CUR);
/**
 * @author IYATT-yx
 * @date 2021-4-28
 * @brief 文件复制 - read、write应用
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdbool.h>

// 缓冲区大小
#define BUFFER  1024

int main(int argc, char **argv)
{
    if (argc != 3)
    {
        printf("请输入源文件和要复制到的文件名!\n");
        exit(EXIT_FAILURE);
    }

    // 打开源文件
    int fromFd = open(argv[1], O_RDONLY);
    if (fromFd == -1)
    {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    // 创建目标文件
    int toFd = open(argv[2], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
    if (toFd == -1)
    {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    // 存储实际读取或者写入的长度
    ssize_t readLenth, writeLenth;
    // 数据缓冲区
    char buffer[BUFFER];
    // 缓冲区指针
    char *ptr = NULL;

    // 复制文件
    // read 返回值为 0 时,读取完毕,停止循环
    while ((readLenth = read(fromFd, buffer, BUFFER)))
    {
        if (readLenth == -1 && errno != EINTR)
        {
            break;
        }
        else if (readLenth > 0)
        {
            ptr = buffer;

            while ((writeLenth = write(toFd, ptr, (size_t)readLenth)))
            {
                if (writeLenth == -1 && errno == EINTR)
                {
                    // goto用于一次性跳出所有循环很方便
                    goto quit;
                }
                else if (writeLenth == readLenth)
                {
                    break;
                }
                else if (writeLenth > 0)
                {
                    ptr += writeLenth;
                    readLenth = writeLenth;
                }
            }
        }
    }

quit:
    close(fromFd);
    close(toFd);
}
/**
 * @author IYATT-yx
 * @date 2021-4-29
 * @brief 文件填充 - lseek的使用
 */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("请输入要填充的文件的名字!\n");
        exit(EXIT_FAILURE);
    }

    int fd = open(argv[1], O_WRONLY | O_CREAT, 0600);
    if (fd == -1)
    {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    // 填充 1024 字节
    // lseek 在文件尾偏移 1022 字节
    lseek(fd, 1022, SEEK_END);
    // write 才能生效,一个空字符串包含一个空格和一个 '\0',计 2 字节。
    write(fd, " ", sizeof(" "));

    close(fd);
}

带缓冲的 I/O 操作

标准 I/O 库提供缓冲的目的是尽可能地减少调用 read 函数和 write 函数地次数,这也对每个 I/O 流自动地进行缓冲管理,从而避免应用程序需要考虑这一点所带来地麻烦。不幸的是,标准 I/O 库最令人迷惑地也是它地缓冲。

3 种类型的缓冲

标准 I/O 提供了 3 种类型的缓冲。

1.全缓冲

在这种情况下,只有等填满标准 I/O 缓冲区后才进行实际 I/O 操作。对于驻留在磁盘上的文件,通常是由标准 I/O 库实施全缓冲的。当在一个流上执行第一次 I/O 操作时,相关标准 I/O 函数通常调用 malloc 函数获取需使用的缓冲区。

术语”冲洗“用于说明 I/O 缓冲区的写操作。缓冲区可由标准 I/O 例程自动冲洗,或者可以调用 fflush 函数冲洗一个流。值得注意的是在 Linux 中,冲洗有两种意思:① 在标准 I/O 库方面,冲洗将缓冲区中的内容写到磁盘上;② 在终端驱动程序方面,冲洗表示丢弃已储存在缓冲区中的数据。

2.行缓冲

在这种情况下,当在输入和输出中遇到换行符时,标准 I/O 库执行 I/O 操作。这允许我们一次输出一个字符,但只有在写了一行后才进行实际的 I/O 操作。当流涉及一个终端时,通常使用行缓冲。

3.不带缓冲

标准 I/O 库不对字符进行缓冲储存。例如,如果用标准 I/O 库 fputs 写15个字符到不带缓冲的流中,则该函数很可能用 write 系统调用函数将这些字符立即写至相关联的打开文件中。

标准出错流 stderr 通常是不带缓冲储存的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。

ISO C 要求下列缓冲特征:当且仅当标准输入和标准输出不涉及交互式设备时,它们才是全缓冲的。标准出错绝不会是全缓冲的,但是,这是这并没有告诉我们如果标准输入和标准输出涉及交互式设备上时,它们是不带缓冲的还是行缓冲的,以及当标准出错时是不带缓冲的还是行缓冲的。很多系统默认使用下列类型的缓冲:

  • 标准出错是不带缓冲的
  • 若涉及终端设备的其它流,则它们是行缓冲的;否则是全缓冲的。

对任何一个给定的流,如果不喜欢系统默认设置,则可调用下列函数更改缓冲类型。

void setbuf(FILE *stream, char *buf);

int setvbuf(FILE *stream, char *buf, int mode, size_t size);

/**
 * @author IYATT-yx
 * @date 2021-4-29
 * @brief I/O 缓冲的演示
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

int main(void)
{
    int varA = 9;
    int varB = 10;

    printf("fork 前\n");

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork error");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        ++varA;
        --varB;
        printf("子进程修改了变量值\n");
    }
    else
    {
        printf("父进程没有修改变量的值\n");
    }

    // 父子进程都会执行的
    printf("varA = %d\tvarB = %d\n", varA, varB);
}

执行结果直接输出到终端

执行结果重定向保存到文件,然后查看文件内容

当程序运行直接输出到终端时,标准输出是行缓冲的。

而重定向到文件后,标准输出是全缓冲的。在调用 fork 创建子进程后,”fork 前”这行依然存在于缓冲中,同时随着 fork(简述创建子进程就是拷贝父进程),子进程中也有了一份缓冲的拷贝。然后在子进程输出“子进程修改了变量值”的时候将缓冲区中的“fork 前”一起冲洗了,因此再次出现了”fork 前“。(注意:父子进程执行没有绝对的先后关系,看谁先获得CPU执行,我这里写的代码,可能父进程比子进程先执行的概率大很多)

带缓冲的 I/O 函数

       FILE *fopen(const char *pathname, const char *mode);
---- 打开文件
  • r 打开只读文件,文件必须存在
  • r+ 打开可读写的文件,文件必须存在
  • w 打开只写文件,如果文件已存在则会清空,不存在则创建
  • w+ 打开可读写文件,如果文件已存在则会清空,不存在则创建
  • a 以追加方式打开只写文件,如果文件存在,则写入数据在原数据末尾追加,不存在则创建文件。
  • a+ 以追加方式打开可读写的文件,如果文件存在,则写入数据在原数据末尾追加,不存在则创建文件。
  • b 配合前面的 mode 使用,以二进制的方式打开,而非纯文本文件。不过在 POSIX 系统中会忽略。
       int fclose(FILE *stream);
---- 关闭文件
       FILE *fdopen(int fd, const char *mode); 
---- 文件描述符转指针
       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);
---- 将数据写入文件流
       int fseek(FILE *stream, long offset, int whence); 
---- 移动文件流的读写位置
/**
 * @author IYATT-yx
 * @date 2021-5-11
 * @brief C 库函数读写 demo
 */
#include <stdio.h>

int main(void)
{
    // 打开写文件
    FILE *w = fopen("test", "w");
    if (w == NULL)
    {
        perror("fopen write");
        return 0;
    }

    // 写入
    const char s[] = "IAYYT-yx\niyatt.com";
    size_t len = fwrite(s, 1, sizeof(s), w);
    if (ferror(w))
    {
        printf("fwrite error\n");
        fclose(w);
        return 0;
    }
    fclose(w);

    // 打开读文件
    FILE *r = fopen("test", "r");
    if (r == NULL)
    {
        perror("fopen read");
        return 0;
    }

    // 读取
    char ss[len];
    fread(ss, 1, len, r);
    if (ferror(r))
    {
        printf("fread error\n");
        fclose(r);
        return 0;
    }
    else
    {
        printf("%s\n", ss);
    }

    fclose(r);
}
       int fgetc(FILE *stream); 
---- 从文件流读取一个字符,返回值为 EOF 时即读取到文件尾无数据。
       int getc(FILE *stream); 
---- 作用同上,但是这是一个宏定义。
       int getchar(void); 
---- 从终端读取一个字符

       int fputc(int c, FILE *stream); 
---- 将一个字符写入文件流,如果返回值为 EOF 则写入失败。
       int putc(int c, FILE *stream); 
---- 作用同上,但是这是一个宏定义。
       int putchar(int c); 
---- 将一个字符显示到终端

       char *fgets(char *s, int size, FILE *stream); 
---- 从文件读取一个字符串
/**
 * @author IYATT-yx
 * @date 2021-5-11
 * @brief C11 中已废弃 gets,可以用 fgets 实现 gets 的功能,而且弥补了 gets 无法限制输入长度的问题
 */
#include <stdio.h>

int main(void)
{
    char s[10];

    fgets(s, 10, stdin);

    printf("%s\n", s);
}
       int printf(const char *format, ...);

{1
%[flags][width].[prec]type

flags:
- 左对齐(默认右对齐)
+ 正数时在前面也强行显示正号,不省略
#    %#o 显示八进制会在开头加上 0;%#x 显示十六进制会在开头加上 0x;e、f、g 的时候会强制打印小数点。

width:
参数的最小长度,如果用 * 号,则在后面printf参数中指定最小长度(对应此处要显示的printf参数的前一个)

prec:
正整数的最小位数;
浮点数中小数的位数;
%g 中有效位数的最大值
%s 中字符串的最大长度
* 最大长度,在printf参数中指定(对应此处要显示的printf参数的前一个)

type:
%d / %i  有符号十进制
%u 无符号十进制
%o 无符号八进制
%x / %X 无符号十六进制(小写/大写)
%f 浮点数,6位小数
%e / %E  浮点数,科学计数法显示,6位小数 (小写/大写)
%g / %G 浮点数,自动选择 %f 或 %e 来显示,同时小数末尾不会用 0 强行填充满 6 位 (小写/大写)
%c 无符号字符
%s 字符串,直到遇到 NULL 或者 \0
%p 使用十六进制显示 void *
1}
       int fprintf(FILE *stream, const char *format, ...);
---- 使用和 printf 相似,不过这个是将字符串输入到文件流中而不是在终端中直接显示
       int sprintf(char *str, const char *format, ...);
---- 使用也差不多,只是存入字符数组中
       int scanf(const char *format, ...);

{1
%[*][size][l][h]type

* 参数可忽略不保存
size 允许输入的数据长度
l 输入的数据以 long int 或 double 保存
h 输入的数据以 short 保存

type同 printf
1}
/**
 * @author IYATT-yx
 * @date 2021-5-11
 * @brief scanf 忽略读取
 */
#include <stdio.h>

int main(void)
{
    int num1, num2;

    scanf("%d %*s %d", &num1, &num2);

    printf("%d %d\n", num1, num2);
}
       int fscanf(FILE *stream, const char *format, ...);
---- 从文件流中读取数据,格式化方式同 scanf
       int sscanf(const char *str, const char *format, ...); 
---- 从字符串中读取数据,格式化方式同

文件的其它操作

       int truncate(const char *path, off_t length);
---- 修改文件大小,设定的 length 比原文件大则填充空,比原文件小,则剪切掉多余的
/**
 * @author IYATT-yx
 * @date 2021-5-8
 * @brief 使用 truncate 修改文件大小
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main(void)
{
    // 文件名
    const char *name = "test";

    // 创建一个文件
    if (creat(name, 0644) == -1)
    {
        perror("creat error");
        exit(EXIT_FAILURE);
    }

    // 修改文件大小
    if (truncate(name, 1024) == -1)
    {
        perror("truncate error");
        exit(EXIT_FAILURE);
    }
}
       int chmod(const char *path, mode_t mode);
---- 修改文件权限
       int chown(const char *path, uid_t owner, gid_t group); 
---- 修改文件用户组和所属用户
       int rename(const char *old, const char *new); 
---- 重命名文件
       int mkdir(const char *path, mode_t mode); 
---- 创建目录
       int remove(const char *pathname); 
---- 删除文件
       int access(const char *path, int amode); 
---- 检测文件是否存在、是否可读、可写、可执行等
/**
 * @author IYATT-yx
 * @date 2021-5-9
 * @brief 文件属性检测 - 存在、读、写和执行
 */
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("参数错误\n");
        return 0;
    }

    // 存在
    if (access(argv[1], F_OK) == -1)
    {
        perror("access");
        return 0;
    }

    // 权限
    if (access(argv[1], R_OK) == 0)
    {
        printf("可读 | ");
    }
    if (access(argv[1], W_OK) == 0)
    {
        printf("可写 | ");
    }
    if (access(argv[1], X_OK) == 0)
    {
        printf("可执行");
    }
    printf("\n");
}
       int stat(const char *restrict path, struct stat *restrict buf);
       int lstat(const char *restrict path, struct stat *restrict buf); 
       int fstat(int fildes, struct stat *buf); 
---- 文件属性查询

使用stat与lstat的主要区别在软链接文件,当查询的是软链接文件时,stat会穿透查询到软链接指向的文件,而lstat可以查询软链接文件本身的属性.而fstat主要是参数区别, stat和lstat都是通过文件名查询,而fstat是通过open打开文件的返回值文件描述符进行查询.

struct stat
{
    // 文件的设备编号
    dev_t   st_dev;
    // 节点
    ino_t   st_ino;
    // 文件的类型和存取的权限
    mode_t  st_mode;
    // 链到该文件的硬链接数目
    nlink_t st_nlink;
    // 用户ID
    uid_t   st_uid;
    // 组ID
    gid_t   st_gid;
    // (设备类型)若此文件为设备文件,则为其设备编号
    dev_t   st_rdev;
    // 文件字节数
    off_t   st_size;
    // 块大小 (文件系统的I/O缓冲区大小)
    blksize_t   st_blksize;
    // 块数
    blkcnt_t    st_blocks;
    // 最后一次访问时间
    time_t  st_atime;
    // 最后一次内容修改时间
    time_t  st_mtime;
    // 最后一次属性修改时间
    time_t  st_ctime;
};
/**
 * @author IYATT-yx
 * @date 2021-5-9
 * @brief 文件类型检测 - stat
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("参数错误\n");
        return 0;
    }

    // 获取文件属性
    struct stat st;
    if (stat(argv[1], &st) == -1)
    {
        perror("stat error");
        return 0;
    }
    printf("%s的大小为%ld\n", argv[1], st.st_size);

    // 检测文件类型 - 方法一
    if (S_ISREG(st.st_mode))
    {
        printf("普通文件\n");
    }
    else if (S_ISDIR(st.st_mode))
    {
        printf("目录文件\n");
    }
    else if (S_ISLNK(st.st_mode))
    {
        printf("符号链接文件\n");
    }
    else if (S_ISSOCK(st.st_mode))
    {
        printf("套接字文件\n");
    }
    else if (S_ISBLK(st.st_mode))
    {
        printf("块设备文件\n");
    }
    else if (S_ISCHR(st.st_mode))
    {
        printf("字符设备文件\n");
    }
    else if (S_ISFIFO(st.st_mode))
    {
        printf("管道文件\n");
    }

    // 方法二
    int type = st.st_mode & S_IFMT;
    switch (type)
    {
        case S_IFREG:
            printf("普通文件\n");
            break;
        case S_IFDIR:
            printf("目录文件\n");
            break;
        case S_IFLNK:
            printf("符号链接文件\n");
            break;
        case S_IFSOCK:
            printf("套接字文件\n");
            break;
        case S_IFBLK:
            printf("块设备文件\n");
            break;
        case S_IFCHR:
            printf("字符设备文件\n");
            break;
        case S_IFIFO:
            printf("管道文件\n");
            break;
        default:
            break;
    }
}
       DIR *opendir(const char *name);
---- 打开目录
       struct dirent *readdir(DIR *dirp); 
---- 读目录

struct dirent
 {
     // 此目录进入点的inode
     ino_t d_ino;
     // 目录文件开头到此目录进入点的位移
     ff_t d_off;
     // d_name 的长度,不包括NULL字符
     signed short int d_reclen;
     // d_name所指的文件类型
     unsigned char d_type;
     // 文件名
     har d_name[256];
 }

d_type:
DT_BLK 块设备
DT_CHR 字符设备
DT_DIR 目录
DT_LNK 软链接
DT_FIFO 管道
DT_REG 普通文件
DT_SOCK 套接字
DT_UNKNOWN 未知
/**
 * @author IYATT-yx
 * @date 2021-5-9
 * @brief 读目录
 */
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("参数错误\n");
        return 0;
    }

    DIR *od = opendir(argv[1]);
    if (od == NULL)
    {
        perror("opendir error");
        return 0;
    }

    struct dirent *rd;
    while ((rd = readdir(od)) != NULL)
    {
        // 过滤上级和自身目录
        if (strcmp(".", rd->d_name) == 0 || strcmp("..", rd->d_name) == 0)
        {
            continue;
        }

        // 输出文件名
        printf("%s\n", rd->d_name);
    }
}
       int dup(int fildes);
       int dup2(int fildes, int fildes2); 
---- 创建新的文件描述符指向已打开文件的描述符(复制文件描述符)

dup 创建一个新的文件描述符作为返回值,它和 fildes 打开的是同一个文件;

dup2 让 fildes2 打开和 fildes1 一样的文件(会关闭 fildes2 原来打开的文件)。

/**
 * @author IYATT-yx
 * @date 2021-5-6
 * @brief dup2 重定向文件示例
 */
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
    // 以只写创建或打开一个文件(实际不使用)
    int fd = open("test", O_WRONLY | O_CREAT, 0644);
    if (fd == -1)
    {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    // 将 open 打开的文件描述符 fd 重定向到标准输出文件描述符 STDOUT_FILENO 所指向的文件
    dup2(STDOUT_FILENO, fd);

    // 向 fd 指向的文件写字符串
    // 因为 fd 指向的是终端输出文件,会直接在终端中显示
    const char s[] = "IYATT-yx\niyatt.com\n";
    write(fd, s, sizeof(s));

    close(fd);
}
       int fcntl(int fildes, int cmd, ...);
---- 对文件描述符进行操作,有 dup 的功能,可以修改 open 时设定的 flag......
/**
 * @author IYATT-yx
 * @date 2021-5-6
 * @brief 阻塞读终端(默认阻塞) - 没有回车输入就一直等待输入
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
    char buf[1024];

    // 从终端读入数据
    ssize_t readLen = read(STDIN_FILENO, buf, sizeof(buf));
    if (readLen == -1)
    {
        perror("read error");
        exit(EXIT_FAILURE);
    }

    // 向终端写数据
    write(STDOUT_FILENO, buf, (size_t)readLen);
}
/**
 * @author IYATT-yx
 * @date 2021-5-6
 * @brief fcntl 的应用,修改 flag ,实现非阻塞读终端
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

int main(void)
{
    char buf[1024];

    // 获取 STDIN_FILENO 的 flag
    int flag = fcntl(STDIN_FILENO, F_SETFL);
    // 追加非阻塞
    flag |= O_NONBLOCK;
    fcntl(STDIN_FILENO, F_SETFL, flag);

    // 从终端读入数据
    ssize_t readLen = read(STDIN_FILENO, buf, sizeof(buf));
    if (readLen == -1)
    {
        // 因为不再阻塞,就不再等待用户输入,会出现“资源不可用的错误”
        perror("read error");
        exit(EXIT_FAILURE);
    }

    // 向终端写数据
    write(STDOUT_FILENO, buf, (size_t)readLen);
}

作者 IYATT-yx