目录
一、重温C语言文件操作
1.1 文件打开方式
1.2 文件写操作
1.3 文件读操作
1.3 标准输入输出
二、系统接口的使用
2.1 open 函数
2.2 close 函数
2.3 write 函数
2.4 read 函数
三、文件描述符
3.1 如何管理文件
3.2 0 & 1 & 2
3.3 文件描述符的分配规则
3.4 FILE
3.5 重定向
3.6 dup2 函数
一、重温C语言文件操作
本节我们的内容是学习Linux中的系统调用——相关的文件操作
首先我们来回忆一下C语言中的文件操作。
为什么学习系统的文件操作我们要先重温一下C语言的文件操作呢?
答: 因为当我们编写程序访问磁盘上的文件,本质上的过程其实是这样的:
本质上文件并不是我们访问的,而是进程访问的。因为当我们调用语言的文件操作,其内部封装了对应的系统调用,当程序运行起来时,就会去调用系统调用,从而访问文件。
所有的跨平台编程语言,内部的文件操作都是封装了对应系统的文件操作系统调用,所以我们先复习一下C语言的文件操作,然后再来复习对应的文件系统调用操作。
1.1 文件打开方式
看一下这三种常见的打开方式。
当前路径:即一个进程运行起来时,其当前所处的工作路径。
1.2 文件写操作
//fwrite
//第三个参数为要写出数据 基本单元 的个数
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
//fprintf
int fprintf(FILE *stream, const char *format, ...);
//fputs
int fputs ( const char * str, FILE * stream );
//fputc
int fputc ( char character, FILE * stream );
接下来看一下使用示例:
运行效果如下:
这里有一个问题:写文件时,要不要将'\0'进行写入?
不用,因为字符串结尾带上 '\0' 是C语言的字符串规定,而不是操作系统中文件的规定,所以我们不需要将'\0'写入,而 '\n' 可以写入,因为这可以使文本进行换行。
再重温一下如何使用shell脚本向文本文件中写入内容(重定向)
发现,之前写入的内容都被删除了,那我们可以这样操作来实现清空文本内容。
1.3 文件读操作
重温了写操作后,接下来实现一下读操作。
代码如下,就是将打开方式从 w 变成 r 。
结果如下:
有了读操作,其实我们就可以实现一下Linux中的 cat 命令。
实现方式就是将传入的命令行参数作为参数以读的方式打开,然后进行打印。
代码如下:
这样,cat命令就实现了。 效果如下:
1.3 标准输入输出
不难发现,我上面使用了这样的代码
fprintf(stdout,"%s",line);
这其中的 stdout 是什么呢?
我们平时使用的 printf 函数要包含头文件<stdio>,其实 std 表示 standard(标准),而io则是读写。所以我们平时经常引入的该头文件就是用于我们进行文件读写的。而stdin、stdout、stderr是C语言默认打开的三个文件,称为标准输入输出流,其对应的是硬件设施。
后面会进行更详细的讲解。
- stdin ---> 键盘
- stdout ---> 显示器
- stderr ---> 显示器
二、系统接口的使用
其实上面使用的fopen、fclose、fwrite、fgets都是调用的系统接口。
2.1 open 函数
首先我们来看看 open 接口
第一个参数是文件路径;
第二个参数是以为位图形式传入标志位作为其函数参数,一个 int 有32位,所以理论上最多可以传入32个选项,每一个选项对应一格比特位。
光是文字的讲解非常费劲,我们直接用代码来形象地解释位图的形式。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
void show(int flags) //0000 0111;
{
if (flags & ONE) printf("hello one\n");
if (flags & TWO) printf("hello two \n");
if (flags & THREE) printf("hello three\n");
}
int main()
{
show(ONE);
show(TWO);
show(ONE | TWO);
show(ONE | TWO | THREE);
return 0;
}
让我们来观察结果:
有了上面位图的理解,接下来我们来看一下 open 函数第二个参数的选项。
Flags:
O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWR 可读可写打开
当我们附带了权限后,打开的文件就只能按照这种权限来操作。
以上这三个常数中应当只指定一 个。下列常数是可选择的:
- O_CREAT--->如果目标文件不存在则创建文件,存在则无影响。
- O_TRUNC---->清空内容
- O_APPEND---->追加
fopen函数中的 a 和 w 打开方式的区别就在于一个清空(trunc),一个追加(append)。
返回值:成功:新打开的 文件描述符失败:-1
上面就是其中常用的一些选项,我们来尝试使用 open 函数以写的形式打开文件 log2.txt。
结果运行如下:
为什么打开文件失败了呢?
因为 fopen 是C语言的文件访问操作,虽然它本质是调用的open函数,但是其内部封装了许多的选项,但是我们没有感知。所以,如果我们想模拟 fopen 函数中的只写的方式打开,应该再添加选项,即文件存在则创建文件的 O_CREAT.如下图:
打开结果:
好的,文件就成功创建出来了,但是我们发现创建出的文件权限有点问题。
接下来我们看一下 open 的第三个参数——mode。 mode 是被创建文件的权限,我们可以对其进行设置。
文件权限如下:
发现文件的权限发生的变化,但是不是我们想要的0666,即rw-rw-rw-。因为系统中存在默认的umask,所以我们想在创建文件时,将umaks清空,这时我们可以调用系统接口 umask。
umask接口使用方式如下:
但是,这还不够,在C语言中,w方式打开文件会进行删除文件原本的内容,而a方式打开文件会在文本的末尾进行写入。这两种方式的区别就在于我们还差一个选项没有加入:
a和w的区别就在于一个清空(O_TRUNC)原本内容,和一个进行追加(O_APPEND)内容。
2.2 close 函数
上面介绍了 open 函数,接下来就是 close 函数的介绍
使用非常简单,将 fd 传入即可。
2.3 write 函数
介绍了打开、关闭文件,接下来就是 write 函数,其对应就是C语言中的fwirite、fprintf、fputs、fputc等等。
使用举例:
效果如下:
2.4 read 函数
接下来就是以读的方式打开,然后我们进行读文件。
参数:
- 第一个参数为读入文件的fd。
- 将读入的数据存放到 buf 中
- count 则为期望多少个字符
返回值:
实际读到的字符个数
使用举例如下:(因为read不会为读入的字符串末尾添加\0,所以我们使用memset先将字符串全初始化为'\0')
效果如下:
三、文件描述符
3.1 如何管理文件
在linux认为,一切皆文件,站在系统的角度,能够被input读取,或者能够被output写出的设备就叫做文件。
- 侠义的文件:普通的磁盘文件
- 广义的文件:显示器、键盘、网卡、声卡、显卡、磁盘,几乎所有的外设,都可以称之为文件
上面我们使用open、write、close、read,都传入了 fd( flie descriptor ) 这个参数,
通过上面的学习,我们知道了文件描述符就是一个小整数。
文件本质是被进程进行访问的,进程访问文件必须先打开文件,所以:
一个进程:打卡的文件= 1:n ---> 如果多个进程都打开了自己的文件,则系统中会存在大量被打开的文件。所以,OS要将各个进程所打开的文件进行管理,创建struct file的对象,该对象中存放着文件的所有内容(不仅仅包含属性),如果存在多个被打开的文件,再使用双链表进行连接起来。
其中进程控制块 task_struct 是这样控制对应的文件
3.2 0 & 1 & 2
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0(stdin),标准输出1(stdout),标准错误2(stderr)。
- 0,1,2对应的物理设备一般是:键盘、显示器、显示器。
所以我们其实可以直接从使用 fd 这个整数,在默认打开的文件 1 (屏幕)上进行输出。
运行结果如下:
3.3 文件描述符的分配规则
文件描述符的规则是有规律的,因为其默认会打开0、1、2三个文件。
其分配规则是优先最小正整数进行分配,我们来看代码:
运行结果:
3.4 FILE
学习了文件描述符,那我们再想一下C语言中的 FILE 是什么呢?
FILE是一个结构体,其内部封装了文件描述符 fd ,所以我们在C语言中使用FILE同样能访问文件,本质就是其内部封装了文件描述符。
所以,像stdin、stdout、strerr这些C语言定义的文件结构体,内部就封装了文件描述符fd,接下来我们就来验证一下,直接打印出FILE结构体内的文件描述符是多少。
结果如下:
3.5 重定向
我们已知道 stdout 的 fd 为 1,如果我们将本来要输出到显示器上的内容输出到了文件中,是不是就是输出重定向的原理?
重定向的本质,其实是在OS内部,更改fd对应的内容的指向!
接下来让我们看一段代码(关闭为什么使用fflush后面会详聊):
结果如下:
即:输入重定向的实现就是将原本从键盘输入的内容从文件中进行输入。
结果如下:
所以,追加重定向就是将open函数中的选项O_TRUNC(内容清空)改为O_APPEND(追加)。
效果如下:
虽说我们实现了重定向,但是,上面这种写法,并不是实现实现重定向的主流写法,操作系统早为我们提供了一个系统调用来实现 fd 的替换。
3.6 dup2 函数
这个函数的大概功能是:在文件描述符表,将 old 文件的 fd 拷贝到 new 文件的fd处,是将文件描述符表中 new 处拷贝为 old 的信息,即 new 和 old 都指向 old 指向的文件。
即,我们已经打开了log.txt,其文件描述符表中对应下标为3。我们想让下标为1处存放的结构体指针指向的不再是显示器,而是指向下标为3指向的文件。所以我们下标为3处的内容拷贝到下标为1数组中。即dup2(3,1).
- oldfd copy to newfd ---> 最后与oldfd一致
- 3 的内容拷贝到 1 ---> 最后与3一致
好的,接下来举例使用一下dup2接口(因为dup2是系统调用接口,不需再用fflush)。
效果如下:
好的,本篇博客就到此结束了,这里留了一个坑,为什么我们对文件描述符表进行替换时,什么时候使用fflush,什么时候不用使用呢?
因为篇幅过长,而且这个问题比较复杂,所以这个问题将在下篇博客进行详细讲解。但是这个问题不太影响本篇博客的思路与实现,大家放心观看。
这篇博客介绍的文件描述符以及文件操作在Linux和C语言中非常重要,也希望大家认真阅读。下期再见!