前言
printf函数,对于C程序员来说,熟悉得不能再熟悉,基础得不能再基础了。不管是什么版本的C语言教材,按照惯例,第一个示例必然是用printf打印一串hello world。
我当时学C语言的时候,也只是依葫芦画瓢把示例代码输入电脑,按步骤编译、运行,屏幕上果然出现了hello world。当时也没有去思考程序是如何将hello world显示到屏幕上的,按当时的水平,也想不出个所以然。
前两天偶然刷到一个视频:《linux嵌入式开发:字符设备驱动》,里面讲到了linux下printf打印字符到屏幕的整个流程,视频截图如下:
作者讲的整个流程大致如下:
- printf
- write
- sys_write
- LCD_write
对于该流程我实在不敢苟同,视频作者应该不了解显示驱动。对于一个显示驱动或者LCD驱动来说,最重要的就是调用register_framebuffer注册一个fb驱动,fb驱动中最重要的就是实现一套struct fb_ops接口。
而这套接口中,根本就没有write函数,也没有直接输出字符的函数,有的只是针对屏幕像素、光标进行操作的接口函数:
struct fb_ops { ... /* Draws a rectangle */ void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect); /* Copy data from area to another */ void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region); /* Draws a image to the display */ void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image); /* Draws cursor */ int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor); ...}
视频作者在不清楚显示驱动的情况下,有点猜测和想当然了,这也勾起了我想重新梳理整个流程的兴趣。
printf流程
整个调用流程如下图所示:
1.调用printf打印字符串:printf(string);
2.write(1, string, len);将字符串写入标准输出stdout,在linux字符模式控制台下,标准输出指向/dev/tty[n],其中n为当前虚拟控制台(在tty章节中,也称它为虚拟终端VT)的编号。
3.sys_write,该函数位于内核空间,是系统调用入口。
4.vfs_write,调用file->f_op->write,f_op接口在open设备文件时安装,实际指向tty_write。
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos){ if (file->f_op->write) return file->f_op->write(file, p, count, pos); else if (file->f_op->write_iter) return new_sync_write(file, p, count, pos); else return -EINVAL;}static const struct file_operations tty_fops = { .llseek = no_llseek, .read = tty_read, .write = tty_write, .poll = tty_poll, .unlocked_ioctl = tty_ioctl, .compat_ioctl = tty_compat_ioctl, .open = tty_open, .release = tty_release, .fasync = tty_fasync,};
5.tty_write,获取tty的线路规程ld,并调用ld->ops->write,实际指向n_tty_write。
static struct tty_ldisc_ops n_tty_ops = { .magic = TTY_LDISC_MAGIC, .name = "n_tty", .open = n_tty_open, .close = n_tty_close, .flush_buffer = n_tty_flush_buffer, .read = n_tty_read, .write = n_tty_write, .ioctl = n_tty_ioctl, .set_termios = n_tty_set_termios, .poll = n_tty_poll, .receive_buf = n_tty_receive_buf, .write_wakeup = n_tty_write_wakeup, .receive_buf2 = n_tty_receive_buf2,};
6.n_tty_write,调用tty->ops->write,实际指向con_write。
static const struct tty_operations con_ops = { .install = con_install, .open = con_open, .close = con_close, .write = con_write, .write_room = con_write_room, .put_char = con_put_char, .flush_chars = con_flush_chars, .chars_in_buffer = con_chars_in_buffer, .ioctl = vt_ioctl,#ifdef CONFIG_COMPAT .compat_ioctl = vt_compat_ioctl,#endif .stop = con_stop, .start = con_start, .throttle = con_throttle, .unthrottle = con_unthrottle, .resize = vt_resize, .shutdown = con_shutdown};
7.con_write,调用vc->vc_sw->con_putcs,实际指向fbcon_putcs。
static const struct consw fb_con = { .owner = THIS_MODULE, .con_startup = fbcon_startup, .con_init = fbcon_init, .con_deinit = fbcon_deinit, .con_clear = fbcon_clear, .con_putc = fbcon_putc, .con_putcs = fbcon_putcs, .con_cursor = fbcon_cursor, .con_scroll = fbcon_scroll, .con_switch = fbcon_switch, .con_blank = fbcon_blank, .con_font_set = fbcon_set_font, .con_font_get = fbcon_get_font, .con_font_default = fbcon_set_def_font, .con_font_copy = fbcon_copy_font, .con_set_palette = fbcon_set_palette, .con_scrolldelta = fbcon_scrolldelta, .con_set_origin = fbcon_set_origin, .con_invert_region = fbcon_invert_region, .con_screen_pos = fbcon_screen_pos, .con_getxy = fbcon_getxy, .con_resize = fbcon_resize, .con_debug_enter = fbcon_debug_enter, .con_debug_leave = fbcon_debug_leave,};
8.fbcon_putcs,调用fbcon_ops->putcs,实际指向bit_putcs。
void fbcon_set_bitops(struct fbcon_ops *ops){ ops->bmove = bit_bmove; ops->clear = bit_clear; ops->putcs = bit_putcs; ops->clear_margins = bit_clear_margins; ops->cursor = bit_cursor; ops->update_start = bit_update_start; ops->rotate_font = NULL; if (ops->rotate) fbcon_set_rotate(ops);}
9.bit_putcs,该函数完成字符串的光栅化操作,并调用fbops->fb_imageblit将结果复制到屏幕上,fb_imageblit实际指向显示驱动提供的回调函数。
static struct fb_ops foo_fb_ops = { .owner = THIS_MODULE, .fb_imageblit = foo_imageblit,};
10.foo_imageblit,将光栅化之后的图像从系统内存复制到显示帧存指定位置,最终完成上屏显示操作。
一个简单的printf函数,为了输出字符到屏幕,从上到下经历了:libc -> vfs -> tty -> line discipline -> vt -> fbcon -> fbdev -> hardware,最终才被显示出来。
如果控制台是串口或网络终端,则整个过程就要简单一些;如果控制台是图形窗口下的终端,则整个过程更加复杂,此处不再一一分析。