【Linux】文件描述符、文件操作、重定向的模拟实现

目录

一、重温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语言的文件操作呢?

答: 因为当我们编写程序访问磁盘上的文件,本质上的过程其实是这样的:

620e8b392d5e4878a24d366655dbbebe.png

本质上文件并不是我们访问的,而是进程访问的。因为当我们调用语言的文件操作,其内部封装了对应的系统调用,当程序运行起来时,就会去调用系统调用,从而访问文件。

所有的跨平台编程语言,内部的文件操作都是封装了对应系统的文件操作系统调用,所以我们先复习一下C语言的文件操作,然后再来复习对应的文件系统调用操作。

1.1 文件打开方式

看一下这三种常见的打开方式。8ecc067d7cd144a9b3e2c80462f72fe8.png

 当前路径:即一个进程运行起来时,其当前所处的工作路径。

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 );

接下来看一下使用示例:

855b30cfa3e74d7cb6673441ff3e3d90.png

 运行效果如下:

888bd01851144fe4995d8b5d44888b6c.png

 这里有一个问题:写文件时,要不要将'\0'进行写入?

不用,因为字符串结尾带上 '\0' 是C语言的字符串规定,而不是操作系统中文件的规定,所以我们不需要将'\0'写入,而 '\n' 可以写入,因为这可以使文本进行换行。

再重温一下如何使用shell脚本向文本文件中写入内容(重定向)

93442faa136d4b5f8da4a3db4a3bb985.png

 发现,之前写入的内容都被删除了,那我们可以这样操作来实现清空文本内容。

d867e2780e7d4d0cbd498bfcd6a0c6fa.png

1.3 文件读操作

重温了写操作后,接下来实现一下读操作。

代码如下,就是将打开方式从 w 变成 r 。

 f31e8f15ced8465eb5868215f59f9dc8.png

 结果如下:

e2375ab8b09f417e95fc32bea488729e.png

 有了读操作,其实我们就可以实现一下Linux中的 cat 命令。

实现方式就是将传入的命令行参数作为参数以读的方式打开,然后进行打印。

代码如下:

53ac0de824634b1a90e33fb24260051f.png

这样,cat命令就实现了。 效果如下:

9683116588b547d38e26c50f98af78a6.png

1.3 标准输入输出

不难发现,我上面使用了这样的代码

fprintf(stdout,"%s",line);

这其中的 stdout 是什么呢?

我们平时使用的 printf 函数要包含头文件<stdio>,其实 std 表示 standard(标准),而io则是读写。所以我们平时经常引入的该头文件就是用于我们进行文件读写的。而stdin、stdout、stderr是C语言默认打开的三个文件,称为标准输入输出流,其对应的是硬件设施。

后面会进行更详细的讲解。

  • stdin ---> 键盘
  • stdout ---> 显示器
  • stderr ---> 显示器

二、系统接口的使用

其实上面使用的fopen、fclose、fwrite、fgets都是调用的系统接口。

8630df4995504ff6876d3d2b9c6ab63a.png

2.1 open 函数

首先我们来看看 open 接口 

4dc5cb9e5bba4dd0a095a3b494f240cc.png

第一个参数是文件路径;

第二个参数是以为位图形式传入标志位作为其函数参数,一个 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;
}

让我们来观察结果:

9aee079681994ca9b6a79b8759292a8d.png

 有了上面位图的理解,接下来我们来看一下 open 函数第二个参数的选项。

Flags:

    O_RDONLY 只读打开         O_WRONLY 只写打开         O_RDWR  可读可写打开

    当我们附带了权限后,打开的文件就只能按照这种权限来操作。

    以上这三个常数中应当只指定一 个。下列常数是可选择的:     

  • O_CREAT--->如果目标文件不存在则创建文件,存在则无影响。
  • O_TRUNC---->清空内容
  • O_APPEND---->追加

fopen函数中的 a 和 w 打开方式的区别就在于一个清空(trunc),一个追加(append)。

返回值:
成功:新打开的 文件描述符
失败:-1

上面就是其中常用的一些选项,我们来尝试使用 open 函数以写的形式打开文件 log2.txt。

