printf 从用户层到OS层之间的调用关系

了解printf函数的调用机制,我将以PowerPC为例子介绍它到OS的调用过程,首先我们先来看看printf的函数的具体的实现,如下:
static char sprint_buf[1024];            //定义一个buf存储空间来存放参数;
int printf(const char *fmt, ...)
{
 va_list args;
 int n;
 va_start(args, fmt);
 n = vsprintf(sprint_buf, fmt, args);           //主要用于输出格式的匹配工作;
 va_end(args);
 if (console_ops.write)
     console_ops.write(sprint_buf, n);        //终端的操作,注册和初始化串口;
 return n;
}
        从上面的函数来看,printf函数主要做的就是两件事情,首先确定内容如何输出,以何种格式输出,然后初始化串口,通过串口将数据输出到硬件层。

下面我们将来介绍它的调用过程,以帮助我们对printf函数的理解。
int vsprintf(char *buf, const char *fmt, va_list args)
{
 int len;
 unsigned long long num;
 int i, base;
 char * str;
 const char *s;
 int flags;                            /* flags to number() */
 int field_width;                  /* width of output field */
 int precision;                     /* min. # of digits for integers; max  number of chars for from string */
 int qualifier;                      /* 'h', 'l', or 'L' for integer fields */
                                           /* 'z' support added 23/7/1999 S.H.    */
/* 'z' changed to 'Z' --davidm 1/25/99 */
for (str=buf ; *fmt ; ++fmt) {
  if (*fmt != '%') {
   *str++ = *fmt;
   continue;
  }
   
  /* process flags */
  flags = 0;
  repeat:
   ++fmt; /* this also skips first '%' */
   switch (*fmt) {
    case '-': flags |= LEFT; goto repeat;
    case '+': flags |= PLUS; goto repeat;
    case ' ': flags |= SPACE; goto repeat;
    case '#': flags |= SPECIAL; goto repeat;
    case '0': flags |= ZEROPAD; goto repeat;
    }
 
  /* get field width */
  field_width = -1;
  if ('0' <= *fmt && *fmt <= '9')
   field_width = skip_atoi(&fmt);
  else if (*fmt == '*') {
   ++fmt;
   /* it's the next argument */
   field_width = va_arg(args, int);
   if (field_width < 0) {
    field_width = -field_width;
    flags |= LEFT;
   }
  }

  /* get the precision */
  precision = -1;
  if (*fmt == '.') {
   ++fmt;
   if ('0' <= *fmt && *fmt <= '9')
    precision = skip_atoi(&fmt);
   else if (*fmt == '*') {
    ++fmt;
    /* it's the next argument */
    precision = va_arg(args, int);
   }
   if (precision < 0)
    precision = 0;
  }

  /* get the conversion qualifier */
  qualifier = -1;
  if (*fmt == 'l' && *(fmt + 1) == 'l') {
   qualifier = 'q';
   fmt += 2;
  } else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'
   || *fmt == 'Z') {
   qualifier = *fmt;
   ++fmt;
  }

  /* default base */
  base = 10;

  switch (*fmt) {
  case 'c':
   if (!(flags & LEFT))
    while (--field_width > 0)
     *str++ = ' ';
   *str++ = (unsigned char) va_arg(args, int);
   while (--field_width > 0)
    *str++ = ' ';
   continue;

  case 's':
   s = va_arg(args, char *);
   if (!s)
    s = "<NULL>";

   len = strnlen(s, precision);

   if (!(flags & LEFT))
    while (len < field_width--)
     *str++ = ' ';
   for (i = 0; i < len; ++i)
    *str++ = *s++;
   while (len < field_width--)
    *str++ = ' ';
   continue;

  case 'p':
   if (field_width == -1) {
    field_width = 2*sizeof(void *);
    flags |= ZEROPAD;
   }
   str = number(str,
    (unsigned long) va_arg(args, void *), 16,
    field_width, precision, flags);
   continue;
  case 'n':
   if (qualifier == 'l') {
    long * ip = va_arg(args, long *);
    *ip = (str - buf);
   } else if (qualifier == 'Z') {
    size_t * ip = va_arg(args, size_t *);
    *ip = (str - buf);
   } else {
    int * ip = va_arg(args, int *);
    *ip = (str - buf);
   }
   continue;

  case '%':
   *str++ = '%';
   continue;

  /* integer number formats - set up the flags and "break" */
  case 'o':
   base = 8;
   break;

  case 'X':
   flags |= LARGE;
  case 'x':
   base = 16;
   break;

  case 'd':
  case 'i':
   flags |= SIGN;
  case 'u':
   break;

  default:
   *str++ = '%';
   if (*fmt)
    *str++ = *fmt;
   else
    --fmt;
   continue;
  }
  if (qualifier == 'l') {
   num = va_arg(args, unsigned long);
   if (flags & SIGN)
    num = (signed long) num;
  } else if (qualifier == 'q') {
   num = va_arg(args, unsigned long long);
   if (flags & SIGN)
    num = (signed long long) num;
  } else if (qualifier == 'Z') {
   num = va_arg(args, size_t);
  } else if (qualifier == 'h') {
   num = (unsigned short) va_arg(args, int);
   if (flags & SIGN)
    num = (signed short) num;
  } else {
   num = va_arg(args, unsigned int);
   if (flags & SIGN)
    num = (signed int) num;
  }
  str = number(str, num, base, field_width, precision, flags);
 }
 *str = '\0';
 return str-buf;
}

