博客网址: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:结束可变参数

这里我们是宏函数实现

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

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

  1. void kernel_init()
  2. {
  3. console_init();
  4. int cnt = 30;
  5. while (cnt--)
  6. {
  7. printk("hello system %#010x\n", cnt);
  8. }
  9. }

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

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

  1. static char buf[1024];
  2. int printk(const char *fmt, ...)
  3. {
  4. va_list args;
  5. int i;
  6. // 此时的args就是fmt下一个参数的地址
  7. va_start(args, fmt);
  8. // 将内容格式化到buf里面
  9. i = vsprintf(buf, fmt, args);
  10. va_end(args);
  11. // 写到控制台上
  12. console_write(buf, i);
  13. return i;
  14. }

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

  1. 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中的源码,因为这个函数确实是大佬才能写,太多边界情况了

  1. int vsprintf(char *buf, const char *fmt, va_list args)
  2. {
  3. int len;
  4. int i;
  5. // 用于存放转换过程中的字符串
  6. char *str;
  7. char *s;
  8. int *ip;
  9. // number() 函数使用的标志
  10. int flags;
  11. int field_width; // 输出字段宽度
  12. int precision; // min 整数数字个数;max 字符串中字符个数
  13. int qualifier; // 'h', 'l' 或 'L' 用于整数字段
  14. // 首先将字符指针指向 buf
  15. // 然后扫描格式字符串,
  16. // 对各个格式转换指示进行相应的处理
  17. for (str = buf; *fmt; ++fmt)
  18. {
  19. // 格式转换指示字符串均以 '%' 开始
  20. // 这里从 fmt 格式字符串中扫描 '%',寻找格式转换字符串的开始
  21. // 不是格式指示的一般字符均被依次存入 str
  22. if (*fmt != '%')
  23. {
  24. *str++ = *fmt;
  25. continue;
  26. }
  27. // 下面取得格式指示字符串中的标志域,并将标志常量放入 flags 变量中
  28. flags = 0;
  29. repeat:
  30. // 掉过第一个 %
  31. ++fmt;
  32. switch (*fmt)
  33. {
  34. // 左对齐调整
  35. case '-':
  36. flags |= LEFT;
  37. goto repeat;
  38. // 放加号
  39. case '+':
  40. flags |= PLUS;
  41. goto repeat;
  42. // 放空格
  43. case ' ':
  44. flags |= SPACE;
  45. goto repeat;
  46. // 是特殊转换
  47. case '#':
  48. flags |= SPECIAL;
  49. goto repeat;
  50. // 要填零(即'0'),否则是空格
  51. case '0':
  52. flags |= ZEROPAD;
  53. goto repeat;
  54. }
  55. // 取当前参数字段宽度域值,放入 field_width 变量中
  56. field_width = -1;
  57. // 如果宽度域中是数值则直接取其为宽度值
  58. if (is_digit(*fmt))
  59. field_width = skip_atoi(&fmt);
  60. // 如果宽度域中是字符 '*',表示下一个参数指定宽度
  61. else if (*fmt == '*')
  62. {
  63. ++fmt;
  64. // 因此调用 va_arg 取宽度值
  65. field_width = va_arg(args, int);
  66. // 若此时宽度值小于 0,则该负数表示其带有标志域 '-' 标志(左对齐)
  67. if (field_width < 0)
  68. {
  69. // 因此还需在标志变量中添入该标志,并将字段宽度值取为其绝对值
  70. field_width = -field_width;
  71. flags |= LEFT;
  72. }
  73. }
  74. // 取格式转换串的精度域,并放入 precision 变量中
  75. precision = -1;
  76. // 精度域开始的标志是'.' 其处理过程与上面宽度域的类似
  77. if (*fmt == '.')
  78. {
  79. ++fmt;
  80. // 如果精度域中是数值则直接取其为精度值
  81. if (is_digit(*fmt))
  82. precision = skip_atoi(&fmt);
  83. // 如果精度域中是字符'*',表示下一个参数指定精度
  84. else if (*fmt == '*')
  85. {
  86. // 因此调用 va_arg 取精度值
  87. precision = va_arg(args, int);
  88. }
  89. // 若此时宽度值小于 0,则将字段精度值取为其绝对值
  90. if (precision < 0)
  91. precision = 0;
  92. }
  93. // 下面这段代码分析长度修饰符,并将其存入 qualifer 变量
  94. qualifier = -1;
  95. if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L')
  96. {
  97. qualifier = *fmt;
  98. ++fmt;
  99. }
  100. // 下面分析转换指示符
  101. switch (*fmt)
  102. {
  103. // 如果转换指示符是'c',则表示对应参数应是字符
  104. case 'c':
  105. // 此时如果标志域表明不是左对齐,
  106. if (!(flags & LEFT))
  107. // 则该字段前面放入 (宽度域值 - 1) 个空格字符,然后再放入参数字符
  108. while (--field_width > 0)
  109. *str++ = ' ';
  110. *str++ = (unsigned char)va_arg(args, int);
  111. // 如果宽度域还大于 0,则表示为左对齐
  112. // 则在参数字符后面添加 (宽度值-1) 个空格字符
  113. while (--field_width > 0)
  114. *str++ = ' ';
  115. break;
  116. // 如果转换指示符是 's',则表示对应参数是字符串
  117. case 's':
  118. s = va_arg(args, char *);
  119. // 首先取参数字符串的长度
  120. len = strlen(s);
  121. // 若其超过了精度域值, 则扩展精度域=字符串长度
  122. if (precision < 0)
  123. precision = len;
  124. else if (len > precision)
  125. len = precision;
  126. // 此时如果标志域表明不是左对齐
  127. if (!(flags & LEFT))
  128. // 则该字段前放入 (宽度值-字符串长度) 个空格字符
  129. while (len < field_width--)
  130. *str++ = ' ';
  131. // 然后再放入参数字符串
  132. for (i = 0; i < len; ++i)
  133. *str++ = *s++;
  134. // 如果宽度域还大于 0,则表示为左对齐
  135. // 则在参数字符串后面,添加(宽度值-字符串长度)个空格字符
  136. while (len < field_width--)
  137. *str++ = ' ';
  138. break;
  139. // 如果格式转换符是'o',表示需将对应的参数转换成八进制数的字符串
  140. case 'o':
  141. str = number(str, va_arg(args, unsigned long), 8,
  142. field_width, precision, flags);
  143. break;
  144. // 如果格式转换符是'p',表示对应参数的一个指针类型
  145. case 'p':
  146. // 此时若该参数没有设置宽度域,则默认宽度为 8,并且需要添零
  147. if (field_width == -1)
  148. {
  149. field_width = 8;
  150. flags |= ZEROPAD;
  151. }
  152. str = number(str,
  153. (unsigned long)va_arg(args, void *), 16,
  154. field_width, precision, flags);
  155. break;
  156. // 若格式转换指示是 'x' 或 'X'
  157. // 则表示对应参数需要打印成十六进制数输出
  158. case 'x':
  159. // 'x'表示用小写字母表示
  160. flags |= SMALL;
  161. case 'X':
  162. str = number(str, va_arg(args, unsigned long), 16,
  163. field_width, precision, flags);
  164. break;
  165. // 如果格式转换字符是'd', 'i' 或 'u',则表示对应参数是整数
  166. case 'd':
  167. case 'i':
  168. // 'd', 'i'代表符号整数,因此需要加上带符号标志
  169. flags |= SIGN;
  170. // 'u'代表无符号整数
  171. case 'u':
  172. str = number(str, va_arg(args, unsigned long), 10,
  173. field_width, precision, flags);
  174. break;
  175. // 若格式转换指示符是 'n'
  176. // 表示要把到目前为止转换输出的字符数保存到对应参数指针指定的位置中
  177. case 'n':
  178. // 首先利用 va_arg() 取得该参数指针
  179. ip = va_arg(args, int *);
  180. // 然后将已经转换好的字符数存入该指针所指的位置
  181. *ip = (str - buf);
  182. break;
  183. default:
  184. // 若格式转换符不是 '%',则表示格式字符串有错
  185. if (*fmt != '%')
  186. // 直接将一个 '%' 写入输出串中
  187. *str++ = '%';
  188. // 如果格式转换符的位置处还有字符,则也直接将该字符写入输出串中
  189. // 然后继续循环处理格式字符串
  190. if (*fmt)
  191. *str++ = *fmt;
  192. else
  193. // 否则表示已经处理到格式字符串的结尾处,则退出循环
  194. --fmt;
  195. break;
  196. }
  197. }
  198. // 最后在转换好的字符串结尾处添上字符串结束标志
  199. *str = '\0';
  200. // 返回转换好的字符串长度值
  201. i = str - buf;
  202. assert(i < 1024);
  203. return i;
  204. }

