博客网址:www.shicoder.top

微信:18223081347

欢迎加群聊天 :452380935

这一次我们来实现最基础,也是最常见的函数print,大家都知道这个是可变参数函数,那具体怎么实现呢,我们慢慢来说吧

大家都知道我们常见的格式化输出函数printf里面有很多参数,比如

%[flags][width][.prec][h|l|L][type]

  • %:格式引入字符
  • flags:可选的标志字符序列
  • width:可选的宽度指示符
  • .prec:可选的精度指示符
  • h|l|L:可选的长度修饰符
  • type:转换类型

首先来说下变参是怎么实现的,其实变参和三个参数有关

  • va_list:保存可变参数指针
  • va_start:启用可变参数
  • va_arg:获取下一个参数
  • va_end:结束可变参数

这里我们是宏函数实现

typedef char *va_list; // 保存可变参数
// 因为是将参数全部压栈,最后一个压栈的是参数的个数,所以只需要挨着在内存中找多少个就知道了
#define va_start(ap, v) (ap = (va_list)&v + sizeof(char *)) // 启用可变参数 指向v下一个参数地址
#define va_arg(ap, t) (*(t *)((ap += sizeof(char *)) - sizeof(char *))) // 获取下一个参数 并将值转换为t格式
#define va_end(ap) (ap = (va_list)0) // 结束可变参数

基本上懂了这个原理,就可以基本实现printf函数,只不过这个函数多了一些格式化字符串的参数。先直接看下我们要最终得到什么

void kernel_init()
{
console_init();
int cnt = 30;
while (cnt--)
{
printk("hello system %#010x\n", cnt);
}
}

其实就是输出30个十六进制数,占10位,不足前面用0填充,结果如下

那么就是如何实现这个printk了,先看下它的代码

static char buf[1024];

int printk(const char *fmt, ...)
{
va_list args;
int i;
// 此时的args就是fmt下一个参数的地址
va_start(args, fmt);
// 将内容格式化到buf里面
i = vsprintf(buf, fmt, args); va_end(args);
// 写到控制台上
console_write(buf, i); return i;
}

我先来大概说下思路把,比如我们这一行

printk("hello system %#010x\n", 29);

那么进入printk之后,先定义了一个char * args,然后使用va_start(args,fmt),此时args就会指向hello system %#010x\n的下一个参数,即29,那我们可以猜一下vsprintf的功能:将此时args的值,也就是29的地址,可能在函数里面先取*,得到29,然后按照不断遍历fmt,遇到格式化的时候,就将29给格式化进去,普通字符直接复制到buf

最后使用console_writebuf的值打印出来

那么我们就去看下vsprintf的实现吧,这里是参考linux中的源码,因为这个函数确实是大佬才能写,太多边界情况了

