【Linux系统编程】--基础IO和fd重定向

一、回顾C/C++文件操作

1、基础概念

前面我们在对语言的学习的时候,也对文件的操作的接口进行了学习,但是那会我们就是学习了其使用,并没有对其进行过多的深究。所以对于文件的理解,我们可能就是打开文件,对文件写入的操作。

首先我们前面也提到:文件=文件内容+文件属性(原数据)

那么我们对文件进行操作,无非就是对文件的 内容或者文件的属性进行操作!!!

然后我们要对一个文件进行操作,那么得先打开这个文件,那么我们得先找到这个文件,那么就是路径+文件名。

不过有的时候,我们不需要对应文件的路径。

这又是为啥呢?

首先我们明确一个,我们对于文件的操作,是谁来进行操作的呢?

我们写好一个对于文件操作的程序后,我们没有进行运行的话,那么这个文件是不会被操作的,那么本质就是要有一个进程运行,然后这个文件才被操作的,那么也就是说,对于文件的操作,实际上是进程进行的。

那么,前面我们对于进程基础概念的学习中,讲到我们进程信息中,其会有一个CWD,那么是记录我们这个进程的工作路径的,那么如果我们对于文件的操作,没有传入路径,那么其会在当前进程的路径下寻找这个文件。

然后我们也可以修改这个cwd,使进程在指定的路径下工作。使用chdir。

我们打开文件,实际上是做啥?

实际上是将我们的文件冲磁盘加载到内存中,这样我们的进程才可以访问到我们的文件。

所以对文件进行操作的本质:进程通过CPU访问内存中文件。

在我们的操作系统中,存在着大量的文件,从文件的存在方式来看,我们的文件分为两种:

1、磁盘文件

2、内存级被打开的文件

那么在我们操作系统中,是可以存在很多文件同时被打开的,那么我们就要对其进行管理。

那么在我们的操作系统中,肯定会存在一个结构体变量,对其进行管理的。

2、接口回顾

首先是对于文件的打开,我们使用fopen接口。

可以看到,其有两个参数,第一个参数是我们要打开的文件,其传入可以带路径,也可以不带路径。

第二个是我们的打开方式:

|----|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 模式 | 作用 | 核心行为总结 |
| r | 以只读模式打开文本文件,文件流指针置于文件开头 | 1. 文件必须存在,不存在直接打开失败 2. 仅允许读,不可写 |
| r+ | 以读写模式打开,文件流指针置于文件开头 | 1. 文件必须存在 2. 可读可写;写入会覆盖原有内容 |
| w | 若文件存在则截断清空长度为 0;不存在则新建只写文本文件,指针在开头 | 1. 文件存在:直接清空原有全部内容 2. 文件不存在:自动创建 3. 仅能写入,不能读取 |
| w+ | 以读写模式打开;文件不存在则创建,存在则清空截断,指针置于文件开头 | 1. 兼容读写 2. 打开瞬间直接清空原文件数据,风险高 |
| a | 追加写入模式(数据永远写在文件末尾);不存在则创建,打开后指针在文件末尾 | 1. 只能写,不能读 2. 无论怎么移动读写指针,写入操作强制追加到文件尾部,不会覆盖原有内容 |
| a+ | 可读 + 追加写入;文件不存在则创建,所有输出强制追加到文件末尾 | 1. 写操作永远在文件末尾 ,不受文件偏移量影响2. 读指针初始位置系统存在差异: ・glibc (Linux):读指针默认在文件开头 ・macOS/BSD/Android:读指针默认在文件末尾3. POSIX 标准未规定a+初始读位置,跨平台需手动rewind()/fseek()调整读指针 |

如上就是fopen接口的使用。

然后我们还有下面几个接口,其可从文件的指定位置开始读和写入:

我们知道,对于文件,其内容实际上就是一堆字符集合。

那么我们对于文件的设计,设计上就是一个字符数组,那么我们的读写位置,本质不就是一个数组下标嘛!!!

二、Linux下对文件的操作

1、open文件打开操作

