C语言文件IO
我们在研究Linux的IO之前,我们先复习一下C语言学习的相关知识。其他不多说,先复习文件的写和读吧。
我们直接在Linux中创建文件,写代码。
写
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt", "w");
if (fp == NULL){
perror("fopen");
return 1;
}
int count = 5;
while (count){
fputs("hello world\n", fp);
count--;
}
fclose(fp);
return 0;
}
这是我们“写”的C语言代码,我们看看运行结果
确实如我们所愿在log.txt中写入了五次hello bit。但是需要注意的一点就是本来是没有log.txt这个文件的,但是我们要执行操作,系统就会在当前路径下创建所需要的文件。
读
看代码
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt", "r");
if (fp == NULL){
perror("fopen");
return 1;
}
char buffer[64];
for (int i = 0; i < 5; i++){
fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);
}
fclose(fp);
return 0;
}
运行结果
默认打开的三个流
在学习这个知识前需要知道的是在Linux下,一切皆文件,这句话很重要,也就是说,键盘也是文件,显示器也是文件,叫键盘文件,显示器文件,但是我们从键盘里获取和在显示器上打印都是需要打开键盘文件和显示器文件,所以这些都是默认打开的,也就是默认打开的流,输入流,输出流和错误流,C语言称作stdout,stdin,stderr。键盘对应的是输入流,显示器对应的输出流和错误流。
在man手册中,这三个流都是FILE*类型的。
也就是说,我们C语言里使用的printf,scanf等都是包装的,底层肯定都封装了这三个流,这才让代码有了可移植性。我们看看刚才的代码,做个大胆的猜想,将fp文件改成显示器文件,会不会打印在显示器上。答案是会。因为“一切皆文件”,可以自己下去试试。
fputs("hello bit\n",stdout);
fputs("hello bit\n",stdout);
fputs("hello bit\n",stdout);
系统文件IO
我们刚刚回顾了C语言中的文件操作,并且知道了,语言中对文件的操作其实就是封装了系统调用的接口,其本质就是系统调用。那我们透过表层,直接来了解系统调用接口。
open
open函数的原型是:
int open(const char *pathname, int flags, mode_t mode);
open的第一个参数
pathname的给出有两种情况:
- 给出的是路径,那就在给出路径下创建该文件
- 给出的是文件名,那就在当前路径下创建该文件
open的第二个参数
这个参数的含义是打开方式。
我们看看常见的几种打开方式:
参数 | 权限 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
O_CREAT | 若文件不存在,则创建它,需要使用mode选项 |
O_APPEND | 追加写 |
打开文件时,如果有多个选项的话就用"|"隔开。
open的第三个参数
mode就是创建文件时的使用权限
如0666就是我们的常规设置,权限如下图所示:
但是我们操作之后发现文件的权限是
这是因为有文件掩码umask,他的默认值是0002.所以我们得到的是0664.如果想摆脱文件掩码的限制,我们就可以设置文件掩码为0.
umask(0);
open函数的返回值
在了解open的返回值之前,我们先了解两个概念,库函数和系统调用。
- 上⾯的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数。
- ⽽ open close read write lseek 都属于系统提供的接口,称之为系统调⽤接口。
我们看一个图就能一目了然
我们尝试建立几个文件,并且打印他们的返回值。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
运行结果是
可以发现第一个文件的返回值是从三开始依次递增的。什么能联想到依次递增的呢?毫无疑问,是数组。是不是很像数组下标。但为什么是从3开始呢。大家还记不记得前面所提到的标准输入,标准输出,标准错误。没错,0,1,2分别指向的就是这三个,是默认打开的,所以从三开始。是不是柳暗花明又一村了?
close
close函数的原型:
int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
write
write就是向文件里写入,函数原型:
ssize_t write(int fd, const void *buf, size_t count);
这三个参数的含义就是从buf位置开始向后的count字节数据写入文件描述符fd的文件中。我们试验一下。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <string.h>
7 #include <stdlib.h>
8
9
10 int main()
11 {
12 umask(0);
13 int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
14 if(fd < 0)
15 {
16 perror("open");
17 return 1;
18 }
19 const char* msg = "hello gaoyichen\n";
20 int i = 0;
21 for(i = 0; i < 5;i++)
22 {
23 write(fd,msg,strlen(msg));
24
25 }
26 close(fd);
27 return 0;
28 }
29
30
运行并查看log,txt的内容结果如下
read
read函数的原型如下:
ssize_t read(int fd, void *buf, size_t count);
这三个参数的含义就是在fd文件中读取count字节的数据放到buf位置中。结果有两种:
- 成功,返回读取成功的字节数
- 不成功,返回-1
我们实验一下:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0){
perror("open");
return 1;
}
char ch;
while (1){
ssize_t s = read(fd, &ch, 1);
if (s <= 0){
break;
}
write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
}
close(fd);
return 0;
}
结果是
文件描述符的分配规则
我们知道0,1,2是标准输入,标准输出和标准错误的三个下标。我们关闭一下0,2.
close(0);
close(2);
结果是
显而易见,文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的。
重定向
重定向原理
我们通过了解输出重定向原理来了解追加重定向和输入重定向。
输出重定向就是本来应该往一个文件里写入内容,然后写到了另一个文件中。
我就用一段代码来带大家了解一下重定向。我们将本来就打开的1,标准输出close,然后,在进行一些操作
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <string.h>
7 #include <stdlib.h>
8
9 int main()
10 {
11 close(1);
12 int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
13 if(fd < 0)
14 {
15 perror("open");
16 return 1;
17 }
18 printf("fd: %d\n", fd);
19 fflush(stdout);
20 close(fd);
21 return 0;
22 }
23
此时,我们发现,本来应该输出到显⽰器上的内容,输出到了⽂件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。
所以,重定向的本质我们用一张图来说明
dup2
函数原型如下:
int dup2(int oldfd, int newfd);
在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。不止可以重定向键盘文件和显示器文件,还可以重定向别的文件,我们看个代码就可以很好的解释了。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <string.h>
7 #include <stdlib.h>
8
9
10 int main() {
11 int fd = open("./log", O_CREAT | O_RDWR);
12 if (fd < 0)
13 {
14 perror("open");
15 return 1;
16 }
17 close(1);
18 dup2(fd, 1);
19 for (;;)
20 {
21 char buf[1024] = {0};
22 ssize_t read_size = read(0, buf, sizeof(buf) - 1);
23 if (read_size < 0)
24 {
25 perror("read");
26 break;
27 }
28 printf("%s", buf);
29 fflush(stdout);
30 }
31 return 0;
32 }
printf是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问⽂件的时候,找的还是fd:1, 但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。
缓冲区
什么是缓冲区
缓冲区分为用户级缓冲区和内核缓冲区,都是内存空间的一部分,用来缓冲输入和输出的数据,根据其对应的设备分为输入缓冲区和输出缓冲区。系统调用的成本是很高的,为了更好地服务用户,每个语言都提供了缓冲区,就拿C语言来举例吧,C语言也提供了缓冲区,有printf/fprintf/fput/fwrite这些函数,当用户需要打印的时候,这些函数需要格式化打印内容,因为他们要将打印内容拼接在缓冲区中,当缓冲区“满了”以后,就将这些内容一次性打印,大大提高了工作效率。就像妈妈让我下楼倒垃圾,今天一个垃圾,明天一个垃圾,就会发现我会很累,而且倒的垃圾很少,于是妈妈要我找一个大垃圾桶,每天要扔的垃圾放到大垃圾桶里面,等满了再倒掉,这样我也轻松,而且垃圾也倒掉了。大大提高了效率。
为什么有缓冲区的存在
我们平时在对文件读和写都需要操作系统在硬件资源和用户之间完成任务,如果没有缓冲区,让操作系统直接对磁盘进行操作,那么执行一次就要调用一次,可以想象得到效率会变得多么低。所以就有了缓冲区的存在,我们举个例子,想要提取磁盘文件里的信息,就可以一次性读取大量的信息到缓冲区里,后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤⼤快于对磁盘的操作,故应⽤缓冲区可⼤⼤提⾼计算机的运⾏速度。所以,作用就是提高使用者的效率。
缓冲区类型 (刷新方案)
对于语言级缓冲区,一般来说全缓冲区刷新方案效率最高,普通文件用的这种刷新方式。行缓冲区刷新方案是遇到\n时,需要刷新,显示器就是用的这种刷新方案。至于操作系统,他有自己的一套方案,随着情况变化,交给操作系统就相当于交给了硬件。当然,你想控制操作系统也有相应的接口。后面再说。
缓冲类型 | 规则 |
全缓冲区 | 填满整个缓冲区 |
行缓冲区 | 遇到换行符 |
无缓冲区 | 系统直接调用,没有缓冲区 |
FILE
FILE是一个struc,结构体,这个结构体里面定义了文件描述符,还定义了一些指针,可以将他维护起来,就是我们的缓冲区。所以以后定义一个FILE*, 那就自带一个缓冲区。