Linux中的基础IO -- C语言文件操作,系统级文件操作(open, write, read),文件描述符,重定向,dup2,缓冲区机制

目录

1. 理解文件

2. C语言中文件操作的接口

2.1 打开文件 -- fopen

2.2 写文件 -- fwrite

2.3 读文件 -- fread

2.4 输出信息到显示器的几种方法

2.5 stdin,stdout,stderr

3. 系统文件IO

3.1 一种传递标志位的方法 -- 位图

3.2 接口介绍

3.2.1 打开文件 -- open

3.2.2 写文件 -- write

3.2.3 读文件 -- read

3.3 文件描述符fd

3.3.1 理解文件描述符

3.3.2 文件描述符的分配规则

 3.3.3 重定向

3.3.4 使用dup2系统调用进行重定向

3.3.5 重定向的操作

 4. 理解“Linux系统下一切皆文件”

5. 缓冲区

5.1 什么是缓冲区

5.2 为什么要引入缓冲区机制

5.3 缓冲类型

5.4 简单实现C标准库的文件操作接口


1. 理解文件

        (1)文件是在磁盘中的,磁盘是永久性存储介质,所以文件在磁盘上的存储是永久性的。磁盘是外设,即输入输出设备,所以对磁盘上的文件的所有操作,本质上都是对外设的输入和输出,简称IO。

        (2)Linux系统下一切皆文件,如键盘,显示器,网卡,磁盘等,系统都会对其进行管理起来。

        (3)文件 = 文件属性(元数据)+ 文件内容。对于0KB的空文件也是要占用磁盘空间,所有的文件操作本质上就是对文件内容和文件属性进行操作

        (4)对文件的操作都是正在运行的程序进行操作的,所以对文件的操作就是进程对文件的操作。但是文件在磁盘中,磁盘的管理者是操作系统,所以对磁盘中的文件进行操作绕不开操作系统。文件的读写本质不是通过C/C++的库函数来操作的,而是通过文件相关的系统调用接口实现的,C/C++的库函数只是封装了这些系统调用接口,使用户用起来更加的方便。

2. C语言中文件操作的接口

2.1 打开文件 -- fopen

#include <stdio.h>

int main()
{
    FILE *fp = fopen("myfile", "w");    //以‘w’的方式打开myfile文件
    if(!fp) {
    printf("fopen error!\n");
    }
    while(1);
    fclose(fp);
    return 0;
}

        打开的myfile文件默认是在当前程序的工作目录下的,myfile文件的绝对路径就是当前程序的工作目录 + 文件名。 

        使用 ls /proc/进程id -l ,命令查看当前正在运⾏进程的信息:

        cwd:指向当前进程运行目录的一个符号链接。

        exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

        打开文件是进程打开的,进程知道自己在哪个工作目录下,所以文件不带路径,进程默认会打开工作目录下的文件。以 ‘w’ 的方式打开即使没有对应的文件,也会在该进程的工作目录下创建一个。

2.2 写文件 -- fwrite

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

int main()
    {
    FILE *fp = fopen("result", "w");
    if(!fp){
        printf("fopen error!\n");
    }
    const char *msg = "hello xiaoc!\n";
    int count = 5;
    while(count--){
        fwrite(msg, strlen(msg), 1, fp);
    }
    fclose(fp);
    return 0;
}

        上述fwrite接口的意思是向 fp 中写入 1个 大小为 strlen(msg) 的 msg 。 循环写五次,得到如下结果:

2.3 读文件 -- fread

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

int main()
{
    FILE *fp = fopen("result", "r");
    if(!fp)
    {
        printf("fopen error!\n");
        return 1;
    }

    while(1)
    {
        //注意返回值和参数,仔细查看man⼿册关于该函数的说明
        char buf[1024];
        memset(buf, 0 ,sizeof(buf));
        size_t s = fread(buf, sizeof(buf)-1, 1, fp);
        printf("%s", buf);

        if(feof(fp))
        {
                break;
        }
    }
    fclose(fp);
    return 0;
}

2.4 输出信息到显示器的几种方法

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

int main()
{
        printf("hello printf\n");
        fprintf(stdout, "hello fprintf\n");
        const char *msg = "hello fwrite\n";
        fwrite(msg, strlen(msg), 1, stdout);
        return 0;
}

        这里列举了三种方法,分别是printf,fprintf,fwrite。

