🥁作者: 华丞臧.
📕专栏:【LINUX】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站
一、文件I/O
C语言当中fopen和fclose通过文件的路径来打开和关闭文件,fread可以读取文件的内容,fwrite可以向文件中写入数据,在C语言当中还有各种入fputc、fputs、fget等用于读写文件的函数。
操作文件出了上述C接口,我们还可以使用系统调用接口来进行文件访问。
1.1 文件描述符fd
- 文件存放在磁盘上的,也是数据,即便创建一个空文件也要占据磁盘空间;磁盘上的文件等于文件的内容加上文件的属性,即使内容为空描述该文件的属性也是不为空的。
- 那么操作文件是在做什么呢?文件包括两个部分文件内容、文件属性,那么对文件的操作无疑就是对内容或者属性的操作。
- 冯诺依曼体系结构规定,CPU只能从内存中读取数据,所以打开文件实质上是将磁盘中的文件加载到内存当中,并不是所有文件都是打开状态,而是需要使用时再打开。
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
结构体数组。
文件描述符分配规则:在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);
}
此时,我们发现本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, < 。
重定向本质就是改变file
结构体数组中文件描述符位置上指针的指向。
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文件的地址。
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的文件,打开该文件其内容如下图:
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;
}
可以看到结果并不是顺序打印,printf 是封装了 write 系统调用的函数,上述结果也说明了printf函数首先会将数据写入缓冲区中,当数据积累到一定程度才会刷新缓冲区,write会将数据写入文件中。
那么上述缓冲区在哪里呢?修改上述代码在休眠五秒后关闭标准输出,再运行程序结果如下图:
当我们将标准输出关闭时,看到程序最后并没有打印出 “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();
}
结果原理:文章来源:https://www.toymoban.com/news/detail-421230.html
- 第一次运行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 0x2
typedef 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;
}
文章来源地址https://www.toymoban.com/news/detail-421230.html
到了这里,关于[Linux] 基础IO的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!