接着上一節講。在用戶程序中調用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(控制檯終端)”的概念
printf函數從應用層到內核的調用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.