> 文章列表 > [Linux] 基础IO

[Linux] 基础IO

[Linux] 基础IO

🥁作者华丞臧.
📕​​​​专栏:【LINUX】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站


文章目录

  • 一、文件I/O
    • 1.1 文件描述符fd
    • 1.2 重定向
      • dup2系统调用
    • 1.3 接口介绍
      • open
      • close
      • read
      • write
      • lseek
      • 接口实验
    • 1.4 缓冲区
      • 什么是缓冲区?
      • 为什么要有缓冲区?
      • 缓冲区的刷新策略
      • 缓冲区在哪里?
    • 1.5 模拟实现IO函数

一、文件I/O

C语言当中fopen和fclose通过文件的路径来打开和关闭文件,fread可以读取文件的内容,fwrite可以向文件中写入数据,在C语言当中还有各种入fputc、fputs、fget等用于读写文件的函数。

操作文件出了上述C接口,我们还可以使用系统调用接口来进行文件访问。

1.1 文件描述符fd

  • 文件存放在磁盘上的,也是数据,即便创建一个空文件也要占据磁盘空间;磁盘上的文件等于文件的内容加上文件的属性,即使内容为空描述该文件的属性也是不为空的。
  • 那么操作文件是在做什么呢?文件包括两个部分文件内容、文件属性,那么对文件的操作无疑就是对内容或者属性的操作。
  • 冯诺依曼体系结构规定,CPU只能从内存中读取数据,所以打开文件实质上是将磁盘中的文件加载到内存当中,并不是所有文件都是打开状态,而是需要使用时再打开。

[Linux] 基础IO

open是一个系统调用接口,其原型如下:

int open(const char *pathname, int flags, mode_t mode);

C语言中fopen打开文件后返回的是一个文件指针,而这里返回的是一个整型,这个整型代表什么呢?

这个整型就是文件描述符,首先我们要知道在内存中的一个进程中可以打开多个文件,当一个程序运行时会默认打开三个输出流分别是stdin、stdout、stderr,并且在Linux操作系统下一切皆文件,底层的一些硬件对于操作系统而言都是文件,操作系统启动肯定会打开某一些文件如屏幕、磁盘等。

  • 对于加载到内存中的文件,操作系统需要管理他们,因此需要先描述内存中的文件,再组织这些描述文件的数据结构,实现对文件的管理。
  • 而在Linux操作系统中使用结构体来描述文件,结构体中描述了文件的各种属性,然后再将这些结构体的地址用一个数组组织起来管理,这也就是为什么文件描述符是整型,它相当于是数组的下标。
  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。0、1、2对应的物理设备一般是:键盘,显示器,显示器。
  • 每个进程的PCB都有一个指向file结构体数组的指针,指向自己打开的file结构体数组。

[Linux] 基础IO
文件描述符分配规则:在files_struct数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。一般默认从3开始,因为0、1、2通常默认是标准输入stdin, 标准输出stdout, 标准错误stderr。

1.2 重定向

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

[Linux] 基础IO
此时,我们发现本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, < 。
重定向本质就是改变file结构体数组中文件描述符位置上指针的指向。
[Linux] 基础IO

dup2系统调用

函数原型如下:

#include <unistd.h>
//将oldfd重定向到newfd中
int dup2(int oldfd, int newfd);oldfd:源文件描述符
newfd:目的文件描述符
返回值:成功,返回newfd失败,返回-1

使用方式如下:

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

上述代码的功能就是将log文件重定向到文件描述符1的位置上,重定向完成后fd:1表示log文件的地址。
[Linux] 基础IO

1.3 接口介绍

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: 追加写返回值:成功:新打开的文件描述符失败:-1

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

系统调用库函数

  • fopen、fclose、fread、fwrite都是C标准库当中的函数,称之为库函数。
  • 而open、close、read、write、lseek都是系统提供的接口,称之为系统调用接口。
  • C语言当中关于文件读写的库函数都是对系统调用接口的封装,方便二次开发。

close

#include <unistd.h>
//关闭文件描述符对应的文件
int close(int fd);返回值:成功,返回0失败,返回-1

read

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);fd:文件描述符
buf:存放读取数据的空间
count:读取数据字节数返回值:成功,返回读取到的字节数失败,返回-1

write

#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);fd:文件描述符
buf:需要写入数据的地址
count:写入数据的字节数返回值:成功,返回写入数据的字节数失败,返回-1

lseek

#include <sys/types.h>
#include <unistd.h>off_t lseek(int fd, off_t offset, int whence);fd:文件描述符
offset:偏移量	
whence:SEEK_SET:偏移量设置为偏移字节。SEEK_CUR:偏移量被设置为其当前位置加上偏移字节。SEEK_END:偏移量设置为文件大小加上偏移字节。返回值:成功,返回从文件开始的以字节为单位的结果偏移位置失败,返回-1

接口实验

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>int main() 
{umask(0);int fd = open("myfile", O_CREAT | O_RDWR | O_TRUC, 0666); //O_TRUC表示打开时清空文件内容if (fd < 0) {perror("open");return 1;}char buf[] = "hello world!";ssize_t read_size = read(fd, buf, sizeof(buf));if (read_size < 0) {perror("read");}int write_size = write(fd, buf, sizeof(buf));if(write_size < 0){perror("write");}return 0;
}

在运行上述代码后,可以看到在当前目录下创建了一个myfile的文件,打开该文件其内容如下图:
[Linux] 基础IO

1.4 缓冲区

什么是缓冲区?

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

