首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Linux 之 详谈系统I/O文件及内核级缓冲区(看这一篇就够了)

Linux 之 详谈系统I/O文件及内核级缓冲区(看这一篇就够了)

作者头像
用户11317877
发布2025-02-16 19:51:23
发布2025-02-16 19:51:23
20800
代码可运行
举报
文章被收录于专栏:学习学习
运行总次数:0
代码可运行

打开文件的方式不仅仅是fopen, ifstream等流式, 语言层的方案, 其实系统才是打开文件最底层的方案. 不过, 在学习文件IO之前, 先要了解一下如何给函数传递标志位, 该方法在系统文件IO接口中会使用到:

1. 标志位的传递

系统级接口open

代码语言:javascript
代码运行次数:0
运行
复制
#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: 追加写
返回值:
 成功:新打开的⽂件描述符
 失败:-1
open返回值

在认识返回值之前,先来认识⼀下两个概念: 系统调⽤ 和 库函数

• 上⾯的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。 • ⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝

回忆⼀下我们讲操作系统概念时,画的⼀张图

系统调⽤接⼝和库函数的关系,⼀⽬了然。 所以,可以认为, f# 系列的函数,都是对系统调⽤的封装,⽅便⼆次开发。

2. 文件描述符

通过对open函数的学习,我们知道了⽂件描述符就是⼀个⼩整数

0 & 1 & 2

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

0,1,2对应的物理设备⼀般是:键盘,显⽰器,显⽰器

所以输⼊输出还可以采⽤如下⽅式:

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
	 char buf[1024];
	 ssize_t s = read(0, buf, sizeof(buf));
	 if(s > 0)
	 {
		 buf[s] = 0;
		 write(1, buf, strlen(buf));
		 write(2, buf, strlen(buf));
	 }
 	return 0;
}

⽽现在知道,⽂件描述符就是从0开始的⼩整数。当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files,指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

对于以上原理结论我们可通过内核源码验证:

⾸先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.0- 1160.71.1.el7.x86_64/include/linux/sched.h (3.10.0-1160.71.1.el7.x86_64是内核版 本,可使⽤ uname -a ⾃⾏查看服务器配置,因为这个⽂件夹只有⼀个,所以也不⽤刻意去分辨,内核版本其实也随意)

2.1 文件描述符的分配规则

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 close(fd);
 return 0;
}

输出发现是fd: 3 关闭0或者2,在看

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
 close(0);
 //close(2);
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 close(fd);
 return 0;
}

发现是结果是: fd: 0 或者 fd 2 ,可⻅,⽂件描述符的分配规则:在files_struct数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。

2.2 重定向

那如果关闭1呢?看代码:

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
 close(1);
 int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 fflush(stdout);
  close(fd);
 exit(0);
}

此时,我们发现,本来应该输出到显⽰器上的内容,输出到了⽂件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常⻅的重定向有: > ,>> ,<

那重定向的本质是什么呢?

2.3 dup2系统调用

函数原型如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include <unistd.h>
int dup2(int oldfd, int newfd);

⽰例代码

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
 	int fd = open("./log", O_CREAT | O_RDWR);
 	if (fd < 0) 
 	{
 		perror("open");
	 	return 1;
 	}
  	close(1);
 	dup2(fd, 1);
 	for (;;) 
 	{
		 char buf[1024] = {0};
		 ssize_t read_size = read(0, buf, sizeof(buf) - 1);
 		 if (read_size < 0) 
 		 {
 			perror("read");
 			break;
 		 }
 		printf("%s", buf);
 		fflush(stdout);
 	}
 	return 0;
}

printf是C库当中的IO函数,⼀般往stdout中输出,但是stdout底层访问⽂件的时候,找的还是fd:1,但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。那追加和输⼊重定向如何完成呢?

3. 缓冲区

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

3.1 为什么要引⼊缓冲区机制

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区可⼤ 提⾼计算机的运⾏速度。

⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。

3.2 缓冲类型

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

  1. 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。
  2. ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
  3. ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。

除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执⾏flush语句;

3.3 FILE

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

来段代码在研究⼀下:

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <string.h>
int main()
{
 const char *msg0="hello printf\n";
 const char *msg1="hello fwrite\n";
 const char *msg2="hello write\n";
 printf("%s", msg0);
 fwrite(msg1, strlen(msg0), 1, stdout);
 write(1, msg2, strlen(msg2));
 fork();
 return 0;
}

运⾏出结果:

代码语言:javascript
代码运行次数:0
运行
复制
hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file ,我们发现结果变成了:

代码语言:javascript
代码运行次数:0
运行
复制
hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现 printf 和 fwrite (库函数)都输出了2次,⽽ write 只输出了⼀次(系统调⽤)。为什么呢?肯定和fork有关

  1. ⼀般C库函数写⼊⽂件时是全缓冲的,⽽写⼊显⽰器是⾏缓冲。
  2. printf fwrite 库函数+会⾃带缓冲区(进度条例⼦就可以说明),当发⽣重定向到普通⽂件时,数据的缓冲⽅式由⾏缓冲变成了全缓冲。
  3. ⽽我们放在缓冲区中的数据,就不会被⽴即刷新,甚⾄fork之后
  4. 但是进程退出之后,会统⼀刷新,写⼊⽂件当中。
  5. 但是fork的时候,⽗⼦数据会发⽣写时拷⻉,所以当你⽗进程准备刷新的时候,⼦进程也就有了同样的⼀份数据,随即产⽣两份数据。
  6. write 没有变化,说明没有所谓的缓冲。

综上: printf fwrite 库函数会⾃带缓冲区,⽽ write 系统调⽤没有带缓冲区。另外,我们这⾥所说的缓冲区,都是⽤⼾级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区. 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调⽤,库函数在系统调⽤的“上层”,是对系统调⽤的“封装”,但是 write 没有缓冲区,⽽ printf fwrite 有,⾜以说明,该缓冲区是⼆次加上的,⼜因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体: typedef struct _IO_FILE FILE; 在/usr/include/stdio.h

代码语言:javascript
代码运行次数:0
运行
复制
在/usr/include/libio.h
struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关 
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;
 int _fileno; //封装的⽂件描述符 
#if 0
 int _blksize;
#else
 int _flags2;
#endif
 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];
 /* char* _save_gptr; char* _save_egptr; */
 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

4. 关于内核文件缓冲区

上面这段代码, 编译运行, 结果是创建了一个新的文件log1.txt, 但是按照我们正常的思维是有东西的, 这里为什么没有呢 ? 这里就要重点解释一下了, 这是因为当我们改变stdout的描述符, 此时1这个描述符指向的就是log1.txt, 而log1.txt的文件缓冲区是写满刷新的, 所以就不会把内容刷新到系统里面, 而是放在了文件的缓冲区里, 然后此时你"啪"一下, close掉这个进程那么内容就还在文件缓冲区中, 这里要区分系统内核缓冲区和文件缓冲区.

此时我们需要手动进行刷新, 使用语言级函数fflush手动把文件缓冲区内容刷新到系统中.

在这里插入图片描述
在这里插入图片描述

文件缓冲区的刷新方式有三种, 这些都是语言级别的

而系统缓冲区在linux内核中是什么时候刷新到外设即磁盘中是由操作系统自己决定的. 当然也有函数可以直接手动刷新, 可以使用fsync()函数进行手动刷新.


代码语言:javascript
代码运行次数:0
运行
复制
int open(const char *pathname, int flags, mode_t mode);

参数说明

  1. pathname: 类型: const char * 说明: 要打开的文件的路径(可以是绝对路径或相对路径)。这个参数指定了你希望打开的文件的名称。
  2. flags: 类型: int 说明: 打开文件时的标志,控制文件的打开方式。常用的标志包括: O_RDONLY: 以只读方式打开文件。 O_WRONLY: 以只写方式打开文件。 O_RDWR: 以读写方式打开文件。 O_CREAT: 如果文件不存在,则创建文件。此时需要提供第三个参数 mode。 O_EXCL: 与 O_CREAT 一起使用,如果文件已经存在,则 open 函数将失败。 O_TRUNC: 以写入方式打开文件,并将文件长度截断为零。 O_APPEND: 指定写入操作应该添加到文件末尾。
  3. mode (如果 O_CREAT 被使用): 类型: mode_t 说明: 当 O_CREAT 标志被设置时,表示新文件的权限模式。通常使用八进制数字来表示权限,例如: 0644: 所有者有读写权限,组和其他用户有读权限。 0755: 所有者有读、写、执行权限,组和其他用户有读和执行权限。 返回值 成功时,open 函数返回一个非负整数,表示打开的文件描述符。该文件描述符可以用于后续的文件操作(如 read、write 和 close)。 如果发生错误,返回 -1,并将 errno 设置为相应的错误代码。

示例

代码语言:javascript
代码运行次数:0
运行
复制
#include <fcntl.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <stdlib.h>  

int main() {  
    const char *filename = "example.txt";  
    int fd;  

    // 以只读模式打开文件  
    fd = open(filename, O_RDONLY);  
    if (fd == -1) {  
        perror("Error opening file");  
        return EXIT_FAILURE;  
    }  

    // 进行文件操作(读取、处理等)  

    // 关闭文件  
    close(fd);  
    return EXIT_SUCCESS;  
}

下面这段代码没有写close关闭进程, 所以当进程结束之前会自动将所有缓冲区内容进行刷新, 不管是文件缓冲区还是内核缓冲区都会进行刷新, 所以test1.txt中会有内容.

