Linux 终端初始化 console_init 及 tty 驱动框架

系统 Linux
先前分析了 Linux 入口地址和 Linux 系统启动流程,本文详细分析一下 Linux 启动流程中的 console_init 终端初始化函数。

[[427998]]

先前分析了 Linux 入口地址和 Linux 系统启动流程,本文详细分析一下 Linux 启动流程中的 console_init 终端初始化函数。

上两篇文章如下:

Linux 内核入口分析

手把手教你分析 Linux 启动流程

讲解终端初始化之前我们先讲解一个概念:tty

在Linux系统中,终端是一类字符型设备,它包括多种类型,通常使用tty来简称各种类型的终端设备。我们一般分为三类:

串口终端(/dev/ttyS*)

串口终端是使用计算机串口连接的终端设备。Linux 把每个串行端口都看作是一个字符设备。这些串行端口所对应的设备名称是 /dev/ttySAC0;/dev/ttySAC1……

控制台终端(/dev/console)

在Linux系统中,计算机的输出设备通常被称为控制台终端(Console),这里特指printk信息输出到的设备。/dev/console是一个虚拟的设备,它需要映射到真正的tty(物理终端)上,比如通过内核启动参数” console=ttySAC0”就把console映射到了串口0。

虚拟终端(/dev/tty*)

当用户登录时,使用的是虚拟终端。使用Ctcl+Alt+[F1—F6]组合键时,我们就可以切换到tty1、tty2、tty3等上面去。tty1–tty6等称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名。

console_init 分析

Linux 启动函数 start_kernel 会调用 console_init 函数。

linux4.14/kernel/printk/printk.c

linux4.14/drivers/tty/n_tty.c

我们可以看到,console_init 主要做了两件事情:

1、n_tty_init 主要调用 tty_register_ldisc(N_TTY, &n_tty_ops) 注册 tty 线路规程。

  1. call = __con_initcall_start; 
  2.  while (call < __con_initcall_end) { 
  3.   (*call)(); 
  4.   call++; 
  5.  } 

这里主要是调用 __con_initcall_start 到 __con_initcall_end 之间的函数。

__con_initcall_start 和 __con_initcall_end 定义在:

linux4.14/include/asm-generic/vmlinux.lds.h

中间包含了 .con_initcall.init 段:

linux4.14/include/linux/init.h

我们通过 console_init 声明的驱动模块,就会出现在这个段中,被调用。普通我们声明的驱动模块都是使用 module_init,如果我们写的是串口驱动,可以使用console_init 声明。

如果要看具体中间有什么函数,可以查看编译 Linux 内核的输出 System.map 文件,这个文件记载了从头到尾 Linux 干了什么,具体的地址存储了什么东西。

System.map 文件默认在编译后的 Linux 内核根目录下, 当然我们也可以修改到其他目录。

这里会有三列:地址,区,函数名字。

如果后面我们使用 console_init(serial_5685_xxxx)去声明我们的驱动,那么这个 serial_5685_xxxx 就会出现在 __con_initcall_start 和 __con_initcall_end 之间,就会被调用。

initcall机制

注意上述流程,我们来理解一下 initcall 机制:

普通我们写一个程序,想要它被调用,需要在主流程中调用这个函数,才算被调用。

那么这种方式如果放在 Linux 中,是难以想象的,我们自己写的代码要在多少个地方声明。

而你如果采用initcall机制,意思就是说,你使用一个字符串声明你的驱动初始化函数,那么所有的驱动初始化函数都存在内存中一个连续的段中,系统启动以后,会从这个段的第一个函数开始,一个一个遍历,进而一个一个调用,这就是 initcall 机制。这就是为什么我们写驱动只需要使用 module_init 声明,编译进去即可自动被调用的原因!!!

System.map

编译后的内核根目录 System.map 文件记载了所有的驱动加载顺序,如果你不确定驱动的加载顺序,在这里查看就可以,每次编译 Linux 内核就会产生一个新的 System.map。

tty 驱动

我们不要把 tty 驱动和 串口驱动 弄混了,tty 驱动架构如下:

其中 tty driver 等价于我们普通写的驱动,可以自己写。

也就是说,在 tty 驱动框架主要有三层:tty core、tty line discipline、tty driver,另外最上层是用户空间,最下层是硬件。

tty core 称之为 tty 核心,主要作用是向用户提供统一的接口。

tty line discipline 称之为 tty 线路规程,主要从上下两层接收数据,并按照一定协议进行转换,比如 ppp 或者蓝牙协议,这样你的 tty 终端就不止可以用普通的串口,还可以通过其他协议访问到我们的系统。比如手机链接 PCB 板子的 WiFi 接入系统控制终端,输入 ls、cd 等命令。这一层并不是必须的,你可以直接使用驱动和 tty core 进行通信,但一般这一层都会有。

tty driver 就是我们常说的串口驱动。

在 console_init 函数中,它做的两件事,就是注册 tty 线路规程,注册 tty 驱动,tty 核心是包含在内核当中的。tty 线路规程和 tty 驱动可以有很多个。

有的人会有疑问,为什么有了 tty 驱动了,还会有一个 tty 线路规程。得益于 Linux 模块化的思想,这里主要是为了分层与隔离。tty 驱动只和硬件相关,只解析基本的硬件信息,把硬件信息转换成字符。所有的对字符的进一步处理包括加入蓝牙协议传输,监控数据等都放在 tty 线路规程当中。这样 tty 驱动是可以完美复用和移植的。

分享一张彭大佬的图,本文我只讲了概念,彭大佬讲解过 tty 源码:

这里只需要注意一点,在右下角,tty driver 是没有 read 函数的,tty driver 层有 buffer,输入的数据会存储在 buffer 中,被读取。

原因很简单,对于 tty 来说,输入设备和输出设备不是同一个设备,输入设备是键盘,输出设备是屏幕,这和普通的 IIC、SPI 驱动同一个设备不一样。因此在设计上 tty driver 没有 read 函数。

本文转载自微信公众号「嵌入式Linux系统开发」

 

责任编辑:姜华 来源: 嵌入式Linux系统开发
相关推荐

2021-02-05 12:04:45

LinuxUARTLinux系统

2020-11-13 10:06:47

XignCode3

2011-03-31 16:46:10

LinuxMySQL

2023-11-12 23:08:17

C++初始化

2012-12-28 14:53:34

Android开发初始化窗体事件

2010-02-24 15:41:19

Linux Light

2022-01-06 16:16:21

鸿蒙HarmonyOS应用

2011-06-17 15:29:44

C#对象初始化器集合初始化器

2010-06-21 10:25:09

Linux APM

2011-07-06 14:16:25

Linux服务器Shell脚本

2010-07-28 10:22:33

FlexApplica

2022-07-06 10:37:45

SpringServlet初始化

2021-03-12 10:30:11

SpringMVC流程初始化

2020-12-03 09:50:52

容器IoC流程

2019-11-04 13:50:36

Java数组编程语言

2009-09-08 09:48:34

LINQ初始化数组

2009-11-11 15:29:15

ADO初始化

2024-01-15 06:34:09

Gin镜像容器

2017-09-13 14:28:02

Linux初始化系统运行级别

2012-03-13 13:38:42

Java
点赞
收藏

51CTO技术栈公众号