从这个函数我们可以看出printf的格式符的重要性,以及它所支持的所有输出格式,在这个函数中调用了两个重要的函数,下面我们来介绍这俩个重要的函数函数的功能;
static char * number(char * str, unsigned long long num, int base, int size, int precision, int type)
{
 char c,sign,tmp[66];
 const char *digits="0123456789abcdefghijklmnopqrstuvwxyz";
 int i;
 if (type & LARGE)
  digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 if (type & LEFT)
  type &= ~ZEROPAD;
 if (base < 2 || base > 36)
  return 0;
 c = (type & ZEROPAD) ? '0' : ' ';
 sign = 0;
 if (type & SIGN) {
  if ((signed long long)num < 0) {
   sign = '-';
   num = - (signed long long)num;
   size--;
  } else if (type & PLUS) {
   sign = '+';
   size--;
  } else if (type & SPACE) {
   sign = ' ';
   size--;
  }
 }
 if (type & SPECIAL) {
  if (base == 16)
   size -= 2;
  else if (base == 8)
   size--;
 }
 i = 0;
 if (num == 0)
  tmp[i++]='0';
 else while (num != 0) {
  tmp[i++] = digits[do_div(num, base)];
 }
 if (i > precision)
  precision = i;
 size -= precision;
 if (!(type&(ZEROPAD+LEFT)))
  while(size-->0)
   *str++ = ' ';
 if (sign)
  *str++ = sign;
 if (type & SPECIAL) {
  if (base==8)
   *str++ = '0';
  else if (base==16) {
   *str++ = '0';
   *str++ = digits[33];
  }
 }
 if (!(type & LEFT))
  while (size-- > 0)
   *str++ = c;
 while (i < precision--)
  *str++ = '0';
 while (i-- > 0)
  *str++ = tmp[i];
 while (size-- > 0)
  *str++ = ' ';
 return str;
}
        
        这个函数的功能主要是将数字字符串按2,8,16进制的形式输出结果的处理,以及对输出的结果正负的处理和空格的处理。同时也解决了数据的输出对齐问题,从而让数据根据我们想要的形式输出结果,下面我们来介绍下该函数所用到的一些宏的定义,以便于帮助我们来理解该函数的功能。