1. 小问题

那么下面这段代码呢?

由于进行了fork()创建了新的进程, 代码和数据子进程会进行拷贝, 形成自己独立的一份, 因为此时是普通文件, 普通文件会写满刷新, 所以父子进程结束时都会进行文件缓冲区的刷新, 所以就会形成两份, 而write是系统级的接口, 直接写入系统内核缓冲区中, 不会受到影响.

2. 简单设计⼀下libc库

my_stdio.h

代码语言:javascript
代码运行次数:0
运行
复制
  1 #pragma once
  2 
  3 #define SIZE 1024
  4 
  5 #define FLUSH_NONE 0
  6 #define FLUSH_LINE 1
  7 #define FLUSH_FULL 2
  8 
  9 struct IO_FILE
 10 {
 11     int flag; //刷新方式
 12     int flieno; //文件描述符
 13     char outbuffer[SIZE];
 14     int cap;
 15     int size;
 16 };
 17 
 18 typedef struct IO_FILE mFILE;
 19 
 20 mFILE *mfopen(const char *filename, const char *mode);
 21 int mfwrite(const void *ptr, int num, mFILE *stream);
 22 void mfflush(mFILE *stream);
 23 void mfclose(mFILE *stream);  

my_stdio.c

代码语言:javascript
代码运行次数:0
运行
复制
  1 #include "my_stdio.h"
  2 #include<string.h>
  3 #include<stdlib.h>
  4 #include<sys/stat.h>
  5 #include<sys/types.h>
  6 #include<fcntl.h>
  7 #include<unistd.h>
  8 
  9 mFILE *mfopen(const char* filename, const char *mode)
 10 {
 11     int fd = -1;
 12     if(strcmp(mode, "r") == 0)
 13     {
 14         fd = open(filename, O_RDONLY);
 15     }
 16     else if(strcmp(mode, "w") == 0)
 17     {
 18         fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
 19     }
 20     else if(strcmp(mode, "a") == 0)
 21     {
 22         fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
 23     }
 24     if(fd < 0) return NULL;
 25     mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
 26     if(!mf)
 27     {
 28         close(fd);                                                                                                                                                  
 29         return NULL;
 30     }
 31 
 32     mf->flieno = fd;
 33     mf->flag = FLUSH_LINE;
 34     mf->size = 0;
 35     mf->cap = SIZE;
 36 
 37     return mf;
 38 }
 39 
 40 void mfflush(mFILE *stream)
 41 {
 42     if(stream->size > 0)
 43     {
 44         //写入内核文件的文件缓冲区中!
 45         write(stream->flieno, stream->outbuffer,stream->size);
 46         //刷新到外设
 47         fsync(stream->flieno);
 48         stream->size = 0;
 49     }                                                                                                                                                               
 50 }
 51 
 52 int mfwrite(const void *ptr, int num, mFILE* stream)
 53 {
 54     //1. 拷贝
 55     memcpy(stream->outbuffer + stream->size, ptr, num);
 56     stream->size += num;
 57 
 58     //2.检测是否要刷新
 59     if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size-1] == '\n')
 60     {
 61         mfflush(stream);
 62     }
 63     return num;
 64 }
 65 
 66 void mfclose(mFILE *stream)
 67 {
 68     if(stream->size > 0)
 69     {
 70         mfflush(stream);
 71     }
 72     close(stream->flieno);
 73 }

main.c

代码语言:javascript
代码运行次数:0
运行
复制
  1 #include "my_stdio.h"
  2 #include<stdio.h>
  3 #include<string.h>
  4 #include<unistd.h>
  5 
  6 int main()
  7 {
  8     mFILE* fp = mfopen("./log.txt","a");
  9     if(fp == NULL)
 10     {
 11         return 1;
 12     }
 13     int cnt = 10;
 14     while(cnt)
 15     {
 16         printf("write %d\n", cnt);
 17         char buffer[64];
 18         snprintf(buffer, sizeof(buffer), "hello message, number is : %d", cnt);
 19         cnt--;
 20         mfwrite(buffer, strlen(buffer), fp);
 21         mfflush(fp);
 22         sleep(1);
 23     }
 24     mfclose(fp);                                                                                                                                                    
 25     return 0;
 26 }
~
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 标志位的传递
    • 系统级接口open
      • open返回值
  • 2. 文件描述符
    • 0 & 1 & 2
    • 2.1 文件描述符的分配规则
    • 2.2 重定向
    • 2.3 dup2系统调用
  • 3. 缓冲区
    • 3.1 为什么要引⼊缓冲区机制
    • 3.2 缓冲类型
    • 3.3 FILE
  • 4. 关于内核文件缓冲区
    • 1. 小问题
    • 2. 简单设计⼀下libc库
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档