2db982cd25a646c6a96f198dcd401292.png

 结果运行如下:

7fe1f655f9284c7d9a5b66889839e2f4.png

为什么打开文件失败了呢?

因为 fopen 是C语言的文件访问操作,虽然它本质是调用的open函数,但是其内部封装了许多的选项,但是我们没有感知。所以,如果我们想模拟 fopen 函数中的只写的方式打开,应该再添加选项,即文件存在则创建文件的 O_CREAT.如下图:

369de5795a4c41ae97e9e6cae2aee2df.png

打开结果:

 a7aceb4a55ab4880a62f51c9076ce88e.png

 好的,文件就成功创建出来了,但是我们发现创建出的文件权限有点问题。

接下来我们看一下 open 的第三个参数——mode。 mode 是被创建文件的权限,我们可以对其进行设置。

e8cd992b6c2949ad850039fb3bfda787.png

 文件权限如下:

ea8ac490c0674957b76b11c243a0ece3.png

 发现文件的权限发生的变化,但是不是我们想要的0666,即rw-rw-rw-。因为系统中存在默认的umask,所以我们想在创建文件时,将umaks清空,这时我们可以调用系统接口 umask。

9e5dc6fc12cb42c286288727555ae814.png

 umask接口使用方式如下:

2a1afe3b244346e5a3092f4cfb640acc.png

但是,这还不够,在C语言中,w方式打开文件会进行删除文件原本的内容,而a方式打开文件会在文本的末尾进行写入。这两种方式的区别就在于我们还差一个选项没有加入:

a和w的区别就在于一个清空(O_TRUNC)原本内容,和一个进行追加(O_APPEND)内容。

2.2 close 函数

上面介绍了 open 函数,接下来就是 close 函数的介绍

0eabcf8ca6114153bc87c938d4809a87.png

使用非常简单,将 fd 传入即可。

c9f880eaa90e45648a8773e4936beca9.png

2.3 write 函数

介绍了打开、关闭文件,接下来就是 write 函数,其对应就是C语言中的fwirite、fprintf、fputs、fputc等等。

354097caa3e646be93872d4b5772bf1f.png

使用举例:

3512232be35b4cd38624045f7794a9b6.png

效果如下:

cde688ffd9064bcda28242a31987f93b.png

2.4 read 函数

接下来就是以读的方式打开,然后我们进行读文件。

6e437e7815f747f8956051f6d79ca199.png

参数:

  • 第一个参数为读入文件的fd。
  • 将读入的数据存放到 buf 中
  • count 则为期望多少个字符

返回值:

        实际读到的字符个数

使用举例如下:(因为read不会为读入的字符串末尾添加\0,所以我们使用memset先将字符串全初始化为'\0')

066609b529414d4bb8ba88d2b416063b.png

 效果如下:

d9e44a278e324cd5b62404a6172446fb.png

三、文件描述符

3.1 如何管理文件

在linux认为,一切皆文件,站在系统的角度,能够被input读取,或者能够被output写出的设备就叫做文件。

  • 侠义的文件:普通的磁盘文件
  • 广义的文件:显示器、键盘、网卡、声卡、显卡、磁盘,几乎所有的外设,都可以称之为文件

上面我们使用open、write、close、read,都传入了 fd( flie descriptor ) 这个参数,

通过上面的学习,我们知道了文件描述符就是一个小整数。

文件本质是被进程进行访问的,进程访问文件必须先打开文件,所以:

一个进程:打卡的文件= 1:n  ---> 如果多个进程都打开了自己的文件,则系统中会存在大量被打开的文件。所以,OS要将各个进程所打开的文件进行管理,创建struct file的对象,该对象中存放着文件的所有内容(不仅仅包含属性),如果存在多个被打开的文件,再使用双链表进行连接起来。

d77ace0f3813423cb6b98a58f8fe0a19.png

9234d2d71d3149b390c9eb4c87d49bcc.png

其中进程控制块 task_struct 是这样控制对应的文件

ffd3f899ef244a05913cbf529381f85b.png