2.5 stdin,stdout,stderr

        C程序会默认打开三个输入输出流,分别是 stdin,stdout,stderr。这三个流的类型都是FILE*。

        stdin:标准输入 -- 键盘文件。

        stdout:标准输出 -- 显示器文件。

        stderr:标准错误 -- 显示器文件。

知识点1:
        为什么要默认打开这三个输入输出流?
                因为程序是用来做数据处理的,需要有一个数据输入源和数据输出地,所以在默认情况下会打开这三个输入输出流作为输入源和输出地。

3. 系统文件IO

        使用fopen,ifstream这种打开文件的方式是语言层的方案,其实这些语言层的函数都封装了系统层面的文件操作函数,底层都是调用系统的接口进行操作的。

3.1 一种传递标志位的方法 -- 位图

#include <stdio.h>

#define ONE   1 // 0000 0001
#define TWO   2 // 0000 0010
#define THREE 4 // 0000 0100

void func(int flags)
{
        if (flags & ONE) printf(" flags has ONE! ");
        if (flags & TWO) printf(" flags has TWO! ");
        if (flags & THREE) printf(" flags has THREE! ");
        printf("\n");
}

int  main()
{
        func(ONE);
        func(THREE);

        func(ONE | TWO);
        return 0;
}

        位图的定义就是使用一个变量中的比特位来当作标志位。如上述代码,定义的ONE,TWO,THREE对应的就是变量中比特位的第0位,第1位,第2位为 1。然后在func函数中使用与运算就可以判断传递的flags中是否存在上述标志位。如果想传递多个标志位,就可以在传递参数的时候将多个标志位进行或操作。

3.2 接口介绍

3.2.1 打开文件 -- open

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

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

        pathname:要打开或创建的目标文件路径。

        flags:打开文件时,可以传⼊多个参数选项,这里就是使用位图的方式来传递标志位,⽤下⾯的⼀个或者多个常量进⾏“或”运算,构成flags。

        O_RDONLY:只读打开。
        O_WRONLY:只写打开。
        O_RDWR:读,写打开。
        这三个常量,必须指定⼀个且只能指定⼀个。

        O_CREAT:若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限。
        O_APPEND:追加写。

        mode:指定创建文件时的默认权限。
        返回值:
                成功:返回新打开的⽂件描述符。
                失败:返回 -1。

3.2.2 写文件 -- write

        下面代码中open函数传入的参数表示:以只读(O_WRONLY),清除(O_TRUNC)的方式打开文件,若文件不存在则创建该文件(O_CREAT),并把默认权限设置为666。

        只写入一次,write函数表示向 fd 文件描述符指定的文件中写入大小为 strlen(msg)的msg。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
        //设置文件权限的掩码,如果没有设置使用系统中的文件权限掩码
        umask(0);       
        //对应C语言的w方式打开
        int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);   

        if (fd < 0)
        {
                perror("open failed");
                return -1;
        }

        const char *msg = "abcd\n";
        int cnt = 1;
       
        while(cnt)
        {
                write(fd, msg, strlen(msg));
                cnt--;
        }
        close(fd);
        return 0;
}

        下列代码以追加的方式进行写入。 

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
        //设置文件权限的掩码,如果没有设置使用系统中的文件权限掩码
        umask(0);       
        //对应C语言的a方式打开
        int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);

        if (fd < 0)
        {
                perror("open failed");
                return -1;
        }

        const char *msg = "abcd\n";
        int cnt = 1;
       
        while(cnt)
        {
                write(fd, msg, strlen(msg));
                cnt--;
        }
        close(fd);
        return 0;
}

        执行两次上述代码,文件中就有之前以w方式写入的一次abcd,加上以a方式写入的abcd,一共三次。 

知识点1:
        write函数的第二个形参是const void*类型的参数,如果传入的是一个const int* 类型的数据,也会将其写入文件中,文件显示的时候以文本的形式进行显示,就会出现乱码的现象,所以文本写入和二进制写入都是语言层的概念。底层如果传给write的是const char*类型的数据就是文本写入,如果传入的如const int* 类型的数据就是二进制写入。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
        umask(0);      
        int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);  
        if (fd < 0)
        {
                perror("open failed");
                return -1;
        }

        const char *msg = "abcd\n";
        int cnt = 1;
        int a = 1234567;
        while(cnt)
        {
                write(fd, msg, strlen(msg));
                write(fd, &a, sizeof(a));
                write(fd, msg, strlen(msg));
                cnt--;
        }
        close(fd);
        return 0;
}

