Ⅰ. 标准库函数与系统调用

1.fopen(3)

调用open(2)打开制定的文件,返回一个文件描述符FD(就是一个int类型的编号),分配一个FILE结构体,其中包含改文件的描述符、I/O缓冲区和当前读写位置等信息,返回这个FILE结构体的地址。

Linux系统编程笔记(2)——文件与I/O(1)-萤火
  • I/O缓冲区:buffer
  • 当前读写位置:相对buffer首地址的偏移量
  • 用户程序通过得到的是FILE结构体的地址,通过操作这个结构体实现对文件的操作

FILE* 其实就是句柄(或者也可以叫做上下文)。柄就是把手的意思,我们开门关门其实只对门把手进行操作,而不是直接对门操作,即我们对FILE* 进行操作其实就可以实现对文件的操作。

2.fgetc(3)

通过传入的FILE* 参数找到该文件的描述符、I/O缓冲区和当前读写位置,判断能否从I/O缓冲区中读取到下一个字符,如果能读到就直接返回该字符,否则就调用read(2),把文件描述符传进去,让内核读取该文件的数据到I/O缓冲区,然后返回下一个字符。

  • tip:第一次去读buffer中肯定是没有东西的,因此肯定会去调用read
#include <stdio.h>

int main() {
    FILE *fp = fopen("./hello.txt", "r");
    if (!fp) {
        perror("open file");
        return -1;
    }
    char c;
    while ((c = fgetc(fp)) != EOF) {
        printf("%c", c);
    }

    fclose(fp);

    return 0;
}

3.fputc(3)

判断该文件的I/O缓冲区是否有空间在存放一个字符,如果有空间则直接保存在I/O缓冲区中并返回,如果I/O缓冲区已满就调用write(2),让内核把I/O缓冲区的内容写回文件。

#include <stdio.h>

int main() {
    FILE *fp = fopen("./hello.txt", "r+");
    if (!fp) {
        perror("open file");
        return -1;
    }
    fputc('A', fp);

    fclose(fp);

    return 0;
}

4.fclose(3)

如果I/O缓冲区中还有数据没写回文件,就调用write(2)写回文件,然后调用close(2)关闭文件,释放FILE结构体和I/O缓冲区。

Q:open、read、write、close等系统函数称为无缓冲I/O(Unbuffer I/O)函数,因为它们位于C标准库函数的I/O缓冲区的底层。用户在读写文件时既可以调用C标准I/O库函数,也可以直接调用底层的Unbuffer I/O函数,那么用哪一组函数更好呢?

A:对于大部分不需要实时操作的场景,优先使用C标准I/O库函数,因为可以提高效率,减少用户空间和内核空间的切换。可以将buffer类比成快递驿站,用户寄快递时将快递放到驿站中,等到驿站满了,快递员再去发快递。或者快递员送快递的时候将快递放在驿站中,让用户自己去取,而不是将每个快递挨个送到不同的用户手中。
但是对于需要实时操作的内容(例如网络操作、stderr),最好直接使用系统调用。

Ⅱ. 缓冲(buffer)分类

  • 全缓冲:当把buffer填满就触发系统调用
  • 行缓冲:当出现换行(\n)时就触发系统调用(当然,buffer满了也是会触发的)
#include <stdio.h>

int main(void) {
    fputc('A', stdout);  // stdout:标准输出,行缓冲
    fputc('\n', stdout);

    // 将1kb的缓冲区写满
    //for (int i = 0; i < 1024; i++) {
    //    fputc('B', stdout);
    //}

    while (1) {
      // 写个死循环是为了不让程序结束,否则会自动调用系统调用将缓冲区写入文件
    }

    return 0;
}
  • 无缓冲:有缓冲区,但是就是不缓冲
#include <stdio.h>

int main(void) {
    fputc('A', stderr);  // stderr:标准错误输出,无缓冲
    while (1) {
        // 写个死循环是为了不让程序结束,否则会自动调用系统调用将缓冲区写入文件
    }

    return 0;
}

Ⅲ. 文件描述符FD

Linux系统编程笔记(2)——文件与I/O(1)-萤火

每个进程在Linux内核中都有一个task_struck结构体来维护进程相关的信息,称为进程控制块(Process Control Block,PCB)。task_struct中有一个指针指向file_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开的文件的指针,用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符(File Descriptor),用int型变量保存。

(句柄思想)

程序启动时会自动打开三个文件:标准输入,标准输出和标准错误输出。在C标准库中分别用FILE*指针stdin、stdout和stderr来表示。这三个文件的描述符分别是0、1、2,保存在相应的FILE结构体中。头文件unistd.h中有如下的宏定义来表示这三个文件描述符:

/* Standard file descriptors */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("%d\n", STDIN_FILENO);
    printf("%d\n", STDOUT_FILENO);
    printf("%d\n", STDERR_FILENO);

    return 0;
}
//输出结果:
//0
//1
//2