Linux系统-基础IO

卸载Office 16 Click-to-Run Extensibility Component详细步骤 卸载Office 16 Click-to-Run Extensibility Component详细步骤 第一步:按住键盘上的“Win+R”,打开运行窗口 在运行窗口输入“Installer”回车,打开Installer文件夹 第二步:在Installer文件夹红框最右侧,即“大小”后面,右击选择“其他”,找到“主题”,勾选,点击“确定”,添加主题列成功 第三步:在“主题”列找到“Office 16 Click-to-Run Extensibility Component 阅读详情

目录

一、理解文件

1.1、狭义理解

1.2、广义理解

1.3、文件的归类认知

1.4、系统角度

二、回顾C语言文件接口

2.1、hello.c打开文件

2.2、写文件

2.3、读文件

2.4、实现一个简单的cat命令

2.5、将信息输出到显示器

2.6、stdin、stdout、stderr

 2.7、文件打开的方式

三、系统IO

3.1、一种传递标志位的方法

​编辑 3.2、写文件

 关于写文件是否关心写进的内容的形式

3.3、读文件

3.4、接口相关知识

3.5、文件描述符fd

重定向操作

myshell实现重定向操作

重谈重定向(标准错误)

四、理解“一切皆文件”

五、缓冲区

简单glibc库的实现


一、理解文件

1.1、狭义理解

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

1.2、广义理解

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

1.3、文件的归类认知

  • 对于0kb的空文件也是占用磁盘空间的
  • 文件是文件内容和文件属性(元数据)的集合(文件=内容+属性(元数据))
  • 所有对文件的操作本质上是对文件内容和文件属性的操作

1.4、系统角度

  • 对文件的操作本质上是进程对文件的操作(进程打开文件!)
  • 磁盘的管理者是操作系统
  • 文件读写的本质不是通过C/C++库函数实现的,这些库函数只是为了方便使用;实际上是通过与文件相关的系统调用接口来实现的
  • 操作系统管理磁盘上的文件是通过先描述再组织的方式,也就是文件会有一个与之对应的内核数据结构,又因为对文件的操作本质上是进程对文件的操作,而进程在操作系统层面也是通过内核数据结构管理的;那么对文件的操作实际上可以理解为对两个内核数据结构之间的操作

二、回顾C语言文件接口

2.1、hello.c打开文件

1 #include<stdio.h>
  2 
  3 int main()
  4 {
  5     FILE * fp = fopen("myfile" , "w");
  6     if(fp == NULL)
  7     {
  8         perror("open failed!\n");
  9         return 1;
 10     }
 11     printf("open secceed!\n");                                                                                                                           
 12     fclose(fp);
 13     return 0;
 14 }

成功以写的打开文件不存在的文件会默认先生成这个文件;生成的位置为当前的路径;

这是因为cwd记录着当前进程的路径,这个进程打开文件时,也会把文件拼接在这个路径之后:

2.2、写文件

介绍两个函数:fwrite、fread

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

相关说明:
The function fwrite() writes nmemb elements of data, each size bytes long, to the stream pointed to by stream, obtaining them  from  the  location given by ptr.

将ptr里面的内容写入stream里面,写入nmemb个单位,每个nmemb单位长度是size个字节


size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

相关说明:

The  function fread() reads nmemb elements of data, each size bytes long, from the stream pointed to by stream, storing them at the location given by ptr.

将stream中的数据读取到ptr里面,读取nmemb个单位,每个单位size个字节


返回值:

On success, fread() and fwrite() return the number of items read or written.  This number equals the number of bytes transferred only when size is 1.  If an error occurs, or the end of the file is reached, the return value is a short item count (or zero).

成功后,fread() 和 fwrite() 返回读取或写入的项目数。此数字等于仅当 size 为 1 时传输的字节数。如果发生错误或到达文件末尾,则返回值为较短的项目计数(或零)。

写文件:将msg和cnt格式化写入buffer,再使用fwrite函数将buffer的内容写入myfile文件 

5 int main()
  6 {
  7     FILE * fp = fopen("myfile", "w");
  8     if(fp == NULL)
  9     {
 10         perror("open failed!\n");
 11         return 1;
 12     }
 13     const char* msg = "hello linux !:";                                                                                                                  
 14     int cnt = 5;
 15     while(cnt)
 16     {
 17         char buffer[1024];
 18         snprintf(buffer, sizeof(buffer), "%s%d\n" , msg , cnt--);
 19         fwrite(buffer , strlen(buffer) , 1 , fp);
 20     }
 21     fclose(fp);
 22     return 0;
 23 }

2.3、读文件

7 int main()
    8 {
    9     FILE * fp = fopen("myfile" , "r");
   10     if(fp == NULL)
   11     {
   12         perror("open failed\n");
   13         return 1;
   14     }
   15     const char* msg = "hello linux !:";//用来计算每次读取的字节数
   16     while(1)
   17     {
   18         char* buffer[1024];
   19         size_t s = fread(buffer , strlen(msg)+2 , 1 , fp);//写入文件时有一个数字和一个'\n'                                                             
   20         if(s > 0)
   21         {
W> 22             printf("%s" , buffer);
   23         }
   24         if(feof(fp))
   25         {
   26             break;
   27         }
   28     }
   29     return 0;
   30 }

