前言
在本篇之前我们讲的大部分都是关于进程的相关知识,从本篇开始我将带领大家进入IO的世界,常听闻Linux下一切皆文件,它到底是什么意思?文件的定义是什么?让我们开始学习起来吧!
1.预备知识-从系统角度理解文件
1.1 文件 = 内容 + 属性
之前我们讲过文件 = 内容 + 属性,这很好理解,一个文件不仅包括它的内容还包括一些描述内容的属性,比如大小、格式、存储位置等等,但是这里要强调的是属性和内容一样都是数据。
1.2 文件的操作类型
文件的所有操作无外乎两种:
1. 对内容
2. 对属性
1.3 怎么访问文件?
文件在磁盘(硬件)上放着,我们访问文件的流程:
先写代码 → 编译 → exe文件 → 运行 → 访问文件
本质是谁在访问文件?
进程
要向硬件写入只有谁有权力?
操作系统(通过驱动程序等)
普通用户也想写入怎么办?
必须调用操作系统提供的文件类的系统调用接口
文件类的系统调用接口,为什么之前都没怎么听过呢?
1。因为它比较难,为了让接口更好的使用,语言上对这些接口做了封装,这也导致了不同的语言有不同的语言级别的文件访问接口(都不一样),但是封装的都是系统接口。
为什么要学习OS层面上的文件接口呢?这样的接口只有一套。
为什么呢?因为你正在使用的操作系统只有一个。
2。跨平台 如果语言不提供对文件的系统接口的封装是不是所有的访问文件的操作,都必须直接使用OS的接口?
是。
而用语言的客户要不要访问文件呢?
当然要
一旦使用系统接口,编写所谓的文件代码,无法在其他的平台中直接运行了,就不具备跨平台性。
怎么做到跨平台的?
把所有的平台的代码都实现一遍,使用条件编译,然后动态裁剪。
1.4 显示器是硬件吗?
printf 向显示器打印,为什么从来没有感到奇怪过?
因为它比较直观,但是其实它和磁盘写入没有本质区别。
1.5 怎么理解Linux下一切皆文件?
曾经我们理解的文件就是
- .exe文件、.c文件、.txt文件这样的 → 可以进行read、write等操作。
- 显示器:printf、cout → 可以进行write操作
- 键盘:scanf、cin → 可以进行read等操作
站在我们写程序的角度,这个操作是加载到内存
站在内存的角度它则是执行input(read)、output(write)
普通文件的写入过程:
普通文件 → fopen/fread → 进程内部(内存) → fwrite → 文件中
|------------------- input -----------------|------------------ output ---------|
1.6 总结
1.6.1 什么叫文件?
站在系统角度,能够被input读取,或者能够output写出的设备就叫做文件
狭义的文件:普通磁盘文件
广义的文件:显示器、键盘、网卡、声卡等几乎所有的外设,都可以称之为文件。
1.6.2 文件种类
文件有两种:
1、被进程打开的文件(内存文件)
2、没有被打开的文件(磁盘上,文件=内容➕属性)(磁盘文件)
1.6.3 文件属性从哪里来?
来自磁盘,在打开文件时会将属性载入到struct file结构体中.【后面详细了解】
2. 复习一下接口使用
2.1 C语言接口
vim下——底行模式
!man fopen
(【查看fopen】,查看其他的接口也是同样的)
使用一下
mycode.c
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { FILE *fp = fopen("log.txt","w"); if(fp==NULL) { perror("fopen"); return 1; } const char*s1 = "hello fwrite"; fwrite(s1,strlen(s1),1,fp); fclose(fp); return 0; }
运行后向log.txt写入文件
2.2 什么是当前路径?
当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径,这个叫进程的工作路径,也叫当前路径。
... cwd(当前工作目录) -> /home/venus/linuxtest/5_基础IO/test_c
举例:
为了让大家看到进程的情况,所以我们直接设计一个死循环的函数
#include<stdio.h> int main(){ while(1){} return 0; }
查看进程:
[venus@localhost test_c]$ ps ajx | head -1 && ps ajx | grep mycode PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3063 3468 3468 3063 pts/0 3468 S+ 1000 0:01 vim mycode.c 4583 6982 6982 4583 pts/1 6982 R+ 1000 2:15 ./mycode 6983 7064 7063 6983 pts/2 7063 S+ 1000 0:00 grep --color=auto mycode [venus@localhost test_c]$ ls /proc/4583 -l 总用量 0 ... -r--r--r--. 1 venus venus 0 8月 31 18:02 cpuset lrwxrwxrwx. 1 venus venus 0 8月 31 18:02 cwd -> /home/venus/linuxtest/5_基础IO/test_c -r--------. 1 venus venus 0 8月 31 18:02 environ lrwxrwxrwx. 1 venus venus 0 8月 31 18:02 exe -> /usr/bin/bash dr-x------. 2 venus venus 0 8月 31 18:02 fd dr-xr-xr-x. 2 venus venus 0 8月 31 18:02 fdinfo -rw-r--r--. 1 venus venus 0 8月 31 18:02 gid_map -r--------. 1 venus venus 0 8月 31 18:02 io ..
此时创建文件test.txt,就会用当前路径加上创建文件的文件名,再利用系统接口来创建文件。
2.3 文件写入接口问题
示例代码:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { FILE *fp = fopen("log.txt","w"); if(fp==NULL) { perror("fopen"); return 1; } const char*s1 = "hello fwrite\n"; fwrite(s1,strlen(s1),1,fp); const char*s2 = "hello fprintf\n"; fprintf(fp,"%s",s2); const char*s3 = "hello fputs\n"; fputs(s3,fp); fclose(fp); return 0; }
2.3.1 写入是时要不要使用strlen(s1)+1[也就是要不要考虑\0?]
不用考虑,因为\0是C语言的规定,文件不用遵守,文件保存的是有效数据。
2.3.2 tips
⚡fopen以w的方式打开文件时,会直接清空文件
⚡ '>'符号,输出重定向
这样使用时也是fopen的相同原理和效果↓
> log.txt
log.txt文件会被清空
⚡以a(append)的方式打开则是追加
⚡以r的方式打开是只读
回想一下其实cat命令的底层其实也是只读,不过是把文件内容打印到了显示器上而已,那我们来简单实现一个cat命令。
#include<stdio.h> int main(int argc,char *argv[]) { if(argc != 2) { printf("argv error!\n"); return 1; } FILE *fp = fopen(argv[1],"r"); if(fp==NULL) { perror("fopen"); return 2; } char line[64]; while(fgets(line,sizeof(line),fp)!=NULL) { fprintf(stdout,"%s",line); } return 0; }
这样,我们就可以利用它来查看文件,如下:
2.2 系统接口(open、close、read、write)
2.2.1 C库函数与系统接口的对应:
C库函数:fopen fclose fread fwrite
系统接口:open close read write
2.2.2 系统接口的使用
2.2.2.1 open
以open为例
open成功后会返回file descriptor 失败返回-1
以写的方式打开:O_WONLY
举例:
#include<stdio.h> #include <fcntl.h> int main() { int fd=open("log.txt",O_WONLY); ... return 0; }
但是只有这个就可以创建文件了吗?
非也!
你在应用层看到一个很简单的动作,在系统层甚至OS层面,可能要做非常多的动作。
修改后
#include<stdio.h> #include <fcntl.h> #include <sys/stat.h> int main() { umask(0);//为了不影响我们测试,将该文件的掩码改为0 //umask函数用于设置文件创建时的权限掩码 int fd=open("log.txt",O_WONLY|O_CREATE,0666); ... return 0; }
2.2.2.2 标志位
open 函数是用于打开文件的系统接口。
其中的flags参数用于指定打开文件的方式和属性
flags参数是一个整数值,可以使用不同的标志位进行按位或(bitwise OR)操作,以同时提供多个选项
以下是常见的些fags参数选项
- O_RDONLY:以只读方式打开文件
- O_WRONLY:以只写方式打开文件
- O_RDWR:以读写方式打开文件
- O_CREAT:如果文件不存在,则创建文件
- O_APPEND:追加方式打开文件,写入数据时会在文件未尾添加而不是覆盖原有内容
- O_TRUN:如果文件存在且成功打开,将其长度截断为0。
- O_EXCL:与O_CREAT一起使用,如果文件已存在,则打开失败
**当flags参数等于0时,它表示没有指定何特殊选项,默认使用最基本的打开方式。相当于只读方式打开文件,类似于O_RDOMLY。**需要注意的是,flags参数的含义可能受到操作系统和文件系统的影响,因此在实际使用时应参考相关文档和规范来确定具体行为。
操作系统传递标志位的一种方案:
2.2.2.3 close
关闭文件
... close(fd); ...
2.2.2.4 write
写入
... const *s="hello world!\n" write(fd,s,strlen(s));//strlen(s)不需要+1,前面解释过了 ...
继续写入呢?
... const *a="aa" write(fd,a,strlen(a)); ...
预测结果显示aa,因为之前我们说写入会先清空再写入。
实际上:显示 aallo world!
之前说过在应用层一个简单的动作,其实底层并不简单,
我们要在open时
int fd=open("log.txt",O_WONLY|O_CREATE,0666); • 1
上加上 O_TRUNC
变为:
int fd=open("log.txt",O_WONLY|O_CREATE|O_TRUNC,0666);
那w+底层是什么样的?
需要加上O_APPEND
int fd=open("log.txt",O_WONLY|O_CREATE|O_APPEND,0666);
2.2.2.5 read
读取
int fd=open("log.txt",O_RDONLY)
3. 分析接口细节,引入fd(文件描述符)
3.1 如何深入理解?
当我们连续写入↓
int fd1=open("log.txt",O_WONLY|O_CREATE|O_TRUNC,0666); printf("%d\n",fd1); ... int fd4=open("log.txt",O_WONLY|O_CREATE|O_TRUNC,0666); printf("%d\n",fd4); //关闭 close(fd1) ... close(fd4)
cat log.txt
结果显示:
3 4 5 6
那么0 1 2在哪里?
其实:
它们分别对应着
stdin → 0 标准输入
stdout → 1 标准输出
stderr → 2 标准错误
怎么验证?它们之间是什么关系?
往1里面写,依旧可以输出到显示器上
fprintf(stdout,"hello stdout\n"); //等效于↓ const char *s="hello stdout\n"; write(1,s,strlen(s));
读取到0里面,依旧可以输入到显示器上
char input[16]; ssize_t s=read(0,input,sizeof(input)); if(s>0) { input[s]='\0'; printf("%s\n",input); }
3.2 FILE是什么?
之前我们使用过fopen函数,它的返回值是一个指向 FILE 的指针
FILE *fopen(const char *path,const char *mode)
FILE是什么?
它是由C标准库提供的一个结构体,内部有多种成员。
我们知道,C文件、库函数内部一定要调用系统调用的。在系统角度,它是认FILE还是fd呢?
只认fd!所以FILE中必定封装了fd
所以stdin、stdout、stderr→都是 FILE* →都含有 fd
怎么证明?
printf("stdin:%d\n",stdin->_fileno); printf("stdout:%d\n",stdout->_fileno); printf("stderr:%d\n",stderr->_fileno);
输出结果:
stdin:0 stdout:1 stderr:2
4.周边文件(fd的理解,fd和FILE的关系,fd分配规则,fd和重定向,缓冲区?)
4.1 fd的理解
- 进程要访问文件,必须先打开文件。
- 进程可以打开多个文件吗?
- 一般而言,进程:打开文件 = 1:n
- 文件要被访问,前提是加载到内存中,才能被直接访问。
- 进程:打开文件 = 1:n -> 如果有多个进程都打开自己的文件呢?
- 系统会存在大量的被打开的文件!
- 所以os要不要把如此多的文件也管理起来呢?
- 要,先描述,再组织!
- 那么在内核中如何看待打开的文件?OS为了管理每一个被打开的文件,构建
struct file { struct file *next; struct file *prev; //包含一个被打开的文件的几乎所有的内容,不仅仅是属性 }
它会创建struct file 的对象,充当一个被打开的文件,如果有很多就用双向链表组织起来。
进程和文件的对应关系是怎样的?
每一个进程都有一个task_struct,这个task_struct会指向一个struct files_struct结构体,这个结构体里会有一个指针数组struct file* fd_array[32],而这个指针数组就是文件描述符对应的数组。
fd的本质是一个数组下标!
4.2 fd和FILE的关系
4.3 fd分配规则
最小的、没有被占用的文件描述符
4.4 fd和重定向
close(1)后,fd=1,根据fd的分配原理,就都打印到了文件里,这个功能叫做输出重定向.
输出重定向原理:
系统一开始会给进程默认关联打开的三个文件:标准输入、标准输出、标准错误,它们在内核中都是一个struct_file对象,重定向的本质其实是在OS内部更改fd的对应的内容的指向。
4.4.1 dup
4.4.1.1 查看手册
4.4.1.2 原理
oldfd copy to newfd → 最后要和oldfd一样
4.4.1.3 测试代码
输出重定向
追加重定向
《Linux从练气到飞升》No.22 Linux 基础IO(二)+https://developer.aliyun.com/article/13898