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

Linux中C语言open函数打开或创建文件详细讲解.pdf 立即下载

目录

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的作用是将文件内核级缓冲区的内容立即刷新到磁盘文件中。

 

 

零基础用AHK操作Excel:从数据读取到自动填写(附完整脚本解析) 本文为零基础用户详细介绍了如何使用AutoHotkey(AHK)通过COM接口高效操作Excel,实现从数据自动读取到填写的完整自动化流程。文章包含完整的脚本解析、环境搭建指南、核心代码逐行讲解以及实际应用案例,帮助办公人员大幅提升数据处理效率,告别重复劳动。 阅读详情

相关推荐

codesys【modbusRTU】主站

【代码】modbusRTU【codesys】

cfqq1989的博客 4376

C语言 open()、read()和write()

在Unix-like操作系统中,文件描述符(file descriptor)是一个用于标识打开文件或I/O设备的整数值。它是对底层文件系统的抽象,用于在应用程序和操作系统之间传递文件信息。文件描述符是一个非负整数,通常是一个小整数。在C语言中,文件描述符被表示为int类型。每个进程在其打开的文件或设备上都有一组文件描述符,它们是连续的、非重复的整数值。当一个文件或设备被打开时,操作系统会为该文件分配一个文件描述符,并将其返回给应用程序。

tiffany_263的博客 8045

告别串口助手!手把手教你用Matlab直接读取STM32的传感器数据(附完整代码)

本文详细介绍了如何利用Matlab直接与STM32进行串口通信,实现传感器数据的实时采集与分析。通过完整的代码示例和系统架构设计,帮助开发者告别传统的串口助手工具链,打造高效、自动化的数据采集系统。文章涵盖了从硬件配置、通信协议设计到Matlab数据处理与可视化的全流程,特别适合嵌入式开发与数据分析结合的实践场景。

weixin_42629671的博客 430

C中的open函数,write函数,read函数

1.创建新文件并保存数据。3.文件描述符与套接字。2.读取文件中的数据。

weixin_51883798的博客 771

基于Dify1.9知识库流水线模板的思考——图像和表格数据处理

本文探讨了Dify1.9知识库流水线中LLM节点对文档图像/表格增强处理的优化方案。针对当前仅在内容后追加说明的简单做法,提出了结构化标注的改进方法:通过唯一ID和注释块标记图像/表格及其描述,确保内容可精准对齐。文章详细阐述了该方案的解析处理流程、数据结构化表示方法,以及在RAG检索和文档还原中的具体应用。关键经验包括:必须结构化标注非文本内容、LLM增强的必要性、语义向量化处理等。该方案解决了原始方法在内容重排、相似元素区分和检索精度等方面的问题。

roseey的专栏 1440

超级详细!!!新手使用AutoDL跑深度学习(MixVPR项目)的学习记录

新手第一次在AutoDL上跑深度学习的实验,遇到一些问题,简单记录一下。如果大家发现问题,欢迎和我讨论。

weixin_45152283的博客 5万+

Linux系统调用函数open()、标准C库函数fopen()函数讲解以及它们之间的使用区别

open() 是 Linux 系统调用,用于打开或创建文件。它返回一个文件描述符,用于后续的文件操作。open() 提供了底层的、无缓冲的 I/O,直接与操作系统内核交互。fopen() 是标准 C 库函数,也用于打开或创建文件。它返回一个 FILE * 指针,用于后续的文件操作。fopen() 提供了更高级别的接口,包括缓冲和文件位置管理。由于 fopen() 是库函数,所以它在所有支持 C 标准库的系统上都可用。

weixin_49503250的博客 5456

【EB_Tresos专题】-01-EB Tresos导入工程

<--返回「Autosar_BSW高阶配置」总目录--> <--返回「Autosar_BSW高阶配置」专栏主页--> <--返回「个人博客」首页--> <--返回「Autosar_BSW高阶配置」总目录--> <--返回「Autosar_BSW高阶配置」专栏主页--> <--返回「个人博客」首页--> 结尾 ...