3.2.3 读文件 -- read

        log.txt中的内容如下:

        以只读(O_RDONLY)的方式打开文件,read函数传递的参数表示从fd指向的文件中读取 sizeof(buffer) - 1 大小的内容到 buffer 中。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
        int fd = open("log.txt", O_RDONLY);
        if (fd < 0)
        {
                perror("open failed");
                return 1;
        }
        while(1)
        {
                char buffer[64];
                int n = read(fd, buffer, sizeof(buffer)-1);
                if (n > 0)
                {
                        buffer[n] = 0;
                        printf("%s", buffer);
                }
                else if (n == 0)
                {
                        break;
                }
        }
        return 0;
}

3.3 文件描述符fd

        文件描述符就是一个 int 类型的整数。

         Linux中的进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2。

        0,1,2对应的物理设备默认情况下是:键盘,显示器,显示器。

3.3.1 理解文件描述符

        一个进程可以打开多个文件,被打开的文件要被管理起来,所以在系统中,当一个文件被打开,就会创建一个 file 类型的结构体对象(如下图),该结构体对象中保存着文件的相关属性以及一个文件缓冲区,系统会将磁盘中文件的内容加载到文件缓冲区中。

        当多个文件被打开的时候,就会存在多个file类型的结构体对象系统将每个file类型的结构体对象用双链表的形式连接起来进行管理。

        一个进程被创建出来,系统中就会有一个 task_struct 类型的结构体对象,task_struct 中存在一个files_struct类型的指针,指向 files_struct 类型的结构体对象,files_struct 中存在一个指针数组,数组中存储的元素为 file*类型的指针,指向该进程打开的文件对应的 file 类型的结构体对象。

        文件描述符fd的本质就是file* fd_array[]数组中对应的数组下标,进程通过这个下标就能找到描述对应文件的 file 结构体对象。

        源码中的相关代码: 

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        printf("stdin: %d\n", stdin->_fileno);
        printf("stdout: %d\n", stdout->_fileno);
        printf("stderr: %d\n", stderr->_fileno);

        printf("\n\n");

        umask(0);

        int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
        int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
        int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
        if (fd1 < 0) exit(1);
        if (fd2 < 0) exit(1);
        if (fd3 < 0) exit(1);

        printf("fd1: %d\n", fd1);
        printf("fd2: %d\n", fd2);
        printf("fd3: %d\n", fd3);

        close(fd1);
        close(fd2);
        close(fd3);
        return 0;
}

        上述代码输出默认情况下,标准输入,标准输出,标准错误的文件描述符,以及进程新打开的3个文件的文件描述符。 

3.3.2 文件描述符的分配规则

        在 file* fd_array[] 数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。一个进程默认会打开 stdin,stdout,stderr,所以新打开的文件的文件描述符默认情况下为3。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        close(0);
        int fd = open("log.txt", O_WRONLY | O_CREAT, 0644);
        if (fd < 0)
        {
                perror("open failed");
                return 1;
        }
        printf("fd: %d\n", fd);
        fflush(stdout);
        return 0;
}

        当把标准输入关闭之后,如果再打开新的文件,就会从最小的开始,这时候最小的下标为0。 

 3.3.3 重定向

        将标准输出关闭之后再打开一个新的文件log.txt,会发生下列的现象:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        close(1);
        int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd < 0)
        {
                perror("open failed");
                return 1;
        }
        printf("fd: %d\n", fd);
        fflush(stdout);
        return 0;
}

        printf打印的数据没有往显示器上打印,而是打印到了log.txt文件中。

        所以重定向的本质是在系统层面上,将文件描述符1对应的file*指针的指向进行了改变,改成了指向新打开的文件。上层C语言的printf函数在打印的时候,只认文件描述符1中 file* 指向的文件。 

        重定向的本质就是将文件描述符1中 file* 的指向更改为指向新打开的文件。

3.3.4 使用dup2系统调用进行重定向

#include <unistd.h>

