Linux基础IO—上
复习c语言接口
你真的懂文件吗?
文件的打开与关闭
深入了解文件读与写(C语言级别)
系统文件I/O
我们知道,文件是放在磁盘(硬件)上的,我们用代码访问文件的思路是:
写代码 -> 编译 -> 生成可执行exe -> 运行 -> 访问文件…
我们利用c语言提供的库文件的接口来完成文件访问,但你有没有想过,C语言又是如何做到访问磁盘上的文件的呢?
也就是说,访问文件的本质上到底是谁在访问??
其实答案很显然,访问硬件,本质上只有操作系统OS能够访问!!
而普通用户想访问,只能让操作系统提供特定的接口
因此,调用库函数访问文件时,我们调用的接口实际上底层是封装了系统接口
这样再来看刚刚提出的问题:访问文件本质上是谁在访问 ??
首先,我们写完代码之后,通过编译器进行编译链接,会形成一个可执行程序。此时,可执行程序本质上是一个文件,它是放在磁盘上的,而当我们去运行此可执行程序是,本质上就是将文件内的代码和数据加载到内存当中,经过操作系统的先描述再组织变成进程,说白了就是程序需要运行起来调用系统接口才能访问文件,而程序运行起来就会变成进程,因此访问文件本质上是进程在访问文件!!
那既然这样,为什么计算机语言还要大费周章专门为访问文件提供接口呢?
其实是因为系统接口比较难!!因此语言上进行了封装,为了让接口更好的被使用!
这样也导致了不同的语言有着不同的文件访问接口,但系统层面的文件接口只有一套!!因此我们学习系统接口,能让我们对访问文件的理解更加深刻!
默认打开的三个流
都说Linux下一切皆文件,我们也学习认识了其实显示器和键盘(详见复习篇)也是一种文件,因此显示器显示内容以及在键盘上打字的过程不就是访问文件的过程吗?那访问文件是要通过编写代码,调用接口,编译后执行程序才能访问的啊!我们平常写代码也没打开过显示器和键盘这两个文件呀?
其实,是系统帮助我们做了这些事情!(人还怪好嘞!)
任何程序,不管有没有手动打开显示器和键盘,只要运行后经过先描述再组织成进程后,系统都会默认帮助任何进程打开三个流:标准输入流(stdin),标准输出流(stdout)以及标准错误流(stderr)
其中stdin对应的就是键盘(输入,是一种读操作,内存读文件也就是读取人通过键盘传达的信息)
stdout对应的就是显示器(输出,是一种写操作,内存通过显示器这个文件写入信息能够被人所读取)
stderr对应的也是显示器,只是写给人类的信息都是有关错误的信息
*extern FILE stdin;
*extern FILE stdout;
*extern FILE stderr;
这三个流的类型实际上是FILE *类型,也就是文件指针,更加印证了显示器和键盘也是一种文件
系统打开文件接口open
接口介绍
#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); pathname: 要打开或创建的目标文件 flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。 mode: 若是创建文件,设置该文件被创建时的权限 flags参数: O_RONLY:只读打开 O_WRONLY: 只写打开 O_RDWR : 读,写打开 这三个常量,必须指定一个且只能指定一个 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 O_APPEND: 追加写 O_TRUNC: 若文件已存在,先清空原本的文件内容 //返回值: 成功:返回新打开文件的文件描述符(后面会介绍) 失败:-1
当前路径 “.”
我们在Linux系统下,若没有将可执行程序添加到环境变量时,我们运行程序一般都会使用 “ ./+ xxx.exe” 的方式,而其中 “ ./ ”的含义就是当前路径下的.exe文件,那么当前路径的参照物是谁呢?当前路径的当前指的又是哪一条路径呢?
其实,当前路径指的是可执行程序运行时变成进程时所处的路径,也就是一个进程是可以知道当前程序执行时的位置的,这样一来,也能解释了为什么我们之前使用fopen时,若文件不存在会自动在当前路径下创建的原因了:
当我运行程序后,在程序还没退出前,此程序会经过系统描述组织成进程,而此时可执行程序运行时所处的路径也会被进程所记录,这样一来,当前路径的概念也就确定了。
pathname参数
pathname表示需要打开或者创建的文件
当需要打开文件时,需要指定对应的路径和文件名,若没有路径则默认会在当前路径查找,若当前路径没有,则会默认在当前路径下创建一个新的文件。
flags参数
flags简称标志位,对应的是打开或创建文件时,所赋予文件的权限:
O_RONLY:只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 以追加写的方式打开
//举例:若需要以只读的方式创建并打开文件:
open("test.txt",O_RONLY | O_CREAT,0666);
若需要传多个选项则需要在中间用分隔符 “|” 隔开
这看似很方便且简单的传选项的方式,底层是如何实现的呢?
其实,我们以用户的视角在应用层上看到的一个看似简单且方便的动作,在系统接口层面,可能是经过了非常多复杂的动作完成的!
而flags的底层实现正是印证了这一说法,flags的底层需要用到位图思想!
flags参数的类型为整型,有32个比特位,每个比特位都可以用来设置成选项的标志位,而每一个标志位都可以实现不同的功能,使得函数效率大大提高,降低了冗余
我们直接模仿实现一下标志位功能:
这就是flags参数的底层原理
mode参数
mode参数是提供给用户如有创建文件的需求时能够给文件设置权限的参数,比如我想给用户设置的权限为
rw-rw-rw-,对应的参数就为0666,若这一块忘记的可以复习权限Linux权限一章
注意:若发现设置的值跟实际的不一致,可能是被umask值过滤了,要注意umask值默认掩码的存在,我们可以设置当前进程的umask值,这样不会影响其他进程
系统关闭文件接口close
这个跟我们学习过的fclose是一样的,只不过传的参数不同,fclose传的参数是打开文件时返回的文件指针,而close传的是打开文件时返回的文件描述符fd;
int close(fd);
//关闭成功返回0,失败则返回-1;
系统"写"文件接口write
系统接口对应向文件内写入内容的接口为write:
//原型 ssize_t write(int fd,const void* buf,size_t count); //第一个参数为打开文件时返回的文件描述符 //第二个参数为从内存的哪个位置的内容向文件中写入 //第三个参数为写入的大小,单位为字节
返回值:成功时返回实际写入的字节数的大小,失败则返回-1
示例:
// test: write
int main()
{
int fd = open("test.txt", O_WRONLY | O_CREAT ,0666);
if (fd < 0)
{
perror("open");
exit(1);
}
const char *buffer = "hello write!\n";
write(fd, buffer, strlen(buffer));//这里用的是指针,因此使用strlen
close(fd);
return 0;
}
系统"读"文件接口read
(注意看文件接口的小技巧就是站在内存的角度)
系统接口中对应向文件内读出内容到内存中的接口为read:
//原型 ssize_t read(int fd, void *buf, size_t count); //第一个参数为打开文件时返回的文件描述符 //第二个参数为从文件中读出内容后放入内存中的位置 //第三个参数为读出内容的大小,单位为字节
返回值:返回实际读出内容的大小,单位为字节,若返回0,则代表已经读到了文件末尾,若读取失败,则返回-1;
示例:
//test:read
int main()
{
int fd=open("test.txt",O_RDONLY);
if(fd<0)
{
perror("read");
}
char buffer[128];
memset(buffer,0,sizeof buffer);
ssize_t s=read(fd,buffer,sizeof buffer);
if(s<0)
{
perror("read");
}
printf("%s",buffer);
return 0;
}
文件描述符file description
我们在学习open接口时,若成功打开文件,则会返回一个整型:
这个整型就称为文件描述符file description,那么什么是文件描述符呢,在我们之前的学习中,fopen成功打开后返回的是一个维护FILE结构体的结构体指针FILE*(详见深入了解文件一章中关于文件指针的部分占位符),他跟文件描述符又有什么关系呢?
首先我们要明白,FILE结构体是C语言标准库所提供的用来便于管理文件的结构体,而库函数的内部一定是封装了系统层面的东西用来标识文件的,在系统的角度上看,他只认文件描述符,而不认识文件指针,因此FILE的底层必定是封装了文件描述符才得以管理文件的.
文件描述符既然这么重要,那他究竟是什么?
让我们一步一步的来探索:
一般情况下,进程要访问文件,肯定要打开文件的,那么一个进程可以打开多个文件吗,还是一个文件能被多个进程访问呢?其实,一般而言,进程:文件的比例为1:n,大量的文件在系统中被打开,就像大量杂乱无章的资料被扔在了领导的桌子上一样,系统肯定是会非常不满的,因此,系统为了使这些杂乱无章的被打开的文件被有条不紊的整理并管理好,他对这些文件进行了先描述,再组织的过程,也就形成了FILE结构体,再把这些FILE结构体以双链表的形式链接了起来,此双链表上串的都是被打开的文件,也就是从磁盘上加载到内存里的文件(因为只有被加载到内存中才能被进程直接访问)。但这仅仅是把文件给管理好了,进程要访问文件除了要打开文件还要和这个被打开的文件建立好联系,以便访问时能在双链表的众多文件中找,也就是被管理起来的文件,需要有一种方法知道该文件是被哪个进程所打开的并同时做好管理。
如何建立进程与被打开文件间的联系呢?
刚刚我们提到被打开文件被描述组织成结构体后被放入了双链表中,而为了将其中的文件与进程联系起来,在内存中还存在着 files_struct 结构体,在此结构体内,有一个指针数组,而其中存放的指针,正是指向这些被打开文件的结构体指针!而其中数组的下标,就是我们对应的文件描述符!这样真相就水落石出了,原来文件描述符是指针数组的下标!!
但还要与进程建立链接还需要一部,那就是让进程每次都能够找到存放指针数组的结构体files_struct,因此,我们的程序在经过编译运行后被描述组织成进程后,进程中的进程控制块内还会存放一个files*,用来指向 files_struct 结构体,这样一来,进程就不怕把打开的文件弄丢了!
由于前面我们学习过系统会默认帮助我们打开三个流,标准输入流,标准输出流与标准错误流,而这三个流所对应的硬件是键盘与显示器,而我们又知道Linux下一切皆文件,因此,这三个也属于被打开的文件,既然是被打开的文件,那同样也经过了被描述并组织成结构体FILE,并将指向该结构体的指针放入files_struct中的指针数组fd_array中,对应的数组下标分别是:
标准输入流:0
标准输出流:1
标准错误流:2
现在我们已经了解了文件描述符是什么以及认识了进程是如何与被打开文件是如何联系起来的,下面让我们来将打开访问文件时的整套流程(以文件已经被创建为前提):
首先,有一个在磁盘上的文件,此时小陈同学写了一份代码,需要打开这份文件,小陈的代码中使用了系统调用接口open函数,此时系统就会用他自己的方法在路径中找到这个文件,并将此文件通过描述组织成FILE结构体并放入双链表中, 然后将指向此结构体的结构体指针写入files_struct中的指针数组fd_array中,写入成功后则返回fd给小陈接收,此时进程中也存在指向files_struct结构体的指针,小陈也知道了他打开文件的文件描述符,因此要访问这个文件时,只需要告诉接口read或者write他需要访问他手上拿的fd所对应的这个文件,那么接口就会通过他的方法在双链表中找到这个文件并写入或者读出小陈所需要的东西,这样也就完成了对文件的打开与访问操作!!!
文件描述符的分配规则
我们直接通过代码来测试一下文件描述符的分配规则:
// test: file description
int main()
{
int fd0 = open("test_fd1", O_RDONLY | O_CREAT,0666);
int fd1 = open("test_fd2", O_RDONLY | O_CREAT,0666);
int fd2= open("test_fd3", O_RDONLY | O_CREAT,0666);
int fd3 = open("test_fd4", O_RDONLY | O_CREAT,0666);
printf("fd0: %d\n", fd0);
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
close(fd0);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
运行后发现fd是从3开始按照打开的顺序往后依次递增,鉴于我们前面提到的三个标准流占用了0,1,2文件描述符,初步推断,文件描述符是通过文件打开的顺序依次往后填入的,但为了进一步验证,我们再做如下测试:
// test:fd
int main()
{
close(0);
// close(2);
int fd = open("myfile", O_RDONLY | O_CREAT,0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
这时运行后我们发现结果是
通过验证,我们基本可以确定文件描述符的分配规则:遍历指针数组并找到没有被使用的最小下标作为新的文件描述符
;
// close(2);
int fd = open(“myfile”, O_RDONLY | O_CREAT,0666);
if (fd < 0)
{
perror(“open”);
return 1;
}
printf(“fd: %d\n”, fd);
close(fd);
return 0;
}
这时运行后我们发现结果是[外链图片转存中...(img-dlV5UjFS-1707561019361)]
<font color='red'>通过验证,我们基本可以确定文件描述符的分配规则:遍历指针数组并找到没有被使用的最小下标作为新的文件描述符</font>