目录
前言:
在往期博客《Linux基础概念》中,我们聊过关于什么是文件,那时我们说的是文件=内容+属性
这也是我们今天铺垫的第一个结论
文件 = 内容+属性,所以如果对一个文件发生修改,那么修改的要么是内容,要么是属性
在我们使用C语言进行编程的时候,会发现如果需要对一个文件发生各种操作,比如读/写,那么我们做的第一件事一定是先打开这个文件。
所以我们说在我们访问文件之前,都得先打开这个文件,这是由我们得常识得到的
当然,单单打开文件的话是没有对文件进行任何修改的,实际上我们在C语言中修改一个文件的数据都是通过CPU执行代码的方式进行修改的。而代码属于程序的一部分,换句话来说执行代码实际上就是把程序加载到内存中变为进程以后执行的。
所以既然要把程序加载到内存中,那么通过程序的代码修改文件,文件本身也一定要加载到内存中,因为CPU不与外设直接交互!
打开一个文件是通过进程执行代码的方式进行打开的,所以文件本身其实是由进程来打开的。
而通过我们得常识可以知道,一个程序之中可以有多个fopen语句,也就是可以打开不同的文件,也就是说一个进程可以打开多个文件
也就是说在系统中,一个进程打开的文件可能有很多,那么接下来的问题是既然打开的文件有那么多,那么操作系统是否需要对进程打开的文件进行管理呢?
答案是肯定的,因为操作系统是文件的管理者
那么操作系统如何对被打开的文件进行管理呢?
答案是先描述后组织
而Linux操作系统是由C语言进行编写的,在C语言中我们如何描述一个事物呢?
答案是结构体
如何把这些结构体组织起来呢?
答案是数据结构,假设是链表
到这里,我们虽然对于被打开的文件还没有什么概念,但是我们可以知道,在Linux的内核中一定会有描述被打开的文件的结构体,并把它定义为对象,然后用链表组织起来。
完成上述工作后呢,操作系统就把对被打开的文件的管理变为了对链表的增删查改
但此时还有一个问题就是我们上面聊得都是被打开的文件,操作系统中难道所有的文件都是被打开的吗?
答案:并不是,被打开的文件在操作系统中是占少数的,我们一个系统中可能存在成千上万个文件,大部分文件都是放在磁盘中的
所以,对于操作系统中的文件我主要分为两种,一种是被打开的文件,称之为内存文件。而没有被打开的文件,称之为磁盘文件
接下来本文都是对被打开文件(内存文件)的介绍
stdin&& stdout && stderr
首先,我们先了解一个结论:Linux下的文件默认会打开三个文件流
分别是stdin,stdout,stderr,如下为这三个文件流的man手册介绍
STDIN(3) Linux Programmer's Manual STDIN(3)
NAME
stdin, stdout, stderr - standard I/O streams
SYNOPSIS
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
实际上,我们能看到上述介绍中,这三个文件流实际上就是文件指针,指向三个不同的文件。
那么这三个流指向的什么文件呢?为什么要默认打开这三个文件流呢?
标准输入/输出/错误文件流的本质
首先我们得先知道Linux下一切皆文件的概念,这是Linux操作系统的指导思想。
既然Linux下一切皆文件,那么对于Linux来说键盘、显示器、...这种硬件是否是文件呢?
答案是肯定的
所以所谓的stdin标准输入流,实际上对应的就是键盘文件
stdout标准输出流,实际上对应的就是显示器文件
stderr标准错误流,实际上也是显示器文件
输出到显示器和打开显示器文件的先后顺序
我们在学习C语言的时候第一次写代码,我们大概率写的其实是打印Hello,World!
当我们写好这一份代码的时候,会发现结果确实打印到了显示器上,我们上面说过由于Linux下一切皆文件,所以显示器也是文件。我们还说过凡是文件如果发生修改就一定要先打开,那么我们往显示器上打印是对显示器进行修改吗?
答案是肯定的,那么我们如果没有把显示器文件进行打开的话我们如何对它进行修改呢?由此我们可以得出结论,在我们对显示器进行写入的时候,一定是因为显示器在我们写入前,他所对应的文件被打开了。
为什么要默认打开三个文件流
在C语言中我们对一个文件的操作都是基于FILE*文件指针进行的,Linux是C语言写的,所以系统为进程默认打开的三个文件流可以使得用户可以对显示器、键盘进行操作/访问
而至于系统为什么默认要打开这三个文件流,最根本的原因其实是你作为用户,不管你有什么需求需要进行编写代码,大多都要使用键盘来输入数据,使用显示器来输出数据,使用显示器来输出错误信息。由于都有这样的需求,所以系统就默认打开了这三个文件流
文件操作函数与打开的3个文件流
所以我们除了使用C语言的printf函数把数据打印到显示器以外,还可以使用fprintf指定文件为显示器也就是stdout,也就是如下demo代码:
#include <stdio.h>
int main()
{
printf("Hello,World!\n");
fprintf(stdout,"Hello,World!\n");
return 0;
}
运行结果:
[yyf@VM-24-5-centos 24.06.22]$ ./test
Hello,World!
Hello,World
当然,C库函数中还有不少是可以把一个字符串打印到指定文件流的函数,例如fputs,fwrite。这里不过多介绍!
而与printf配套的scanf其实也是如此,fscanf就是指定一个文件,从这个文件中读取数据,而scanf就是直接读取键盘,不需要指定文件,所以如果我们把fscanf的参数设置为stdin,那么fscanf和scanf就没有什么区别了
Linux文件操作之系统调用
为什么要提供文件操作系统调用?
首先,我们先思考一个问题,那就是当你用printf进行打印的时候,最本质的特征是不是你的显示器中显示出来了你要打印的信息呢?
显示是的,而显示器是一个硬件!
根据我们了解的操作系统是硬件的管理者,那么我们使用硬件显然是无法绕开操作系统这一层的!
而操作系统不允许用户直接访问硬件资源。所以操作系统为用户提供了各种各样的系统调用,方便用户访问硬件资源!
由此我们可以知道,对文件的访问不仅仅可以使用C库中的文件操作接口,还有系统提供的各种系统调用,而接下来我们要逐个学习这些系统调用
打开文件
首先介绍的第一个关于文件操作的系统调用:open
如下为man手册中对这个系统调用的介绍
NAME
open, creat - open and possibly create a file or device
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
- open的功能,就是打开一个文件(可能会创建)
- open的头文件:sys/types.h、sys/stat.h、fcntl.h
- 对于第一个参数,也就是pathname,实际上就是要打开的文件所处的路径
而接下来要重点介绍的分别为:flags参数,mode参数,open的返回值
open参数之flags参数的基本功能
在我们C语言中要打开一个文件可能是r方式打开,可能是w方式打开,也可能是a方式打开。
C语言中是通过指定字符串来控制打开方式的
系统调用open则是通过这个flag参数来控制打开方式的。
flags参数如何控制打开方式
但我们能看到一个问题,flags只是一个整形变量,在我们的一般实现中,一个整形变量通常只能指定一个选项,而对于一个文件来说打开方式是有很多种的,比如读方式打开,比如读写方式打开,比如追加方式打开....。那么对于如此多的打开方式,如何使用一个整形来表示呢?
实际上系统是使用位图传参的方式使得一个整形可以表示不同的选项的。
换句话来说也就是可以根据一个整形中的一个比特位来表示某个是否可读/可写。。。
flags参数为什么要使用位图传参?
首先,对于一个文件来说,它的某个权限,比如读权限,对于别人只存在着两种情况,第一种别人可以读这个文件,第二种别人不可以读这个权限。而一个比特位则正好能表示出这两种情况,可以根据比特位为0或1进行区别
以下一个整形表示不同选项的具体操作
#include <stdio.h>
#define READ 1<<0 //读
#define WRITE 1<<1//写
#define APPEND 1<<2//追加
void IsBite(int sub)
{
if(sub & READ)
{
printf("This file have read\n");
}
if(sub & WRITE)
{
printf("This file have write\n");
}
if(sub & APPEND)
{
printf("This file have append\n");
}
printf("\n------------------------------\n");
}
int main()
{
int sub1 = 0;
int sub2 = 0;
int sub3 = 0;
//1.只有读权限
sub1 = READ;
//2.既有读又有写
sub2 = READ | WRITE;
//3.读写追加都有
sub3 = READ | WRITE | APPEND;
IsBite(sub1)