【TTY子系统】printf与printk深入驱动解析
tty子系统解析
tty子系统是一个庞大且复杂,也是内核维护者所头大的子系统。
At a first glance, the TTY layer wouldn’t seem like it should be all that challenging. It is, after all, just a simple char device which is charged with transferring byte-oriented data streams between two well-defined points. But the problem is harder than it looks. Much of the TTY code has roots in ancient hardware implementing the RS-232 standard - one of the loosest, most variable standards out there. TTY drivers also have to monitor the data stream and extract information from it; this duty can include S/Q flow control, parity checking, and detection of control characters. Control characters may turn into out-of-band information which must be communicated to user space; ^D may become an end-of-file when the application reads to the appropriate point in the data stream, while other characters map onto signals. So the TTY code has to deal with complex signal delivery as well - never a path to a simple code base. Echoing of data - possibly transforming it in the process - must be handled. With the addition of pseudo terminals (PTYs), the TTY code has also become a sort of interprocess communication mechanism, with all of the weird TTY semantics preserved. The TTY code also needs to support networking protocols like PPP without creating performance bottlenecks.
乍一看,TTY 层似乎并没有那么具有挑战性。毕竟,它只是一个简单的字符设备,负责在两个明确定义的点之间传输面向字节的数据流。但问题比看起来更难。大部分 TTY 代码都源于实现 RS-232 标准的古老硬件,这是最宽松、变化最多的标准之一。TTY 驱动程序还必须监视数据流并从中提取信息;该职责可以包括S/Q 流量控制、奇偶校验和控制字符检测。控制字符可能会变成带外信息,必须传送到用户空间;当应用程序读取到数据流中的适当点时,^D 可能会成为文件结尾,而其他字符映射到信号上。因此,TTY 代码也必须处理复杂的信号传递,而不是通往简单代码库的路径。必须处理数据的回显(可能会在过程中转换数据)。随着伪终端 (PTY) 的添加,TTY 代码也成为一种进程间通信机制,并保留了所有奇怪的 TTY 语义。TTY 代码还需要支持 PPP 等网络协议,而不会造成性能瓶颈。
迄今为止,tty子系统依旧是一个被开发人员称为臃肿的家伙。
printf与printk
printf和printk是我们日常编写代码时经常使用的函数。
那么printf和printk在代码上有什么区别,在哪里有了分叉点?这篇文章做一个简要说明。
这两个函数是经常用到的函数,闲暇之余,剖析下这两个函数的原理。这两个函数都是把字符串打印到终端上。其最终所要做的就是把存放在缓存区里的内容输出到串口。
printf
printf在glibc-2.38中的源码是:
int
__printf (const char *format, ...)
{va_list arg;int done;va_start (arg, format);done = __vfprintf_internal (stdout, format, arg, 0);va_end (arg);return done;
}
printf其本质就是通过write系统调用完成的。如果感兴趣可以用strace观察下。那么就从sys_write这个系统调用开始分析吧。该系统调用的定义位于fs/read_write.c
中:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
printf的系统调用具体是如何调用的暂时还没有弄清楚。后续了解后再对这部分进行补充。
光从这个调用流程来看,就足够复杂了。可以用户态要打印一个字符可真不容易。
大家一定都注意到:如果是在串口终端调用printf,会打印在串口终端上;在telnet终端调用printf,会打印在telnet终端上。我们在glibc库里看到的是向stdout写数据。
这里还要先说一个概念,控制终端(/dev/tty),这是个在应用程序中的一个概念,其实就是当前终端设备的一个链接。我们可以在当前终端下输入 tty 命令查看,例如在telnet终端下输入 tty ,会输出:/dev/pts/0,它代表当前终端设备。猜想在glibc库里有一个重定位过程,把stdout对到/dev/tty,然后进行sys_write,所以每次printf的输出都在当前的控制终端上。
至于为什么,请参见下面的博文,里面会讲系统调用的原理和swi异常处理。
好接着上面的vfs_write函数:
vfs_write
ret = file->f_op->write(file, buf, count, pos);
那么上面的这个write是谁?我们去看一下tty的初始化函数:
tty_init->cdev_init(&console_cdev, &console_fops);static const struct file_operations console_fops = {.write = redirected_tty_write...
}
redirected_tty_write
函数判断终端重定向(通过ioctl的TIOCCONS
控制字)。
redirected_tty_write->tty_write(file, buf, count, ppos);//看到这里的tty,它就代表我们现在运行的控制终端,从glibc库里传进来
struct tty_struct *tty = ((struct tty_file_private *)file->private_data)->tty; do_tty_write(ld->ops->write, tty, file, buf, count);
do_tty_write
里第一个参数write函数指针,其实就是struct tty_ldisc_ops tty_ldisc_N_TTY
中的n_tty_write
操作函数。
do_tty_write
通过copy_from_user(tty->write_buf, buf, size)
把要打印的字符拷贝到内核空间,再调用ld->ops->write
函数。(注册ldisc
是在console_init
函数中。tty_ldisc_begin
函数完成ldisc
的设置。)
n_tty_write ssize_t num = process_output_block(tty, b, nr);i = tty->ops->write(tty, buf, i);
这里tty->ops->write
指的是哪个呢,经追踪发现是serial_core.c
中uart_register_driver
在注册串口驱动时的uart_write
操作函数。
static const struct tty_operations uart_ops = { .open = uart_open, .close = uart_close, .write = uart_write,...
}uart_register_driver{struct tty_driver *normal;tty_set_operations(normal, &uart_ops);
}tty_open->tty_init_dev->alloc_tty_struct(struct tty_driver *driver, int idx){struct tty_struct *tty;tty->driver = driver; tty->ops = driver->ops;}
tty_open
是tty初始化时调用的,这里不做过多说明。
定位到uart_write
这个点之后,一下子就简单了很多:
serial_out(up, UART_IER, up->ier);//打开串口中断uart_write{struct circ_buf *circ;port = state->uart_port;circ = &state->xmit;while(1){c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);if (count < c)c = count;if (c <= 0)break;memcpy(circ->buf + circ->head, buf, c);circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);buf += c;count -= c;ret += c;}->__uart_start()->port->ops->start_tx();->serial_out(up, UART_IER, up->ier);//打开串口中断
简单整理以后,打印出信息所要经过的流程如下:
vfs_write
-> redirected_tty_write // tty_io.c:tty_init 中设置file->f_op->write指向该函数
-> tty_write // 关键在于调用 ret = do_tty_write(ld->ops->write, tty, file, buf, count);
-> n_tty_write
-> process_output_block
-> uart_write
-> uart_start
-> __uart_start
-> serial8250_start_tx
-> transmit_chars
为什么用户程序的打印如此复杂呢?内核在用户和硬件中间加了一个tty层以保证设备驱动可以专心处理和硬件相关的事。而不必考虑复杂的数据格式化。
printk
printk函数在kernel/printk.c
中,其把主要工作交给了vprintk
。vprintk
经过vscnprintf
把要打印的数据格式化后存放到printk_buf
缓存区中,然后通过emit_log_char
把要打印的数据放到__log_buf
里。emit_log_char
保证了__log_buf
不会下标越界——因为每次到了缓存区末又从头开始存放数据。代码中使用new_text_line
变量来判断当前字符是不是行首,因为内核在配置下可能会在行首打印时间或者当前打印的级别。
真正调用打印的函数在console_unlock
里面,在该函数里会执行call_console_drivers(_con_start, _log_end)
.接下来的调用流程是:
_call_console_drivers(start_print, end, msg_level);
-> __call_console_drivers(start, end);
-> for_each_console(con) { … con->write(con, &LOG_BUF(start), end - start); … }
con->write
其实就是serial8250_console_write
。这个函数所做的就是对硬件进行操作。
相关文章:

【TTY子系统】printf与printk深入驱动解析
tty子系统解析 tty子系统是一个庞大且复杂,也是内核维护者所头大的子系统。 At a first glance, the TTY layer wouldn’t seem like it should be all that challenging. It is, after all, just a simple char device which is charged with transferring byte-o…...

无涯教程-PHP - 全局变量函数
全局变量 与局部变量相反,可以在程序的任何部分访问全局变量。通过将关键字 GLOBAL 放置在应被识别为全局变量的前面,可以很方便地实现这一目标。 <?php$somevar15;function addit() {GLOBAL $somevar;$somevar;print "Somevar is $somevar";}addit(); ?> …...

shell脚本之循环语句
循环语句 循环含义 将某代码段重复运行多次,通常有进入循环的条件和退出循环的条件 for循环语句 一般知道循环次数使用for循环 第一类 格式1: for名称 in 取值次数;do;done; 格式2: for 名称 in {取值列表} do done# 打印20次 for i i…...

派森 #P122. 峰值查找
描述 给定一个长度为n的列表nums,请你找到峰值并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个所在位置即可。 (1)峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于; &…...

基础网络详解4--HTTP CookieSession 思考
一、cookie技术思考 一台多用户浏览器发起了三笔请求,将某款产品放入购物车中,A一次,选择了篮球;B两次,第一次选了足球,第二次选了钢笔。如何确认选择篮球、足球、钢笔的请求属于谁呢?如果不确认…...

