printf函数从应用层到内核的调用

接着上一节讲。在用户程序中调用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(控制台终端)”的概念

你可能感兴趣的:(linux内核)