feof判断文件指针是否指向文件末尾,若是返回值非0,否则返回0 

2.4、实现一个简单的cat命令

 5 //基于文件读操作实现一个简单的cat
    6 int main(int argc , char* argv[])
    7 {
    8     if(argc != 2)
    9     {                                                                                                                                                  
   10         printf("argc error!\n");
   11         return 1;
   12     }
   13     FILE * fp = fopen(argv[1] , "r");
   14     if(fp == NULL)
   15     {
   16         perror("open failed\n");
   17         return 2;
   18     }
   19     const char* msg = "hello linux !:";//用来计算每次读取的字节数
   20     while(1)
   21     {
   22         char* buffer[1024];
   23         size_t s = fread(buffer , strlen(msg)+2 , 1 , fp);//写入文件时有一个数字和一个'\n'
   24         if(s > 0)
   25         {
W> 26             printf("%s" , buffer);
   27         }
   28         if(feof(fp))
   29         {
   30             break;
   31         }
   32     }
   33     return 0;
   34 }

2.5、将信息输出到显示器

       int printf(const char *format, ...);
       int fprintf(FILE *stream, const char *format, ...);
       size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

 5 //输出信息到显示器
  6 int main()
  7 {
  8     const char* msg = "hello file!\n";
  9     printf("%s:%s\n", "printf", msg);
 10     fprintf(stdout, "%s:%s\n", "fprintf", msg);
 11     char buffer[128];
 12     snprintf(buffer, sizeof(buffer), "%s:%s\n", "fwrite", msg);                                                                                          
 13     fwrite(buffer, strlen(buffer), 1, stdout);
 14 
 15     return 0;
 16 }

2.6、stdin、stdout、stderr

       #include <stdio.h>

       extern FILE *stdin;
       extern FILE *stdout;
       extern FILE *stderr;

stderr作为标准错误流,程序在运行期间遇到错误时,可借助它输出错误信息。将错误信息和正常输出分开,能让用户更清晰地区分哪些是程序正常运行的结果,哪些是程序出错的提示 

  • C默认会打开三个输入输出流,分别是stdin、stdout、stderr
  • 这三个文件流需要频繁使用,默认打开

 2.7、文件打开的方式

       r      Open text file for reading.  The stream is positioned at the beginning of the file.

       r+     Open for reading and writing.  The stream is positioned at the beginning of the file.

       w      Truncate file to zero length or create text file for writing(文件存在先清空不存在先新建此文件).  The stream is positioned at the beginning of the file.

       w+     Open for reading and writing.  The file is created if it does not exist, otherwise it is truncated.  The stream is positioned at the beginning of the file.

       a      Open for appending (writing at end of file).  The file is created if it does not exist.  The stream is positioned at the end of the file.

       a+     Open  for reading and appending (writing at end of file).  The file is created if it does not exist.  The initial file position for reading is at the beginning of the file, but output is always appended to the end of the file.

看看"w"和"a"的对于一个不为空文件的区别

"w"打开文件,存在则清空 

echo >重定向操作和其类似

"a"打开文件,写入时是追加的方式不会清空

先清空文件,再用"a"打开文件写入

这个打开方式和echo >> 类似,写入是以追加的方式


三、系统IO

3.1、一种传递标志位的方法

按照位图的方式,这种方式在open的标志位中使用

  8 void Func(int flag)
  9 {
 10     if(flag & ONE_FLAG)
 11     {
 12         printf("flag one\n");
 13     }
 14     else if(flag & TWO_FLAG)
 15     {
 16         printf("flag two\n");
 17     }
 18     else if(flag & THREE_FLAG)
 19     {
 20         printf("flag three\n");
 21     }
 22     else
 23     {
 24         printf("flag four\n");
 25     }
 26 }
 27 int main()
 28 {
 29     Func(ONE_FLAG);
 30     Func(TWO_FLAG);
 31     Func(THREE_FLAG);
 32     Func(FOUR_FLAG);                                                                                                                                     
 33     return 0;
 34 }

看看open系统调用接口中常用的几种标志位,它们都是定义的宏对应有具体的值

 3.2、写文件

先看看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);

  1.  pathname就是我们要打开的文件,这个可以只指定文件名,因为进程会记录当前路径;
  2. flags就是标志位,有只读、只写、新建、追加、清空;
  3. mode权限位:当新建一个文件时,用于指定这个文件的权限,比如传0666一类的,当然此后文件的具体权限还和umask掩码有关
  4. open返回的是打开的这个文件的文件描述符,用来标识这个文件;要是打开失败就返回-1

用一个例子引入

先看看write系统调用接口

 头文件

#include <unistd.h>

接口使用

 ssize_t write(int fd, const void *buf, size_t count);

返回值:要是写入成功就返回成功写入的字节;反之返回-1

 On success, the number of bytes written is returned (zero indicates nothing was written).  On error, -1 is returned, and errno  is  set  appropriately.

打开也要关闭 系统调用接口close

#include <unistd.h>

int close(int fd);
 

