【Linux】基础 IO(文件描述符、重定向、缓冲区)

发布于:2025-04-16 ⋅ 阅读:(13) ⋅ 点赞:(0)

1.理解文件

狭义理解:

  1. 文件在磁盘中。
  2. 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的。
  3. 磁盘是外设(输出设备/输入设备)
  4. 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,简称 “IO”。

广义理解:

Linux 下一切皆文件(键盘、显示器、网卡、磁盘……)

文件操作:

  1. 对于 0KB 的空文件是占用磁盘空间的。
  2. 文件 = 文件属性(元数据) + 文件内容。
  3. 所有的文件操作本质是文件内容操作和文件属性的操作。

系统角度:

  1. 访问文件的前提是先打开文件,谁打开文件呢?答案是 “进程打开文件”。对文件的操作本质就是 “进程对文件的操作”。
  2. 磁盘的管理者是操作系统,访问文件本质就是 “访问磁盘”,只有操作系统才能访问磁盘文件。
  3. 文件的读写本质不是通过C语言/C++的库函数来操作的,这些库函数只是为用户提供方便,而是通过文件相关的系统调用接口来实现的。fopen 和 fclose 封装了操作系统对文件的系统调用。
  4. 操作系统通过 “先描述,再组织” 的方式对文件进行管理。在操作系统内部对被打开的文件创建 struct 结构体(包含被打开文件的相关属性),这些结构体通过链表的形式组织起来,对被文件的管理转化成对链表的 “增删查改”。

2.C文件接口

1.打开 + 写文件

#include<stdio.h>
#include<string.h>
 
int main()
{
    FILE* fp = fopen("log.txt", "w");
    if(fp == NULL)
    {
        perror("fopen:");
        return 1;      
    }
    int cnt = 1;
    const char* msg = "Hello Linux:";                           
    while(cnt <= 10)                                      
    {                                                                                                 
        char buffer[1024];                              
        snprintf(buffer, sizeof(buffer), "%s%d\n", msg, cnt++);
        fwrite(buffer, strlen(buffer), 1, fp);
    }   
    fclose(fp);
	
	return 0;
}

在这里插入图片描述

2.读文件 + 简单实现cat命令

#include<stdio.h>
#include<string.h>
 
int main(int argc, char* argv[])
{
	if(argc != 2)
	{
  		printf("Usage:%s filename\n", argv[0]);                                                      
		return 1;
    }
    FILE* fp = fopen(argv[1], "r");
    if(fp == NULL)
    {
    	perror("fopen");
     	return 2;
	}
    while(1)
    {
    	char buffer[128];
     	memset(buffer, 0, sizeof(buffer));
     	int n = fread(buffer, sizeof(buffer) - 1, 1, fp);
    	if(n > 0)
    		printf("%s", buffer);
    	if(feof(fp)) //当到了文件的结尾,退出循环
    		break;
    }                                                                                                 
	fclose(fp);

    return 0;
}

在这里插入图片描述

3.输出信息到显示器的方式

Linux 中一切皆文件,显示七也是一种文件

#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;
}                                                                           

在这里插入图片描述

4.stdin、stdout、stderr

  1. C 默认会打开三个输入输出流,分别是stdin、stdout、stderr
  2. 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型是文件指针。
#include <stdio.h>

extern FILE *stdin;   //标准输入:键盘文件
extern FILE *stdout;  //标准输出:显示器文件
extern FILE *stderr;  //标准错误:显示器文件

5.打开文件的方式

在这里插入图片描述

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

3.系统接口 IO

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

1.传递标志位

  1. 当存在多个标记位时,一般的做法是传递多个参数,用起来非常麻烦。
  2. 操作系统采用 “位图” 来 “传递标志位” 的方式,32个比特位,每一个比特位的 0/1 代表是否被设置。

传递标志位的代码案例:

#include<stdio.h>

#define ONE_FLAG   1<<0  //0000 0000 ... 0000 0001
#define TWO_FLAG   1<<1  //0000 0000 ... 0000 0010
#define THREE_FLAG 1<<2  //0000 0000 ... 0000 0100
#define FOUR_FLAG  1<<3  //0000 0000 ... 0000 1000

