本着从使用中学习原则,我不打算讲太多的细节,也不想讲太多有关内核的知识。但是适当的了解一点内核,对学习 linux 编程会非常有帮助。
printf 在干什么
学过C语言的朋友第一天学习的函数恐怕就是 printf("Hello world\n")
了,无论是在 Windows下,还是 Linux 下,这个函数都能使用。对我们程序员来说,它的底层实现是透明的。而 printf ,就是所谓的 C 语言库函数,它依赖于操作系统。
在 linux 中,printf 的实现,实际上间接的调用了 linux 的系统调用接口,这类似于 windows api。那么 printf 调用了 linux 的哪个系统调用呢?我们来扒一扒源码(linux 0.11,为什么用这么老的版本,因为我们都是初学者,完不起太高大上的几百万行的linux 2.X)。
// 文件:init/main.c
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args)); // 重点在这里
va_end(args);
return i;
}
短短几行代码,把 write 给暴露出来了,也就是说,真正干活的,就是 linux 的系统调用 write 函数。我们不需要关心太多细节,知道 printf 调用了 linux 的系统调用接口 write 就够了。
实际上,很多 C 语言库比如 fwrite, fputs 等等这些写文件的函数,最终也是调用了 write 这个函数,区别就在于 write 的第一个参数,这是一个整型数,它标记了你的数据要写到哪去。
再深入一点,想看看 write 函数长啥样?
对于大多数朋友,后面可以不用再看了,如果你感兴趣,可以继续往下读。
在 linux 0.11 中,write 本质上是一个宏来制作的函数。
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
上面代码看着有点恶心,没关系,我们来简化一下。看下面这个简化版本,最终形成的函数是这样。
int write(int fd, const char *buf, off_t, count) {
long __res;
__asm {
//... 参数传递,这里省略
mov eax, __NR_write // __NR_write的值定义为 4
int 0x80 // 这是重点,32位陷阱门
//... 返回值处理,这里省略
}
return __res;
}
有些同学不禁要问,你在写啥呢?为了简化理解,你把重点放在下面两行足以。
mov eax, 4
int 0x80
这两句你可以简单理解成,他调用了另外一个函数,这个函数是真正的内核函数,名为 sys_write,为什么是这个函数而不是别的,实际是由那个数字 4 决定的,不同的数字,会调用不同的内核函数,如果那个数字是 3,那将调用 sys_read 这个内核函数。
总结
printf 的调用链,实际上就是长成这样:
printf 和 write 属于用户层函数,int 0x80 相当于一扇门,通过这扇门,进入到了内核函数 sys_write 。sys_write 完成后,再一层一层的将结果返回到 printf。
为什么要通过“门”才能调用 sys_write 而不能直接调用 sys_write ? 这个问题可不简单,你需要系统学习一下 CPU 的保护模式了。如果你对上面的 int 0x80 这个“门”很感兴趣,请参考我的另一系列博文,《OS学习笔记》,这里将有很详细的介绍。
不过提醒一下,千万别跑偏了,我们现在只是在学习 Linux 环境编程,不要在内核上走的太远。
到这里,我想应该够了。