文章目录
- 🦄 什么是IO
- 🦄 文件IO(库级别)
- 👾 文件的打开与关闭
- 👾 当前路径
- 👾 文件的读写
- 🦄 标准输入输出流
- 🦄 文件IO(系统级别)
- 👾 文件的打开
- 👾 文件的关闭
- 👾 文件的读写
- 🦄 文件描述符(file descriptor)
- 👾 FILE*指针与文件描述符fd的关系
🦄 什么是IO
在计算机当中,I/O
即为输入和输出的英文缩写版,input/output
;
而在计算机当中,任何与输入输出有关的操作都可以被称作为IO
,例如简单的对于设备上的将信息以打印的方式在显示屏(终端)中进入显示或者是在文件的读写操作都可以被看成是一种IO
行为;
一般在计算机系统当中,IO
操作可以分为两大类:
- 基于硬件层面的
IO
- 基于软件层面的
IO
而若是再进行细分的话可以分为:
- 网络IO
- 设备IO
- 内存IO
- 标准输入/输出
- 数据库IO
- 进程间通信
- …
|
---|
🦄 文件IO(库级别)
在上文中列举常见的IO
当中提到了 标准输入/输出 ;
而在 C语言 当中则存在一个 标准输入输出库 即<stdio.h>
;
在这个头文件当中包含了许多关于IO
操作的函数,这包括scanf()
,printf()
以及一些文件IO操作的接口;
对应的 文件IO操作 的函数分别有fopen()
,fclose()
,fwrite()
,fread()
,fscanf()
,fprintf()
等等;
当然这些操作对应着也会根据不同的层级平台拥有着不同的级别位;
对于这些在 C语言 的标准库当中出现的这些接口一般称为库级别的;
对应的除了库级别以外还有对应的系统级别的操作,但由于系统级别的操作可能会出现复杂或者冗余,故在其他高级语言当中会重新将这些接口进行封装优化,使得用户在调用时能够更加"随心所欲";
👾 文件的打开与关闭
而对于Linux
操作系统而言 “一切皆文件” ;
当然,在C语言当中存在一个名为FILE
的类型,这个类型是C语言当中表示一个文件流的一个抽象数据类型;
-
fopen()
FILE *fopen(const char *path, const char *mode);
该函数用于打开文件;
当文件打开成功时将返回一个
FILE*
的指针;若是文件打开失败则返回空指针
NULL
;-
const char *path
该参数表示传入一个字符串作需要打开文件的文件名;
可在传入的文件名当中添加相对路径或者是绝对路径,若是未传路径而是单纯的只传文件名那么该文件操作将设置文件打开路径为 当前路径;
-
const char *mode
该参数表示传入一个字符串参数表示打开文件时的方式;
一般的打开方式有
a
,w
,a+
,w+
等等;传参 打开方式 详细 “r” 以只读方式打开 文件必须存在,否则打开失败; “r+” 以读/写方式打开 文件必须存在,否则打开失败;
该模式允许对文件进行读和写操作;“w” 以只写方式打开 如果文件存在,则其内容会被截断为0,即内容会被清空;
如果文件不存在,则会创建一个新文件;“w+” 以读/写方式打开 如果文件存在,则其内容会被截断为0,即内容会被清空;
如果文件不存在,则会创建一个新文件;
该模式允许对文件进行读和写操作;“a” 以追加方式打开 如果文件存在,写操作会从文件末尾开始添加内容,文件原有的内容不会被截断;
如果文件不存在,则会创建一个新文件;“a+” 以读/追加方式打开 如果文件存在,写操作会从文件末尾开始添加内容,文件原有的内容不会被截断;
如果文件不存在,则会创建一个新文件;
这种模式允许对文件进行读和写操作,但所有写操作都会追加到文件末尾;在
Linux
当中存在一个符号为重定向;这个符号一般为
>
或是>>
;重定向的底层实际上就是一个将文件打开并进行写入的一个过程;
只不过是打开的方式不同;
-
>
该重定向与
fopen("w")
方式相同;即以只写的方式打开,文件若是存在则清空文件,文件若是不存在则创建文件;
-
>>
该重定向与
fopen("a")
方式相同;即以追加的方式将文件打开,文件若是存在则在文件的最末尾处进行写入;
文件若是不存在则创建文件;
-
-
-
fclose()
int fclose(FILE* fp);
该函数用于关闭一个文件;
FILE* fp
表示传入一个FILE*
类型的文件流指针;该函数若是关闭成功则返回
0
,若是关闭不成功则返回EOF
(文件结束标志)表示文件关闭失败;
👾 当前路径
在上文当中提到fopen()
函数在打开时的文件名传参;
在传const char *path
时可以在文件名中加入对应的绝对路径或者相对路径;
而若是未在文件名中加入路径,则文件的打开路径默认为 当前路径 ;
- 那么这里的 当前路径 指的是什么当前路径, 当前路径 又是谁的路径?
实际上当前路径所指的路径是进程的路径;
当一个程序被运行时其将会在内存当中化为进程,而进程实际上也需要有对应的运行路径;
-
存在一段代码
int main(){ FILE* fp = fopen("log.txt","w"); if(!fp){ perror("fopen"); return 1; } pid_t id = getpid(); printf("%d\n",id); sleep(1000); fclose(fp); return 0; }
在这段代码当中利用
fopen()
函数以w
的方式打开一个名为log.txt
的文件;使用
getpid()
函数打印该进程的PID
;打印结束后调用
sleep()
函数使得进程进入睡眠; -
运行这段代码后得出的结果为
28252
在Linux
当中,可以在/proc/(PID)
路径下找到对应PID
进程的信息;
其中在进程的信息中存在一条cwd
的信息存放着进程的工作路径;
$ ls -l /proc/28252 | grep cwd
lrwxrwxrwx 1 _XXX _XXX 0 May 2 21:39 cwd -> /home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write
从该段指令以及结果中可以看到,在使用指令查找对应cwd
时返回了一个路径/home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write
;
而实际上这个路径即为进程当前的工作路径,也就是上文中所提到的 当前路径 ;
$ ls /home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write
log.txt makefile mytest test.c
从结果中可以看出,在进程的工作路径当中生成了一个名为log.txt
的文件;
当然还可以使用另一种方式进行验证;
-
在
<unistd.h>
头文件当中存在一个函数:int chdir(const char *path)
这个函数能够在当前进程中修改进程的工作路径;
当工作路径成功被修改时,该函数将返回
0
,若是工作路径修改失败,对应的函数将会返回-1
;
既然创建文件时的当前路径指的是进程的当前路径,若是这样的话使用chdir()
转移进程对应的工作路径后则能在对应的路径当中找到新建的文件;
-
存在一段代码:
int main(){ chdir("./../../../.."); FILE* fp = fopen("log.txt","w"); if(!fp){ perror("fopen"); return 1; } pid_t id = getpid(); printf("%d\n",id); sleep(1000); fclose(fp); return 0; }
这段代码与上段代码并无太大差异,在这段代码当中仅仅是调用了
chdir()
函数从而改变进程的工作路径从而验证 “当前路径” 指的是进程的当前路径;运行该段代码对应的结果为:
$ ./mytest 31545
利用上文当中出现的查找进程工作路径的方式一样进行验证;
$ ll /proc/31545 | grep cwd lrwxrwxrwx 1 _XXX _XXX 0 May 3 10:23 cwd -> /home/_XXX/Begin
验证过后发现该进程的当前工作路径由
/home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write
变为了/home/_XXX/Begin
;查找对应的
Begin
目录当中是否存在新建的文件;$ ls /home/_XXX/Begin/ log.txt my_-linux
👾 文件的读写
在 C语言 当中对于文件的读写通常是采用标准库当中提供的一些接口函数;
这些接口函数通常与fopen()
,fclose()
等函数一起使用;
-
fwrite()
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
该函数为写入内容至文件,一般与
fopen("w")
配合一起使用;-
const void *ptr
该参数表示根据用户需求传入一个
const void*
的指针;这意味着用户可以根据需求传入需要的数据(不一定非要是字符串,可根据需要传入任意类型的指针);
-
size_t size
该参数表示用户在传入数据后应当传入所写入数据当中每个数据块的大小(字节);
-
那么假设传入一组数据位为字符串,在传该参数的使用使用了
strlen()
计算字符串的大小,strlen()
过后的数据是否需要+1
?为什么?对于以往的关于字符串的函数的传参而言,当在进行传参的时候需要进行
+1
,原因是在 C语言 当中字符串是以\0
作为结束标志的;故当若是需要进行拷贝或者其他操作的时候通常需要将
\0
的数量划到 字符串 的区域内;那么在这个参数的传参当中是否需要将
\0
与字符串混为一谈?答案是不用;
对于 C语言 来说,将
\0
划为字符串的结束标志是因为 C语言 在内存当中不能很好的去标明一个字符串的结束,故需要一个非显字符作为字符串的结束标志;而在 C语言 当中所定的规则实则在
OS
当中并不适用,OS
本身可以识别数据类型,故当调用该函数传该参数时并不需要考虑\0
的个数(占位);
-
-
size_t nmemb
该参数表示用户需要传入缩写数据的数据块个数;
-
FILE *stream
该参数表示用户在给文件写入数据时必须给定要在哪个文件当中进行写入,故对应的应该将文件的文件流
FILE*
指针进行传入才能进行后续操作; -
Return Value
若是写入成功,函数将返回写入元素的个数;
对应的若是返回的数据与
nmemb
参数不同或是为0
则表示可能表示发生了错误或到达了文件末尾;
-
-
fread()
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
该函数为从一个文件当中读取数据,通常配合
fopen("r")
一起使用;该函数的参数与
fwrite()
函数的参数大致相同;唯一不同的是对于
ptr
而言,在fwrite()
函数当中将传入一个const void*
的指针从而能够将需要写入的数据传入至函数内部;而在
fread()
函数当中应确保有一块足够大以至于能够容纳从文件当中读取到的数据的一块空间,故需要传入对应的地址; -
fprintf()
int fprintf(FILE *stream, const char *format, ...);
该函数与
fwrite()
函数的作用相同,即为传入一个FILE*
文件流指针,并将对应的信息传入至该文件流当中;该函数同样一般需要配合
fopen()
函数进行使用,即在使用该函数的时候确保文件要被可写入的方式打开;该函数与
printf()
函数中唯一不同的点就是需要传入一个文件流指针从而保证对应的数据信息能够通过文件流写入到对应的文件当中;其余参数与
printf()
函数无异; -
fscanf()
int fscanf(FILE *stream, const char *format, ...);
该函数与
fprintf()
函数的情况相同,在此不再进行过多赘述;
具体其他与文件相关的接口函数在此不再进行赘述;
很有趣的一件事是对于上述文件的接口函数的函数名可以进行一个思考;
对于printf()
与scanf()
函数而言,其可以看成是直接打印数据至终端或者是从输入设备(键盘)中读取数据;
而对于fprintf()
与fscanf()
函数而言只是将对应的终端/输入设备转化为了文件流;
🦄 标准输入输出流
在上文中提到了 C语言 当中的与文件相关的接口;
-
那么联系上文的接口思考
"Linux"
下一切皆文件是否与其有对应关系?在C语言的学习当中有着这么一句话:
- 在C语言当中,默认会打开三个标准输入输出流(标准输入,标准输出,标准错误),分别为
stdin
,stdout
,stderr
;
在
Linux
当中使用man
来查找对应的信息man stdin
;对应的信息可以看到:
NAME stdin, stdout, stderr - standard I/O streams SYNOPSIS #include <stdio.h> extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
在手册中可以发现实际上三个标准输入输出流的类型都是为
FILE*
的类型;而
FILE*
类型又是文件流的类型,以Linux
操作系统当中一切皆文件 来理解的话是否可以将对应的硬件设备也视为文件;且是否可以使用
fprintf()
或者fscanf()
来调用对应的设备; - 在C语言当中,默认会打开三个标准输入输出流(标准输入,标准输出,标准错误),分别为
根据上面的假设可以进行验证;
-
存在一段代码
#include <stdio.h> int main() { char str[100] = {0}; str[99] = '\0'; printf("从stdin中读取数据:\n"); fscanf(stdin, "%99s", str); printf("在stdout中写入数据:\n"); fprintf(stdout, "%s\n", str); printf("在stderr中写入数据:\n"); fprintf(stderr, "%s\n", str); return 0; }
这段代码定义了一个
str
的数组作为接收数据的空间;调用
fscanf()
函数并将stdin
即标准输入流作为文件流参数传入函数当中,以str
作为数据的中转站,即将从stdin
文件流中读入的数据保存至str
当中;调用了
fprintf()
函数,并分别将标准输出stdout
与标准错误stderr
作为文件流的参数传入至函数当中,将str
数据写入至stdout
与stderr
文件流当中;
运行程序后发现在打印从stdin中读取数据:
后进程将会阻塞;
原因是因为fscanf()
需要从stdin
文件流中读取数据,然而stdin
文件流中并没有任何数据;
意思是需要从键盘当中读取数据;
当从键盘当中获取数据后进行回车时对应的信息将会被打印两次;
原因是以当前计算机而言,stdin
默认指的是键盘,而stderr
与stdout
默认指的是显示器;
同时可以进行验证 “Linux
下一切皆文件” ;
从演示中可以看出,对于操作系统而言,键盘与显示器等硬件设备也是被以文件的形式所看待;
实际上并不是只有 C语言 当中存在上述所提到的 “在C语言当中,默认会打开三个标准输入输出流”,任何语言在 默认会打开三个标准输入输出流 这句话当中都是适用的;
-
打开这三个标准输入输出流实际上是操作系统的行为
-
其每个流所对应的硬件如下:
🦄 文件IO(系统级别)
该图列出了对应的计算机体系的组成部分;
以该图为例,若是由下往上看的话为每一个底层都在直接/间接的服务以它为基础的上层;
而若是从上往下观察,实际上在访问的过程当中也不能越过其中的任何一个障碍去进行访问;
-
以用户访问硬件为例:
用户访问硬件必须以贯穿的形式进行访问而不能越过任何一个其他步骤;
在上文当中提到了库级别的文件IO
接口函数(libc
);
而正如刚刚所说的,服务是层级向上的;
而访问是贯穿向下的,libc
也是如此,实际上libc
是由 系统调用(System Call
)进行封装的;
对于在上文中提到的接口函数(例如fopen()
,fclose()
,fwrite()
等…)作为 C语言 的库函数(libc
)都存在于 C标准库中的 <stdio.h>
头文件当中;
而系统调用都存在于 系统库 的头文件当中,在Linux
当中,常用的头文件有:
-
<sys/types.h>
该头文件当中定义了一系列的数据类型,这些类型通常用于系统调用;
-
<sys/stat.h>
提供了访问文件状态信息的功能,比如文件权限,最后修改时间等;
-
<sys/wait.h>
提供了进程控制相关的功能,比如等待子进程结束;
-
<sys/mman.h>
提供了内存管理功能,包括内存映射,保护,解除映射等;
-
<sys/socket.h>
提供了创建套接字,监听套接字,接受连接等网络编程相关的功能;
-
<sys/ioctl.h>
提供了对设备I/O操作的接口,比如终端,网络接口的控制;
-
<sys/time.h>
和<sys/times.h>
提供了时间相关的功能,包括设置时间,获取时间等;
-
<sys/resource.h>
提供了资源控制的功能,比如设置和获取进程优先级,资源限制等;
-
<sys/uio.h>
提供了向量I/O操作的功能,允许一次性从多个缓冲区读写;
对于 C标准库 而言,由于是标准库,所以具有可移植性,可在多个平台中以统一的规则去调用同样的接口函数;
而 系统库 不同, 系统库 顾名思义即为根据不同操作系统所设计出来的库,一般来说这种库是并不具有可移植性;
举个最简单的例子,在上面提到了许多 <sys/ ... .h>
的头文件,由于这些头文件是Linux
当中的系统调用,在Windows
本身是不适用的;
👾 文件的打开
在上文当中提到了库级别的文件IO操作函数;
库级别的文件打开使用的是fopen()
函数,当文件打开成功后将会返回一个FILE*
的文件留置针,打开失败后将会返回NULL
空指针;
而在Linux
当中打开文件使用的函数为open()
函数;
与fopen()
不同,open()
函数为一个系统调用级别的接口;
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);
该段代码为在Linux
当中使用man
手册查看open()
函数的结果;
从代码中看出若是需要调用该函数则需要包含三个头文件<sys/types.h>
,<sys/stat.h>
与<fcntl.h>
;
同时open()
函数共有两个版本,但两个版本的差异仅仅为一个参数的差异;
但实际上两个函数确实是根据不同的需求所使用的,对应的相差的那个参数mode_t mode
即为权限的参数;
同时对于接口的返回值其将会返回一个int
类型的值;
当文件打开失败后将会返回-1
作为erro
的判断;
当打开成功时将会返回一个new file descriptor
该参数即为 文件描述符 ;
|
-
int open(const char *pathname, int flags);
该函数一般用于打开 已经被创建/已经存在 了的文件;
-
int open(const char *pathname, int flags, mode_t mode);
该函数既可以用于打开 已经被创建/已经存在 的文件,也可以创建文件;
同时在该函数的man
手册当中可以看到,实际上在该函数当中存在许多选项;
而这些选项实际上对应的是该函数的选项;
一般常用的选项如下:
O_RDONLY
- 以只读的方式打开文件O_WRONLY
- 以只写的方式打开文件O_RDWR
- 以读写的方式打开文件O_APPEND
- 以追加的方式打开文件O_CREAT
- 若是文件不存在则创建文件O_TRUNC
- 若是文件存在则清空文件
而在open()
函数的传参的方式并不与以往的函数传参相同;
-
以只读的方式打开文件
open("filename",O_RDONLY);
-
以只读的方式打开文件,若是文件不存在则新建文件
open("filename",O_RDONLY|O_CREAT);
从这两行代码当中可以观察到实际上该函数的传参方式为 比特位级别的标志传参方式 ,从而能够使得在一个参数位当中传递多种信息从而使得传参能够存在一定的灵活性;
|
已知二进制是以 2n 进行分布;
1byte == 8bit
由左至右分别为 27 , 26 , 25 … 20 ;
以该图为例;
而从第一行可以看出,实际上一个数至代表一个位置,其具有唯一性;
而以位的方式进行传参正是依赖于这种这种传参的唯一性;
- 存在一段代码
#define ONE (1 << 0) // 1 #define TWO (1 << 1) // 2 #define THREE (1 << 2) // 4 #define FOUR (1 << 3) // 8 //其中上述定义了四个掩码都是唯一的掩码 //当在进行与&的操作时将判断该位置上是否为1 从而能够进行精准的传参 void show(int flag) { if (flag & ONE) { printf("ONE\n"); } if (flag & TWO) { printf("TWO\n"); } if (flag & THREE) { printf("THREE\n"); } if (flag & FOUR) { printf("FOUR\n"); } } int main() { show(ONE); printf("-------------------------\n"); show(ONE | TWO); printf("-------------------------\n"); show(TWO | THREE); printf("-------------------------\n"); show(ONE | FOUR); printf("-------------------------\n"); return 0; }
在这段代码当中,使用了
#define
定义了几个宏(以位运算的方式);这几个宏分别代表了
4
个掩码;即为上图中出现的 23 , 22 , 21 , 20 ;
- 运行结果
$ ./mytest ONE ------------------------- ONE TWO ------------------------- TWO THREE ------------------------- ONE FOUR -------------------------
从运行的结果可以看到,所打印的与代码中的测试无异;
同时对于使用 比特位级别的标志传参方式 并不单单是因为其具有唯一性灵活性和高效率;
同时其还具有兼容性;
位操作是在所有支持 C语言 的平台都可使用的操作,这代表这个接口函数可以在大部分的操作系统当中保持一致;
根据上文的介绍可以大致明白open()
函数的调用方式;
假设存在一个文件夹,其内容大致如下:
$ ls -l
total 20
-rw-rw-r-- 1 _XXX _XXX 81 May 3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10984 May 3 11:55 mytest
-rw-rw-r-- 1 _XXX _XXX 469 May 6 16:14 test.c
在这个目录当中存在三个文件,分别为makefile
,mytest
,test.c
;
而test.c
文件的main()
内容如下(头文件不过多赘述):
int main()
{
int ret = open("log.txt", O_WRONLY);
if (ret < 0) {
printf("open fail\n");
printf(-1);
}
return 0;
}
在这段代码当中调用open()
函数并传参O_WRONLY
,以 只写 的方式打开log.txt
文件,若是打开文件失败将会打印open fail
并退出进程;
运行该程序后其结果为:
$ ./mytest
open file
从结果可以看出这里的文件打开失败;
其对应的文件也并未被创建:
$ ls -l
total 20
-rw-rw-r-- 1 _XXX _XXX 81 May 3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10808 May 6 16:26 mytest
-rw-rw-r-- 1 _XXX _XXX 573 May 6 16:26 test.c
原因是因为在上文当中我们比较了关于open()
函数与fopen()
函数的差异;
对于fopen()
函数而言,其在文件不存在时将会新建一个对应的文件,而对于open()
而言其并不会主动去新建文件,故在尝试使用open()
函数打开一个不存在的文件时将会打开文件失败;
若是需要open()
函数在打开文件时使其在文件不存在的情况下新建文件则需要添加新的参数,即为:
int ret = open("log.txt", O_WRONLY|O_CREAT);
添加对应的参数后对应的代码为:
int main()
{
int ret = open("log.txt", O_WRONLY|O_CREAT);
if (ret < 0) {
printf("open fail\n");
printf(-1);
}
return 0;
}
$ ./mytest
$ ll
total 20
--wsr-x--- 1 _XXX _XXX 0 May 6 16:37 log.txt
-rw-rw-r-- 1 _XXX _XXX 81 May 3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10808 May 6 16:37 mytest
-rw-rw-r-- 1 _XXX _XXX 581 May 6 16:37 test.c
修改代码后再次运行程序时发现并没有报错;
且对应的在目录当中出现了一个名为log.txt
的文件;
-
但可以发现一点问题
已知在
Linux
当中权限一般以rwx
的方式进行展示,但是在此处的log.txt
文件当中其对应的权限变为了--wsr-x---
;而实际上这是一个乱码,即在创建文件的时候并没有按照对应的权限进行创建;
文件的权限是与文件相伴而生的,同时在上文当中提到open()
函数有两个版本;
-
一个版本一般用于打开已存在的文件
int open(const char *pathname, int flags);
-
一个版本既可以打开已存在的文件也可以打开不存在的文件
int open(const char *pathname, int flags, mode_t mode);
而只有第二个版本的open()
函数才能指定新建文件时文件的权限;
其中mode_t mode
即为权限,一般采用八进制的方式进行传参;
int main()
{
int ret = open("log.txt", O_WRONLY|O_CREAT,0777);
if (ret < 0) {
printf("open file\n");
return(-1);
}
return 0;
}
修改代码,在代码当中添加文件新建时所伴随的权限为0777
;
将原有的log.txt
文件删除并重新运行程序;
$ ll
total 20
-rwxrwxr-x 1 _XXX _XXX 0 May 6 16:47 log.txt
-rw-rw-r-- 1 _XXX _XXX 81 May 3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10808 May 6 16:47 mytest
-rw-rw-r-- 1 _XXX _XXX 586 May 6 16:46 test.c
从最终结果来看,文件确实是被成功创建,同时伴随着权限;
我们指定以0777
的方式创建文件,但最终其权限为0775
,是因为在当前环境当中(我的环境)对应的umask
为0002
;
$ umask
0002
而最终的文件权限为 原有权限&(~umask)
;
当然也可以在当前进程当中设置umask
;
int main()
{
umask(0);//设置当前进程的umask
int ret = open("log.txt", O_WRONLY|O_CREAT,0777);
if (ret < 0) {
printf("open file\n");
return(-1);
}
return 0;
}
运行结果为:
$ ll
total 20
-rwxrwxrwx 1 _XXX _XXX 0 May 6 16:58 log.txt
-rw-rw-r-- 1 _XXX _XXX 81 May 3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10856 May 6 16:58 mytest
-rw-rw-r-- 1 _XXX _XXX 601 May 6 16:58 test.c
👾 文件的关闭
在C语言当中的文件关闭为fclose()
函数,其中参数的传递为其对应的文件流FILE*
的指针;
而在系统调用当中采用的函数为close()
函数;
NAME
close - close a file descriptor
SYNOPSIS
#include <unistd.h>
int close(int fd);
在上文当中提到了open()
函数的返回值将会返回一个int
类型的 文件描述符 ;
而文件描述符实际上是代表一个文件的;
在使用 close()
函数关闭文件的时候只需要传入对应的文件描述符其就能将该文件关闭;
对于其返回值而言,当文件关闭成功时将返回0
;
若文件关闭失败则返回-1
并设置error
;
👾 文件的读写
在 C语言 当中对于文件的读写的接口函数多种多样;
fread()
,fwrite()
,fprintf()
,fscanf()
…;
而在Linux
当中,操作文件的系统调用接口函数主要为write()
与read()
;
-
read()
NAME read - read from a file descriptor SYNOPSIS #include <unistd.h> ssize_t read(int fd, void *buf, size_t count);
对于系统调用接口的从文件中读取数据主要依靠
read()
函数;从上述信息可以看出
read()
在头文件<unistd.h>
当中;而其对应的三个参数分别为:
-
int fd
文件描述符(file descriptor),这个参数为
int
类型,代表要操作的文件,该参数一般是通过调用open()
函数得到的; -
void *buf
这个参数为指向了一个缓冲区的指针,函数从文件中读取的数据将会被存放到这个缓冲区当中;
由于这个参数的类型为
void*
的类型,这意味着这个指针可以是任何类型的指针; -
size_t count
这个参数指定了要读取数据的大小,一般以字节为单位;
与
fread()
函数相同,在计算大小时并不需要+1
来区别对待字符串结束标志\0
;
其返回值的类型为
ssize_t
类型的数据(POSIX
标准定义的一个数据类型,与size_t
不同,这是一个有符号的数据类型);当文件读取成功时将会返回读取到的数据的大小;
若是读取失败时则会返回
-1
并设置error
; -
-
write()
NAME write - write to a file descriptor SYNOPSIS #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
与
read()
函数相同,write()
函数也是对于文件操作的系统调用接口函数中较为主要的一个函数;主要的功能为将数据写入文件当中;
其对应的三个参数范别为:
-
int fd
文件描述符,一般由调用
open()
函数获得; -
const void* buf
这个参数与
read()
函数的缓冲区相同;该参数为指向一个缓冲区的指针,而该缓冲区包含了要写入文件的数据,其中
const void*
表示其可以指向任何类型的数据类型;但是在进行写入操作(
write
)的时候缓冲区的内容不会被改变,故使用consst
修饰; -
size_t count
该参数表示需要写入的数据大小(字节数);
其返回值类型与
read()
函数相同,返回一个有符号的数据ssize_t
;若是写入成功,将会返回写入数据的大小(字节);
若是写入失败,将会返回
-1
并设置error
; -
这两个函数都是底层的系统调用,将会直接与操作系统内核交互,不经过任何缓冲;
两个函数都是POSIX
标准的一部分;
在上文 文件IO(系统级别) > 文件的打开 部分中介绍了使用open()
函数时要设置对应的写入方式;
文件的打开方式决定了文件在写入时的方式;
-
存在一段代码
int main() { umask(0); int fd = open("log.txt", O_WRONLY ); char *str = "xxxxxxxxxxxxxxx\n"; if (fd < 0) { printf("open file\n"); return(-1); } write(fd, str, strlen(str)); return 0; }
在这段代码当中以 只读 的方式打开一个已经存在的
log.txt
文件并设置了一个名为str
的缓冲区,其中缓冲区的内容为"xxxxxxxxxxxxxxx\n"
;当运行这段程序后使用
cat
打印出log.txt
文件的内容后结果如下:$ cat log.txt xxxxxxxxxxxxxxx
从结果来看,这段程序已经成功的将对应的数据写入到了文件当中;
但若是再多次运行该程序呢?
$ cat log.txt xxxxxxxxxxxxxxx
即使多次运行该程序其最终的结果也相同;
在原代码的基础上进行修改,将缓冲区的内容修改为
"aaa\n"
并再次运行程序;$ cat log.txt aaa xxxxxxxxxxx
当将缓冲区的内容进行替换之后发现
"aaa\n"
被写入至了文件当中;在调用
open()
函数时单纯以只读的方式打开文件时 文件的读写位置将在文件的开头;实际上系统接口调用级别的读写与库级别的读写相同,打开的方式决定了读写的位置;
同时在上文介绍
open()
函数时提到了一个名为O_APPEND
的选项;O_APPEND The file is opened in append mode. Before each write(2), the file offset is positioned at the end of the file, as if with lseek(2). O_APPEND may lead to corrupted files on NFS file systems if more than one process appends data to a file at once. This is because NFS does not support appending to a file, so the client kernel has to simulate it, which can't be done without a race condition.
若是以该方式打开文件则文件将以追加的方式进行读写;
同样的将该上述代码进行修改;
int main() { umask(0); int fd = open("log.txt", O_APPEND ); char *str = "aaaa\n"; if (fd < 0) { printf("open file\n"); return(-1); } write(fd, str, strlen(str)); return 0; }
打开
log.txt
文件手动清空后运行该程序;$ ./mytest $ cat log.txt $
但从结果来看,似乎并没有数据被写入进
log.txt
文件当中;原因是因为在使用
O_APPEND
打开文件进行写入时并没有指定以读的方式打开文件,此时对应的文件的写入位置将会挪动到文件末尾;O_APPEND
选项只会移动文件的读写位置,而其本身并不提供读写权限,若是需要提供读写权限则要追加O_WRONLY
的选项;将
int fd = open("log.txt", O_APPEND );
修改为int fd = open("log.txt", O_APPEND|O_WRONLY );
并再次运行程序;$ cat log.txt $ ./mytest $ cat log.txt aaaa $ ./mytest $ cat log.txt aaaa aaaa $ ./mytest $ cat log.txt aaaa aaaa aaaa
成功以追加的方式将数据写入文件当中;
在上文当中举例了使用O_APPEND
选项打开文件并进行写入;
从这个选项当中可以看出该接口的组合使用方式与C语言当中的库级别的文件接口基本相似;
而事实也是如此,在Linux
环境下,C语言的库级别的文件操作接口正是封装了系统调用接口;
以fopen()
为例,在使用fopen()
打开文件时将存在几个选项,以常用的a
和w
为例(关于写入方式);
-
fopen("filename","a")
以
a
的方式打开文件进行写入操作时,将是以追加的方式进行写入,若是文件不存在则新建文件;根据描述可以看出与上文中的
O_APPEND
选项相似,但是稍加组合就可以成为:open("filename", O_WRONLY|O_APPEND|O_CREAT , 0777);
即以追加(文件读写位置位于文件的末尾)写入的方式打开文件,若是文件不存在则新建文件;
-
fopen("filename","w")
以
w
的选项打开文件也是如此;open("filename", O_WRONLY|O_TRUNC|O_CREAT , 0777);
即为以读写的方式打开文件,若是文件不存在则新建文件,若是文件存在则清空文件,且文件的读写位置位于文件的开头;
🦄 文件描述符(file descriptor)
在上文当中提到了在调用open()
函数时将返回一个int
类型的返回值,而这个返回值即为 文件描述符 ;
- 那么什么是文件描述符?
在上文当中提到,在计算机当中,由上层至下层的顺序依次为:
- 用户 -> 用户操作接口 -> 系统调用(System cell) -> 操作系统 -> 驱动程序 -> 硬件
每一个层次都在以直接或者间接的形式为上层提供服务;
且以用户的视角来看其是无法绕过中间任何一层的,只能通过贯穿的形式逐级乡下进行访问;
用户无法直接访问操作系统;
但为了使得用户可以成功的与OS
进行交互,那么其就必须为用户提供对应的系统调用处接口;
而在 操作系统 当中,无论是硬件还是文件都需要被操作系统进行管理,而管理的方式一般为 先描述,后组织;;
如上图所示,在Linux
内核当中,每当打开一个文件都要对对应的文件进行管理,在进行描述的过程当中将为该打开的文件创建一个对应的结构体变量struct file
,并在这些结构体变量当中存储文件的信息;
所存储的信息主要包含为:
- 文件在磁盘的位置
- 文件的基本属性,大小,读写位置和打开者
- 文件内核缓冲区信息
- 对应的
struct file*next/prev
指针(并不是所有内核都会将struct file
结构体以双链表的形式进行管理) - 引用计数(判断引用计数是否为
0
从而判断是否需要将该struct file
结构体对象进行回收)
在之前的有关进程的博客当中提到,当一个程序被运行时,OS
也要对其以 先描述后组织 的方式进行管理;
实际上进程与文件之间的关系是属于一对多的,即一个进程可以打开多个文件;
如图所示,进程被运行后OS
将为其创建一个task_struct
进程控制块从而对该进程进行管理操作,而在这个进程控制块当中存在着一个指向struct file_struct
结构体变量的指针,这个指针指向了一个file_struct
的结构体变量,在这个结构体变量当中存在着一个名为struct file*fd_array[]
的指针数组;
而这个struct file*fd_array[]
指针数组即为对应的文件描述符表
;
而对应的:
- 该
fd_array[]
数组的下标即为文件描述符(file descriptor)
在这个指针数组中每个元素的类型即为struct file*
对应着刚才所说的文件打开时为管理文件所创建的struct file
数据结构;
可以进行猜测,当打开一个文件时将创建一个对应的struct file
数据结构对应的进程也将在文件描述符表与该数据结构产生联系,调用open()
函数返回其对应的下标;
每当需要对文件进行操作时则传入对应的文件描述符即可;
既然是fd_array[]
数组的下标,且是int
类型,这意味着该数据是可以被打印的;
-
存在一段代码:
int main() { int fd1 = open("log.txt", O_WRONLY | O_CREAT,0777); int fd2 = open("log.txt", O_WRONLY | O_CREAT,0777); int fd3 = open("log.txt", O_WRONLY | O_CREAT,0777); int fd4 = open("log.txt", O_WRONLY | O_CREAT,0777); printf("fd1 = %d\n", fd1); printf("fd2 = %d\n", fd2); printf("fd3 = %d\n", fd3); printf("fd4 = %d\n", fd4); return 0; }
在这段代码当中以写的方式打开4个文件,并将对应的返回值(文件描述符)进行打印;
$ ./mytest fd1 = 3 fd2 = 4 fd3 = 5 fd4 = 6
在上文中提到了一个概念 “在操作系统当中,默认会打开三个标准输入输出流” ;
而在该程序中的打印结果来看,对应返回的文件描述符是从3
开始的;
这意味着在文件描述符表当中,下标为0
,1
,2
已经被占用,那么着三个文件描述符是否可以对应 输入 , 输出 , 错误 ?
-
验证
在上文当中提到了,在
Linux
当中的系统调用级别的文件操作接口是以文件描述符来进行操作的;那么对应的
0
,1
,2
也是文件描述符,这意味着可以直接传入这三个文件描述符从而验证他们的关系;int main() { char buf[1024]; buf[1024] = 0; ssize_t ret = read(0, buf, sizeof(buf)); if (ret > 0) { buf[ret] = '\0'; write(1, buf, strlen(buf)); write(2, buf, strlen(buf)); } return 0; }
在这段代码当中,从文件描述符
0
当中获取数据并存储至缓冲区buf
当中;再将数据写入至文件描述符
1
,2
的文件当中;从结果来看,当从文件描述符
0
当中获取数据时进程将会进行阻塞并等待键盘的输入;当从键盘中读取数据后将数据写入文件描述符
1
,2
时对应从键盘中获取的数据将会被打印在显示器当中;同时在在上文当中提到,对于操作系统级别的文件操作接口的关闭文件是利用
close()
进行的;对应的这个接口将传入一个文件描述符从而将文件进行关闭;
int main() { char buf[1024]; buf[1024] = 0; ssize_t ret = read(0, buf, sizeof(buf)); if (ret > 0) { close(1); buf[ret] = '\0'; fprintf(stdout, buf, strlen(buf)); } return 0; }
在这段代码当中使用了
close()
将文件描述符1
对应的文件关闭并调用了fprintf()
传入标准输出对缓冲区内的信息进行打印;运行结果如下:
$ ./mytest aaaaa $
从结果可以看出,由于文件描述符
1
被关闭,对应的使用标准输出stdout
进行输出时将不做反应;那么此时将标准输出对应的代码修改为标准错误的代码,即
fprintf(stderr, buf, strlen(buf));
,其余部分不变时再次运行程序;$ ./mytest aaaaaa aaaaaa
从结果可以看出,当文件描述符
1
被关闭时调用fprintf()
函数并以标准输出的方式进行打印时则无法输出,而由于文件描述符2
对应的文件未被关闭故可以使用标准错误进行打印;
在上文中的例子当中验证了实际上三个文件描述符0
,1
,2
对应着C语言当中的三个标准输入输出流;
与上文联系,可以得出三个文件描述符也对应着输入设备与输出设备;
- 既然说 Linux当中一切皆文件 那么为什么使用
close()
将文件描述符1
中的文件关闭为什么还能使用标准错误进行打印?为什么对应的硬件接口未被关闭?
在上文当中提到,在struct fiel
结构体当中存储了许多与文件相关的信息;
其中的一个信息即为 引用计数 ;
-
引用计数
引用计数即可以判断当前资源是否有被哪个进程所使用;
文件是可以被多个进程打开的,当调用
open()
函数打开某个文件时该文件的引用计数将会+1
;对应的当调用
close()
函数将该文件关闭时,其引用计数将会-1
;当引用计数变为
0
时,则表示该资源需要被进行回收;
👾 FILE*指针与文件描述符fd的关系
在上文当中介绍了FILE*
指针与文件描述符fd
;
那么为什么文件描述符是以int
类型存在,而在C语言当中确实以一个名为FILE*
指针进行区别;
实际上FILE
类型是C语言封装出来的一种结构体类型;
而其要进行对应的文件操作意味着其必定需要调用操作系统的系统调用接口;
那么文件描述符fd
是必不可少的元素;
可以推断出实际上在FILE
这个类型的定义当中,主要的核心是依靠文件描述符fd
进行的;
-
那么如何能够证明?
在
C++
当中,由于类是可以被设为私有的,故若是在C++
当中需要访问一个较为核心的成员是不被允许的(一般会被设置为私有或保护);而在C语言当中结构体是没有权限的,这意味着可以直接将对应的信息进行打印;
int main() { printf("stdin->fd1:%d\n",stdin->_fileno); printf("stdout->fd2:%d\n",stdout->_fileno); printf("stderr->fd3:%d\n",stderr->_fileno); return 0; }
对应的结果为:
$ ./mytest stdin->fd1:0 stdout->fd2:1 stderr->fd3:2
故实际上在上文当中的 “在操作系统当中,默认会打开三个标准输入输出流” 是不准确的;
由于 标准输入输出流 这个概念是在C语言层面的,而在OS
层面只有文件描述符fd
;
故这句话在OS
层面应该为: “在操作系统当中,默认会打开三个文件描述符,分别为0
,1
,2
”
- 那么为什么操作系统默认会打开这三个文件描述符?
在操作系统当中,操作系统为用户提供了一个抽象层的接口,而这个接口就是对应的C语言当中的标准输入输出流,也是操作系统当中的文件描述符;
同时由于他们是一个抽象层的接口所以并不需要用户进行思考;
实际上这三个文件描述符的打开本身与硬件是没有太大关系的;
在OS
的启动当中,OS
将会默认去检测当前的硬件,这包括CPU
,GPU
,内存,输入输出设备等以保证这些硬件功能的良好同时判断自己可以使用哪些硬件;
而这个检测出来的硬件本身与三个文件描述符无关;
因为无论是否有检测到输入输出设备都会将三个文件描述符进行占用,因为即使没有检测到输入输出设备也可以通过其他的方式进行输入输出;