基础的 IO
一、理解文件
狭义上理解
1、文件在磁盘里面,对文件的访问本质就在系统和外设(磁盘)之间进行IO操作
2、磁盘是永久性的存储介质,因此文件在磁盘上的存储是永久性的
广义上理解
linux操作系统下一切皆文件(键盘、显示器、磁盘等)
对文件操作的归类认知
1、对于0KB大小的空文件是占有磁盘空间的,因为文件是文件属性和文件内容的集合(文件=属性+内容)
2、所有的文件操作本质上都是文件内容和文件属性操作
从系统角度上的理解
1、对文件的操作本质是进程对文件的操作
2、磁盘的管理者是操作系统
3、文件的读写本质不是通过C语言/C++的库函数来操作的,而是通过文件相关的系统调用接口来实现的(比如fopen,fclose等库函数都封装了系统调用 )
4、会同时存在多个被打开的文件,操作系统要把被打开的文件管理起来,“先描述,再组织”
二、回顾C文件接口
1、open
打开文件:fopen("路径","打开方式")
#include <stdio.h>
int main()
{FILE* fp = fopen("myfile", "w");if (!fp) {printf("fopen error!\n");}while (1);fclose(fp);return 0;
}
2、write
写文件fwrite("写的源文件","一次写的大小","写多少个","往哪写")
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
}
const char *msg = "hello bit!\n";
int count = 5;
while(count--){
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
3、fread
从文件读,fread("读到哪","单位","大小","读的源文件")
#include <stdio.h>
#include <string.h>
int main()
{FILE* fp = fopen("myfile", "r");if (!fp) {printf("fopen error!\n");return 1;}char buf[1024];const char* msg = "hello bit!\n";while (1) {//注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明ssize_t s = fread(buf, 1, strlen(msg), fp);if (s > 0) {buf[s] = 0;printf("%s", buf);}if (feof(fp)) {break;}}fclose(fp);return 0;
}
实现cat命令
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{if (argc != 2){printf("argv error!\n");return 1;}FILE* fp = fopen(argv[1], "r");if (!fp) {printf("fopen error!\n");return 2;}char buf[1024];while (1) {int s = fread(buf, 1, sizeof(buf), fp);if (s > 0) {buf[s] = 0;printf("%s", buf);}if (feof(fp)) {break;}}fclose(fp);return 0;
}
输出信息到显示器的方法
#include <stdio.h> #include <string.h> int main() {const char* msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0; }
linux下一切皆文件,输出信息到显示器就是相当于输出信息到“显示器文件”上,系统会默认打开3个流,stdin(标准输入流)、stdout(标准输出流)、stderr(标准错误流),其目的是为了提供默认的数据源和数据结果
三、系统访问文件的常用接口
1、open
如果打开成功会返回一个新的文件描述符,如果失败就返回-1,并且错误码被设置。
flags是一个标记位,也是一个宏,它有O_RDONLY、O_WRONLY、O_RDWR、O_APPEND、O_TRUNC等选项。
mode是权限位,如果利用open打开一个需要新建的文件则需要用到这个来设置权限,不然权限就会乱码。
2、close
3、write
返回实际写入的字节数,参数fd是文件描述符
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() {umask(0);int fd = open("myfile", O_WRONLY | O_CREAT, 0644);if (fd < 0) {perror("open");return 1;}int count = 5;const char* msg = "hello bit!\n";int len = strlen(msg);while (count--) {write(fd, msg, len);}close(fd);return 0; }
注:在系统接口write不关心你写入的是什么类型,只有语言层面的接口才关心,文本写入和二进制写入只是语言层面的概念
4、read
如果成功会返回实际读到的字节数,返回0表示读完了,小于0则代表失败
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() {int fd = open("myfile", O_RDONLY);if (fd < 0) {perror("open");return 1;}const char* msg = "hello bit!\n";char buf[1024];while (1) {ssize_t s = read(fd, buf, strlen(msg));//类⽐writeif (s > 0) {printf("%s", buf);}else {break;}}close(fd);return 0; }
四、文件描述符fd
在C语言库函数中fopen函数的返回值是 FILE* 类型,FILE是C语言提供的一个结构体,这个结构体内一定封装了文件描述符,因为在系统层面只认文件描述符
int main() {int fd = open("myfile", O_RDONLY);printf("%d",fd);return 0; }
上面的代码执行完后文件描述符fd打印出来的值是3,因为在这之前系统默认打开了标准输入(描述符:0),标准输出(描述符:1),标准错误(描述符:2)这3个文件,把0,1,2占用了,所以后面打开的文件的描述符只能从3开始
从底层进理解解fd
fd重定向原则
fd重定向就是更改文件描述符表的指针的指向,fd的分配原则:最小的,没有被使用的,作为新的fd给用户
如果把fd=1,也就是把标准输出关掉,此时fd=1就是最小的,没有被使用的,在下次打开文件时就会把1作为新的fd给用户, 而在C语言中原来fd=1指向的是stdout,现在是fd=1就指向新的文件,printf所打印的内容不会打印到屏幕,而是在文件里。
重定向的系统调用:int dup2(int oldfd, int newfd)
将newfd变成oldfd的拷贝,如将fd=1重定向,dup2(fd,1)
标准错误(fd=2)
#include <cstdio.h> #include <iostream>int main() {std::cout <<"hello cout"<< std::endl;printf("hello printf\n");return 0; }
以上代码生成可执行文件后,重定向到某一个文件时,一般这样写:./可执行文件 > 目标文件
它会默认是这样的:./可执行文件 1 > 目标文件
但是它的完整写法也是:./可执行文件 1 > 目标文件
其实向标准输入和标准输出都是向显示器打印,它们都指向显示器文件:
#include <cstdio.h> #include <iostream>int main() {//向标准输出里面打印std::cout <<"hello cout"<< std::endl;printf("hello printf\n");//向标准错误里面打印,fd=2,也是显示器std::cerr <<"hello cout"<< std::endl;fprintf(stderr, "hello stderr\n");return 0; }
用一般的写法把它生成可执行文件后重定向到某个文件里面时的现象是:向标准输出里面打印的代码重定向到文件里面了,但是向标准错误里面打印的只打印到了显示器没有打印到文件里
原因是:虽然它们都指向标准输出这个文件,而一般的写法默认是把fd=1重定向到文件里,而fd=2还是指向标准输出这个文件
所以把fd=2也重定向:./可执行文件 2 > 目标文件
标准输入和标准输出都占用者不同文件描述符,但是都指向显示器文件,标准错误存在的意义就是通过重定向的能力,把常规消息和错误消息进行分离!
把stderr和stdout打印到同一个文件的方法:./可执行文件 1>目标文件 2>&1
五、理解“在Linux中一切皆文件”
在windows中是文件的东西,它们在linux中也是文件;在windows中不是文件的东西,比如进程磁盘、显示器、键盘这样硬件设备也被抽象成了文件,可以使用访问文件的方法访问它们获得信息,甚至管道也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。这样做最明显的好处是,开发者仅需要使用一套 API和开发工具,即可调取 Linux 系统中绝大部分的资源。
例如:Linux 中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用 read 函数来进行;
几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。
上图中的外设,每个设备资源都可以有自己的read、write,但一定是对应着不同的操作方法,在打开一个文件的时候,操作系统的内核当中,存在struct file结构体,进程都是通过它来访问文件的,它里面包含了file operations结构体指针,指向了不同设备、资源的操作的方法,所以操作系统通过文件描述符访问文件的时候,直接通过read、write等函数回调找到对应的方法,这就让系统仅仅通过VFS(虚拟文件系统)可调取 Linux系统中绝大部分的资源,让系统认为“Linux下一切皆文件”。
六、缓冲区
什么是缓冲区
缓冲区是内存空间的一部分,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
通过一个现象了解缓冲区
int main() {close(1);int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND,0666);printf("fd: %d\n",fd);printf("hello bit\n");close(fd);return 0; }
(代码6-1)
代码6-1执行完成后生成了log.txt文件,但是它里面是0KB,没有任何内容;
int main() {close(1);int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND,0666);printf("fd: %d\n",fd);printf("hello world\n");const char *msg = “hello write\n”;write(fd,msg,strlen(msg));close(fd);return 0; }
(代码6-2)
代码6-2,执行完后 在log.txt里面仅有“hello write”;
造成这个现象的原因:
printf是c标准库的库函数,当我们用库函数的接口时,它并不会把要操作的内容写入到文件内核缓冲区中,而是会写入到c标准库提供的一个用户级的语言层缓冲区,这个缓冲区在FILE结构体里面,当用户强制刷新、刷新条件满足、进程退出时再把语言级的缓冲区通过fd+系统调用(比如:write) 拷贝到文件内核缓冲区。
代码6-1就是在没有满足刷新条件时关闭了fd,导致在进程结束时想要把语言级缓冲区的内容拷贝到文件内核缓冲区时找不到对应的文件描述符;
而代码6-2中write是直接把数据往内核缓冲区里面写的。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <unistd.h>int main()
{//库函数printf("hello printf\n");fprintf(stdout,"hello fprintf\n");const char *s = "hello fwrite\n");fwrite(s, strlen(s), 1, stdout);//系统调用const char *ss = "hello write\n";write(1, ss, strlen(ss));fork();return 0;
}
(代码6-3)
将代码6-3运行:
重定向后的库函数打印了2次,而系统调用只打印了1次。
原因同样是因为:fork创建子进程后,库函数打赢的类容还在语言级缓冲区里面,执行到return 0时,父进程和子进程都要把语言级的缓冲区刷新到内核级缓冲区,而系统调用已经是在内核级缓冲区了
./a.out运行时是往显示器写入的缓冲方式是行缓冲,而重定向后更改了缓冲方式为全缓冲(满了再刷新)
缓冲区类型
标准I/O提供了3种类型的缓冲区。
• 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通
常使用全缓冲的方式访问。
• 行缓冲区:在行缓冲情况下,当在输⼊和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使用行缓冲方式。因为标准
I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行
I/O系统调用操作,默认行缓冲区的大小为1024。
• 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:
1. 缓冲区满时;
2. 执行flush语句;