环境:Linux 2.6
在 Linux 中,有句话叫做「一切皆文件」,指的是在 Linux 中的设备,资源等几乎一切资源都抽象成了文件,然后只需要提供对文件进行操作的接口,就可以让我们用统一的方式来读取,写入等各种操作,从而来管理 Linux 中的各种资源和数据。这种设计模式不仅简化了 Linux 架构,还简化了开发人员对资源的操作
当进程打开一个文件的时候,操作系统会为其分配一个文件描述符(先了解,下文讲),当我们获取这个文件描述符之后,就可以对这个文件进行读写等各种操作了
并且这个文件被打开后,在操作系统内核中会为其创建一个结构体 struct file
来进行管理(这和 C 语言的 FILE
结构体不一样),它记录了文件的状态,读写位置等文件信息,它是在内核空间中的,而 C 语言的 struct FILE
是在用户空间中的
并且在 struct file
中存在一个指向缓冲区的指针,可以将数据暂存在缓冲区中,而文件通常会被划分成若干个页(4KB),在打开文件 / 读取文件的时候,操作系统会读取相应的块到该文件的缓冲区中
Linux
中,进程被描述成 task_struct
进行管理(PCB),而task_struct
中有个指针 struct files_struct* files
,该指针负责描述该进程的文件相关数据信息struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
...
struct files_struct *files; // 负责描述进程的文件信息数据
...
}
struct files_struct* files
指向的结构体中有一个成员 struct file* fd_array
,就是「文件描述符表」(新版本的内核版本可能称为 fdt
)fd_array[i]
就是 struct file*
,是指向内核中用来描述被打开文件的结构体注意:
综上所述,这里画个图:
⭐实际上,Linux 在操作文件时,大部分是通过文件描述符表中的下标来进行操作的,而这个下标就是文件描述符 fd
,这个很重要,比如 wrtie
,read
,send
等系统调用接口都是对 fd
进行操作的。
当进程打开一个文件的时候,在内核中,会创建这个文件对应的 struct file
,然后在文件描述符表中分配一个没有被使用下标 chosenIndex
,然后让 fd_array[chosenIndex]
中填入这个 struct file
的地址,再将这个下标 chosenIndex
返回给用户
fd
作操作Linux 中所有的进程都会默认打开 3 个文件,分别是 stdin
(标准输入),stdout
(标准输出),stderr
(标准错误),它们所占用的文件描述符分别是 0,1,2
一般情况下,标准输入就是键盘,标准输出和标准错误都是和显示器绑定,所以向标准输出和标准错误中输出,都会在显示屏上显示
标准输出一般是接收程序的正常打印结果,标准错误一般是接收程序的错误或者异常结果
这里可以试试,我直接往 1 号和 2 号文件描述符中写入数据:
可以看出,向 1 号和 2 号打印的数据都往显示屏上打印了,
所以我们在程序中,打开文件获取到的文件描述符通常是从 3 开始
先了解什么是重定向:是一种改变输入源或者输出目标的方式,允许就输入或输出从默认的位置转移到其他位置。还是晦涩的话,这么理解就好了:本来应该写到 xxx 中,现在写到了 yyy 中
举例子:echo
是向显示屏中打印数据:
我们现在使用 >
,它在 Linux
命令行中是输出重定向(类似的>>
是追加重定向)
现在我将上面那段输出 重定向 到文件中:
可以看出本来是打印到显示屏上的,现在写到了 hello.c
文件中,这也就是重定向了
首先我们先了解关于重定向的一个接口 dup2
这个接口要求传入两个文件描述符,这个函数的作用简单来说可以这么理解:
fd_array[newfd] = fd_array[oldfd]
,就是将文件描述符表中,oldfd
对应的内容拷贝给 newfd
对应的位置。也就是会有两个文件描述符指向同一个文件比如下面这个代码,我打开了一个文件,然后执行 dup2(fd, 1)
,那么文件描述符表发生了啥 o.0?
int main()
{
int fd = open("hello.c", O_RDONLY);
if (fd < 0) return 0;
dup2(fd, 1);
return 0;
}
如下图,
struct file
数据结构,然后为其分配文件描述符,并放到 fd_array
中管理,再返回文件描述符fd_array[1]
中的指针指向这个文件的 struct file
中
⭐关键点就是将 fd_array[newfd]
的指针改成成指定的 struct file
然后看看下面代码演示一下:重定向之后,原本写在 1 号文件(也就是显示屏)中的数据现在写到了文件 hello.c
文件中。
其实到这里,重定向的实现原理就七七八八了,我们下面再梳理一下应该就晓得了
dup2(fd, 1)
的时候,将文件描述符 fd
的内容拷贝给 1
号,这时候 fd
和 1
对应的指针都指向 hello.c
write / read
同样是对文件描述符作操作,通俗点,就是操作系统只会对 fd
下标操作,并不关心fd_array[fd]
里面是谁write(1, str, strlen(str))
的时候,操作系统就直接往 1 号下标对应的文件进行写入了,它并不关心 1 号下标的文件是不是stdout
(强调)hello.c
文件中」众所周知,电脑可以有很多外设,比如键盘,鼠标,声卡,音响,显示屏,网卡,磁盘…,而 Linux 怎么将这些看起来就离谱的硬件也抽象成文件的
这些外设,大都有自己的读操作和写操作,而 Linux 不应该专门为每个硬件设计相应的读写方法,甚至设计相应的数据结构,这样不便于维护和管理,而且很麻烦,如果统一起来就好了
还记得 struct file
结构体吗,前面说了这是一个操作系统内核中描述 / 管理文件的结构体,那么要怎么利用这个结构体来将底层硬件结合 / 统一起来?
可以在 struct file
中提供若干个函数指针,比如 ssize_t (*read)()
,ssize_t (*write)()
,然后这些函数指针直接指向驱动层提供的读写接口,指针不必关心自己到底指向了哪个函数,对于 Linux 来说,它只知道只要调用 struct file
中的 write
函数,就一定会执行对应驱动提供的写操作接口,从而以管理文件的方式,实现从底层硬件的控制