close() returns zero on success.  On error, -1 is returned, and errno is set appropriately.

  7 int main()
  8 {
  9     int fd = open("log.txt", O_CREAT | O_WRONLY  , 0666);
 10     if(fd < 0)
 11     {
 12         perror("open failed!\n");
 13         return 1;
 14     }
 15     const char* msg = "open a file\n";
 16     int cnt = 5;
 17     while(cnt)
 18     {
 19         write(fd, msg, strlen(msg));
 20         cnt--;
 21     }                                                                                                                                                    
 22     close(fd);
 23     return 0;
 24 }

首先看看flags

这里使用了创建和只写,我们若是向此时的文件再写一串abcd呢

此时会发现会覆盖并且也没有清空,这是因为我们没有设置对应的标志位,若是想要清空写就加上O_TRUNC;若是想要追加写就加上O_APPEND

再C语言中fwrite中w就会自带清空,这是因为这个函数接口封装了系统调用接口;在我们使用系统调用接口时要自己设置

使用O_TRUNC,清空写

添加标志位O_APPNED追加写

追加写肯定不能和清空写一起出现

看看权限位mode

权限为设置的是0666,但是显示的是这样的,也就是0664,这是因为权限掩码umask的存在

可以设置权限掩码:

系统默认的

在我们的代码中设置

 关于写文件是否关心写进的内容的形式

在C语言阶段,有文本写入和二进制写入,但是在系统层面没有这个概念就是说,系统写文件时不关心写的内容的格式,从write的参数就可以得知,第二个参数是void*

实验一下,之前写入的都是字符串,现在写一个整形变量看看:

在写入字符串时,系统使用ASCLL码表将对应字符的字节写入文件,打印时就是ASCLL码表中对应的字符;但是在写入整形变量时,会将整形变量对应的二进制数据原封不动的写入文件,cat 命令并不知道这些数据代表的是整数,它只是简单地把文件中的字节当作 ASCII 码来尝试显示,由于二进制数据并不是有效的 ASCII 码,所以显示出来的就是乱码或者看似二进制的数据。

但是这个不是最重要的,重要的是我们使用系统调用接口写入文件时,不关系写入内容的形式

而所谓的文本写入或者是二进制写入是语言层面关心的;所以语言层面才会封装一系列接口去写入文件

3.3、读文件

读文件时打开文件使用open但是此时标志位只需要传O_RDONLY,权限位也不用传,因为写文件时创建的文件权限位已经设置好了,这也是为什么open会有两个接口

先看看read接口

 #include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
 

返回值,正常情况返回大于0的数表示读取到的字节数;返回0,则是读取到文件的末尾;返回-1表示读取出现错误

  8 int main()
  9 {
 10     int fd  = open("log.txt", O_RDONLY);
 11     if(fd < 0)
 12     {
 13         perror("open failed!\n");
 14         return 1;
 15     }
 16     char buffer[128];
 17     read(fd, buffer, sizeof(buffer));
 18     printf("%s\n", buffer);                                                                                                                              
 19     return 0;
 20 }

3.4、接口相关知识

在认识返回值之前,先来认识⼀下两个概念: 系统调⽤ 和 库函数
•
上⾯的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数
(libc)。
•
⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝
•
回忆⼀下我们讲操作系统概念时,画的⼀张图
Linux下的这些接口到Windows其他操作系统不能用;但是语言却具有可迁移性,举个例子,C语言的相关接口比如fwrite在Windows下就能用,这是因为C语言中也封装了Windows下的系统调用接口。
至于为什么,当然是为了提高这门语言的竞争性,让更多的人使用

3.5、文件描述符fd

打开几个文件,查看它们的返回值文件描述符fd;发现没有0、1、2;这是因为这三个文件描述符代表的是三个标准文件流,stdin、stdout、stderr

打印0、1、2文件描述符

fd就是数组下标,那么是什么数组下标呢?解析下图:

首先文件是存储在磁盘上的,对于每个存储在磁盘上的文件在操作系统中都存在一个struct file的内核数据结构用来存储这个打开的文件的相关信息(注意是打开的文件才有,因为只有打开的文件才会在操作系统中被交互);并且每个打开的文件都有一个文件缓冲区,这个文件缓冲区中就是磁盘上文件内容的拷贝 ,对于文件的任何操作都需要先把文件内容拷贝到文件缓冲区,对文件缓冲区操作之后再把文件缓冲区刷新到磁盘上的文件中。

其次这个数组就是操作系统中的一个指针数组,这个数组中存放的指针类型是struct file*的,就是存放的地址就是打开文件的组织形式即struct file的地址;

那么我们对文件的操作具体是怎么实现的呢?就拿写文件来说,首先,通过write中传的fd(充当下标)和打开这个文件的进程中struct file*从文件描述符指针中找到要写的文件的地址,再通过这个地址找到对应的struct file以及文件缓冲区;将内容写道文件缓冲区中再刷新缓冲区到磁盘。同样的读文件也是,先通过相同操作找到对应文件,再将文件缓冲区的内容拷贝到buffer里面,所以read本质上是系统到用户空间的拷贝函数

打开的文件也是先描述再组织

重定向操作

首先来看一组现象

关闭文件描述符0代表的文件stdin之后,再打开一个新的文件发现这个新打开的文件的文件描述符是0;

由此得出一个结论:文件描述符分配规则就是最小的还没有被使用的文件描述符分配给新打开的文件

那么将fd为2的文件描述符代表的文件stdout关掉,再打开一个新文件,那么输出就默认输出到这个新文件里面了;本质上是指针的指向的改变