前面提到的对于文件的操作,都是基于语言层面上的操作,那么在我们的操作系统中,也提供了一系列的系统调用接口。

如下:

其返回值是一个整数。

这个系统调用是用于文件的打开的,其有两种,一种是两个参数的,一种是三个参数的。

第一个参数pathname就是我们要打开的文件了。

然后第二个参数就是我们要如何打开这个文件,但是其传参和我们语言中的fopen是不一样的。

其有如下几种选择:

首先是上面三个最基础的选择,其三个是必选的,而且是只能选择一个,但是我们发现,第二个参数实际上是一个int类型的参数,那么其实际上就是一个宏。

那么对于文件已经存在,或者文件不存在是否进行创建等,如何处理呢?

那么其还有下面的选项:

O_CREAT

文件不存在则创建;存在则正常打开,不修改原有内容。 使用此标志时,必须传第三个参数 mode。

O_EXCL

必须搭配 O_CREAT:O_CREAT | O_EXCL 作用:文件已存在时直接报错,防止覆盖旧文件(原子创建)。

O_TRUNC

打开时清空文件全部内容;只对可写模式生效。 等价 fopen "w"。

但是,我们就一个形参,要如何传入两个实参呢?

其实际上是运用了我们的位运算,通过比特位来判断,首先上面的选项,其都是一个整型,而且其只有一个比特位是1,其余都是0。

那么其进行位运算的结果,肯定是会多一个比特位为1的。所以通过这种方式,达到了我们可以一个形参,实现多个实参的方式。

然后我们要是想实现追加的方式对文件进行写入,那么还可以使用

O_APPEND

每次 write 都自动跳到文件末尾,多进程写文件不会交叉错乱,等价 fopen "a"

下面是几个选项的使用示例:

那么其还有一个参数,是干嘛的呢?

其是我们在创建文件的时候,要对文件的权限进行设置。

其是一个八进制的整型。

不过要注意的是,我们的操作系统中,还存在一个权限掩码umask。

大部分系统普通用户为002

那么实际上创建文件的权限=传入的mode-umask。

我们还可以在命令行中对umask进行设置。不过这个值只对当前的shell有效。

在我们的C语言程序中,我们也可以调用umask函数对其进行设置。

示例如下:

然后其可以配合我们的open系统调用使用:

如上就是对于文件打开的操作。

那么open和C语言中的库函数fopen有啥区别呢?

实际就是上下级的关系,C语言中通过的库函数,实际上是对我们的open进行了封装。

其函数内部实际还是调用的open系统调用。

2、open的返回值

我们前面提到,open系统调用的返回值是一个整数,不是一个指针类型。

其实际上是一个文件描述符,在操作系统中,会对我们在内存中的文件进行管理,那么会对这些文件进行分配文件描述符。

后续对于已经在内存中的文件,就可以直接通过文件描述符找到它。

要注意的是,我们的文件描述符,是从3开始分配的,这是因为,我们的0、1、2三个文件描述符一开始就被使用了。

其分别表示:

0->标准输入(stdio)

1->标准输出(stdout)

2->标准错误(stderr)

后续每次打开新的文件,都是返回最小的没有被使用的fd。

那么我们可以不使用printf在显示器上打印内容,就是使用write,对文件写内容的系统调用,然后呢我们传入fd为1,那么就可以在显示器上输出内容了。

三、文件描述符

前面我们在对open的使用时,提到其返回值fd。这是文件描述符

下面我们来对其进行深入了解。

1、文件描述符本质

前面我们提到,对于文件的操作,实际上是进程和文件之间的关系。

我们要打开一个文件,也是进程要打开,那么我们的进程task_struct中,会有一个对于文件管理的结构体成员->*file指针,其指向的是一个struct file结构体。

然后,我们知道的是,我们一个进程未来,是会打开多个文件的,所以进程和要打开的文件之间的比例为1:n。

那么我们就需要将其使用一个数据结构进行管理。