当然这个函数里面使用的一些辅助函数也可以在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. centos下安装配置maven

    下载maven安装包 wget http://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.3.9/binaries/apache-maven ...

  2. jsp技术之隐藏域

    隐藏域 hidden:隐藏域属性,不显示到页面上,但是会提交的表单项 注意:表单中增加了一个隐藏域,是用户的id.稍后修改联系人信息,提交表单时需要使用到 <!-- hidden:隐藏域,不显示 ...

  3. ROS环境变量的设置

    一.前言(大神可以直接跳过) 本博客主要就是为了介绍ROS中环境变量的设置过程,还不是很了解ROS的可以去看一下我的博客,ROS简介-从零开始讲解ROS(适合超零基础阅读) ROS为什么需要设置环境变 ...

  4. CSDN博客步骤:

    在SCDN看到喜欢的文章想转载又嫌一个一个敲太麻烦,干脆直接收藏.但有时候作者把原文章删除或设置为私密文章后又看不了.所以还是转载来的好.这篇博文为快速转载博客的方法,亲测有效,教程如下. 原博客原址 ...

  5. 分压杯频LLC变换器

  6. SQL之总结(四)---null问题的处理

    概述:如果表中的某个列是可选的,那么我们可以在不向该列添加值的情况下插入新记录或更新已有的记录.这意味着该字段将以 NULL 值保存. NULL 值的处理方式与其他值不同. NULL 用作未知的或不适 ...

  7. JavaScript实现带正则表达式的表单校验(校验成功后跳转)

    运行结果: 源代码: 1 <!DOCTYPE html> 2 <html lang="zh"> 3 <head> 4 <meta char ...

  8. 自学java如何快速地达到工作的要求?

    自学java如何快速地达到工作的要求,是很多初学者都比较关心的问题,对于初学者来说,盲目自学不但不能快速入门,还会浪费大量的时间. 今天知了堂就来分享自学Java如何快速达到找工作的要求. 1.自学J ...

  9. Struts2-day2总结

    一.结果页面配置 1.全局结果页面 2.局部结果页面 ****注:如果同时配置了全局页面和局部页面配置,那么最终将以局部为准 result标签当中的type属性 默认值:dispatcher做转发 r ...

  10. 小程序容器助力打造企业超级App

    阿拉丁研究院发布<2021 年度小程序互联网发展白皮书>显示,2021 年全网小程序数量已超 700 万,其中微信小程序开发者突破 300 万,DAU 超过 4.5 亿:日均使用次数同比增 ...