void fun(int flags)
{
	if(flags & ONE_FLAG)   printf("one\n");
    if(flags & TWO_FLAG)   printf("two\n");
    if(flags & THREE_FLAG) printf("three\n");
    if(flags & FOUR_FLAG)  printf("four\n");
}
 
int main()                                                                                            
{
    fun(ONE_FLAG); printf("\n");
    fun(ONE_FLAG | TWO_FLAG); printf("\n");
    fun(ONE_FLAG | TWO_FLAG | THREE_FLAG); printf("\n");
    fun(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG); printf("\n");

	return 0;
}

在这里插入图片描述

2.open、close

#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);
  1. pathname:要打开或创建的目标文件。
  2. flags:文件描述符。打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行 “按位或” 运算,构成 flags。参数:O_RDONLY(只读打开)、O_WRONLY(只写打开)、O_RDWR(读,写打开)。这三个常量,必须指定一个且只能指定一个。O_CREAT:若文件不存在,则创建它。O_APPEND:追加写。
  3. mode:当文件不存在时,以O_WRONLY打开文件,指明创建新文件的访问权限。
  4. 返回值:成功时返回新打开的文件描述符。失败时返回-1
#include <unistd.h>

int close(int fd);
  1. fd:文件描述符,open 函数的返回值。
  2. 返回值:成功时返回0,失败时返回-1

在这里插入图片描述

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

mode_t umask(mode_t mask); //修改权限掩码

在这里插入图片描述

3.write、read

#include <unistd.h>

ssize_t write(int fd, const void* buf, size_t count);
  1. fd:文件描述符,open 函数的返回值。
  2. buf:指向要写入的数据的指针。
  3. count:指定了要写入的字节数。
  4. 返回值:成功时返回真实写入文件的字节数,失败时返回-1

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

FILE* fp = fopen("log.txt", "w"); //低层就是下面的系统调用
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

FILE* fp = fopen("log.txt", "a"); //低层就是下面的系统调用
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);

文本写入 VS 二进制写入

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

#include<unistd.h>

ssize_t read(int fd, void* buf, size_t count);
  1. fd:文件描述符。
  2. buf:将数据读入到该指针 buffer 指向的字符串中。
  3. count:需要读取的字节数。
  4. 返回值:成功时返回读取的字节数,失败时返回-1

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

4.文件描述符

1.是什么?

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

在这里插入图片描述

  1. 文件描述符:进程对应的文件描述符表的数组下标。
  2. 当用户层进行 open(“log.txt”, “w”) 调用时:操作系统创建新的 struct file,在文件描述符表中找到未被使用的下标,将 struct file 的地址填写进去,此时进程与文件就关联了。
  3. 当用户层进行 read(fd, buffer, sizeof(buffer)) 调用时:操作系统拿着 fd 索引文件描述符表找到对应的 struct file,每一个 struct file 都对应内存中的一个 “文件缓冲区”,操作系统先将磁盘文件中的内容预加载到文件缓冲区中,再将文件缓冲区中的内容拷贝到 buffer 中。read 函数本质:内核到用户空间的拷贝函数。
  4. 当用户层进行 write(fd, buffer, strlen(buffer)) 调用时:先将 buffer 指向的内容拷贝到文件缓冲区中,再将缓冲区中的内容定期刷新到磁盘文件中。
  5. 对文件做任何操作,都必须先将文件加载(磁盘->内存的拷贝)到内核对应的文件缓冲区中。

内核代码如下:
在这里插入图片描述

通过 open 系统调用的返回值,得知文件描述符(fd)是一个整数。 如下代码所示:

在这里插入图片描述
在这里插入图片描述
思考:文件描述符为什么从3开始,值为0、1、2 的文件描述符是什么?

  1. 文件描述符值为0、1、2 分别是:标准输入、标准输出、标准错误。
  2. C语言中的 fopen 返回值 FILE* 中的 FILE 是一个结构体。
  3. 在操作系统接口层面上,只认文件描述符 fd
  4. 结构体 FILE 一定封装了文件描述符 fd

