基础IO
回顾下c语言的文件操作
文件打开(创建)
#include<stdio.h>
#include<string.h>
#include<assert.h>
int main()
{
FILE* fp = fopen("log.txt", "w");
assert(fp);
(void)fp;
const char* msg = "hello world!\n";
int count = 5;
while (count--)
{
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
- 注意这里的调用fwrite()时,传的长度参数不要考虑加上 ‘\0’,也就直接传strlen(msg),不要+1,因为’\0’只是在c语言中的规定,文件并不管’\0’
读文件操作
#include<stdio.h>
#include<string.h>
int main()
{
FILE* fp = fopen("log.txt", "r");
if (fp == NULL)
{
perror("fopen");
}
char buf[128];
const char* msg = "hello bit\n";
while (1)
{
size_t s = fread(buf, sizeof(char), strlen(msg), fp);
if (s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if (feof(fp))
break;
}
fclose(fp);
return 0;
}
对刚刚创建log.txt的文件进行读操作
- 详细的文件操作:C语言文件操作
系统的文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main()
{
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, "0666");
if (fd < 0)
{
perror("open");
}
int count = 5;
const char* msg = "hello world\n";
int size = strlen(msg);
while (count--)
{
write(fd, msg, size);
}
close(fd);
return 0;
}
仔细一看是不是突然发现很陌生,包含的头文件非常多且陌生,open、write、close都是系统调用接口,先来看下open
open:
这里有两个open函数,类似于C++的函数重载,先看看mode_t mode参数,mode_t是Umask变量,是一个无符号八进制数,文件创建时,根据传入的mode &(~umask)来确定文件的权限,umask一般是0002,上面我设置成了0000,创建出的文件权限就应该是 -rw-rw-rw-
再来看flag
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRUNC: 每次打开清空,再写入
O_WRONLY | O_CREAT | O_TRUNC == “w”
O_WRONLY | O_CREAT | O_APPEND == “a”
这样的传参方式实际是对flag的比特位做修改,把每一个比特位看做成有无,对应的选项做变化
open的返回值:
如果成功创建:返回的是新打开的文件描述符(>= 0)
如果创建失败:返回-1
系统的IO函数于C语言的IO的关系
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而 open close read write 都属于系统提供的接口,称之为系统调用接口。
上下层关系
简单的来说,其实fopen封装了open()
文件描述符
上面讲到open返回值是文件描述符(整数类)
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("log.txt的文件描述符:%d\n", fd);
close(fd);
return 0;
}
为什么log.txt的文件描述符为3呢?而不是为0,或1 、2呢?
其实Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
标准输入对应的物理设备是键盘,而标准输出,标准错误对应的物理设备是键盘
这样就能解释为什么第一个打开的文件的文件描述符是3了,因为0,1,2都被占用了
当启动代码时,会变成进程,OS会为我们进程创建struct task_struct结构体,在这结构体中有一个结构体指针数组struct file_struct* files;OS会初始化指针,这个指针指向的是struct file_struct,这个结构体里有一个struct file* fd_array[]指针数组,一个进程会默认打开三个文件,三个文件对应着标准输入,标准输出和标准错误,会为该三个文件组织三个结构体struct file,而第一个struct file会链接第二个struct file,一直链接完,最后一个指向NULL,而struct file_struct* files[]存储的就是struct file的指针,最后返回file_struct最后一个储存的有效file*的下标,此时这个下标就是文件描述符。
简便图:
所以,当我们打开新的文件返回的文件描述符为3,文件描述符就是该数组的下标。所以,只要拿着文件
描述符,就可以找到对应的文件
在这里可以解释为什么在Linux下一切皆文件这个概念
这些硬件实际上是通过驱动提供的接口来进行文件操作,这也是为什么要下载安装硬件驱动,这样OS才能通过接口调动这些硬件
文件描述符的分配规则
将标注输入关闭之后,再打开新的文件,此时的fd为0
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
重定向
从上面的现象中,本hello world应打印输出在显示器上,用输出重定向到log.txt中.
实际上原理就是把1号标注输出文件改成log.txt
来动手试试:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC);
printf("fd:%d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
模拟了输出重定向
- 有没有注意到 上面的代码printf后面还跟着一句fflush(stdout),这时因为此时数据还存在stdout的缓冲区中,而stdout对应的文件描述符1是关闭的,所以要手动刷新stdout中的缓冲区
dup2系统调用
这样关闭文件,再打开文件是不是又点矬
来,了解dup2这个系统调用,可以帮助我们一键修改
newfd是oldfd的一份拷贝,拷贝的不是fd,而是拷贝fd对应数组中的内容,所以oldfd指向新创建的普通文件
缓冲区
缓冲区本质是一块C语言提供的内存空间,这块内存区用来缓存待处理的数据。
当我们输入数据并要打印出数据,并不是向OS直接输入数据,而是先存入C库定义的缓冲区存入数据,等程序结束时,缓冲区的数据才会向显示器打印。
可是为什么要这样做呢?
实际I/O会使的程序运行变慢,这是因为要访问外设,而当进程等待输入或者输出资源时,会进入阻塞状态,频繁的进行I/O,会效率变低,但是有了缓冲区就不一样了,每次输入数据到缓冲区,根据C库刷新策略再向显示器输出数据,这样从一次I到O,变成了多次I到O。
缓冲区刷新策略:
立即刷新。(IO次数多,效率低)
行刷新 (\n)。
全刷新(全缓冲),即写满缓冲区再刷新。(效率最高)
特殊刷新策略:
调用fflush(强制刷新)
进程退出
不同文件的不同刷新策略:
显示器对应的刷新策略为行缓冲 (为了兼顾用户体验)
而磁盘文件倾向为全缓冲的刷新策略。
其实所有的设备都倾向于全缓冲,因为全缓冲这种刷新策略可以减少IO的次数,也就是减少外设的访问次数,这种方式效率是最高的。