目录

一、理解“文件”

1.1狭义理解

1.2广义理解

1.3文件操作的归类认知

1.4系统角度

二、回顾C文件接口

2.1打开文件

2.2写文件

2.3读文件

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

2.5stdin&stdout&stderr

2.6打开文件的方式

三、系统文件I/O

3.1一种传递标志位的方法

3.2写文件

3.3读文件

3.4接口介绍

3.6文件描述fd

3.6.1 0 & 1 & 2

3.6.2文件描述符的分配规则

3.6.3重定向

3.6.4使用dup2系统调用

3.6.5在minishell中添加重定向功能

四、理解“一切皆文件”

五、缓冲区

5.1什么是缓冲区

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

5.3缓冲类型

5.4FILE

5.5简单设计libc库


一、理解“文件”

1.1狭义理解

  • 文件在磁盘(外设)里
  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设(即是输出设备也是输⼊设备)
  • 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出简称IO

1.2广义理解

Linux下⼀切皆文件(键盘、显示器、网卡、磁盘……这些都是抽象化的过程)

1.3文件操作的归类认知

1.4系统角度

  • 对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统
  • 文件的读写本质不是通过C语⾔/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的

二、回顾C文件接口

2.1打开文件

2.2写文件

#include <stdio.h>
#include <string.h>
int main()
{
 FILE *fp = fopen("myfile", "w");
 if(!fp){
 printf("fopen error!\n");
 }
 const char *msg = "hello bit!\n";
 int count = 5;
 while(count--){
 fwrite(msg, strlen(msg), 1, fp);
 }
 fclose(fp);
 return 0;
}

2.3读文件

#include <stdio.h>
#include <string.h>
int main()
{
     FILE *fp = fopen("myfile", "r");
     if(!fp){
     printf("fopen error!\n");
     return 1;
 }
 char buf[1024];
 const char *msg = "hello bit!\n";
 while(1){
 //注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明 
     ssize_t s = fread(buf, 1, strlen(msg), fp);
     if(s > 0){
     buf[s] = 0;
     printf("%s", buf);
 }
 if(feof(fp)){
     break;
     }
 }
 fclose(fp);
 return 0;
}

稍作修改,实现简单 cat 命令:

#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
 if (argc != 2) 
 {
 printf("argv error!\n");
 return 1;
 }
 FILE *fp = fopen(argv[1], "r");
 if(!fp){
 printf("fopen error!\n");
 return 2;
 }
 char buf[1024];
 while(1){
 int s = fread(buf, 1, sizeof(buf), fp);
 if(s > 0){
 buf[s] = 0;
 printf("%s", buf);
 }
 if(feof(fp)){
 break;
 }
 fclose(fp);
 return 0;
}

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

2.5stdin&stdout&stderr

C默认会打开三个输入输出流,分别是stdin,stdout,stderr

仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,文件指针

2.6打开文件的方式

三、系统文件I/O

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

3.1一种传递标志位的方法

3.2写文件

3.3读文件

3.4接口介绍

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


3.6文件描述fd

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

3.6.1 0 & 1 & 2

3.6.2文件描述符的分配规则

3.6.3重定向

重定向就是更改文件描述符表指针的指向,数组下标是不变的

3.6.4使用dup2系统调用

printf是C库当中的IO函数,⼀般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成(清空式)输出重定向。

输入重定向:

3.6.5在minishell中添加重定向功能

四、理解“一切皆文件”

⾸先,在windows中文件的东西,它们在linux中也是文件;其次⼀些在windows中不是文件的东 西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是⼀致的。

这样做最明显的好处是,开发者仅需要使用⼀套API和开发⼯具,即可调取Linux系统中绝大部分的 资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE)的操作都可以用write 函 数来进行。 之前我们讲过,当打开⼀个文件时,操作系统为了管理所打开的文件,都会为这个文件创建⼀个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; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义 
   loff_t f_pos; // 表⽰当前读写⽂件的位置 
 
   ...
 
} __attribute__((aligned(4)));   /* lest something weird decides that 2 is OK*/

值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。

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_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的⼯作。

五、缓冲区

5.1什么是缓冲区

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

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

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

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

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

5.3缓冲类型

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

全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通 常使用全缓冲的方式访问。

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

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

5.4FILE

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

对进程实现重定向:

5.5简单设计libc库

//my_stdio.h 

#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
 int flag; // 刷新⽅式 
 int fileno; // ⽂件描述符 
 char outbuffer[SIZE];
 int cap;
 int size;
 // TODO
};
typedef struct IO_FILE mFILE;
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 "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.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_CREAT|O_WRONLY|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) 
 {
   close(fd);
   return NULL;
 }
 mf->fileno = fd;
 mf->flag = FLUSH_LINE;
 mf->size = 0;
 mf->cap = SIZE;
 return mf;
}
void mfflush(mFILE *stream)
{
 if(stream->size > 0)
 {
   // 写到内核⽂件的⽂件缓冲区中! 
   write(stream->fileno, stream->outbuffer, stream->size);
   // 刷新到外设 
   fsync(stream->fileno);
   stream->size = 0;
 }
}
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->size > 0 && stream->outbuffer[stream->size-1]== '\n')
  {
   mfflush(stream);
  }
 return num;
}
void mfclose(mFILE *stream)
{
 if(stream->size > 0)
 {
   mfflush(stream);
 }
 close(stream->fileno);
}
//main.c 


#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
 mFILE *fp = mfopen("./log.txt", "a");
 if(fp == NULL)
 {
   return 1;
 }
 int cnt = 10;
 while(cnt)
 {
   printf("write %d\n", cnt);
   char buffer[64];
   snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);
   cnt--;
   mfwrite(buffer, strlen(buffer), fp);
   mfflush(fp);
   sleep(1);
 }
 mfclose(fp);
}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