1、共识原理
1、文件 = 内容 + 属性
2、文件分为打开的文件和没打开的文件
3、打开的文件是谁打开的? 答案是:进程!---本质是研究进程和文件的关系
文件被打开必须先被加载到内存,一个进程可以打开多个文件。因此,在OS内部一定存在大量被打开的文件。OS要管理这些被打开的文件--先组织,再描述---再内核中,一个被打开的文件必须有自己的文件打开对象,包含文件的很多属性。struct XXX{文件属性;struct XXX *next}; 对打开文件的管理就是对链表的增删查改。
4、没打开的文件:在哪里放着?我们最关注什么问题?
首先,没有被打开就没有进程去访问它,它就只能静静的呆在存储介质(磁盘)中。没有被打开的文件非常多,文件如何被放置好方便我们进行增删查改。----文件如何存储
2、回顾C文件接口
#include <stdio.h>
#include <string.h>int main()
{FILE *fp = fopen("log.txt", "w");//w方法,向文件写之前会对文件进行清空//a方法,也是写入,但是它的追加写入//打开文件的路径和文件名,当不写路径的时候是在当前路径下新建一个文件吗?if(fp == NULL){perror("fopen");return 1;}const char *massage = "hello Linux message";//strlen(message) + 1 ? 为什么??//不需要+1,因为字符串末尾是\0,是C语言的规定和文件没有关系。 如果+1的话就会将字符串末尾的\0显示的写入到文件中,而且显示出来的是乱码。 因此,写入时只需要将字符串的内容写进去就okfwrite(message, strlen(message), 1, fp);//向文件写入数据 1代表写入的个数。fprintf(fp, "%s: %d\n", message, 1234);fclose(fp);return 0;
}
什么是当前路径?指的是进程的当前路径。当一个进程要运行时,就要直到它是从哪里来的,就是它的可执行程序是从哪里来的,万一要形成临时文件,我要在那个路径下新建。进程基本属性里是包含它的当前路径是什么的。cwd里面包含了当前进程的工作路径。
那么。如果我更改了当前进程的cwd,就可以把文件新建到其他目录。
chdir("新路径"); //修改当前路径
新建文件带路径就按照带的绝对路径来,不带就按进程的相对路径来。
新建文件不用带路径,因为这个进程有当前路径,文件就会被新建到当前路径。
w:写入之前都会对文件进行清空处理。
a方法,也是写入,但是它的追加写入
C语言会默认打开三个 输入输出流 (其实就是文件):
(C程序在默认启动的时候,会打开三个标准输入输出流)
#include<stdio.h>extern FILE *stdin; //标准输入,,读键盘 键盘设备
extern FILE *stdout;//标准输出,, 显示器文件
extern FILE *stderr;//标准错误,,,显示器文件
C++三个输入输出流:
cin && cout && cerr因此,我们想写入的时候也可以向文件流中直接写入,因为你打开了一个文件,所以直接向文件流写入就可以直接写到文件里。
eg:fwrite(message, strlen(message), 1, stdout);
3、系统文件IO
文件其实是在磁盘上,磁盘是外部设备,访问磁盘文件其实是在访问硬件!
用户是不允许直接访问硬件的,必须要通过操作系统,对硬件进行访问。但是,又因为OS不相信任何人,它必须向用户提供接口,让用户安全的访问硬件。 因此,OS必须提供系统调用。几乎所有的库只要访问硬件设备就必须要进行封装!!
printf/fprintf/fscanf/fwrite/fread/fgets/gets/....库函数 他们的底层都封装了系统调用。
int open(const char *pathname, int flags, mode_t mode);
第一个参数:文件的路径 绝对相对都可以
第二:权限 flags是int类型,32个比特位。 O_RDONLY 以只读方式打开 O_WRONLY 以写方式打开 O_RDWR以读写方式打开 O_CREAT 打开时不存在创建一个 O_APPEND 以追加的方式打开 上面这些宏只有一个比特位
flags给我们提供很多的宏,这些宏里面只有一个比特位,这些宏,可以用或组合,然后再进行按位与 找到对应的标志位(即相应的功能),就可以达到想要的效果。(下面的代码有解释)
三:
补充:比特位级别的传参方式(原理):
OS向系统当中传递参数的方式
#include<stdio.h>
#include<unistd.h>
#include<string.h>#define ONE (1<<0)//1 1左移0位
#define TWO (1<<1)//2 1左移1位
#define THREE (1<<2)//4 1左移2位
#define FOUR (1<<3)//8 2^3//只有一个flags,通过传递不同的标志位,来调不同的功能
void show(int flags)
{if(flags&ONE) //按位与flages 内部用按位与的方式进行条件检测printf("hello function1");if(flags&TWO) //按位与flagesprintf("hello function2");if(flags&THREE) //按位与flagesprintf("hello function3");if(flags&FOUR) //按位与flagesprintf("hello function4");}int main()
{show(ONE);show(TWO);show(ONE|TWO);//在外部相当于只有一个比特位移,我们用或组合一次传两个标志位show(ONE|TWO|THREE);show(ONE|TWO|THREE|FOUR):return 0;
}
试着验证一下open函数
#include<stdio.h>
#include<unistd.h>
#include<string.h>//open需要用到的
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{umask(0);//设置为0,不然他会默认进去值//O_WRONLY只能写 要是文件不存在就必须用O_CREAT先新建一个,//但是Linux中,新建一个文件必须写清楚权限是什么 不写的话就会出现乱码权限 0666int fd = open("log.txt",O_WRONLY|O_CREAT,0666);//以只写方式打开if(fd < 0)//open 打开失败返回-1{printf("open file error\n");return 1;}const char *message = "hhhhhhhhhhhhhhhhhhhhhhhhhhh";//如果第一次写入上面的,第二次写入aaa 那么结果就是 aaahhhhhhhhhhhhhhhhhhhhhhhh//从开始写, 覆盖前面的 不对其他的做清空//但是c语言用w进行写入,会对之前的做清空write(fd, message, strlen(message));close(fd);return 0
}
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);//以写方式打开,不存在就创建,写入时先进行清空。
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//追加写
printf/fprintf/fscanf/fwrite/fread/fgets/gets/....库函数 他们的底层都封装了系统调用。
FILE *fp = fopen("log.txt", "w"); //fopen是库函数 w是清空
它的底层调用的系统调用是:
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
FILE *fp = fopen("log.txt", "a");//a 是追加写
它的底层调用的系统调用是:
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//追加写
不同的语言会有不同的文件操作接口,但是它底层对应的原理都是一样的
那么在数据类型上,FILE * 类型和 int 有什么关系呢???
4、访问文件的本质
一个进程可以打开多个文件,那么文件就需要被管理--先组织,再描述。
int open(const char *pathname, int flags, mode_t mode);
open的返回值类型是int 其实就是数组的下标, 我们可以看到当write的时候, write(fd, message, strlen(message)) 我们传入的第一个参数是fd 我们要向哪个文件写入就是通过数组下标找到对应的文件。 (上面的图就可以看出将进程和文件关联起来了)
int fd1 = open("log.txt",O_WRONLY|O_CREAT,0666);
int fd2 = open("log.txt",O_WRONLY|O_CREAT,0666);
int f3d = open("log.txt",O_WRONLY|O_CREAT,0666);
printf("fd1:\n", fd1);
printf("fd2:\n", fd2);
printf("fd3:\n", fd3);
打印结果是 3 4 5 我们自己创建的文件 第一个文件描述符是从3 开始的, 那0 1 2 下标呢??
其实 0 1 2 在进程启动的时候已经默认被打开了 C语言下,
stdin--键盘 (标准输入) 0
stdout--显示器(标准输出) 1
stderr--显示器 (标准错误) 2
#include <sys/stat.h>
#include <fcntl.h>int main()
{const char *msg = "hello Linux\n"write(1, msg, strlen(msg));write(2, msg, strlen(msg));return 0;
}
./myproc
hello Linux
hello Linux
#include <sys/stat.h>
#include <fcntl.h>int main()
{char buffer[1024];// read并不知道读的是不是字符串ssize_t s = read(0, buffer, sizeof(buffer));//从文件描述符0读 0代表的是键盘文件if(s < 0) return 1; //读失败了//读成功了//C语言要将读上来的信息当成字符串 read读的是字节 它读的是一个一个的字符放在缓冲区 //所以,你想让它读字符串,你就得加\0buffer[s] = '\0';printf("echo : %s\n", buffer);return 0;
}
./myproc
(等待键盘输入) aaabbbccc
(打印出) aaabbbccc
那么,进程启动时打开三个标准输入输出流,是C语言的特性吗?? 答案是:不是!是OS的特性!即进程默认会打开键盘、显示器、显示器
那么为什么OS要这样干? 在计算机开机的时候 显示器和键盘就已经被打开了。而且一般编程的时候都需要显示器和键盘,因此就先会被默认打开。 所以,当一个进程运行时,只需要将对应的文件填到0 1 2 下标的指针数组里就ok了。
FILE *fp = fopen("log.txt", "w"); C语言FILE是什么??
其实是C库自己封装的结构体!因为Linux访问文件必须通过文件描述符,所以,这个这个结构体里面肯定封装了文件描述符!! 那么,0 1 2 里面必定封装了文件描述符
#include <sys/stat.h>
#include <fcntl.h>int main()
{printf("stdin->fd: %d\n", stdin->_fileno);printf("stdout->fd: %d\n", stdout->_fileno);printf("stderr->fd: %d\n", stderr->_fileno);return 0;
}
stdin->fd: 0
stdout->fd: 1
stderr->fd: 2因此,in out err 已经默认将0 1 2打开了 因此自己新建的文件只能从3开始
库函数封装了系统调用,包括两个层面的封装,a、库函数封装了系统调用接口 b、数据类型层面,文件FILE *这个类型也必定包含文件描述符。
struct file 结构体里面很多文件属性,另外,它还包含引用计数count。因为一个文件可以被多个进程打开,当flose(1),假如关闭文件描述符为1的文件,那么就对这个文件进行-- 将文件描述符表的1下标的指针置为空,判断文件的count是否为0,是0就回收这个struct file.