第三章·printf.c
在 xv6 操作系统中,
printf.c
文件包含了实现printf
函数的代码。printf
是一个输出函数,用于将文本格式化后输出到控制台或屏幕。xv6 中的printf
实现较为简单,不像标准 C 库中的printf
那么复杂,它通过系统调用与内核的 I/O 子系统进行交互。因为printf()于其他代码文件相对独立,并且在其他源文件中会反复调用,故而单独拿出一个章节分析理解,可以跳过,不影响其他代码的理解。
void printfinit(void)
:
\kernel\printf.c
void printfinit(void)
{
initlock(&pr.lock, "pr"); // 初始化打印锁
pr.locking = 1; // 设置锁的标志为 1
}
printfinit
函数的作用是初始化 printf
输出时需要的锁,以确保并发环境下多个打印任务之间不会相互干扰。pr.locking
则是用来控制锁的状态,可能用于 printf
函数在实际打印时进行判断。
由于锁的使用存在于大部分源码中,并且是一个相对完整的体系,对于锁的实现方法和相关代码,在下一章会详细分析。在此之前,对于锁,只需要知道其在代码中的具体作用。
void panic(char *s)
:
void panic(char *s)
{
pr.locking = 0; // 禁用锁定
printf("panic: "); // 输出前缀 "panic: "
printf(s); // 输出传入的错误信息 s
printf("\n"); // 输出换行符
panicked = 1; // 设置 panicked 标志为 1,表示发生了 panic
for(;;) // 死循环,阻止程序继续执行
;
}
对于 for(;;)
无限循环,计算机会进入死循环,持续消耗计算资源,无法继续执行其他程序。通常情况下,这是一种在发生严重错误时使用的机制,用来阻止程序继续执行,并等待外部干预(如系统重启或通过调试中断退出)。多核CPU中,只有执行panic命令的核心才会终止运行,其他核心不受影响。
void printf(char *fmt, ...)
(核心函数):
void printf(char *fmt, ...)
{
va_list ap;
int i, c, locking;
char *s;
locking = pr.locking; // 保存当前打印锁定状态
if(locking)
acquire(&pr.lock); // 如果需要锁定,则获取锁
if (fmt == 0)
panic("null fmt"); // 如果格式化字符串为空,触发 panic
va_start(ap, fmt); // 初始化 va_list 类型的变量 ap,用于访问可变参数
for(i = 0; (c = fmt[i] & 0xff) != 0; i++){ // 遍历格式化字符串
if(c != '%'){ // 如果字符不是格式标识符 %
consputc(c); // 直接输出字符
continue;
} // -> 从这里往下就是实现类似于printf("%d",n)这种操作
c = fmt[++i] & 0xff; // 获取格式标识符后的字符
if(c == 0) // 如果格式标识符后面没有字符,跳出循环
break;
switch(c){
case 'd': // 打印十进制整数
printint(va_arg(ap, int), 10, 1);
break;
case 'x': // 打印十六进制整数
printint(va_arg(ap, int), 16, 1);
break;
case 'p': // 打印指针地址
printptr(va_arg(ap, uint64));
break;
case 's': // 打印字符串
if((s = va_arg(ap, char*)) == 0) // 如果字符串为空,打印 "(null)"
s = "(null)";
for(; *s; s++) // 输出字符串的每个字符
consputc(*s);
break;
case '%': // 打印百分号字符 '%'
consputc('%');
break;
default: // 打印不支持的格式标识符
consputc('%');
consputc(c);
break;
}
}
if(locking)
release(&pr.lock); // 如果需要锁定,则释放锁
}
va_list
是 C 语言中的一个类型,用于处理函数的可变参数。它是 <stdarg.h>
头文件中定义的,用来访问传递给函数的参数。va_list
是一个指向函数参数列表的指针,配合 va_start
、va_arg
和 va_end
等宏一起使用,能够在函数内部访问不定数量和类型的参数。
va_start(ap, fmt);
:扩展为:__builtin_va_start(ap,fmt)
:
__builtin_va_start(ap, fmt)
是 GCC 提供的一个内建函数,通常与 va_list
一起使用。它是 va_start
宏的一个底层实现,通常用于初始化 va_list
类型的变量,以便访问可变参数列表。(就是访问参数用到的,再深就不要看了,对于xv6系统的理解到这里就可)下面介绍下该函数中用到的另外两个内置函数:
static void printptr(uint64 x)
:
static void printptr(uint64 x)
{
int i;
consputc('0'); // 输出 '0'
consputc('x'); // 输出 'x'
for (i = 0; i < (sizeof(uint64) * 2); i++, x <<= 4)
consputc(digits[x >> (sizeof(uint64) * 8 - 4)]); // 输出十六进制数字
}
digits[]
数组是一个包含十六进制字符的数组:
static char digits[] = "0123456789abcdef";
这样,通过 digits[x >> 60]
就能得到对应的十六进制字符。
static void printint(int xx, int base, int sign)
:
static void printint(int xx, int base, int sign)
{
char buf[16]; // 存储转换后的字符数组
int i;
uint x;
// 处理负数情况
if(sign && (sign = xx < 0))
x = -xx; // 如果是负数,取绝对值
else
x = xx; // 否则,直接赋值
i = 0;
do {
buf[i++] = digits[x % base]; // 取余数,获得当前数字的字符表示
} while((x /= base) != 0); // 除以基数,得到下一位的数字
if(sign)
buf[i++] = '-'; // 如果是负数,添加负号
while(--i >= 0)
consputc(buf[i]); // 从字符数组末尾开始输出,逆序打印
}
printint
函数,用于将一个整数以指定的基数(如十进制、十六进制等)格式化输出。它支持有符号整数的处理,并且通过 consputc
函数将每个字符输出到控制台。
对于上面的代码,可能会看得比较蒙,翻来覆去的不知道在干啥呢。这是因为对于console控制台的输出,每次只会输出一个”单字节字符“,比如说输出123,那么简化后的输出如下:
consputc('1'); consputc('2'); consputc('3');
也就是先输出1,再输出2,最后再输出3。所以先用反复的计算得到1,2,3,再输出,理解了这个过程,再看上面的代码,就很清晰了。这里我们要学会一个思想,看这种底层源码时,不要把常用的类似于printf()这种函数的实现想的那么理所当然,时刻记住我们正在和傻傻的硬件打交道 (╬▔皿▔)╯。