int vsprintf(char *buf, const char *fmt, va_list args)
{
int len;
int i; // 用于存放转换过程中的字符串
char *str;
char *s;
int *ip; // number() 函数使用的标志
int flags; int field_width; // 输出字段宽度
int precision; // min 整数数字个数;max 字符串中字符个数
int qualifier; // 'h', 'l' 或 'L' 用于整数字段 // 首先将字符指针指向 buf
// 然后扫描格式字符串,
// 对各个格式转换指示进行相应的处理
for (str = buf; *fmt; ++fmt)
{
// 格式转换指示字符串均以 '%' 开始
// 这里从 fmt 格式字符串中扫描 '%',寻找格式转换字符串的开始
// 不是格式指示的一般字符均被依次存入 str
if (*fmt != '%')
{
*str++ = *fmt;
continue;
} // 下面取得格式指示字符串中的标志域,并将标志常量放入 flags 变量中
flags = 0;
repeat:
// 掉过第一个 %
++fmt;
switch (*fmt)
{
// 左对齐调整
case '-':
flags |= LEFT;
goto repeat;
// 放加号
case '+':
flags |= PLUS;
goto repeat;
// 放空格
case ' ':
flags |= SPACE;
goto repeat;
// 是特殊转换
case '#':
flags |= SPECIAL;
goto repeat;
// 要填零(即'0'),否则是空格
case '0':
flags |= ZEROPAD;
goto repeat;
} // 取当前参数字段宽度域值,放入 field_width 变量中
field_width = -1; // 如果宽度域中是数值则直接取其为宽度值
if (is_digit(*fmt))
field_width = skip_atoi(&fmt); // 如果宽度域中是字符 '*',表示下一个参数指定宽度
else if (*fmt == '*')
{
++fmt;
// 因此调用 va_arg 取宽度值
field_width = va_arg(args, int); // 若此时宽度值小于 0,则该负数表示其带有标志域 '-' 标志(左对齐)
if (field_width < 0)
{
// 因此还需在标志变量中添入该标志,并将字段宽度值取为其绝对值
field_width = -field_width;
flags |= LEFT;
}
} // 取格式转换串的精度域,并放入 precision 变量中
precision = -1; // 精度域开始的标志是'.' 其处理过程与上面宽度域的类似
if (*fmt == '.')
{
++fmt;
// 如果精度域中是数值则直接取其为精度值
if (is_digit(*fmt))
precision = skip_atoi(&fmt); // 如果精度域中是字符'*',表示下一个参数指定精度
else if (*fmt == '*')
{
// 因此调用 va_arg 取精度值
precision = va_arg(args, int);
}
// 若此时宽度值小于 0,则将字段精度值取为其绝对值
if (precision < 0)
precision = 0;
} // 下面这段代码分析长度修饰符,并将其存入 qualifer 变量
qualifier = -1;
if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L')
{
qualifier = *fmt;
++fmt;
} // 下面分析转换指示符
switch (*fmt)
{ // 如果转换指示符是'c',则表示对应参数应是字符
case 'c':
// 此时如果标志域表明不是左对齐,
if (!(flags & LEFT))
// 则该字段前面放入 (宽度域值 - 1) 个空格字符,然后再放入参数字符
while (--field_width > 0)
*str++ = ' ';
*str++ = (unsigned char)va_arg(args, int);
// 如果宽度域还大于 0,则表示为左对齐
// 则在参数字符后面添加 (宽度值-1) 个空格字符
while (--field_width > 0)
*str++ = ' ';
break; // 如果转换指示符是 's',则表示对应参数是字符串
case 's':
s = va_arg(args, char *);
// 首先取参数字符串的长度
len = strlen(s);
// 若其超过了精度域值, 则扩展精度域=字符串长度
if (precision < 0)
precision = len;
else if (len > precision)
len = precision; // 此时如果标志域表明不是左对齐
if (!(flags & LEFT))
// 则该字段前放入 (宽度值-字符串长度) 个空格字符
while (len < field_width--)
*str++ = ' ';
// 然后再放入参数字符串
for (i = 0; i < len; ++i)
*str++ = *s++;
// 如果宽度域还大于 0,则表示为左对齐
// 则在参数字符串后面,添加(宽度值-字符串长度)个空格字符
while (len < field_width--)
*str++ = ' ';
break; // 如果格式转换符是'o',表示需将对应的参数转换成八进制数的字符串
case 'o':
str = number(str, va_arg(args, unsigned long), 8,
field_width, precision, flags);
break; // 如果格式转换符是'p',表示对应参数的一个指针类型
case 'p':
// 此时若该参数没有设置宽度域,则默认宽度为 8,并且需要添零
if (field_width == -1)
{
field_width = 8;
flags |= ZEROPAD;
}
str = number(str,
(unsigned long)va_arg(args, void *), 16,
field_width, precision, flags);
break; // 若格式转换指示是 'x' 或 'X'
// 则表示对应参数需要打印成十六进制数输出
case 'x':
// 'x'表示用小写字母表示
flags |= SMALL;
case 'X':
str = number(str, va_arg(args, unsigned long), 16,
field_width, precision, flags);
break; // 如果格式转换字符是'd', 'i' 或 'u',则表示对应参数是整数
case 'd':
case 'i':
// 'd', 'i'代表符号整数,因此需要加上带符号标志
flags |= SIGN;
// 'u'代表无符号整数
case 'u':
str = number(str, va_arg(args, unsigned long), 10,
field_width, precision, flags);
break; // 若格式转换指示符是 'n'
// 表示要把到目前为止转换输出的字符数保存到对应参数指针指定的位置中
case 'n':
// 首先利用 va_arg() 取得该参数指针
ip = va_arg(args, int *);
// 然后将已经转换好的字符数存入该指针所指的位置
*ip = (str - buf);
break; default:
// 若格式转换符不是 '%',则表示格式字符串有错
if (*fmt != '%')
// 直接将一个 '%' 写入输出串中
*str++ = '%';
// 如果格式转换符的位置处还有字符,则也直接将该字符写入输出串中
// 然后继续循环处理格式字符串
if (*fmt)
*str++ = *fmt;
else
// 否则表示已经处理到格式字符串的结尾处,则退出循环
--fmt;
break;
}
}
// 最后在转换好的字符串结尾处添上字符串结束标志
*str = '\0'; // 返回转换好的字符串长度值
i = str - buf;
assert(i < 1024);
return i;
}

当然这个函数里面使用的一些辅助函数也可以在linux源码找到,由于不是本文的代码实现重点,就不再进行讲解

那么我们以后就可以简单的使用printk函数进行实现打印啦