#define ZEROPAD 1                             /* pad with zero */
#define SIGN         2                             /* unsigned/signed long */
#define PLUS         4                             /* show plus */
#define SPACE      8                             /* space if plus */
#define LEFT        16                            /* left justified */
#define SPECIAL  32                            /* 0x */
#define LARGE     64                            /* use 'ABCDEF' instead of 'abcdef' */
          对于这些宏我就不做过多的解释了,只要将它们代入具体的宏的位置去就可以了,它们只是基本的宏替换的功能而已,没有其它过多的用法了,看到这个宏定义,让我想到了枚举,其实我可以用枚举的方法来定义这些宏定义,只是在初始化的时候有点区别,枚举的第一个成员默认被分配为0,之后每个成员要是没有赋值它将会在前一个成员的基础上加一,当让枚举和宏定义还是有区别的,宏定义是在编译器编译时对宏进行宏展开的工作,所以它不分配内存空间,而枚举就有些不一样了,他是结构类型,所以在为它初始化后它会占用内存空间,所以它会分配内存;还是继续来说说另外一个重要的函数的功能吧!

static int skip_atoi(const char **s)
{
 int i, c;
 for (i = 0; '0' <= (c = **s) && c <= '9'; ++*s)
        i = i*10 + c - '0';
 return i;
}
看到这个函数我相信我们大家都很熟悉了,这个函数和我们之前调用的atoi的功能差不多,本函数主要是主要是将字符型数字转化为十进制数字的功能,它是通过一位一位的返回模式转化的。

下面我们再来看看printf函数的另外的一个函数的功能,首先来看看 console_ops 结构体;
struct console_ops console_ops;
/* Console operations */
struct console_ops {
 int (*open)(void);
 void (*write)(const char *buf, int len);
 void (*edit_cmdline)(char *buf, int len);
 void (*close)(void);
 void *data;
};

另外一个函数就是这个结构的write成员函数,下面我们来看看它的功能。
int serial_console_init(void)
{
 void *devp;
 int rc = -1;
 devp = serial_get_stdout_devp();
 if (devp == NULL)
  goto err_out;
 if (dt_is_compatible(devp, "ns16550") ||
     dt_is_compatible(devp, "pnpPNP,501"))
  rc = ns16550_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "marvell,mv64360-mpsc"))
  rc = mpsc_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "fsl,cpm1-scc-uart") ||
          dt_is_compatible(devp, "fsl,cpm1-smc-uart") ||
          dt_is_compatible(devp, "fsl,cpm2-scc-uart") ||
          dt_is_compatible(devp, "fsl,cpm2-smc-uart"))
  rc = cpm_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "fsl,mpc5200-psc-uart"))
  rc = mpc5200_psc_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "xlnx,opb-uartlite-1.00.b") ||
   dt_is_compatible(devp, "xlnx,xps-uartlite-1.00.a"))
  rc = uartlite_console_init(devp, &serial_cd);
 /* Add other serial console driver calls here */
 if (!rc) {
  console_ops.open = serial_open;
   console_ops.write = serial_write;
  console_ops.close = serial_close;
  console_ops.data = &serial_cd;
  if (serial_cd.getc)
   console_ops.edit_cmdline = serial_edit_cmdline;
  return 0;
 }
err_out:
 return -1;
}

当函数执行时,console_ops.write函数就相当于执行serial_write函数,我们来看看它又做了些什么事情;
static void serial_write(const char *buf, int len)
{
 struct serial_console_data *scdp = console_ops.data;
 while (*buf != '\0')
     scdp->putc(*buf++);
}

由上面的console_ops结构体我们已经知道了它有一个data成员,下面我们来具体看看serial_console_data结构的具体内容;
struct serial_console_data {
 int (*open)(void);
 void (*putc)(unsigned char c);
 unsigned char (*getc)(void);
 u8 (*tstc)(void);
 void (*close)(void);
};

