要想理解网络就得理解Io模型要想理解Io模型就得理解流。
那什么是流。我的思路是从文件描述符还是聊起。
为什么对一个变量操作就可以实现真实的文件的读取和写入呢?
这得回到Liunx中的源码寻找线索
我们看这里的liunx源码。文件像一种流。说明流是文件的上层的概念‘
即
/* File is stream-like */
#define FMODE_STREAM
题主是第一次是看Liunx的源码。不禁感慨
光是看注释就有很多值得研究和思考的点了。
什么叫期望随机访问模式。
什么叫文件打开在这个什么O_PATH 路径,然后什么都做不了?为什么做不了?
什么叫文件的原子的访问?文件已经是进程之下的基础建设了。这里的原子指的什么样的概念。
回到起点,为什么讨论流就要聊到文件呢?
fs.h 和fstable.h分别是什么东西?
fs.h 是liunx 内核源码中记录文件系统的主接口
fstable.h 则侧重于文件系统类型注册与挂载表的实现和维护。
但准确来说这两个文件也仅仅停留在用户态的描述。
既然我们已经知道了流应该是文件更抽象的概念。我们就得去翻翻应该怎么描述我们的流。
上网查了下,这边给出来的判断是通过一个seek 的行为去区分的。
seek 是英文 “寻找(search/seek)” 的意思,在文件操作中,它表示:
修改当前读写位置(file position)。
什么叫做修改当前读写位置呢?
注意出现在代码中的这个 FMODE_STREAM的标志位
然后我们上网可以看到这个标志位的代码
而设置了 FMODE_STREAM 的文件具有以下行为特征:
- 不可寻址:不能使用 lseek() 等函数改变文件的读写位置。
- 顺序读写:数据只能按顺序读取或写入,不能随机访问。
- 实时性:数据通常是实时产生或消费的,如串口接收的数据流。
这些行为与传统的磁盘文件不同,磁盘文件支持随机访问,可以使用 lseek() 在文件中跳转到任意位置。
而要弄明白这个文件标志位的作用就得研究为什么会有这样的设计。这里的原因还得再tty在寻找。
探索tty?
揭开 TTY 的神秘面纱 (linusakesson.net)
我找到了一篇文章详细介绍了tty 的历史
所以流的概念本质上是与"行编辑" 和“会话管理”,“定向”,“上下文”, 这些行为相关的。
而其中行编辑还和一种叫行规程这样的东西扯在一块。不知道从哪里找到的一篇博客主。这个东西写的有点厉害
Linux终端和Line discipline图解-CSDN博客
CSDN博客+3Shall We Code?+3CSDN博客+3
总的来说还是看这几位大佬的博客中的图
Line Discipline 是 TTY 子系统中最复杂的部分,主要负责以下功能:
- 行缓冲(Line Buffering):将用户输入的字符进行缓冲,直到用户按下回车键,才将整行数据提交给上层应用程序。
- 行编辑(Line Editing):支持用户在输入过程中进行编辑操作,如退格(Backspace)、删除(Delete)、清除整行(Ctrl+U)等。
- 回显(Echo):将用户输入的字符实时显示在终端上,提供即时反馈。
- 信号处理(Signal Handling):识别特殊的控制字符,并向前台进程发送相应的信号,如 Ctrl+C 发送 SIGINT,Ctrl+Z 发送 SIGTSTP。
- 会话管理(Session Management):维护前台和后台进程的关系,确保只有前台进程可以从终端读取输入。
这些功能主要由 drivers/tty/n_tty.c 文件中的 n_tty_ldisc_ops 结构体实现,该结构体定义了一系列操作函数,用于处理上述功能。
我们可以初步理解为流本质上是一种内核维护的人机交互接口
但是我们不禁问道如果要理解Liunx 做了什么。其他部分的硬件做了什么。这里就需要先了解一个叫行规程的东西。
因为源码中有高度的抽象。
比如这个路径下你会看到大量的什么加锁啊。线路引用啊。释放这些名词。并没有涉及到我们核心的读字节解释自己的部分。
/ drivers / tty / tty_ldisc.c
我们会先找到它的下游是一个叫n_tty_c
然后你会发现这里有说这个原先是写到io.c的然后太多了分离出来的
所以我们直接看
/drivers/tty/tty_io.c
在这个路径下
我们就会发现流的写入行为和内核缓存区和设备标识符相关
缓存区
神奇的是这里会有文件的写入
所以通过源码。我们可以判断。Linux 中“文件”(“文件描述符”)和“流”的行为之所以一致,是因为 Linux 内核在驱动层(tty)实现流行为的时候已经关联了文件结构体。使得上层看来流和文件的对象几乎是一致的。
if (tty_line >= 0 && tty_line < p->num && p->ops &&
p->ops->poll_init && !p->ops->poll_init(p, tty_line, stp)) {
res = tty_driver_kref_get(p);
e = tty_line;
break;
}
重点看这段代码。也就说内核程序去读驱动程序。然后驱动程序中有一个poll的部分。我上网搜了一下这个叫轮询。也就说所谓的驱动就是一套独立的异步的可调用的运行时部分代码。
然后我们可以看到一个数据结构tty-line 。 我上网搜了一下叫做设备行(线)玩过单片机都知道
在硬件和协议设计中,一条通信线路(line)通常对应一个终端设备,于是“行号”就成了对具体设备的简称。
同样的我们可以在相同的文件下找到注册的逻辑。
我这边的想法是直接看下串口部分的设备的行为代码。我想这样的代码的关联肯定涉及到注册的部分就可以论证我的猜想即这种读写化连接的行为应该是一体的。
然后我就看到一个有点陌生有熟悉的概念
uart?通信协议
然后我发现还套了一层就是tty下面还套一层协议层uart。也就是说。tty本质上是对硬件协议的封装和管理
看向这段就涉及到了所谓的内存页的分配和什么环形缓存区的分配
这段代码很关键。我一开始以为这里只是个初始化相关的方法就没有太留意看了下全文这里几乎是唯一一处声明了和寄存器相关的方法。后续的都是这个指针和缓存区的交换操作。
uart_change_line_settings() 是驱动框架调用具体串口硬件驱动设置寄存器的唯一路径,其核心调用是:
uport->ops->set_termios(uport, termios, old_termios);
termios上网查了一下这个结构体好像是用来描述串口的。这个步骤通过这个结构体写入了波特率这样的一些物理参数。
准确来说 serial_core.c 实现了一个uart协议管理者所有的串口设备。然后不是串口设备直接与寄存器这些东西打交道的而是借助这个核心去间接打交道。
这里有一段具体的串口注册和初始化的行为吧
我们同样在核心core 找到 了注册函数
normal = tty_alloc_driver(drv->nr, TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV);
重点是这句。tty_driver 是在这里注册的。
然后注意这里有条注释说明注册完tty_driver 之后才会调用port()
我们找到了一个比较典型的串口注册来证明我们的猜想
追过来再追过去
再追一层
回到了核心文件并且看到了那个所谓的one_port方法
我们不妨在大胆追一层
我们发现这里也有tty。
这里发生了一个奇怪的现象如果按照上面的逻辑似乎是先进行tty-dirver的注册再到port()注册的调用
但是从 serial8250_register_8250_port()这里往上追uart_add_one_port到 serial_ctrl_register_port到serial_core_register_port 好像又变成了从先注册port()再到tty()
但是我们别忘了。我们从串口侧追上来本身就是一个逆向的过程。也就是说这段引用本质上是回调。
我们目前从代码中得到的流程信息是。tty做波特率初始化的时候就已经存在tty_dirver了。然后我们的目标是找到最终的那个tty_line的注册。
好吧哥们这边代码探究能力实在有限。
不过我们这边起码证实了在uart层完成了具体设备的注册以及和tty的关联。然后tty关联到流和文件。大致就是这么一个思路。所以流的有效性和可靠性是依赖于tty的tty和设备中间的行为又是依赖于uart的也就是说本质上是uart协议保证了流的有效性。
所以总的协议栈就是。tty(行规程--SLIP) uart(协议)
那么还没有解决的问题就是我们知道文件在tty层发生类似于流这样的概念。但是我们不知道文件是如何脱离流或者说是描述tty的。这样才能够支持更加上层的设计。
接下来还是直接看答案怎么调用吧。