在这里插入图片描述

封装:

  1. 在 Windows、Linux 等不同的平台下的系统调用不同,使用系统调用不具备可移植性。
  2. C/C++封装各个平台关于文件操作的系统调用,在不同的平台下通过条件编译进行裁剪,成为语言级接口,具备可移植性。
  3. 语言增加可移植性的原因:为了让更多人使用,防止被淘汰。

2.分配规则

文件描述符的分配规则:在 struct file* array[] 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

在这里插入图片描述

3.重定向原理

在这里插入图片描述

解释图如下:

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

  1. printf 函数就是往 stdout 文件中打印内容,也就是文件描述符值为 1 的文件,但修改了 fd_array[1] 的指向时,便打印到了 log.txt 文件中。
  2. 更改文件描述符表中 fd_array[] 数组某个下标内容指针的指向(数组下标不变),叫做 “重定向”

4.通过dup2系统调用重定向

#include <unistd.h>

int dup2(int oldfd, int newfd);

作用:makes newfd be the copy of oldfd, closing newfd first if necessary

在这里插入图片描述

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

5.标准错误重定向

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

  1. 思考:都是输出到显示器中,为什么要区分标准输出printf、cout和标准错误perror、cerr / 为什么存在标准错误?
  2. 答案:标准输出和标准错误占用不同的文件描述符。虽然都是显示器,但是可以通过重定向,将常规消息和错误消息分离。

在这里插入图片描述

将标准输出和标准错误都重定向到一个文件中该如何做呢?

在这里插入图片描述

6.自定义shell添加重定向功能

  1. 如果内建命令做重定向,需要更改 shell 的标准输入、输出、错误。此时需要创建临时文件,类似两个整数交换的过程,进行一次重定向后需要恢复。
  2. 一个文件可以被多个进程打开。若一个进程将其中一个文件关闭,就会影响其它正在读取该文件的进程。所以 struct file 中有一个 ref_count(引用计数)的整形变量,操作系统打开时引用计数为0,指针指向该文件时引用计数++,关闭文件时引用计数–,当引用计数为0时,struct file 被释放。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unordered_map>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

//命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;

//环境变量表
#define MAX_ENVS 128 
char* g_env[MAX_ENVS];
int g_envs = 0;

//别名映射表
std::unordered_map<std::string, std::string> alias_list;

//重定向,关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

int redir = NONE_REDIR;
std::string filename;

//for test 
char cwd[1024];
char cwdenv[1024];

//last exit code 
int lastcode = 0;

void InitEnv()
{
    extern char** environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;
    
    //1.获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        //申请空间
        g_env[i] = (char*)malloc(strlen(environ[i] + 1));
        //拷贝环境变量
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"XZY=123456";
    g_env[g_envs] = NULL;

    //2.导入环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    
    environ = g_env;
}
    
//获取用户名
const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == NULL ? "None" : name;
}

//获取主机名
const char* GetHostName()
{
    const char* name = getenv("HOSTNAME");
    return name == NULL ? "None" : name;
}

//获取当前路径
const char* GetPwd()
{
    //const char* pwd = getenv("PWD"); 根据环境变量PWD获得当前路径(当cd修改路径时:环境变量不会被修改)
    const char* ret = getcwd(cwd, sizeof(cwd)); //通过系统调用getcwd:获取当前路径cwd
    if(ret != NULL) //获取成功时:ret也是当前路径
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd); //格式化环境变量"PWD=cwd"
        putenv(cwdenv); //更新环境变量"PWD=cwd"
    }
    return ret == NULL ? "None" : cwd;
}

//获取家目录
const char* GetHome()
{
    const char* home = getenv("HOME");
    return home == NULL ? "NULL" : home;
}

//根据当前绝对路径修改为相对路径
std::string DirName(const char* pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos + 1);
}