int dup2(int oldfd, int newfd);

        dup2 函数的定义是,将 oldfd 文件描述符中的内容赋值给 newfd 中的内容。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        //close(1);
        int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd < 0)
        {
                perror("open failed");
                return 1;
        }
        dup2(fd, 1);
        printf("fd: %d\n", fd);
        fflush(stdout);
        return 0;
}

        使用dup2也能实现重定向的功能,这里就是将文件描述符 fd 中的存储的内容赋值给了文件描述符 1 中的内容。fd的值是不改变的,所以这里打印出来的是3。上一个代码中打印出来是1,是因为关闭了标准输出后,给新的文件分配的文件描述符为1。

        将open函数中的标志位O_TRUNC改为O_APPEND就是追加重定向的实现了。

       将文件描述符0中 file* 的指向改为文件描述符 fd 中file* 的指向,再把文件的打开方式改为 O_RDONLY,这样就实现了输入重定向。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        int fd = open("log.txt", O_RDONLY, 0644);
        if (fd < 0)
        {
                perror("open failed");
                return 1;
        }
        dup2(fd, 0);
        char buffer[64];

        read(0, buffer, sizeof(buffer));
        printf("%s", buffer);
        return 0;
}

        此时log.txt文件中的内容是fd:3,所以这里输入重定向就是将原本从键盘中输入的内容打印到显示器上,重定向为从log.txt文件读取内容打印到显示器上。 

3.3.5 重定向的操作

         下面代码编译为a.out或者stream的可执行程序。

#include <iostream>
#include <cstdio>

int main()
{
        //向标准输出进行打印
        std::cout << "hello cout" << std::endl;
        printf("hello printf\n");
        return 0;
}

        之前的重定向写法如下,将 ./a.out 中输出到标准输出的内容重定向到 log.txt 文件中。这种写法省略了文件描述符1。

        重定向完整的写法应该如下:

        上述直接表明,将文件描述符 1 对应的 file* 指向的文件中的内容重定向到 log.txt 中。

        修改上述代码:

#include <iostream>
#include <cstdio>

int main()
{
        //向标准输出进行打印
        std::cout << "hello cout" << std::endl;
        printf("hello printf\n");

        //向标准错误进行打印
        std::cerr << "hello cerr" << std::endl;
        fprintf(stderr, "hello stderr\n");

        return 0;
}

        这时使用重定向时,向标准输出打印的部分打印到 log.txt 中,向标准错误打印的部分还是打印到显示器上。因为默认情况下,重定向只会重定向到文件描述符 1 中指针指向的文件。

        如果需要把标准输出和标准错误重定向到同一个文件中,可以写成如下

        另一种写法可以写成如下 

知识点1:
        为什么存在标准输出还要有标准错误?

                因为标准输出和标准错误占用的是不同的文件描述符,再进行调试输出日志信息的时候,可以通过重定向的方式修改标准输出和标准错误对应的文件描述符中 file*  指针指向的文件,就可以将常规信息和错误信息做分离。

 4. 理解“Linux系统下一切皆文件”

        在windows中是文件的东西,在Linux中也是文件;在windows中不是文件的东西,比如进程,磁盘,显示器等在Linux系统中也被抽象成了文件,可以使用访问文件的方式访问它们获得信息。

        这样做最明显的好处是,开发者仅需要使用一套API和开发工具,就可以调取Linux系统中绝大部分的资源。Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE)的操作都可以用write函数来进行。

        当打开文件时,操作系统为了管理打开的文件,都会为这个文件创建一个 file 类型的结构体对象。

struct file {
...
struct inode *f_inode;
/* cached value */
const struct file_operations *f_op;
...
atomic_long_t f_count;
// 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向
它,就会增加f_count的值。
unsigned int f_flags;
// 表⽰打开⽂件的权限
fmode_t f_mode;
// 设置对⽂件的访问模式,例如:只读,只写等。所有
的标志在头⽂件<fcntl.h> 中定义
loff_t f_pos;
// 表⽰当前读写⽂件的位置
...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

        struct file 中的 f_op 指针指向了一个 file_operation 类型的结构体对象,这个结构体中的成员除了struct module* owner 之外,其余都是函数指针。

struct file_operations {
struct module *owner;
//指向拥有该模块的指针;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
"signed size" 类型, 常常是⽬标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 返
回值代表成功写的字节数.
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化设备上的⼀个异步写.
int (*readdir) (struct file *, void *, filldir_t);
//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤返
回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打开⼀个⽂件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
int (*release) (struct inode *, struct file *);
//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
int (*fsync) (struct file *, struct dentry *, int datasync);
//⽤⼾调⽤来刷新任何挂着的数据.
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
//lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实现
它.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};