看到这个结构体就是一个操作串口的操作集合,我们只要调用它就可以操作串口了,serial_write函数主要是操作它的putc函数来输出内容到串口,下面看看putc函数,通过查找得知它又调用其它函数;
int cpm_console_init(void *devp, struct serial_console_data *scdp)
{
 void *vreg[2];
 u32 reg[2];
 int is_smc = 0, is_cpm2 = 0;
 void *parent, *muram;
 void *muram_addr;
 unsigned long muram_offset, muram_size;

 if (dt_is_compatible(devp, "fsl,cpm1-smc-uart")) {
  is_smc = 1;
 } else if (dt_is_compatible(devp, "fsl,cpm2-scc-uart")) {
  is_cpm2 = 1;
 } else if (dt_is_compatible(devp, "fsl,cpm2-smc-uart")) {
  is_cpm2 = 1;
  is_smc = 1;
 }

 if (is_smc) {
  enable_port = smc_enable_port;
  disable_port = smc_disable_port;
 } else {
  enable_port = scc_enable_port;
  disable_port = scc_disable_port;
 }

 if (is_cpm2)
  do_cmd = cpm2_cmd;
 else
  do_cmd = cpm1_cmd;

 if (getprop(devp, "fsl,cpm-command", &cpm_cmd, 4) < 4)
  return -1;

 if (dt_get_virtual_reg(devp, vreg, 2) < 2)
  return -1;

 if (is_smc)
  smc = vreg[0];
 else
  scc = vreg[0];

 param = vreg[1];

 parent = get_parent(devp);
 if (!parent)
  return -1;

 if (dt_get_virtual_reg(parent, &cpcr, 1) < 1)
  return -1;

 muram = finddevice("/soc/cpm/muram/data");
 if (!muram)
  return -1;

 /* For bootwrapper-compatible device trees, we assume that the first
  * entry has at least 128 bytes, and that #address-cells/#data-cells
  * is one for both parent and child.
  */

 if (dt_get_virtual_reg(muram, &muram_addr, 1) < 1)
  return -1;

 if (getprop(muram, "reg", reg, 8) < 8)
  return -1;

 muram_offset = reg[0];
 muram_size = reg[1];

 /* Store the buffer descriptors at the end of the first muram chunk.
  * For SMC ports on CPM2-based platforms, relocate the parameter RAM
  * just before the buffer descriptors.
  */

 cbd_offset = muram_offset + muram_size - 2 * sizeof(struct cpm_bd);

 if (is_cpm2 && is_smc) {
  u16 *smc_base = (u16 *)param;
  u16 pram_offset;

  pram_offset = cbd_offset - 64;
  pram_offset = _ALIGN_DOWN(pram_offset, 64);

  disable_port();
  out_be16(smc_base, pram_offset);
  param = muram_addr - muram_offset + pram_offset;
 }

 cbd_addr = muram_addr - muram_offset + cbd_offset;

 scdp->open = cpm_serial_open;
 scdp->putc = cpm_serial_putc;
 scdp->getc = cpm_serial_getc;
 scdp->tstc = cpm_serial_tstc;

 return 0;
}

这个函数的具体内容就不看了,就去看看cpm_serial_putc函数的具体实现,来看看它实现什么功能;
static void cpm_serial_putc(unsigned char c)
{
 while (tbdf->sc & 0x8000)
 barrier();      //执行空操作;
 sync();
 tbdf->addr[0] = c; 
 eieio();         //上下文同步;
 tbdf->sc |= 0x8000;
}
针对barrier函数和eieio函数的具体实现做一些说明:
static inline void barrier(void)
{
     asm volatile("" : : : "memory");     //执行空指令,但是消耗没存空间;      
}

static inline void eieio(void)
{
     __asm__ __volatile__ ("eieio" : : : "memory");     //同上
}
        eieio 是上下文同步指令。“上下文同步”指的是:处理器内核包含着多个独立的执行单元,所以它能够并行的执行多个指令并且是乱序的。上下文同步指令用于需要严格秩序的地方,进行强制严格的指令顺序。eieio代表“强制按顺序执行IO”。在执行过程中,加载/存储单元等待前一个访问结束之后再开始运行加载/存储指令。eieio的目的就是为了防止执行过程中的随意加载和存储。

我们再来看看tbdf具体是什么结构,看看的结构体的具体作用;
static struct cpm_bd *tbdf, *rbdf;
struct cpm_bd {
 u16 sc;                    /* Status and Control */
 u16 len;                  /* Data length in buffer */
 u8 *addr;                /* Buffer address in host memory */
};
        
        从该结构体的可以看出,putc最后存储的内容其实是存在了buff空间里了,但是疑问也同时产生了tbdf结构体是在什么时候被初始化的,初始化值是什么,带着疑问我们继续来往下看看;
