printf函数能在linux,printf函数从应用层到内核的调用

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值