接着上一节讲。在用户程序中调用printf,会输出数据,我们知道最好肯定会进入到内核里运行,因为数据是由硬件通过串口等进行输出的,必定需要调用硬件的驱动程序。
示例程序如下:
test.c
#include
int main()
{
int i = 1;
printf("number is : %d !\n ,i");
return 0;
}
我们通过 gcc -E test.i test.c 进行预编译,可以看到test.i有:extern int printf (const char *__restrict __format, ...);
这里我们知道printf是一个外部函数,那么是谁定义的呢?
当然是glibc。
那么怎么知道printf属于哪个库呢?
首先,gcc -g -o test test.c 生成test;
然后,输入: ldd test,可以看到有下面的打印:
linux-gate.so.1 => (0x00e5d000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00a01000)
/lib/ld-linux.so.2 (0x0049d000)
从这里可以看出需要三个库;
接着,查看这三个库,看一下里面是否包含我们要找的函数,如: nm libc.so.6 > nm.txt
printf在glibc的源码是:
int
__printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);
return done;
}
起作用的主要是这条语句:done = vfprintf (stdout, format, arg);
它的源码没跟踪到,主要原理是格式化字符串,最后将字符串输出到文件中,也就是stdout中,怎么产生输出的呢?
后来调用了系统调用write,向stdout写(即当前所在的终端),最后产生swi异常,从而陷入内核,执行sys_write。
我们在上一篇说了一个现象:如果是在串口终端调用printf,会打印在串口终端上;在telnet终端调用printf,会打印在telnet终端上。我们在glibc库里看到的是向stdout写数据。
这里还要先说一个概念,控制终端(/dev/tty),这是个在应用程序中的一个概念,其实就是当前终端设备的一个链接。我们可以在当前终端下输入 tty 命令查看,例如在telnet终端下输入 tty ,会输出:/dev/pts/0,它代表当前终端设备。猜想在glibc库里有一个重定位过程,把stdout对到/dev/tty,然后进行sys_write,所以每次printf的输出都在当前的控制终端上。
我们知道在linux中sys_write其实就是:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
ret = vfs_write(file, buf, count, &pos);
至于为什么,请参见下面的博文,里面会讲系统调用的原理和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
所以上面的那个write函数实际是 redirected_tty_write
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);
// 这里其实就是
n_tty_write //struct tty_ldisc_ops tty_ldisc_N_TTY
ssize_t num = process_output_block(tty, b, nr);
i = tty->ops->write(tty, buf, i);
// 看到uart_register_driver函数有tty_set_operations(normal, &uart_ops);
// 它是设置struct tty_driver *normal;的tty_operations,所以这里的write函数就是
uart_write
uart_start(tty);
__uart_start(tty);
// 由定义看,下面的port是uart_port;我们在serial8250_isa_init_ports函数里照到它的初始化
// up->port.ops = &serial8250_pops;
struct uart_port *port = state->uart_port;
port->ops->start_tx(port);
// 所以上面的start_tx 其实就是 serial8250_pops.start_tx =
serial8250_start_tx
serial_out(up, UART_IER, up->ier);
下面就不分析了,驱动硬件输出。我们看到printf的最后动作和printk的最后动作是一样的,都是驱动硬件输出。之所以printk只输出到串口,是因为printk的打印对象被直接定位到了控制台(这里是串口);而printf是先经过glibc处理后才调用sys_write函数,传进来的参数会告诉内核应该打印在哪里(当前控制终端)。
这里只是分析了一下流程,要想更好的理解,请查阅“tty,控制台,虚拟终端,串口,console(控制台终端)”的概念
阅读(1281) | 评论(0) | 转发(0) |