static int cpm_serial_open(void)
{
 disable_port();
 out_8(&param->rfcr, 0x10);
 out_8(&param->tfcr, 0x10);
 out_be16(&param->mrblr, 1);
 out_be16(&param->maxidl, 0);
 out_be16(&param->brkec, 0);
 out_be16(&param->brkln, 0);
 out_be16(&param->brkcr, 0);
 
 rbdf = cbd_addr;
 rbdf->addr = (u8 *)rbdf - 1;
 rbdf->sc = 0xa000;
 rbdf->len = 1;

 tbdf = rbdf + 1;
 tbdf->addr = (u8 *)rbdf - 2;
 tbdf->sc = 0x2000;
 tbdf->len = 1;
 
 sync();
 out_be16(&param->rbase, cbd_offset);
 out_be16(&param->tbase, cbd_offset + sizeof(struct cpm_bd));
 do_cmd(CPM_CMD_INIT_RX_TX);
 enable_port();
 return 0;
}

         从这个函数我们可以看出它对tbdf的初始化工作,我们可以根据它的初始化内容结合putc函数来理解数据的输出功能,do_cmd函数出现过多次,我们来看看这个函数,再看这个函数之前我先来看看几个宏定义;

#define CPM_CMD_STOP_TX     4
#define CPM_CMD_RESTART_TX  6
#define CPM_CMD_INIT_RX_TX  0

下面来看看do_cmd函数的实现,根据函数的查找我们可以看到do_cmd函数其实是在cpm_console_init函数里面被初始化的,实际上它是调用cpm2_cmd(do_cmd = cpm2_cmd)函数;
static void cpm2_cmd(int op)
{
 while (in_be32(cpcr) & 0x10000)
  ;
 out_be32(cpcr, op | cpm_cmd | 0x10000);
 while (in_be32(cpcr) & 0x10000)
  ;
}
         针对这个函数我们来看看它调用的函数的功能,由于它内嵌汇编,所以我们要看看它的汇编指令的意思,结合它一起来理解该函数的功能,先来看看函数本身吧;
static inline unsigned in_be32(const volatile unsigned *addr)
{
 unsigned ret;
 __asm__ __volatile__("lwz%U1%X1 %0,%1; twi 0,%0,0; isync"
        : "=r" (ret) : "m" (*addr));
 return ret;
}