「汽车电子助手」的博客 6437

linux文件操作函数

头文件两种函数原型返回值:fd文件描述符pathname:要打开或要创建的文件名字flag:打开的方式,权限,有以下几种方式:O_RDONLY:只读打开;O_WRONLY:只写打开;O_RDWR:读写;O_APPEND:打开时光标到文件末端,写入时从文件末端开始;O_CREAT:当文件不存在时,创建一个新文件,只有文件不存在才会创建;O_EXCL:同时和O_CREAT使用,而文件已经存在时,则会报错(返回值-1),用于测试一个文件是否存在,如果不存在则创建文件。

weixin_75021890的博客 284

【LAMMPS学习】五、LAMMPS命令(1) LAMMPS 输入脚本

本部分描述了 LAMMPS 输入脚本的格式以及其中的命令如何用于定义 LAMMPS 模拟。主要包括以下内容。

荷塘阅色 2645

metahuman 下载材质 脸部灰白 脸部过亮

metahuman 下载材质 脸部灰白 脸部过亮

jacke121的专栏 245

C语言open,read,write函数,及文件读写

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);函数说明:参数 pat

centor的博客 2万+

贪婪投影三角化算法

贪婪投影三角化算法是一种利用点云数据进行曲面重建的方法。其基本思想是通过迭代地连接点云中的邻近点来生成三角形,从而构建出整个表面。这种方法被称为“贪婪”,因为它在每一步仅考虑当前最优的策略,而不考虑全局的最优性。

twnkie的博客 1567

【Linux】基础IO——文件操作|文件描述符|重定向|缓冲区

Linux下文件的基本操作、文件描述符、重定向和缓冲区的理解

大三小白C++进阶之路 1044

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向

最早我们在C语言中学习关于如何用代码来管理文件,比如文件的输入和文件的输出,一些文件的接口,掌握上述的知识只能说是对文件入门而已,在Linux中我们是一切接文件的,如何深入学习文件的知识这是一个难题,今天我们所探讨就是Linux的基础I/O。⭐主体。

AAlykk的博客 1638

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

【Linux:文件】文件基础IO进阶

在普通用户眼中,文件是磁盘上的.txt或.mp4。狭义文件:存储在磁盘等永久性介质上的数据集合。广义文件:一切皆文件。键盘是文件(只读)、显示器是文件(只写)、网卡、磁盘、甚至进程间通信的管道都是文件。这种抽象极其强大:操作系统通过一套统一的接口(read/write)就能管理所有硬件,屏蔽了不同外设的物理差异。当你调用 open 时,内核会为进程创建一个 struct file 结构体来描述该文件。进程通过一个名为 file_struct 的结构体管理所有打开的文件,其内部包含一个数组(文件描述符表)

艾莉丝努力练剑的博客,感谢大家一直以来的对艾莉丝的支持和鼓励,uu们如果对C/C++方向感兴趣的话,可以通过艾莉丝主页的微信名片加艾莉丝的好友,和努力练剑的艾莉丝一起加油! 9058

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

【Linux】深入理解Linux基础IO:从文件描述符到缓冲区设计

本文系统讲解Linux文件IO的核心机制,涵盖文件描述符、系统调用(open/read/write)、缓冲区原理及实现。通过对比C标准库与系统调用,分析重定向、文件操作流程,并手写模拟FILE结构体,深入理解“一切皆文件”的设计哲学

2401_83431652的博客 1818

【Linux系统编程】重定向和缓冲区:fd与重定向、“一切皆文件“和缓冲区机制

重定向到底是怎么实现的?缓冲区又在哪里、为什么存在? 答案就是——修改文件描述符表中的指针指向 + 用户级语言层缓冲区机制!这两者是理解 Shell 工作原理和进程间通信的关键。

分享 AI 时代程序员的进阶之路:大模型应用、AI 编程工具实战,同时沉淀 C / Linux 底层技术笔记。与君共赴,万里征途。 5344