看代码:

###dup2系统调用接口的操作:

int dup2(int oldfd, int newfd);
 

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary;

RETURN VALUE
       On success, these system calls return the new descriptor.  On error, -1 is returned, and errno is set appropriately.

分析一下oldfd和newfd,所谓的oldfd就是打开这个文件本身应该的fd,newfd就是指定要的fd;也就说,打开这个文件时分配的fd是oldfd,但是我们自己想修改分配的fd,指定为newfd;

在底层对应的变化newfd在文件描述符表中的指针被oldfd在文件描述符表中的指针所覆盖了,也就是newfd处的指针是oldfd处的指针的一份拷贝,那么因为指针指向没变,那么newfd处的指针就会指向oldfd处指针指向的struct file了

不过此时oldfd处的指针还是指向的原来的struct file,也就是说此时的新打开的文件的struct file在文件描述符表中被两个指针同时指向

看一段代码:

从结果可以知道此时的fd还是3,但是呢fd为3对应的文件已经被当作stdout了 ;这是因为dup2的作用,此时fd为1处的指针已经是指向log.txt了,而不是stdout。

追加重定向就是将打开方式中的清空换为追加

输入重定向:

本来应该从stdin键盘文件读取的,但是此时从log.txt读取了,这是因为输入重定向

另一种写法:

将log.txt当作命令行参数传进去实现输入重定向操作

myshell实现重定向操作

添加一个点:

在我们使用dup2进行重定向时,要是0、1,2指向的文件某一个假设就是stdin,被重定向之后,其他的进程使用stdin时会不会受到影响;答案是不会

首先需要知道,文件是可以被多个进程共享的,这才是提出这个问题的前提(从我们之前父子进程打印pid时打印到同一个stdout可知)

那么是怎么消除影响的呢?在stdin里面存在一个计数器,这个计数器记录了自己被多少个进程的文件描述符所指向,当有一个进程的stdin被dup时,计数器减减,只要不到0,其他的进程还是可以使用stdin这个标准文件流。

重谈重定向(标准错误)

重定向的实际写法:

指定文件描述符,就是将log.txt1的地址覆盖到1所在文件描述符表的位置,打印时寻找的是1所在的文件描述符表中的文件地址,而此时1指向的是log.txt1

看看stderr:

虽然stderr和stdout的文件描述符fd分别是2、1;但是它们指向的都是标准输出,上图只是为了方标理解;从运行结果我们也能知道,向stderr打印其实就是向显示器打印

使用重定向:

发现只有stdout的被重定向了,而stderr还是向显示器打印;这是因为这样写只是把1给重定向了,而2还没有被重定向,依旧向显示器打印

分别使用重定向:

为什么会有这种现象?这是因为为了我们可以使用重定向操作将常规信息和错误信息分离,方便日志的形成

若是想要把这两个打印到同一个文件呢?

这种写法只有标准错误测试信息,因为>打开时会先清空文件

可以使用>>,这样是可以的

还有一种比较好的写法:

解释:&1是一个语法,由解释器解释,我们不用关心;重要的是:首先将fd为3的指针覆盖到1,此时打印文件就是向log.txt里面打印,之后2>&1,这是将1中的指针覆盖到2,此时2指向的就也是log.txt


四、理解“一切皆文件”

在OS中硬件的相关信息都被多个内核数据结构struct_device相连成的链表组织描述管理起来了;值得注意的是,我们需要对这些硬件进行读写一类的操作,但是肯定的是,每个设备的配有的read和write的具体实现不一样,读写不同硬件的操作肯定是具有很大差别的;

而Linux下“一切皆文件”就体现在这个位置:OS中进程打开文件,这个进程具有一张文件描述符表,并且通过这张文件描述符表关联了很多struct file的内核数据结构;在我们对硬件进行读写时首先对文件缓冲区进行内容修改,之后是怎么读写到硬件的呢?由于不同硬件的读写差别很大,所以这种操作并不简单,在struct file里面是不能定义函数的,为了解决这个问题,struct file里面具有很多的函数指针,这些函数指针的命名参数都一样,和底层硬件的读写方法相关联;

于是可以通过函数指针,屏蔽底层的差异,而只是简单地将文件缓冲区里面的内容通过函数指针关联不同硬件的读写接口从不同的硬件进行读写;类似于多态思想;

这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤ read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函 数来进⾏。

再来理解“一切皆文件”:就是屏蔽掉底层不同设备的差异,只让进程对VFS(虚拟文件系统,就是一个一个struct file)做出相关操作就能实现对硬件的操作,这样看来硬件也就是一个一个的文件了;所以“一切皆文件”这种说法实际上是站在进程的角度来说的。


五、缓冲区

先看一组现象:

关闭stdout之后,打开一个新的文件,这个文件就是新的stdout,此时打印数据,打印之后不关闭fd:

关闭fd:

之后发现只有系统调用的write能将数据写进文件

解释:

在我们使用fprintf、printf、fputs、fwrite这些库函数时,并不是直接将数据写到文件内核缓冲区的;而是先写到语言层面的缓冲器中;这个缓冲区的维护靠的就是我们常见到的FILE*,这是一个struct,里面包含文件的各种属性,其中就有维护这个FILE对应文件的相关缓冲区信息以及fd;使用fopen打开文件时,这个文件类型是FILE* 的,底层会为这个文件申请一段内存空间也就是struct FILE的,之后再对这个对象填充属性,每个文件都有对应的缓冲区