static inline unsigned in_be32(const volatile unsigned *addr)
{
 unsigned ret;
 __asm__ __volatile__("lwz%U1%X1 %0,%1; twi 0,%0,0; isync"
        : "=r" (ret) : "m" (*addr));
 return ret;
}
         我们可以看到其实这两个函数本身都是在调用汇编指令,由于我们分析的PowerPC架构的代码,所以我们只能去找它的指令集看看,看是否有lwz指令,看它是什么意思了;
            lwz rD,d(rA) ;EA=(rA|0)+d,从EA处读取4个字节的数,并加载到rD。

         从上面的汇编指令我们可以看出,其实就是加载字节的指令,将EA的字节存放到rD中去。针对上面的汇编部分我们做一些简单的解释,

 lwz%U1%X1 %0,%1;   将%1的内容加载到%0中去;
 twi 0,%0,0;                这是内核的寄存器的配置,具体执行什么就需要根据具体的芯片指令手册进行查找了,这里不作具体介绍;
 isync                        这个是同步数据的指令;
 : "=r" (ret)             “=r”表示以寄存器变量的形式输出数据,然后将数据给ret变量,它对应上面指令的%0;
 : "m" (*addr)        “m” 表示输入的是内存中的变量,它对应的是%1,也就是将*addr替换%1;

        我们就先介绍到这里,就不分析细节方面的函数了,如果要深入的理解它可以从细节方面的着手,本文主要是分析printf函数的OS的基本的调用过程,是为让初学者对它有个大概的认识,以便于后续的学习,当然本文跟多的是我个人的理解,所以中间肯定存在理解错误的地方;在后续工作中我将会深入的去了解它的具体的过程,并且会写成文档收集起来以供有需要的人学习。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 北航os lab1中的printk是一个用于内核调试的函数。printk函数是Linux内核中一个非常重要的调试输出函数,用于在内核中输出调试信息。 printk函数的原型为: int printk(const char *fmt, ...); 该函数可以接受一个或多个参数,类似于C语言中的printf函数。第一个参数是一个格式化字符串,后面的参数根据格式化字符串中的占位符来确定输出的内容。 printk函数通过将信息打印到内核缓冲区中,然后再将缓冲区的内容发送到标准输出或日志文件中。这样可以避免在内核中直接使用标准输出函数,因为标准输出函数通常会产生内核安全问题。 在lab1中,我们通过使用printk函数来输出一些调试信息,以便我们在运行内核时能够观察到一些重要的状态信息。这些信息对于调试内核错误和理解内核运行过程非常有帮助。 在实际使用中,我们可以在代码中的关键位置调用printk函数输出调试信息,比如在函数调用的入口处、循环的每一次迭代中等。输出的信息可以是变量的值、函数的返回结果、状态标志等。通过观察这些输出信息,我们可以更好地理解内核的运行过程,并找出潜在的问题和改进的空间。 总之,北航os lab1中的printk函数是一个用于内核调试的重要函数,在调试内核错误和理解内核运行过程中发挥着非常重要的作用。 ### 回答2: 北航OS lab1中的printk是一个用于打印输出信息的函数。在操作系统编程中,打印输出信息对于调试和排错是非常重要的。printk函数可以将我们想要输出的信息打印到控制台或者文件中。该函数可以接受不同类型的参数,包括字符串、整数和指针等。 在lab1中,我们需要实现一个简化版的printk函数。通过实现这个函数,我们可以加深对操作系统内核的理解,学习和掌握操作系统中的内核级调试技术。 在实现printk函数时,需要考虑几个关键点。首先是参数的处理,我们需要根据参数的不同类型来确定打印输出的格式。其次是打印输出的位置,可以选择将打印的信息输出到控制台上,或者写入到一个文件中。最后是打印输出的性能优化,可以通过缓冲区和格式化输出等优化技术来提高打印输出的效率。 通过实现printk函数,我们可以在内核中方便地输出调试信息,帮助我们追踪和分析代码的执行流程,进而更好地理解操作系统的运行机制。同时,printk函数在操作系统开发中也是一个基础的工具函数,熟练掌握它对于后续的实验和项目开发都有很大的帮助。 总之,北航OS lab1中的printk是一个重要的函数,它可以帮助我们实现内核级调试和输出相关信息,对于操作系统的学习和开发都具有重要意义。 ### 回答3: 北航OS Lab1是指北京航空航天大学操作系统实验中的第一个实验,即实现一个简化版的printk函数。 printk函数是操作系统中用于将信息打印到屏幕或日志文件的函数,可以帮助调试程序或输出程序的运行状态。在北航OS Lab1的实验中,我们需要实现一个类似的函数。 具体实现的过程涉及以下步骤: 1. 实现字符串输出功能:我们需要编写代码来输出字符串,将字符串的各个字符逐一输出到屏幕或日志文件。 2. 实现格式化输出功能:在实际开发中,我们通常希望能够输出变量的值,而不仅仅是字符串。因此,我们需要实现格式化输出的功能,即能够根据不同的格式输出不同类型的变量。 3. 添加参数支持:为了使printk函数更加灵活,我们还需要实现可变参数的支持,即能够接收不确定数量的参数。 4. 添加调试信息:为了方便调试程序,我们还需要在输出的内容中添加相关的调试信息,比如输出所在的文件和行号。 在完成上述步骤后,我们就能够实现一个简化版的printk函数。它能够输出字符串、格式化输出不同类型的变量、接收可变数量的参数,并且在输出中添加调试信息。 总之,北航OS Lab1中的打印函数(printk)是一个基于字符串输出的简化版,通过实现字符串输出、格式化输出、参数支持和调试信息,我们能够实现一个功能相对完善的打印函数,用于帮助调试和输出程序运行状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值