        file_operation就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取 file_aperation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

        上图中 struct file 结构体对象存储的是各个被打开的设备的设备信息,其中每一个 file 类型的结构体对象中都声明了读写方法的函数指针,这些函数指针分别指向自己对应设备的读写方法,从而在上层进程看来,对硬件进行操作都是调用的同一套接口,屏蔽掉了底层的差异。所以这里本质上就是一种C版本的多态,struct file 对应着基类。

        所以Linux中的一切皆文件的本质是:从进程的角度看,对各个硬件或者是文件进行访问的时候,都可以通过一套API进行操作。 

5. 缓冲区

5.1 什么是缓冲区

        缓冲区就是一段内存空间。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓存输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

5.2 为什么要引入缓冲区机制

        如果没有缓冲区机制,读写文件时,每次的读写操作都需要调用一次系统调用的接口,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,并且频繁的磁盘访问对程序的执行效率造成很大的影响。

        引入缓冲区机制之后,可以减少系统调用的次数,提高效率。比如从磁盘中读取信息,可以一次从文件中读出大量的数据到缓冲区中,后续进程对这部分数据的访问就不需要再去使用系统调用访问磁盘了,等这部分数据取完再去磁盘中读取。上述操作可以减少磁盘的读写次数,并且对缓冲区的操作是对内存的操作,速度远远大于对磁盘进行操作,可以大大提升计算机的运行速度。

5.3 缓冲类型

        先看一个现象,下面有两段代码,第一段进行写入的时候没有close(fd),第二段写入的时候加上close(fd)。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	close(1);
	int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
	//库函数
	printf("fd: %d\n", fd);
	printf("hello xiaoc\n");
	printf("hello xiaoc\n");
	printf("hello xiaoc\n");
	//系统调用
	const char *msg = "hello write\n";
	write(fd, msg, strlen(msg));

	return 0;
}

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	close(1);
	int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
	//库函数
	printf("fd: %d\n", fd);
	printf("hello xiaoc\n");
	printf("hello xiaoc\n");
	printf("hello xiaoc\n");
	//系统调用
	const char *msg = "hello write\n";
	write(fd, msg, strlen(msg));
    close(fd);
	return 0;
}

        现象是,当在结尾处关闭打开的文件,库函数printf()打印的东西没有被写入文件中;当结尾处没有关闭打开的文件,库函数printf()打印的东西被写入到文件中;结尾有没有关闭打开的文件,系统调用write都能把内容写入到文件中。 

       printf/fprintf/fputs/fwrite都是C标准库提供的库函数,C标准库也会提供一个语言级的缓冲区,当使用上述库函数进行写入时,是先把内容写到了C标准库提供的缓冲区中,而要把C标准库缓冲区中内容刷新到内核级缓冲区中需要满足下列中的一个条件 -- 1.强制刷新(fflush(stdout)),2. 刷新条件满足,3. 进程退出。

        而系统调用write调用的时候就直接写到文件内核级缓冲区中的。

         导致上述现象的原因是:当close(fd),关闭文件描述符时,进程还没退出,不满足语言级缓冲区刷新到内核级缓冲区的条件,所以没有写到文件中,而write不管关不关文件描述符,都已经将内容写入到内核级缓冲区中了,进程退出的时候会自动将内核级缓冲区的内容刷新到硬件磁盘中。

        上述的将语言级缓冲区内容刷新到文件内核级缓冲区的刷新条件,就对应着下述的缓冲类型。

        1. 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。

        2. ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。

        3. ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。

        OS也会使用上述缓冲类型但不仅限于这几种,具体使用什么样的缓冲类型由操作系统自主决定,只要把数据交给OS,就相当于交给了硬件。

        用下列代码验证一下上述缓冲类型。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>