14. 利用Canvas自制时钟组件
1. 说明 在自定义时钟组件时,使用到的基本控件主要是Canvas,在绘制相关元素时有两种方式:一种时在同一个canvas中绘制所有的部件元素,这样需要不断的对画笔和画布的属性进行保存和恢复,容易混乱;另一种就是…...

微信小程序使用云存储和Markdown开发页面
最近想在一个小程序里加入一个使用指南的页面,考虑到数据存储和减少页面的开发工作量,决定尝试在云存储里上传Markdown文件,微信小程序端负责解析和渲染。小程序端使用到一个库Towxml。 Towxml Towxml是一个可将HTML、Markdown转为微信小程…...

【C++】运算符重载 | 赋值运算符重载
Ⅰ. 运算符重载 引入 ❓什么叫运算符重载? 就是:运用函数,将现有的运算符重新定义,使其能满足各种自定义类型的运算。 回想一下,我们以前运算的对象是不是都是int、char这种内置类型? 那我们自定义的“…...
Python学习 -- 类对象从创建到常用函数
在Python编程中,类是一种强大的工具,用于创建具有共同属性和行为的对象。本篇博客将详细介绍Python中类和对象的创建,类的属性和方法,以及一些常用的类函数,通过丰富的代码例子来帮助读者深入理解。 一、类和对象的创…...