为什么要有缓冲区?

  • 解放使用缓冲区进程的时间,进程不用等待传输数据的时间。
  • 缓冲区的存在可以集中处理数据刷新,减少IO的次数,可以提高整机的效率。
  • 缓冲区类似快递驿站,再快递到来之前我们不需要在驿站等待快递,可以去做其他的事情;快递到达驿站后,我们接收到通知去取快递。

缓冲区的刷新策略

  • 常规策略

    • 无缓冲(立即刷新)
    • 行缓冲(逐行刷新)如:显示器
    • 全缓冲(缓冲区满时刷新)如:块设备对应的文件,磁盘文件
  • 特殊策略

    • 进程退出
    • 用户强制刷新

缓冲区在哪里?

首先来看下面这段代码,其运行结果是什么呢?按照代码执行的顺序应该是先打印出 “printf” 再打印 “write” ,真的是这样吗?

#include <stdio.h>
#include <unistd.h>int main()
{printf("printf"); //printf("printf\\n");write(1, "write", 5);sleep(5);//close(stdout->_fileno); //关闭标准输出,stdout是C语言封装的文件指针类型,_fileno表示该文件指针对应的文件描述符return 0;
}

[Linux] 基础IO
可以看到结果并不是顺序打印,printf 是封装了 write 系统调用的函数,上述结果也说明了printf函数首先会将数据写入缓冲区中,当数据积累到一定程度才会刷新缓冲区,write会将数据写入文件中。

那么上述缓冲区在哪里呢?修改上述代码在休眠五秒后关闭标准输出,再运行程序结果如下图:
[Linux] 基础IO
当我们将标准输出关闭时,看到程序最后并没有打印出 “printf”,这说明printf并没有将数据写入内核级的缓冲区中,而是将数据写入C语言提供的语言级缓冲区中,而write是直接写入到文件中。再来看看下面的这段代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{char *str1 = "hello printf\\n";char *str2 = "hello fprintf\\n";char *str3 = "hello fputs\\n";char *str4 = "hello write\\n";printf(str1);fprintf(stdout, str2);fputs(str3, stdout);write(stdout->_fileno, str4, strlen(str4));fork();
}

[Linux] 基础IO
结果原理:

  • 第一次运行test程序时,是将数据写入到stdout即显示器文件中,显示器属于行缓冲因此会每一行每一行刷新,而我们写入的数据中结尾都带有\\n,所以会立即刷新,当fork创建子进程时缓冲区数据已经刷新到显示器上了。
  • 第二次运行test程序并重定向到log.txt文件时,是将数据写入磁盘文件属于全缓冲,C接口缓冲区是自己的FILE内部维护的,属于父进程的数据区域,因此fork创建子进程会写时拷贝父进程的数据,所以父子进程都会刷新一次,write是直接将数据写入到文件中,fork时数据已经写入到文件中去了。

1.5 模拟实现IO函数

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>#define NUM 1024 #define NONE_FLUSH 0x0
#define LINE_FLUSH 0x1
#define FULL_FLUSH 0x2typedef struct MyFile
{int _fileno;char _buffer[NUM];int _end;int _flags;
}MyFile;MyFile *my_fopen(const char *filename, const char *method)
{assert(filename);assert(method);int flags = O_RDONLY; if(strcmp(method, "r") == 0){flags = O_RDONLY;}else if(strcmp(method, "w") == 0){flags = O_WRONLY | O_CREAT | O_TRUNC;}else if(strcmp(method, "r+") == 0){flags = O_RDWR | O_CREAT;}else if(strcmp(method, "w+") == 0){flags = O_RDWR | O_CREAT;}else if(strcmp(method, "a") == 0){flags = O_WRONLY | O_CREAT | O_APPEND;}else if(strcmp(method, "a+") == 0){flags = O_RDWR | O_CREAT | O_APPEND;}umask(0);int fileno = open(filename, flags, 0666);MyFile *fp = (MyFile*)malloc(sizeof(MyFile));if(fp == NULL) {perror("malloc file\\n");return NULL;}memset(fp, 0, sizeof(MyFile));fp->_fileno = fileno;if(fp->_fileno < 0) return NULL;fp->_flags |= LINE_FLUSH;fp->_end = 0;return fp;
}void my_fwrite(MyFile *fp, const char * start, int size)
{assert(fp);assert(start);assert(size > 0);strncpy(fp->_buffer + fp->_end, start, size); //将数据写到缓冲区fp->_end += size;if(fp->_flags & NONE_FLUSH){}else if(fp->_flags & LINE_FLUSH){if(fp->_end > 0 && fp->_buffer[fp->_end - 1] == '\\n'){write(fp->_fileno, fp->_buffer, fp->_end);fp->_end = 0;}}else if(fp->_flags & FULL_FLUSH){}
}void my_fflush(MyFile *fp)
{assert(fp);if(fp->_end > 0){write(fp->_fileno, fp->_buffer, fp->_end);fp->_end = 0;}
}void my_fclose(MyFile *fp)
{my_fflush(fp);close(fp->_fileno);free(fp);
}int main()
{MyFile *fp = my_fopen("log.txt", "w");if(fp == NULL){printf("my_fopen error\\n");return 1;}const char *s = "hello myfile\\n";my_fwrite(fp, s, strlen(s));printf("\\n");sleep(3);const char *ss = "hello myfile";my_fwrite(fp, ss, strlen(ss));sleep(3);printf("写入了一个不满足条件的字符串\\n");my_fclose(fp);return 0;
}

[Linux] 基础IO

世纪图书馆