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(控制檯終端)”的概念

發佈了79 篇原創文章 · 獲贊 28 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章