//制作命令行提示符
void MakeCommandLinePrompt(char prompt[], int size)
{
    //snprintf(prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str()); 
    snprintf(prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

//打印命令行提示符
void PrintCommandLinePrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLinePrompt(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

//获取命令
bool GetCommand(char* command, int size)
{
    char* ret = fgets(command, size, stdin);
    if(ret == NULL) return false;
    command[strlen(command) - 1] = '\0'; //清理\n
    if(strlen(command) == 0) return false;
    return true;
}

//命令解析
bool CommandPrase(char* command)
{
#define SEP " "

    g_argc = 0;
    g_argv[g_argc++] = strtok(command, SEP);
    
    while((bool)(g_argv[g_argc++] = strtok(NULL, SEP)));
    g_argc--;

    return g_argc > 0 ? true : false;
}

//打印命令行参数
void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]:%s\n", i, g_argv[i]);
    }
    printf("argc:%d\n", g_argc);
}

//父进程执行cd命令
bool Cd()
{
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else 
    {
        std::string where = g_argv[1];
        if(where == "~")
        {}
        else if(where == "-")
        {}
        else 
        {
            chdir(where.c_str());
        }
    }
    return true;
}

bool Echo()
{
    if(g_argc == 2)
    {
        std::string opt = g_argv[1];
        if(opt == "$?") //echo $?
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char* env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else 
        {
            std::cout << opt << std::endl;
        }
    }
    return true;
}

//检测并执行内建命令:由父进程执行
bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export") 
    {
        //1.在环境变量表中查找环境变量名:是否存在
        //2.存在修改,不存在新增
    }
    else if(cmd == "alias")
    {
        //std::string nickname = g_argv[1];
        //alias_list.insert(k, v)
    }

    return false;
}

//执行普通命令:由子进程执行
void ExecuteCommand()
{ 
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        
        //子进程检查重定向情况,父进程不能重定向
        int fd = -1;
        if(redir == INPUT_REDIR)
        {
            fd = open(filename.c_str(), O_RDONLY);
            if(fd < 0) exit(1);
            dup2(fd, 0);
            close(fd);
        }
        else if(redir == OUTPUT_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else if(redir == APPEND_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else 
        {}

        //进程替换不影响,重定向的结果
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    //父进程
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
}

void TrimSpace(char command[], int& end)
{
    while(isspace(command[end]))
    {
        end++;
    }
}

void RedirCheck(char command[])
{ 
    redir = NONE_REDIR;
    filename.clear();

    int start = 0, end = strlen(command) - 1;
    while(end > start)
    {
        if(command[end] == '<')
        {
            command[end++] = '\0';
            TrimSpace(command, end);
            redir = INPUT_REDIR;
            filename = command + end;
            break;
        }
        else if(command[end] == '>')
        {
            if(command[end - 1] == '>')
            {
                command[end - 1] = '\0';
                end++;
                TrimSpace(command, end);
                redir = APPEND_REDIR;
                filename = command + end;
                break;
            }
            else 
            {
                command[end++] = '\0';
                TrimSpace(command, end);
                redir = OUTPUT_REDIR;
                filename = command + end;
                break;
            }
        }
        else 
        {
            end--;
        }
    }
}

int main()
{
    //shell启动的时候,需用从系统中获取环境变量
    //我们的环境变量信息应该从父shell中获取
    InitEnv();

    while(true)
    {     
        //1.输出命令行提示符
        PrintCommandLinePrompt();

        //2.获取用户输入的命令
        char command[COMMAND_SIZE];
        if(!GetCommand(command, sizeof(command)))
            continue;

        //3.重定向分析:"ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式
        RedirCheck(command); 
        //printf("redir = %d, filename = %s\n", redir, filename.c_str());

        //4.命令解析:"ls -a -l" -> "ls"、"-a"、"-l" 
        if(!CommandPrase(command))
            continue;
        //PrintArgv();
        
        //检测别名
        //5.检测并处理内建命令
        if(CheckAndExecBuiltin())
            continue;
        
        //6.执行命令
        ExecuteCommand();
    }

    return 0;
}

5.理解一切皆"文件"

在这里插入图片描述

  1. 首先在windows中是文件的东西,它们在linux中也是文件。其次一些在windows中不是文件的东西(进程、磁盘、显示器、键盘)这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息。甚至管道也是文件,网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。
  2. 这样做最明显的好处是,开发者仅需要使用一套 API 和开发工具,即可调取 Linux 系统中绝大部分的资源。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读 PIPE)的操作都可以用 read 函数来进行。几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。
  3. 当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个 file 结构体,该结构体定义在 /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下,以下展示了该结构部分我们关系的内容:
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; //设置对文件的访问模式,例如:只读,只写等。

	loff_t f_pos; //表示当前读写文件的位置 
	//...
};