在达到某种条件之后操作系统会使用系统调用接口例如write和fd将语言层面缓冲区中的数据刷新到文件内核缓冲区

这个语言层面的缓冲区刷新到文件内核缓冲区的方式:1、用户强制刷新,使用fflush直接刷新到stdout;2、满足刷新的条件(比如使用写透模式WT相当于写入语言层面的缓冲区之后直接刷新到内核缓冲区,或者FILE中维护的缓冲区满了这个针对普通文件,或者按行刷新这个主要是方便用户读的即针对显示器);3、进程退出,刷新。

为什么要存在语言层面的缓冲区?这是因为系统调用是具有成本的。所以语言层面的缓冲区的存在是为了提高效率,先将一定量数据写到这个缓冲区,达到一定限度或者满足一定条件之后刷新减少系统调用的次数来提高效率,这也是为什么stl中容器的扩容都是按照接近2倍的方式去扩容的,原因就是扩容需要系统调用,而一次扩容多点就能减少系统调用提高效率

再者库函数或者系统调用对文件的操作都是数据的拷贝,所以计算机内部数据的流动方式本质上都是数据的拷贝

演示强制刷新:

就算最后close但是在前面使用fflush强制刷新缓冲区到文件内核缓冲区,这样就能交付给磁盘上文件了

源代码中的FILE

来一个例子加深对缓冲区的理解:

解释为什么出现这种不同的现象:

首先显示器是行刷新,而上述代码打印的都是带了\n的,也就是每一行放到库缓冲区之后都会刷新到文件内核缓冲区;当代码走完库函数的打印之后此时库缓冲区里面已经没有数据了,系统调用不用说,直接写到文件内核缓冲区;那么再创建子进程时继承父进程的一些数据结构,那么此时的子进程的缓冲区也就没有数据

而重定向打印到普通文件时,库缓冲区是满了之后才会刷新,那么此时父进程库函数将数据写到库缓冲区,不满不会刷新到文件内核缓冲区,也就是说,库缓冲区中的数据会一直存在;而系统调用直接将数据写道文件内核缓冲区再写到磁盘(只要系统把数据交给OS,就相当于把数据交给了硬件设备)。那么在创建子进程之前,父进程库缓冲区中有一份库函数写的数据,而write写的数据已经写到了硬件;创建子进程,子进程的库缓冲区中也会有一份这样的数据,最后进程结束时父进程和子进程都会将自己的库缓存区中的数据写到文件内核缓冲区中,这样就呈现出上述结果。

简单glibc库的实现

头文件:

  1 #pragma once
  2 
  3 #include<stdio.h>
  4 
  5 #define MAX 1024
  6 
  7 #define NONE_FLUSH (1<<0)
  8 #define LINE_FLUSH (1<<1)
  9 #define FULL_FLUSH (1<<2)
 10 
 11 typedef struct MyFile
 12 {
 13     int fileno;//文件描述符
 14     char outbuffer[MAX];//文件内容
 15     int bufferlen;//文件实际内容长度
 16     int flag;//文件打开方式
 17     int flush_method;//向文件内核缓冲区的刷新方式
 18 }MyFile;
 19 
 20 MyFile * MyFopen(const char* path, const char* mode);
 21 void MyFwrite(const void* ptr, size_t len, MyFile* file);                                                                                                
 22 void MyFclose(MyFile* file);
 23 void MyFflush(MyFile* file);

源文件:

1 #include"mystdio.h"
  2 #include<string.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <fcntl.h>
  6 #include<unistd.h>
  7 #include<stdlib.h>
  8 //初始化MyFile
  9 MyFile* BuyFile(int fd, int flag)
 10 {
 11     MyFile* f = (MyFile*)malloc(sizeof(MyFile));
 12     if(f == NULL)
 13     {
 14         perror("malloc fail\n");
 15         return NULL;
 16     }
 17     f->bufferlen = 0;
 18     f->fileno = fd;
 19     f->flag = flag;
 20     memset(f->outbuffer, 0, sizeof(f->outbuffer));
 21     f->flush_method = LINE_FLUSH;
 22     return f;
 23 }
 24 MyFile* MyFopen(const char* path, const char* mode)
 25 {
 26     int fd = -1;                                                                                                                                         
 27     int flag = 0;
 28     if(strcmp(mode,"w") == 0)
 29     {
 30         flag = O_CREAT | O_WRONLY | O_TRUNC;
 31         fd = open(path, flag, 0666);
32     }
 33     else if(strcmp(mode,"a") == 0)
 34     {
 35         flag = O_CREAT | O_WRONLY | O_APPEND;
 36         fd = open(path, flag, 0666);                                                                                                                     
 37     }
 38     else if(strcmp(mode,"r") == 0)
 39     {
 40         flag = O_RDWR;
 41         fd = open(path, flag);
 42     }
 43     else 
 44     {}
 45     if(fd < 0) 
 46     {
 47         perror("open fail\n");
 48         return NULL;
 49     }
 50     //打开文件之后需要申请MyFile
 51     return BuyFile(fd, flag); 
 52 }
 53 
 54 void MyFclose(MyFile* file)
 55 {
 56     //关闭文件要进行刷新
 57     if(file->fileno < 0) return;
 58     MyFflush(file);
 59     close(file->fileno);
 60     free(file);
 61 }
 62 void MyFflush(MyFile* file)
 63 {                                                                                                                                                        
 64     //刷新实际上是将库缓冲区的内容写到文件内核缓冲区
 65     if(file->fileno < 0) return;
 66     write(file->fileno, file->outbuffer, file->bufferlen);
 67     file->bufferlen = 0;//刷新之后file就没了,已经拷贝了
 68     //刷新到文件内核缓冲区之后,应该直接再将数据拷贝到磁盘
 69     fsync(file->fileno);
 70 }
 71 void MyFwrite(const void* ptr, size_t len, MyFile* file)
 72 {
 73     if(file == NULL) return;
 74     //写入实际上是拷贝
 75     memcpy(file->outbuffer + file->bufferlen, ptr, len);
 76     file->bufferlen += len;
 77     //写入之后要考虑要不要刷新的问题
 78     if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n')
 79     {
 80         //行刷新
 81         MyFflush(file);
 82     }
 83 }