我们的struct files_struct中,存在一个数组:*fd_array。其就是我们的文件描述符表,那么也就是说我们的进程task_struct都会存在一个文件描述符表。那么这个表,存储的是一个一个的指针,那么这个指针指向的就是我们这个进程中打开的文件,所以fd的本质就是数组下标。

然后我们也知道,一个文件也有其对应的struct file。文件描述表中的指针就是指向的文件的file结构体。

所以,我们前面使用write等文件操作系统调用,通过fd来查找对应的文件的,其本质就是通过struct files_struct中的文件描述符表struct file*fd_array的下标,查找到对应文件的地址,然后就可以对这个文件进行操作了。

2、系统调用和语言库函数

前面我们提到,C语言库函数中,对于文件的操作,其内部实际还是调用的我们的系统调用,那么其为啥不直接去调用我们的系统调用呢?

这是因为我们的操作系统是有多种的,而我们的语言是要在不同的平台下,都可以被使用的,这是因为其要被更多的人使用,所以必须具备很好的跨平台性。

四、fd重定向

1、理解重定向

我们在学习Linux指令的时候,肯定使用过>>,等这些符号,然后向文件中写入内容。

实际上,其就是利用文件系统中,fd对于标准输入,标准输出的规则。

我们在程序中,可以通过修改fd:1的指向,然后使得我们使用printf函数要在显示器上打印的内容,被输出到文件中,然后也可以通过修改fd:0的指向,从文件中获取输入等。

可以看到,我们先将fd=1的文件描述符指向的文件关闭了。

然后我们编译运行后,发现后面的printf并没有将这个打开的文件的fd打印到显示器,但是我们将文件打开,查看其内容可以发现,我们代码中要打印的内容都在这个文件中。

所以,我们还可以确定一个事情,就是在语言层面,printf这个函数,其只认stdout->1,其并不知道我们的fd:1的指向已经修改。对于这个输出,其只会去找fd=1指向的文件。

然后对于标准输入也是如此,我们在使用sacnf函数的时候,其找输入的时候,也是找的fd:0指向的文件代码如下:

可以看到我们将文件中的内容给获取到了。

我们还有一个系统调用:dup2,那么其就可以用来修改我们的fd的指向,那么其具体是如何的呢?

其函数原型如下:

oldfd:是我们打开的文件的fd

newfd:是我们想将我们要打开的文件的fd修改到的fd。

就比如我们上面,我们想实现输入重定向,那么就要将fd:0的指向我们打开的fd。

所以我们的传参方式:dup2(fd,0);

所以通过上面的示例,我们发现,重定向的本质就是修改数组下标指向的内容。

五、理解Linux下一切皆文件

前面我们刚刚入门Linux的时候,就将到Linux下一切皆文件,那么上面其实我们已经感受到了。

在Linux下,其将一切可以交互,可以利用的资源,都通过一套文件接口进行交互。

在我们的硬件层上,其都有一个结构体进行管理,对于硬件的输入输出,在文件描述结构体中,其含有两个函数指针,那么其分别是*read,和*wriet,那么当我们硬件有这个需求的时候,就会到对应硬件的方法中去获取。

然后对于硬件,其打开,那么在进程中也会存在其对应的struct file结构体,也会有对应的fd指向其file。

所以前面我们看到,通过fd,我们就可以对显示器进行输出,还有键盘的输入等。

所以,在我们的进程角度来看,那么一切资源都是文件,那么进程的角度,就是用户的角度,所以从我们用户的角度来看,Linux下的一切操作,都是对文件进行操作。

Linux 将硬件、磁盘数据、进程通信、网络、内核信息全部封装成文件,提供统一的读写接口;所有资源都可以通过文件描述符操作,这就是一切皆文件的核心思想。

其实,这也就是我们的多态,我们的操作系统内核中,其是基类,我们的各种硬件,在其内部都是一样的。

然后我们各种硬件,就可以再实现我们自己的方法。

每个硬件都有其各自的读写操作,但是我们用户,只需要使用操作系统提供的那一套操作,就可以对我们的硬件进行使用。