操作系统实现-printk的更多相关文章

  1. Linux内核设计第二周——操作系统工作原理

    Linux内核设计第二周 ——操作系统工作原理 作者:宋宸宁(20135315) 一.实验过程 图1 执行效果 从图中可以看出,每执行my_ start_ kernel函数两次或一次,my_ time ...

  2. 《Linux内核分析》第二周 操作系统是如何工作的?

    [刘蔚然 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000] WEEK TWO(2 ...

  3. Linux内核分析——操作系统是如何工作的

    万子惠 + 原创作品转载请注明出处 + <Linux内核分析> 实验部分 使用实验楼的虚拟机打开shell 然后cd mykernel 您可以看到qemu窗口输出的内容的代码mymain. ...

  4. Linux内核分析第二周学习总结:操作系统是如何工作的?

    韩玉琪 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.函数调用堆栈 ...

  5. Linux内核分析之操作系统是如何工作的

    在本周的课程中,孟老师主要讲解了操作系统是如何工作的,我根据自己的理解写了这篇博客,请各位小伙伴多多指正. 一.知识点总结 1. 三个法宝 存储程序计算机:所有计算机基础性的逻辑框架. 堆栈:高级语言 ...

  6. 使用 /proc 文件系统来访问 linux操作系统 内核的内容 && 虚拟文件系统vfs及proc详解

    http://blog.163.com/he_junwei/blog/static/19793764620152743325659/ http://www.01yun.com/other/201304 ...

  7. Linux操作系统工作的基础

    简介: 本文根据 Linux™ 系统工作基础的分析,对存储程序计算机.堆栈(函数调用堆栈)机制和中断机制进行概述.文中将为您提供操作系统(内核)如何工作的细节,进一步从宏观概述结合关键点进行微观(CS ...

  8. 浅析Linux操作系统工作的基础

    环境:lubuntu 13.04   kernel 3.9.7 作者:SA12226265 katao 简介: 本文根据 Linux™ 系统工作基础的分析,对存储程序计算机.堆栈(函数调用堆栈)机制和 ...

  9. 第一次作业:基于Linux操作系统深入源码进程模型分析

    1.Linux操作系统的简易介绍 Linux系统一般有4个主要部分:内核.shell.文件系统和应用程序.内核.shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序.管理文件并使 ...

随机推荐

  1. 如何在 Windows 和 Linux 上查找哪个线程使用的 CPU 时 间最长?

    使用 jstack 找出消耗 CPU 最多的线程代码

  2. linux文本编辑器vim详解

    vim 1.打开文件 vim [option] - file... 打开文件 +# 打开文件后,让光标处于第#行的行首 +/字符串 打开文件后,光标处于第一个被匹配到字符串的行首 -b file 二进 ...

  3. 小程序web开发框架-weweb介绍

    weweb是一个兼容小程序语法的前端框架,你可以用小程序的写法,来写web单面应用.如果你已经有小程序了,通过它你可以将你的小程序运行在浏览器中.在小程序大行其道的今天,它可以让你的小程序代码得到最大 ...

  4. DOM节点的使用(常用方法+代码)

    DOM节点的应用 学习总结 1. 什么是 DOM 2. HTMLDOM 3. 元素获取 元素获取方式 元素节点的属性操作 4. Node 对象的属性和方法 常用属性 常用方法 5. 事件处理 事件驱动 ...

  5. Exchange批量删除邮件

    在实际工作中经常遇到以下问题:邮件发送给错误的收件人,简而言之就是邮件发错了,如果遇到群发更麻烦.Exchange中提供了批量删除邮件功能,当用户发现发送错误后,管理员可以检索并删除指定的邮件. 案例 ...

  6. python---导入模块和包

    导入模块和包 导入模块 import的过程中发生了哪些事情? 寻找模块 如果找到,开辟一块空间,执行这个模块 把这个模块中用到的名字都收录到开辟的空间中 创建一个变量来引用这个模块的空间 注意: 模块 ...

  7. 如何在 Java 中实现最小生成树算法

    定义 在一幅无向图 \(G=(V,E)\) 中,\((u, v)\) 为连接顶点 \(u\) 和顶点 \(v\) 的边,\(w(u,v)\) 为边的权重,若存在边的子集 \(T\subseteq E\ ...

  8. ASMCMD-8102: no connection to Oracle ASM

    通过ASMCMD命令连接ASM,Connected to an idle instance [root@shdb02 ~]# su - oracle [oracle@shdb02 ~]$ asmcmd ...

  9. zabbix server&proxy部署操作过程

    zabbix server&proxy部署操作过程 系统:ubuntu20.04 zabbix版本: 5.4 安装zabbix server 安装方式: 包管理安装,docker,源码,app ...

  10. toFixed()与银行家舍入

    toFixed()与银行家舍入 一直在用toFixed()方法做浮点数的舍入取值,如果只是客户端展示数据是没有多大问题的,但是如果涉及到和后端互交,数据的精度可能会导致接口对接失败,当然了,涉及安全性 ...