深入理解Linux系统内存中文件结构以及缓冲区,模拟实现c语言库文件接口

本文系统介绍了Linux文件操作的核心概念与技术要点。主要内容包括:1)文件描述符的本质是数组下标,通过进程PCB中的files_struct结构体访问文件;2)重定向通过dup2系统调用实现文件描述符表项的覆盖;3)缓冲区分为语言层和系统层,采用不同的刷新策略(行刷新/全刷新);4)模拟实现了C标准库的FILE结构,包含文件描述符、缓冲区和刷新策略。文章通过代码示例详细演示了文件操作接口的使用方法,并深入分析了缓冲区的刷新机制和工作原理。

2302_80105876的博客 2287

Linux的基础IO

那么,这里我们温习一下操作系统的概念我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。这就是因为在根本上操作系统确实像银行一样,并不完全信任用户程序,因为直接开放底层资源(如内存、磁盘、硬件访问权限)给用户程序会带来巨大的风险。所以就向银行一样他的服务是由工作人员隔着一层玻璃,然后对顾客进行服务的。

2301_81265915的博客 7062

[Linux]基础IO上(C文件操作 | 系统文件IO | 文件描述符fd | 缓冲区概念)

[Linux]基础IO上(C文件操作 | 系统文件IO | 文件描述符fd | 缓冲区概念)

weixin_53027918的博客 1344

【Linux:文件】基础IO

文件描述符(fd)是Linux中用于标识“已打开文件”的非负小整数fd是进程中结构体中fd_array数组的下标,通过fd可找到对应的file结构体(内核中描述已打开文件的对象)。重定向:改变文件描述符对应的“实际文件/设备”,使原本输出到A设备的内容,输出到B文件/设备(常见:> 输出重定向、>> 追加重定向、< 输入重定向)。本质:修改进程fd_array数组中,对应fd下标指向的file指针,使其指向目标文件(而非默认设备)。“一切皆文件”的核心是“抽象与统一”:操作系统(OS)通过FILE。

艾莉丝努力练剑的博客,感谢大家一直以来的对艾莉丝的支持和鼓励,uu们如果对C/C++方向感兴趣的话,可以通过艾莉丝主页的微信名片加艾莉丝的好友,和努力练剑的艾莉丝一起加油! 1362

深入理解 Linux 基础 IO:从用户态到内核态的文件操作全解析----《Hello Linux!》(9)

本文深入探讨Linux文件操作的核心机制,从C语言文件接口到系统调用实现。主要内容包括:1)文件本质解析,区分打开/未打开文件的管理方式;2)C语言文件操作函数(fopen/fwrite等)使用要点;3)系统调用open/read/close的实现原理,重点分析文件描述符(fd)的分配规则和底层结构;4)重定向机制,对比dup系统调用和shell重定向符号的实现差异;5)缓冲区机制,通过fork实验揭示C库函数与系统调用write的缓冲差异。文章结合代码示例,帮助开发者理解从用户态到内核态的完整IO链路,揭

刃神太酷啦的博客 2403

Linux —— 基础IO

对之前所学的文件操作进一步的深入理解,结合图例,方便理解

M的博客 2275

Linux_基础io_文件操作_io系统接口(open,write,read)_文件描述符fd_重定向(dup2)_理解缓冲区_简单实现fputs_简单实现命令行重定向_文件系统_5

缓冲区的本质:就是一段内存。

weixin_66484088的博客 1319

【Linux】一切皆文件:深入理解文件与文件IO

本文系统介绍了Linux文件系统的核心概念和操作机制。

Miun123的博客 3961
上一篇: 进程控制 -- 进程创建(fork),进程终止(exit, _exit),进程等待(wait, waitpid),进程程序替换(exec*系列函数)
下一篇: Ext系列文件系统 -- 磁盘结构,磁盘分区,inode,ext文件系统,软硬链接
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值