int main()
{
	//库函数
	printf("hello printf\n");
	fprintf(stdout, "hello fprintf\n");
	const char *s = "hello fwrite\n";
	fwrite(s, strlen(s), 1, stdout);

	//系统调用
	const char *ss = "hello write\n";
	write(1, ss, strlen(ss));

	fork();

	return 0;
}

        当向显示器打印的时候,是采用的行缓冲刷新方式,所以执行到fork()的时候,语言级缓冲区的数据已经被刷新到了文件中。当重定向到磁盘文件中的时候,是采用的全缓冲的刷新方式,执行到fork()的时候,语言级缓冲区的数据还没有被刷新到文件中,所以到进程结束的时候,子进程也会将语言级缓冲区的内容再刷新一遍,所以出现了上述现象。

        采用行缓冲的方式刷新时,执行库函数的时候就将数据刷新到内核级缓冲区中了,所以按照代码执行顺序,hello write在最后。

        采用全缓冲的方式刷新时,执行库函数的时候还没将数据刷新到内核级缓冲区中,而write是立即将数据刷新到内核级缓冲区中,到进程结束的时候才将语言级缓冲区中的内容刷新到内核级缓冲区中,所以hello write在最前面。

 知识点1:

        为什么要有语言级缓冲区?

                因为将内容刷新到内核级缓冲区是需要系统调用的,系统调用是有成本的,如果每次写入都使用系统调用刷新到内核级缓冲区中,系统调用的次数增加会降低效率。有了语言级缓冲区,每次写入就写到语言级缓冲区中,当写满了之后,只需要调用一次系统调用即可将内容全部刷新到内核级缓冲区中,减少了系统调用的次数,提升效率。

知识点2:

        因为IO相关函数与系统调用接口对应,并且库函数封装了系统调用,所以本质上,访问文件都是通过fd访问的。所以C标准库当中的FIFE结构体内部必定封装了fd。

5.4 简单实现C标准库的文件操作接口

my_stdio.h

#pragma once

//定义C标准库缓冲区大小
#define MAX 1024

// 三种缓冲类型标志位
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2

//使用mFILE结构体模拟C标准库中的FILE结构体
typedef struct IO_FILE
{
    int flag;
    int fileno;
    char outbuffer[MAX];
    int size;
}mFILE;

// mfopen == fopen, mfwrite == fwrite, mfflush == fflush, mfclose == fclose
mFILE *mfopen(const char * filename, const char * mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);

my_stdio.c

#include "mystdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

mFILE *mfopen(const char *filename, const char *mode)
{
    int fd = -1;
    if (strcmp(mode, "r") == 0)
    {
        fd = open(filename, O_RDONLY);
    }
    else if (strcmp(mode, "w") == 0)
    {
        fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    }
    else if (strcmp(mode, "a") == 0)
    {
        fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
    }
    if (fd < 0) return NULL;
    mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
    if (mf == NULL)
    {
        close(fd);
    }

    mf->fileno = fd;
    mf->flag = FLUSH_LINE;
    mf->size = 0;

    return mf;
}

int mfwrite(const void *ptr, int num, mFILE *stream)
{
    //1. 拷贝到用户级缓冲区中
    memcpy(stream->outbuffer + stream->size, ptr, num);
    stream->size += num;

    //2. 判断是否为行缓冲
    if (stream->flag == FLUSH_LINE && stream->outbuffer[stream->size - 1] == '\n')
    {
        mfflush(stream);
    }

    return num;
}
void mfflush(mFILE *stream)
{
    //写到文件内核级缓冲区中
    if (stream->size > 0)
    {
        write(stream->fileno, stream->outbuffer, stream->size);
    }
    fsync(stream->fileno);
    stream->size = 0;
}
void mfclose(mFILE *stream)
{
    if (stream->size > 0)
    {
        mfflush(stream);
    }
    close(stream->fileno);
    free(stream);
}

 main.c

#include "mystdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    mFILE *fp = mfopen("log.txt", "w");
    if (fp == NULL)
    {
        return 1;
    }
    int cnt = 10;
    while(cnt--)
    {
        printf("write %d\n", cnt);
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "hello xiaoc, cnt is : %d", cnt);
        mfwrite(buffer, strlen(buffer), fp);
        // mfflush(fp);
        sleep(1);
    }

    mfclose(fp);
    return 0;
}

        当没有使用mfflush()接口将数据刷新到文件内核级缓冲区时,会先将10次信息写入到用户级缓冲区中,在调用mfclose()时,将用户级缓冲区的数据一次刷新到文件内核级缓冲区中。当使用mfflush()接口时,每次循环都将用户级缓冲区的数据刷新到文件内核缓冲区中,fsync的作用是将文件内核级缓冲区的内容立即刷新到磁盘文件中。

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值