值得关注的是 struct file 中的 f_op 指针指向了一个 file_operations 结构体,这个结构体中的成员中存在
read 和 write 等函数指针。如下:

struct file_operations 
{
	//...
	ssize_t(*read) (struct file*, char __user*, size_t, loff_t*);
	ssize_t(*write) (struct file*, const char __user*, size_t, loff_t*);
	//...
};

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

在这里插入图片描述

上图中的外设,每个设备都可以有自己的 read 和 write,但对应着不同的操作方法!通过 struct file 下 file_operation 中的各种函数回调,让我们开发者只用 file 便可调取 Linux 系统中绝大部分的资源!这便是 “linux下一切皆文件” 的核心理解。

6.缓冲区

1.什么是缓冲区?

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

在这里插入图片描述

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

以上是什么导致的呢?

在这里插入图片描述

  1. 通过C语言中的库函数(printf/fprintf/fputs/fwrite)输出数据,并不是直接写到文件内核缓冲区中(若是直接写到文件内核缓冲区中,那么进程关闭该文件时,会将缓冲区中的内容刷新到外设中,上面的代码并未出现该结果)。而是在C语言的标准库中,它为每一个打开的文件创建一个用户层,语言级缓冲区。通过系统调用(write)输出数据,直接刷新到文件内核缓冲区中。
  2. 当用户强制刷新 / 筛选条件满足 / 进程退出时:由C标准库根据文件描述符fd + 系统调用(write),将语言级缓冲区中的内容刷新到文件内核缓冲区中。
  3. 在调用 close 之前,进程还未退出,即没有强制刷新 / 筛选条件满足 / 进程退出,数据会一直在C标准库中的语言级缓冲区。当调用 close 时,文件描述符 fd 被关闭,然后进程退出了,此时打算将语言级缓冲区中的内容刷新到文件内核缓冲区,但是调系统调用时 fd 被关了,无法刷新到文件内核缓冲区,就无法看见内容。

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

2.FILE

  1. 问题:每一个文件都有自己对应的语言级缓冲区,那么语言级缓冲区在哪里呢?
  2. 答案:FILE 是 C 语言中的结构体,其中封装了文件描述符fd语言级缓冲区

如下是FILE的部分内容:

typedef struct _IO_FILE FILE;

struct _IO_FILE 
{
	int _fileno; //封装的文件描述符

	//缓冲区相关
	//...
	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. */
	//...
};

3.缓冲类型

  1. 全缓冲:要求填满整个缓冲区后才进行 I/O 系统调用操作。对于磁盘文件的操作通常使用全缓冲。
  2. 行缓冲:当在输入和输出中遇到换行符时,标准 I/O 库函数将会执行系统调用操作。对于显示器通常使用行缓冲。
  3. 无缓冲:标准 I/O 库不对字符进行缓存,直接调用系统调用。标准出错流 stderr 通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

在这里插入图片描述

数据交给系统,交给硬件的本质全是拷贝。计算机流动的本质:一切皆拷贝!

4.为什么要引入缓冲区机制

  1. 读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行读、写等操作,那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
  2. 为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
  3. 又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,⽤来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。
  4. 多次执行 printf 函数可能内部只执行一次系统调用 write,可以减少系统调用的次数,提高效率。

5.设计文件libc库


网站公告

今日签到

点亮在社区的每一天
去签到