fopen打开的文件返回一个FILE*的对象,这个FILE对象里面存在fd、管理缓冲区的属性,那么我们模拟时也要添加,MyFile就是这样设计的;可以得知的是,库中fopen也是对系统调用open的封装,那么模拟时使用open需要知道打开的文件以及方式,open返回一个fd,这个fd用来构造MyFile; 

fwrite写入是将数据拷贝到FILE结构体里面的缓冲区,若是缓冲区满了就要刷新;这里模拟的是行刷新,刷新方式定义在头文件,只有当写入的字符换末尾有\n并且刷新方式是行刷新才会进行刷新;

刷新就是把库缓冲区的数据写入到文件内核缓冲区,调用write;刷新之后库缓冲区就没数据了;一般来说,刷新之后就直接写到硬件,那么模拟时调用fsync系统调用,这个接口的作用是将文件内核缓冲区的数据写到硬件上;

fclose将打开的文件关闭,那么此时会将没有刷新的数据刷新,之后释放FILE申请的资源;并且因为打开时调用了open,那么关闭时也对应的要调用close

###测试观察现象,加深对缓冲区的理解

1、简单的写入:

2、若是去掉写入字符串的\n,会发生什么现象?将写入执行十次,观察库缓冲区和硬件上log.txt内容的区别。结果应该是不会写一行刷新一行,每次写入我们只能看到库缓冲区也就是模拟的outbuffer里面的内容增加,但是log.txt里面没有内容增加,因为此时不满足刷新条件;只有当文件写完,调用MyFclose时才会刷新

从结果可以知道,每次filep里面的outbuffer都有数据写入,但是log.txt不会按行刷新,因为写入filep的字符串不是以'\n'结尾的,所以log.txt前四次没有数据,只有第五次之后调用的MyFclose中调用了MyFflush函数,会刷新数据,所以只有第五次log.txt里面才会存在数据。

3、接着2,但是写入一次调用一下自己实现的MyFflush,观察:

从以上实验回想在使用printf函数打印字符串时,若是没有带\n,那么数据是不会刷新到磁盘的,而是先在库缓冲区中,所以不带\n,每次打印时才会使用fflush函数

CH340以及CH系列USB转串口驱动下载 在之前使用USB转串口时,很多时候大家可能会不知道去哪下载驱动,然后百度随便下载一个,但往往鱼龙混杂且版本不一,所以今天特意整理一下CH340驱动以及CH系列的官方驱动下载地址,方便大家进行下载也方便自己后期查找,所有链接皆指向官方下载链接,每次下载都可获取官方当前最新驱动。 CH340/CH341 Windows驱动下载链接:CH341SER.EXE下载链接 Linux驱动下载链接:CH341SER_LINUX.ZIP下载链接 Andr... 阅读详情

相关推荐

基于stm32平台上的IC-MU磁绝对值编码器SPI通讯和码盘数据处理

基于stm32平台上的IC-MU磁绝对值编码器SPI通讯和码盘数据处理

watch361的博客 1万+

Linux 基础IO与系统IO

本文系统介绍了文件系统的基础IO与系统IO操作。首先从文件本质(属性+内容)和进程对文件的操作入手,详细讲解了C语言层面的文件读写接口(fopen/fwrite/fread等)及其实现原理。随后深入系统级IO,解析open/write/read/close等系统调用的使用方法和底层机制,特别是文件描述符fd的本质(内核数组索引)和重定向原理(通过dup2修改指针指向)。文章还通过代码示例演示了如何实现简单shell的重定向功能,并分析了进程替换不影响重定向的原因。最后讨论了不同语言对系统调用的封装如何实现跨

2301_79976271的博客 1259

Hopf振荡器在机器人步态控制里到底怎么用?一个CPG仿生案例说清楚

本文深入解析了Hopf振荡器在机器人步态控制中的实际应用,通过CPG仿生案例详细展示了从数学方程到关节指令的完整流程。重点探讨了Hopf振荡器的极限环稳定性、参数调节技巧及自适应控制策略,为机器人运动控制提供了高效解决方案。

weixin_29284657的博客 202