3.2 0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0(stdin),标准输出1(stdout),标准错误2(stderr)。
  • 0,1,2对应的物理设备一般是:键盘、显示器、显示器。

所以我们其实可以直接从使用 fd 这个整数,在默认打开的文件 1 (屏幕)上进行输出。

ea2c2bc062e9492f9334b611b05881c0.png

 运行结果如下:

a1c84067908d4ca099dbe2bcc4eba6f2.png

3.3 文件描述符的分配规则

文件描述符的规则是有规律的,因为其默认会打开0、1、2三个文件。

其分配规则是优先最小正整数进行分配,我们来看代码:

47af98a9461044dbacc1c929478f7ea2.png

 运行结果:

13eb961fb192475da7259c1d2feee639.png

3.4 FILE

学习了文件描述符,那我们再想一下C语言中的 FILE 是什么呢?

FILE是一个结构体,其内部封装了文件描述符 fd ,所以我们在C语言中使用FILE同样能访问文件,本质就是其内部封装了文件描述符。

所以,像stdin、stdout、strerr这些C语言定义的文件结构体,内部就封装了文件描述符fd,接下来我们就来验证一下,直接打印出FILE结构体内的文件描述符是多少。

4d52454ac0be44f982e4a87e225b3bad.png

结果如下:

71699d30637542ac89b24803790f7134.png

3.5 重定向

我们已知道 stdout 的 fd 为 1,如果我们将本来要输出到显示器上的内容输出到了文件中,是不是就是输出重定向的原理?

重定向的本质,其实是在OS内部,更改fd对应的内容的指向!

d6aa17979b62433eb894a50f1894923f.png

接下来让我们看一段代码(关闭为什么使用fflush后面会详聊):

539de160a6a141b6a4a74c8a229700fa.png

结果如下:

b9a850c1474f43d1b75ab953dec14306.png

 即:输入重定向的实现就是将原本从键盘输入的内容从文件中进行输入。

73c2d5764844439cb37c88c865b2ac35.png

结果如下:

f7288be2df44458183068c825bc46227.png

 所以,追加重定向就是将open函数中的选项O_TRUNC(内容清空)改为O_APPEND(追加)。

fa33a286c484434b8eb49cbd7ee643bf.png

效果如下:

0a8a670bbae940f6ae90c1d5e8954452.png

虽说我们实现了重定向,但是,上面这种写法,并不是实现实现重定向的主流写法,操作系统早为我们提供了一个系统调用来实现 fd 的替换。

3.6 dup2 函数

bd91effe5d464ebbb92b9240ed1c0205.png

这个函数的大概功能是:在文件描述符表,将 old 文件的 fd 拷贝到 new 文件的fd处,是将文件描述符表中 new 处拷贝为 old 的信息,即 new 和 old 都指向 old 指向的文件。

113be769519c4f7d8b42d8ee25372200.png

即,我们已经打开了log.txt,其文件描述符表中对应下标为3。我们想让下标为1处存放的结构体指针指向的不再是显示器,而是指向下标为3指向的文件。所以我们下标为3处的内容拷贝到下标为1数组中。即dup2(3,1).

  • oldfd  copy to newfd ---> 最后与oldfd一致
  • 3  的内容拷贝到  1   ---> 最后与3一致

 92a8901a59d3448bacf0f77a005115e0.png

 好的,接下来举例使用一下dup2接口(因为dup2是系统调用接口,不需再用fflush)。

d3337a38ebfe4cdb91830aadda6b2e65.png

 效果如下:

cbda7fa23cb24a53931b75356c2cce0d.png

好的,本篇博客就到此结束了,这里留了一个坑,为什么我们对文件描述符表进行替换时,什么时候使用fflush,什么时候不用使用呢?

因为篇幅过长,而且这个问题比较复杂,所以这个问题将在下篇博客进行详细讲解。但是这个问题不太影响本篇博客的思路与实现,大家放心观看。

这篇博客介绍的文件描述符以及文件操作在Linux和C语言中非常重要,也希望大家认真阅读。下期再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Brant_zero2022

素材免费分享不求打赏,只求关注

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值