欢迎来到博主的专栏:从0开始linux
博主ID:代码小豪
文章目录
- 文件
- 打开文件
- 文件修饰符
- 从linux源码了解文件描述符
文件
首先我们先来搞清楚文件是什么?文件其实就是一段数据,是一段存储在磁盘当中的数据,文件是由文件内容和文件属性组成的。
文件的属性即文件的权限,拥有者,内容大小,文件名等,而内容则是我们保存在文件当中的数据。
博主在C语言专栏当中写过关于C语言文件操作,比如fopen,fclose之类的函数,但是这些东西都是仅仅在语言层接触到了文件,我们还没有在系统方面真正了解文件这个概念。
首先,如果我们要对文件进行读写操作,首先第一步就是要打开文件,所谓的打开文件,其实就是将文件从磁盘,读取到内存当中,因为根据冯诺依曼体系,cpu不会对磁盘这些外设直接做I/O,而是与内存做I/O,因此打开文件的实质,其实是要将文件从磁盘中加载到内存。
而我们在进程(0)当中了解到,我们作为用户,也是不能直接与外设进行交互的,用户与外设之间隔着一个操作系统,所谓的打开文件,其实就是告诉操作系统,我要打开某某文件,接着操作系统再将文件从磁盘加载到内存当中。
既然你打开文件了,那么总有将其关闭的时候吧,那么关闭文件时,我们是不是要将文件的数据保存到磁盘当中呢?这又是一个内存与磁盘做I/O的操作,既然文件从磁盘加载到内存需要操作系统,那么文件从内存保存到磁盘,当然也离不开操作系统了,因此操作系统肯定与文件有着千丝万缕的关系,那么具体操作系统是如何管理文件的呢?请听我娓娓道来。
打开文件
既然我们打算在系统的层面了解文件,那么我们就抛弃语言层面的fopen接口,改为使用linux的系统调用。在linux中,打开文件的系统调用为open()函数。其函数原型如下:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open函数的头文件有:<sys/types.h>
,<sys/stat.h>
,<fcntl.h>
(首先,open函数是用C语言写的,但是C语言没有函数重载这个东西,因此博主也不清楚linux是怎么做到让c函数出现同名函数的情况,也许是宏替换?)
我们先从两参数的open函数说起,首先第一个参数叫做文件路径,即我们要打开的文件,其所处的路径,文件名,我们要用字符串的形式写出。而第二个参数称为标志位,我们常用的标志位有以下几种
- O_RDONLY/O_WRONLY/O_RDWR,分别代表,以只读的方式打开文件,以只写的方式打开文件,以读写的方式打开文件
- O_TRUNC,打开文件时,如果文件存在,而且打开方式为写(O_WRONLY或者O_RDWR),则将文件内的数据清空
- O_APPEND,打开文件时,从文件末尾的位置写入数据
- O_CREAT,打开文件时,若文件不存,则创建一个新的文件。
这些标志位其实可以组合使用,比如我们想用只写的打开某个文件时,顺便将文件的内容给清空了,那么就可以使用O_WRONLY和O_TRUNC组合起来的方式,调用open函数。此时有人可能发现了,open关于标记位的参数只有一个flag,怎么可能传递两个参数组合起来呢?
这是因为标记位采取了通过位运算的方式来执行结果,比如O_RDONLY可能在第5个bit位置为了1,因此当flag的第5个bit位为1时,函数就会进行相应的操作,比如说以只读的方式,打开文件。这么说大家可能还不能理解,我们可以简单的写一下有类似逻辑的代码,代码如下:
#include<stdio.h>
#define A (1<<0) //第一个bit位为1
#define B (1<<1) //第二个bit位为1
#define C (1<<2) //第三个bit位为1
#define D (1<<3) //第四个bit位为1
#define E (1<<4) //第五个bit位为1void PrintBitmap(int flag)
{if(flag&A) //如果flag的第一位为1printf("A");if(flag&B) //如果flag的第二位为1printf("B");if(flag&C) //如果flag的第三位为1printf("C");if(flag&D) //如果flag的第四位为1printf("D");if(flag&E) //如果flag的第五位为1printf("E");printf("\n");
}int main()
{printf("=======================================================\n");PrintBitmap(A);//Aprintf("=======================================================\n");PrintBitmap(A|B);//ABprintf("=======================================================\n");PrintBitmap(A|C);//ACprintf("=======================================================\n");PrintBitmap(A|B|C|D);//ABCDprintf("=======================================================\n");PrintBitmap(A|B|C|D|E);//ABCDEprintf("=======================================================\n");return 0;
}
标志位的逻辑原理与上述代码类似,主要是通过判断flag的某位的bit位是否为1来进行对应的操作。因此,如果我们想让打开log.txt文件,以只读,而且创建新文件的方式,那么传递给open函数的参数应为:
#include<sys/types.h>//别忘了包这三个头文件
#include<sys/stat.h>//别忘了包这三个头文件
#include<fcntl.h> //别忘了包这三个头文件
open("log.txt",O_WRONLY|O_CREAT);
此时我们ls -a -l,查看一下新创建的文件log.txt。
诶,这个创建的log.txt似乎有问题,我们检查一下,发现好像是权限设置出现问题了,没错如果我们使用fopen函数,那么创建的文件,其权限将会是我们的默认权限,但是对于系统调用来说,文件的权限也是需要我们自己设置的。而两参数形式的open函数,显然没有提供设置权限的接口,因此想要设置权限,我们得用三参数的open函数才行。
int open(const char *pathname, int flags, mode_t mode);
linux中,文件权限被分为了三组,分别是所属用户,所属组,与other,而每一组都有对应的三种权限,即r,w,x,而r权限的值为4,w权限的值为2,x权限的值为1(权限章节有提到)。因此,mode参数传入的值,实际上是三组用户,其对应的权限的值。以rw_rw_rw_为例,其mode值应为(0666)。因此我们可以这样写打开log.txt的open函数
open("log.txt",O_WRONLY|O_CREAT,0666);//推荐写四位数字,第一位为0,后面为权限值
接着我们运行该进程,查看log.txt的权限。
可以发现,log.txt变回了正常形态,但是似乎权限不太对啊,我们给log.txt设置的权限为0666,那么应该是rw-rw-rw-才对,但是实际上log.txt的权限为rw-rw-r–。
这是因为,linux中的每个用户,都会有一个权限修正值,叫做umask,我们可以使用umask指令查看umask值,由于umask值的存在,因此我们在创建文件时,我们自己设置的权限还会收到umask修正。因此实际权限=设置的权限+umask值修正
,关于如何修改umask值,以及umaks值是如何修正权限的,博主不太想多聊,主要是由于篇幅原因。
文件修饰符
我们要知道,open函数其实是有返回值的,在linux的在线手册中谈到,如果open失败,则返回值为-1,如果打开成功,则返回值是一个叫做文件修饰符的东西(file descriptor)
那么这个文件修饰符到底是什么呢?
如果你学习过c/c++语言的文件操作函数,那么应该知道,当我们的c/c++程序运行时,会自动打开三个文件流,分别叫做标准输入流(stdio),标准输出流(stdout)以及标准错误流(stderr)。而这三个文件流其实在个人pc上对应的是键盘(stdin),显示器(stdout),显示器(stdout)。(外部输入设备怎么能是文件呢?这是一个伏笔。)
我们都知道,linux系统的作用,有进程管理,其实还有文件管理,而进程管理是通过PCB(即task_sturct)执行的,而文件是不是也有相应的文件结构呢?是的,这个文件结构叫做struct file
。
当我们使用open函数时,系统会在磁盘上的对应路径,找到对应的函数,并且将其加载到内存当中(如果没有,可以通过O_CREAT创建)。与之同时进行的,还有该文件对应的数据结构,即struct file。
当我们打开一个文件时,操作系统会将该文件加载到内存当中,并且创建一个该文件对应struct file,来管理该文件。
我们再来思考一个问题,那就是文件是谁打开的呢?有人这时就说了,这不是我自己打开的吗?这么讲当然没错,但是在仔细一点,文件其实是进程打开的,因为我们在进程中写了open函数,而当进程执行open函数时,文件才被打开,因此实际上一个文件何时被打开,何时关闭,何时创建,并非操作系统决定,而是由相应的进程决定,因此实际上文件与操作系统的关系图应该是这样。
而一个进程不仅仅要管理一个文件,而是能同时管理多个文件,因为前面提到了,一个c/c++进程,会默认打开三个文件,分别是stdin,stdout,和stderr,因此,一个进程并非只管理一个文件,而是多个文件,那么问题来了,系统怎么判断出这些文件到底谁是谁呢?就比如,进程1打开stdin和stdout,并且创建了对应的struct file,那么到底哪个struct file是stdin,哪个struct file是stdout?
操作系统为了分辨出进程,会给进程分配一个进程标识符,即pid,每个进程都有对应的pid,通过pid,系统就能找到对应的进程,并且对其进行操作。那么文件也是一样,由于文件是由进程打开的,为了区分出不同的文件,那么文件也要有对应的标识符,这个标识符就是文件描述符,即file describtor(后面简称fd)。
而open函数的返回值,就是返回被打开的文件对应的文件描述符,我们可以使用文件描述符来找到文件,以系统调用write为例
ssize_t write(int fd, const void *buf, size_t count);
write的作用是向fd(文件描述符)所指向的文件,写入count字节个,buf当中的数据,手册原文如下。
那么如果我们想在进程当中想log.txt文件写入数据,就应该写出这样的代码:
int main()
{int fd1=open("log.txt",O_WRONLY|O_CREAT,0666);//打开log.txt文件,并且获取其fdconst char str[]="hello world\n";//write函数包含在<unistd.h>头文件当中write(fd1,str,strlen(str));//向fd1(即log.txt)写入数据return 0;
}
接着我们运行该程序,并且查看log.txt的内容。可以发现log.txt被写入了数据
文件描述符,其实就是用来标识文件的序号,和pid的性质是一样的,如果我们想使用系统调用对文件进行操作,首先要做的就是使用open函数获取对应文件的文件描述符。
从linux源码了解文件描述符
在前面我们了解到,一个文件的打开,关闭,其实是由进程管理的,那么我们一定能在进程的PCB(task_struct)当中找到管理文件的成员。在linux当中,保存整个进程的所有文件信息的成员叫做files。源码如下:
struct task_struct
{//省略struct files_struct *files;//省略
}
files的是一个files_struct类型的结构体指针,那么这个files_struct的定义是什么?我们接着在源码当中找到
在files_struct当中,我们又找到了一个指针数组,它是一个指向struct file类型的结构体的指针数组,诶,struct file?这不是我们在开头提到的系统为文件记录文件信息的数据结构吗?没错,由于一个进程可以打开多个文件,因此每个进程都有其独立的文件组,当我们使用open函数打开文件时,本质上是将文件对应的struct file,加载到task_struct的fd_array当中,而fd其实就是该文件在fd_array的下标。
如果你不相信stdout的fd(文件描述符)是1,你可以尝试用write函数像fd=1的文件输出helloworld,接着运行看看结果。
int main()
{const char str[]="hello world\n";//write函数包含在<unistd.h>头文件当中write(1,str,strlen(str));//向1(即stdout)写入数据return 0;
}