【Linux系统】基础IO(下)

本文探讨了操作系统文件I/O的底层实现机制。首先指出高级语言API(如fopen)实际上是基于系统调用的封装,重点介绍了Linux的open()、read()、write()和Windows的CreateFile()等系统级接口。通过位掩码(bitmask)技术详细解析了标志位参数的高效传递方法,包括宏定义、按位或组合和按位与检测等核心操作。 深入分析了文件描述符的本质作为内核文件指针数组的索引,标准描述符0/1/2的默认分配,以及最小空闲下标分配规则。通过代码示例展示了重定向的实现原理,即通过替换内核文件

2402_82670467的博客 1458

从零开始安装使用opengauss数据库(高斯数据库)

openGauss(高斯数据库)的环境配置、安装方法、基本使用方法

qq_43764807的博客 1万+

西门子S7-200 SMART PLC控制伺服电机

以下是使用西门子S7-200 SMART PLC控制伺服电机的详细程序框架和配置说明,涵盖硬件接线、PLC配置和基本运动控制逻辑。具体实现需根据伺服电机型号(如松下、三菱、台达等)调整参数。通过以上步骤,可实现伺服电机的基础位置控制。实际应用需根据设备机械参数和伺服驱动器手册调整细节。建议在调试时逐步测试脉冲输出和方向信号是否正常。

CHUXUEZHE8210的博客 6427

Linux系统——基础IO

Linux 中的 IO相关基础

kaneki_11的博客 779

N32串口DMA接收不定长数据:空闲中断与DMA通道的协同处理

本文详细解析了在N32系列MCU中,利用串口空闲中断与DMA通道协同处理不定长数据的核心机制与实战方法。通过剖析空闲中断的触发时机、DMA的配置要点以及两者协同工作的关键时序,提供了避免数据错乱的避坑指南和完整的代码实现,旨在帮助开发者构建高效、可靠的嵌入式串口通信子系统。

weixin_29191669的博客 123

深入了解linux系统—— 基础IO(上)

在C语言中,文件操作是编程中的重要部分。文件存储在磁盘中,分为程序文件、数据文件、二进制文件和文本文件等。文件操作包括打开、关闭、读写等,本质上是对外设的输入输出(I/O)。文件由文件内容和文件属性组成,即使文件大小为0KB,也会占用磁盘空间。在系统层面,文件操作由进程执行,操作系统管理磁盘,C/C++通过库函数调用系统接口进行文件操作。 C语言中,fopen用于打开文件,fclose用于关闭文件。文件可以以读(r)、写(w)、追加(a)等方式打开。写入文件可以使用fputc、fputs、fwrite、fp

LH__1314的博客 2610

liquibase扩展支持达梦数据库

近期项目中需要支持国产数据库,需要liquibase底层支持达梦数据库,记录下操作的详细步骤以便分享。 1.liquibase支持的数据库很多如MySQL, PostgreSQL, Oracle, Sql Server, DB2。但是达梦数据库并不支持,所以需要新建DMDatabase类来适配达梦数据库。达梦语法和Oracle很像所以只需要复制liquibase-core下的Oracle Database做些修改就可了。以下为需要修改的地方 public class DMDatabase extends A

qq_43677719的博客 5472

一文读懂 SAP ICF Services:从 SICF 激活到 ABAP OData V2/V4 安全与实战

本文系统化介绍了ABAP系统中ICF服务的关键概念及实践要点。主要内容包括:ICF服务的核心安全机制,需显式激活才能响应HTTP请求;在SICF中正确激活服务的操作步骤;OData V2/V4服务所需的不同ICF节点配置,如V4统一入口节点/sap/opu/odata4;典型问题如default_host未激活会导致HOST_INACTIVE错误;安全实践包括CSRF防护机制和权限对象(S_ICF_ADM)配置;以及实际案例分析和常见ICF服务节点的功能说明。文章为ABAP开发者与运维人员提供了ICF服务管

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 472

Linux基础IO与文件系统

Linux基础IO精析

2302_81486993的博客 1212

ATECC508A芯片开发笔记(四):自定义配置508功能,规划DataZone数据存储

ATECC508A芯片开发笔记(四):自定义配置508功能,规划DataZone数据存储 508A内部存储区介绍 Data Zone Config Zone 508A Config Zone配置(自定义) - ATECC508A芯片在使用之前,需要自定义配置ConfigZone与DataZone,并同Lock命令将两个区域锁住,否则无法正常使用功能。而一旦锁上就是永久性的,配置区不能够进行解锁

HowieXue 薛永浩的博客 2万+

【Linux基础IO】一、系统IO

​ 我们知道 C语言有文件操作接口,那么当然 C++、JAVA这些语言都有文件操作接口。但是最让人烦的问题是这些语言之间的 IO接口都是不一样的,这样子导致我们学习成本很高!​ 这些语言拥有文件操作接口的目的找到文件,然后对文件进行操作。那么文件是在磁盘上,磁盘是属于硬件。对于硬件的访问只有操作系统才能进行。所有人想访问磁盘都不能绕开操作系统,C语言也好,其他语言也罢都是人表达出意思让操作系统理解我们想要干嘛,所以任何上层语言想要进行对磁盘进行操作,都会使用操作系统提供的接口。

lirendada的博客 1095

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

