【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.cuart_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中,其把主要工作交给了vprintkvprintk经过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。这个函数所做的就是对硬件进行操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
嵌入式系统的tty设备驱动框架如下: 1. tty设备驱动的注册 在Linux内核中,tty设备驱动的注册是通过调用tty_register_driver()函数来实现的。该函数会将tty_driver结构体中的成员变量进行初始化,并将其加入到tty_drivers链表中。 2. tty设备的打开 当用户空间应用程序调用open()函数打开tty设备时,内核会调用tty_driver结构体中的open()函数。在open()函数中,会创建tty_struct结构体,并将其加入到tty_driver结构体中的tty_structs链表中。同时,还会调用tty_driver结构体中的start()函数,该函数会启动tty设备的数据传输。 3. tty设备的读写 当用户空间应用程序调用read()或write()函数读写tty设备时,内核会调用tty_driver结构体中的read()或write()函数。在read()函数中,会从tty_struct结构体中读取数据并返回给用户空间应用程序。在write()函数中,会将用户空间应用程序传递的数据写入到tty_struct结构体中。 4. tty设备的关闭 当用户空间应用程序调用close()函数关闭tty设备时,内核会调用tty_driver结构体中的close()函数。在close()函数中,会释放tty_struct结构体,并将其从tty_driver结构体中的tty_structs链表中移除。同时,还会调用tty_driver结构体中的stop()函数,该函数会停止tty设备的数据传输。 5. tty设备驱动的注销 当不再需要使用tty设备驱动时,可以通过调用tty_unregister_driver()函数将其从tty_drivers链表中移除,并释放相应的资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Van.Ghylivan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值