数组分割(2023省蓝桥杯)n种讨论 JAVA
目录 1、题目描述:2、前言:3、动态规划(bug):3、递归 剪枝(超时):4、数学(正解): 1、题目描述: 小蓝有一个长度为 N 的数组 A [A0, A1,…, AN−…...

很好的启用window10专业版系统自带的远程桌面
启用window10专业版系统自带的远程桌面 文章目录 启用window10专业版系统自带的远程桌面前言1.找到远程桌面的开关2. 找到“应用”项目3. 打开需要远程操作的电脑远程桌面功能 总结 前言 Windows操作系统作为应用最广泛的个人电脑操作系统,在我们身边几乎随处可见。…...

TCP定制协议,序列化和反序列化
目录 前言 1.理解协议 2.网络版本计算器 2.1设计思路 2.2接口设计 2.3代码实现: 2.4编译测试 总结 前言 在之前的文章中,我们说TCP是面向字节流的,但是可能对于面向字节流这个概念,其实并不理解的,今天我们要介…...

YOLOX在启智AI GPU/CPU平台部署笔记
文章目录 1. 概述2. 部署2.1 拉取YOLOX源码2.2 拉取模型文件yolox_s.pth2.3 安装依赖包2.4 安装yolox2.5 测试运行2.6 运行报错处理2.6.1 ImportError: libGL.so.1: cannot open shared object file: No such file or directory2.6.2 ImportError: libgthread-2.0.so.0: cannot…...

23种设计模式攻关
👍一、创建者模式 🔖1.1、单例模式 单例模式(Singleton Pattern),用于确保一个类只有一个实例,并提供全局访问点。 在某些情况下,我们需要确保一个类只能有一个实例,比如数据库连接…...

【jsthreeJS】入门three,并实现3D汽车展示厅,附带全码
首先放个最终效果图: 三维(3D)概念: 三维(3D)是一个描述物体在三个空间坐标轴上的位置和形态的概念。相比于二维(2D)只有长度和宽度的平面,三维增加了高度或深度这一维度…...

unity将结构体/列表与json字符串相互转化
编写Unity程序时,面对大量需要传输或者保存的数据时,为了避免编写重复的代码,故采用NewtonJson插件来将定义好的结构体以及列表等转为json字符串来进行保存和传输。 具体代码如下: using System; using System.IO; using Newtons…...

【Vue】vue2项目使用swiper轮播图2023年8月21日实战保姆级教程
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、npm 下载swiper二、使用步骤1.引入库声明变量2.编写页面3.执行js 总结 前言 swiper轮播图官网 参考文章,最好先看完他的介绍,再看…...

【算法日志】贪心算法刷题:单调递增数列,贪心算法总结(day32)
代码随想录刷题60Day 目录 前言 单调递增数列 贪心算法总结 前言 今天是贪心算法刷题的最后一天,今天本来是打算刷两道题,其中的一道hard题做了好久都没有做出来(主要思路错了)。然后再总结一下。 单调递增数列 int monotoneIncreasingDigits(int n…...

MATLAB算法实战应用案例精讲-【深度学习】模型压缩
目录 模型压缩概述 1. 为什么需要模型压缩 2. 模型压缩的基本方法 Patient-KD 1. Patient-KD 简介...

Matlab使用
Matlab使用 界面介绍 新建脚本:实际上就是新建一个新建后缀为.m的文件 新建编辑器:ctrlN 打开:打开最近文件,以找到最近写过的文件 点击路径,切换当前文件夹 预设:定制习惯用的界面 常见简单指令 ;…...

BladeX多数据源配置
启用多租户数据库隔离,会默认关闭mybatis-plus多数据源插件的启动,从而使用自定义的数据源识别 若不需要租户数据库隔离只需要字段隔离,而又需要用到多数据源的情况,需要前往LauncherService单独配置 数据源切换失败 详情请看说明…...

go里面关于超时的设计
设想一下你在接收源源不断的数据,如果有700ms没有收到,则认为是一个超时,需要做出处理。 逻辑上可以设计一个grouting,里面放一个通道,每收到一条数据进行相应处理。通道中夹杂一个timer定时器的处理,若通道在700ms内…...

Qt下使用ModbusTcp通信协议进行PLC线圈/保持寄存器的读写(32位有符号数)
文章目录 前言一、引入Modbus模块二、Modbus设备的连接三、各寄存器数据的读取四、各寄存器数据的写入五、示例完整代码总结 前言 本文主要讲述了使用Qt的Modbus模块来进行ModbusTcp的通信,实现对PLC的线圈寄存器和保持寄存器的读写,基于TCP/IP的Modbus…...

ElasticSearch学习2
1、索引的操作 1、创建索引 对ES的操作其实就是发送一个restful请求,kibana中在DevTools中进行ES操作 创建索引时需要注意ES的版本,不同版本的ES创建索引的语句略有差别,会导致失败 如下创建一个名为people的索引,settings&…...

3D角色展示
先看效果: 再看代码: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>3D卡片悬停</title><style>font-face {font-family: "Exoct";src: url("htt…...

前端面试:【Angular】打造强大Web应用的全栈框架
嗨,亲爱的Angular探险家!在前端开发的旅程中,有一个全栈框架,那就是Angular。Angular提供了模块化、组件化、依赖注入、路由和RxJS等特性,助力你构建强大、可扩展的Web应用。 1. 什么是Angular? Angular是…...

数据结构:栈和队列
文章目录 一、栈1.栈的概念及结构1.栈的概念及结构2.栈的实现 2.栈的顺序表实现1.栈的结构体和实现的功能函数2.栈的初始化,入栈和出栈操作3.栈的其他操作 3.栈的链表实现1.栈的结构体和实现的功能函数2.栈功能函数的实现 二、队列1.队列的概念及结构1.队列的概念及…...

SpringCloud Gateway服务网关的介绍与使用
目录 1、网关介绍2、SpringCloudGateway工作原理3、三大组件3.1 、Route(路由)3.2、断言 Predicate3.3、过滤器 filter 4、Gateway整合nacos的使用4.1 、引入依赖4.2、 编写基础类和启动类4.3、 编写基础配置和路由规则4.4 、测试结果 1、网关介绍 客户…...

深入解析:如何打造高效的直播视频美颜SDK
在当今数字化时代,视频直播已经成为人们交流、娱乐和信息传递的重要方式。然而,许多人在直播时都希望能够呈现出最佳的外观,这就需要高效的直播视频美颜技术。本文将深入解析如何打造高效的直播视频美颜SDK,以实现令人满意的视觉效…...

每日一博 - MPP(Massively Parallel Processing,大规模并行处理)架构
文章目录 概述优点缺点小结 概述 MPP(Massively Parallel Processing,大规模并行处理)架构是一种常见的数据库系统架构,主要用于提高数据处理性能。它通过将多个单机数据库节点组成一个集群,实现数据的并行处理。 在 …...