linux系统基础知识-基础IO

基础IO的概念,位图,标准输入输出流,文件系统

羊羊羊 1003

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

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

【Linux】基础IO_文件系统基础

我们知道,当fopen以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是什么呢?例如,我们在BasicIO目录下运行可执行程序myproc,那么该可执行程序创建的log.txt文件会出现在BasicIO目录下。那是否意味着这里所说的“当前路径”是指“当前可执行程序所处的路径”呢?这时我们可以将刚才可执行程序生成的log.txt文件先删除,然后再做一个测试:回退到上级目录,在上级目录下运行该可执行程序。

2301_80636070的博客 1017

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

【Linux系统编程】基础IO

Linux基础IO,与文件相关的系统调用、文件描述符、理解一切皆文件、文本与二进制、重定向、内核级缓冲区、用户级缓冲区

2301_80277275的博客 992

Linux系统:基础IO

ELF格式的可执行程序,二进制是有自己的固定格式的(elf可执行程序的头部,可执行程序的属性)

m0_61088872的博客 996

【Linux系统】基础IO(上)

本文系统梳理了文件操作的核心概念与C语言实现。首先从狭义和广义角度解析文件本质:狭义指磁盘存储的数据集合,强调永久性和IO特性;广义遵循Linux"一切皆文件"理念,将设备抽象为文件。详细阐述了文件组成(元数据+内容)、操作分类(内容/属性操作)及系统调用机制。重点介绍了C语言文件操作接口,包括打开关闭(fopen/fclose)、字符读写(fgetc/fputc)、字符串处理(fgets/fputs)、格式化I/O(fprintf/fscanf)、块数据读写(fread/fwrite)

2402_82670467的博客 917

Linux - 文件系统、基础IO

要深入理解文件系统,我们可以将磁盘视为一种线性存储介质。想象一下磁带,当它被卷起时,磁带是圆形的,类似于磁盘的形状。但如果我们将磁带拉直,它就变成了一条线性的带子,这有助于我们理解磁盘数据存储的连续性。磁盘分区的概念:块设备:磁盘通常被视为块设备,数据以块或扇区为单位存储,每个扇区通常大小为512字节或更大。扇区划分:以一个512GB的磁盘为例,如果每个扇区大小为512字节,那么这个磁盘可以被划分为超过十亿个扇区。磁盘分区的目的:管理效率:计算机通过分区来更有效地管理磁盘空间。

Russ_Leo的博客 1893

Linux系统编程:基础IO

本文系统介绍了Linux文件操作的核心概念。从文件的基本理解(狭义与广义)出发,详细阐述了C语言文件接口和系统文件I/O操作,包括open、close、read、write等系统调用。重点解析了重定向原理及实现方法,深入探讨了Linux"一切皆文件"的设计哲学,揭示了VFS虚拟文件系统的机制。文章还详细分析了缓冲区的工作原理(全缓冲、行缓冲、无缓冲),并对比了用户缓冲区和内核缓冲区的区别。最后通过模拟实现libc库,展示了文件操作底层逻辑的实现方式。全文贯穿了操作系统管理文件的核心思想:

_dindong的博客 1113

Linux系统编程03:基础IO

本文讲解基础 IO 知识,涵盖文件基础概念、C 语言及系统文件操作接口,阐述文件描述符、重定向、缓冲区原理与应用,介绍文件系统结构、inode 作用,分析软硬链接差异,以及动静态库制作与使用要点。

2301_80030580的博客 757

Linux系统编程(六)--基础IO

open函数的第一个参数是pathname,表示要打开或创建的目标文件。若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)open函数的第二个参数是flags,表示打开文件的方式。参数选项含义O_RDONLY以只读的方式打开文件O_WRNOLY以只写的方式打开文件O_APPEND以追加的方式打开文件O_RDWR。

weixin_65132948的博客 1193

【Linux系统与系统编程】10.基础IO

从C语言的文件IO入手,介绍其底层原理,详细介绍了IO的基本过程,穿插式讲解了缓冲区

caryon的博客 762

Linux系统编程—基础IO

pathname:要打开或创建的目标文件flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。参数:O_RDONLY:只读打开O_WRONLY:只写打开O_RDWR:读,写打开上面三个常量,必须指定一个且只能指定一个O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限O_APPEND:追加写返回值:成功:新打开的文件描述符失败: - 1mode_t理解:直接 man 手册,比什么都清楚。

Awkwardx的博客 780

【Linux操作系统】基础IO

目录一、接口使用1.1 铺垫知识1.2 C接口使用1.3 系统接口使用二、认识fd三、缓冲区四、文件系统五、软硬连接六、动静态库6.1 静态库的制作和使用6.1 动态库的制作和使用七、理解动态库加载 一、接口使用 1.1 铺垫知识 文件=文件内容+文件属性 。一个文件如果它的文件内容为空,并不代表它没有占用内存,因为它的文件属性也是要占内存的,比如文件名、文件类型、文件权限等。 访问文件前,文件必须被打开,必须加载到内存中。修改文件,都是通过执行代码的方式进行修改,也就是执行这个代码的程序运行起来。 是进

2301_77459845的博客 1175
上一篇: MySQL索引
下一篇: 数据结构-排序扩展
溪苏.
博客等级 码龄3年 995粉丝 138原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值