博客网址: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. java-等待唤醒机制(线程中的通信)-线程池

    为什么需要线程间的通信 多个线程并发执行时,在默认情况下CPU时随机切换线程的,当我们需要多个线程共同完成一件任务,并且 希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共 ...

  2. 业务网关之AK中心建设

    啥是AK AK(Access Key)是一种身份证明,它解决了"资源的使用者是谁"这个问题,比如在生活中,身份证可以证明你是你,而在云计算或程序中,AK能证明你是这个应用的拥有者. ...

  3. SphinxJS——把字符串编码成png图片的超轻量级开源库

    体验地址:https://jrainlau.github.io/sp...项目地址:https://github.com/jrainlau/s... SphinxJS 一个能够把字符串编码成png图片 ...

  4. HTML5中dialog元素尝鲜

    对话框(别称模态框,浮层)是web项目中用于用户交互的重要部分,我们最常见的就是js中 alert(),confirm(),但是这个对话框的不美观,也不能自定义样式,所以在开发的过程中,一般根据自己自 ...

  5. 多态polymorphism,向上转型和动态方法调度有什么用?

    多态有什么用?马 克  -   t   o - w   i  n:https://blog.csdn.net/qq_44639795/article/details/103117332我给大家想了两个 ...

  6. vue获取验证码倒计时

    <template> <div> <el-button :disabled="disabled" @click="sendcode" ...

  7. ansible模块解析及使用

    模块一:setup(收集远程主机信息) [root@zabbix30 /]# ansible test -m setup 模块二:ping(测试主机是否在线) [root@zabbix30 /]# a ...

  8. 面渣逆袭:RocketMQ二十三问

    基础 1.为什么要使用消息队列呢? 消息队列主要有三大用途,我们拿一个电商系统的下单举例: 解耦:引入消息队列之前,下单完成之后,需要订单服务去调用库存服务减库存,调用营销服务加营销数据--引入消息队 ...

  9. 2021牛客暑期多校训练营3 J 思维

    传送门 J-Counting Triangles_2021牛客暑期多校训练营3 (nowcoder.com) 题目 Goodeat finds an undirected complete graph ...

  10. gin框架使用【4.请求参数】

    GET url: http://127.0.0.1:8080/users?id=1&name=卷毛狒狒 package mainimport ( "github.com/gin-go ...