printk实现分析

转自:http://blog.chinaunix.net/u3/112940/showart_2450064.html

   由于前两天在看netconsole的源码实现中,发现其跟printk的实现机制相关,加之之前一直是很普通的使用printk,从不清楚printk到底是怎样工作的,因此就趁这个机会把printk的实现代码也给大致看了一下,代码流程并不复杂,下面就简要说明一下。
 
   printk在内核中的实现代码如下。
   asmlinkage int printk(const char *fmt, ...)
   {
      va_list args;
      int r;
 
      /*将fmt后的参数信息保存到args中*/
      va_start(args, fmt);
      /*处理printk流程的主要函数*/
      r = vprintk(fmt, args);
      /*va_end函数貌似是空函数*/
      va_end(args);
 
      return r;
   }
   
   下面就接着看vprintk函数的处理流程,vprintk实现printk的主要操作。
 

asmlinkage int vprintk(const char *fmt, va_list args)
{
    int printed_len = 0;
    int current_log_level = default_message_loglevel; //printk函数的默认输出级别

    unsigned long flags;
    int this_cpu;
    char *p;

    boot_delay_msec();

    /*禁止内核抢占*/
    preempt_disable();
    /* This stops the holder of console_sem just where we want him */
    raw_local_irq_save(flags);
    this_cpu = smp_processor_id();

    /*
     * Ouch, printk recursed into itself!
     */

     /*这一段的代码没有理解的很明白。。。*/
    /*printk的递归调用 ? 是在说printk的时候发生oops吗? */
    if (unlikely(printk_cpu == this_cpu)) {
        /*
         * If a crash is occurring during printk() on this CPU,
         * then try to get the crash message out but make sure
         * we can't deadlock. Otherwise just return to avoid the
         * recursion and return - but flag the recursion so that
         * it can be printed at the next appropriate moment:
         */

         //oops_in_progress只有在panic函数中才为1
        if (!oops_in_progress) {
            recursion_bug = 1;
            goto out_restore_irqs;
        }
        /*如果在printk运行时,这个CPU崩溃,确信不能死锁,10秒1次初始化锁logbuf_lock和console_sem,留时间给控制台打印完全的oops信息*/
        zap_locks();
    }

    lockdep_off();
    spin_lock(&logbuf_lock);
    printk_cpu = this_cpu;

    if (recursion_bug) {
        recursion_bug = 0;
        strcpy(printk_buf, recursion_bug_msg);
        printed_len = strlen(recursion_bug_msg);
    }
    /* Emit the output into the temporary buffer */
    /*将要输出的字符串按照fmt中的格式编排好,放入printk_buf中,并返回应该输出的字符个数*/
    printed_len += vscnprintf(printk_buf + printed_len,
                 sizeof(printk_buf) - printed_len, fmt, args);


    /*
     * Copy the output into log_buf. If the caller didn't provide
     * appropriate log level tags, we insert them here
     */

     /*拷贝printk_buf数据到环形缓冲区中,如果调用者没有提供合适的日志级别,则插入默认级别*/
     /*拷贝的过程由函数emit_log_char实现,每次拷贝一个字节*/

     for (p = printk_buf; *p; p++) {
        if (new_text_line) {
            /* If a token, set current_log_level and skip over */
            if (p[0] == '<' && p[1] >= '0' && p[1] <= '7' &&
             p[2] == '>') {
                current_log_level = p[1] - '0';
                p += 3;
                printed_len -= 3;
            }

            /* Always output the token */
            emit_log_char('<');
            emit_log_char(current_log_level + '0');
            emit_log_char('>');
            printed_len += 3;
            new_text_line = 0;

            /*如果设置了此选项,则在每一条printk信息前都要加上时间参数*/
            if (printk_time) {
                /* Follow the token with the time */
                char tbuf[50], *tp;
                unsigned tlen;
                unsigned long long t;
                unsigned long nanosec_rem;

                t = cpu_clock(printk_cpu);
                nanosec_rem = do_div(t, 1000000000);
                tlen = sprintf(tbuf, "[%5lu.%06lu] ",
                        (unsigned long) t,
                        nanosec_rem / 1000);

                for (tp = tbuf; tp < tbuf + tlen; tp++)
                    emit_log_char(*tp);
                printed_len += tlen;
            }

            if (!*p)
                break;
        }

        emit_log_char(*p);
        if (*p == '/n')
            new_text_line = 1;
    }

    /*
     * Try to acquire and then immediately release the
     * console semaphore. The release will do all the
     * actual magic (print out buffers, wake up klogd,
     * etc).
     *
     * The acquire_console_semaphore_for_printk() function
     * will release 'logbuf_lock' regardless of whether it
     * actually gets the semaphore or not.
     */

    /*
    * 网上有这样一句话,要想对console进行操作,必须获取console结构的信号量。如果获取信号量,则可以通过log the output and call the console

    * drivers. 反之,则place the output into the log buff and return. 现有的信号量holeder在release函数中将会注意到
    * 新的output,在释放信号量前将会把output信息发送给console 

    */
    /*
    * 在acquire_console_semaphore_for_printk函数的注释中有这样一句话:此函数被调用时拥有logbuf_lock的自旋锁,并且处于禁止中断状态

    *  在返回时(无论成功get sem)应保证logbuf_lock的自旋锁被释放,但是仍然禁止中断

    */
    if (acquire_console_semaphore_for_printk(this_cpu))
        /*此函数将log_buf中的内容发送给console,并且唤醒klogd*/
        release_console_sem();

    lockdep_on();
out_restore_irqs:
    raw_local_irq_restore(flags);

    preempt_enable();
    return printed_len;
}

(自己直接贴代码总是不能对齐,还在抱怨uc对于代码支持不好呢。。第一次用这个粘贴代码。。。)

   对于printk来说,一共有两个缓冲区printk_buf以及log_buf,前者有种临时缓冲的意思,后者用来存储最终要输出的字符串。后面将详细说一下其中最主要的log_buf。

   对于vscnprintf函数来说,其就是最终通过vsnprintf()函数将printk的参数根据fmt格式进行转换,并将转换的结果暂存到printk_buf中,最终又将printk_buf中的数据保存到log_buf中。
    下面在讨论往log_buf缓冲区写数据的函数emit_log_char之前,先简要说一下printk中的log_buf缓冲区。

/*
 * The indices into log_buf are not constrained to log_buf_len - they
 * must be masked before subscripting
 */

static unsigned log_start;    /* Index into log_buf: next char to be read by syslog() */
static unsigned con_start;    /* Index into log_buf: next char to be sent to consoles */
static unsigned log_end;    /* Index into log_buf: most-recently-written-char + 1 */

   其中的log_end标志着下一个写入的位置,其是上一次写的末尾+1;而log_start和con_start则是syslog和consoles读取数据的起始位置。在缓冲区写入的时候正是通过这三个变量以及C语言的特性完成环形的实现。

   下面看一下写缓冲区的具体函数实现。

static void emit_log_char(char c)
{
    LOG_BUF(log_end) = c;
    log_end++;
    if (log_end - log_start > log_buf_len)
        log_start = log_end - log_buf_len;
    if (log_end - con_start > log_buf_len)
        con_start = log_end - log_buf_len;
    if (logged_chars < log_buf_len)
        logged_chars++;
}

   这个写入满足每次只写入一个字符,通过LOG_BUF将字符c赋值给缓冲区,通过后面的长度变化来实现环形的概念。其中的LOG_BUF是这样定义的:

   #define LOG_BUF(idx) (log_buf[(idx) & LOG_BUF_MASK])
   在写入时根据log_end的大小mod缓冲区长度,获取最终的写入位置。

   环形缓冲区在字面看来就是一个数组 static char __log_buf[__LOG_BUF_LEN];其长度一般为4096大小(内核可修改)。而log_end长度为unsigned long范围,远远大于数组的大小,对于每一个字符的赋值log_end则只管++,在加一之后进行判断,如果log_end的值大于log_start,则表示缓冲区的长度已经达到最大,下一次的写入就将覆盖之前最旧的位置,因此log_start = log_end - log_buf_len,将log_start的位置向后移一位(因为每次只写入一个字符,不可能超过一位)。log_end和log_start通过unsigned long的自然溢出来实现环形的判断,而对其中每一次赋值则不再考虑环形的实现形式。(罗里啰嗦了这么多,也不知道能不能看明白,不过我是明白了。。。感觉方法挺巧的。。)

   函数的最后,则是release_console_sem函数,在此函数中完成console相关的操作。主要过程就是将con_start与log_end间的数据通过call_console_drivers函数来完成数据往控制台的传递,并且在最后环形klogd进程。

   而call_console_drivers函数则是遍历内核中的console链表console_driver,对于其中的每一个console结构,调用其注册的write函数。

   这两个函数的代码都比较简单,就不再多说了。

void release_console_sem(void)
{
    unsigned long flags;
    unsigned _con_start, _log_end;
    unsigned wake_klogd = 0;

    if (console_suspended) {
        up(&console_sem);
        return;
    }

    console_may_schedule = 0;

    for ( ; ; ) {
        spin_lock_irqsave(&logbuf_lock, flags);
        wake_klogd |= log_start - log_end;
        if (con_start == log_end)
            break;            /* Nothing to print */
        _con_start = con_start;
        _log_end = log_end;
        con_start = log_end;        /* Flush */
        spin_unlock(&logbuf_lock);
        stop_critical_timings();    /* don't trace print latency */
        call_console_drivers(_con_start, _log_end);
        start_critical_timings();
        local_irq_restore(flags);
    }
    console_locked = 0;
    up(&console_sem);
    spin_unlock_irqrestore(&logbuf_lock, flags);
    if (wake_klogd)
        wake_up_klogd();
}

   call_console_drivers函数在最终是通过__call_console_drivers函数来实现的。

/*
 * Call the console drivers on a range of log_buf
 */

static void __call_console_drivers(unsigned start, unsigned end)
{
    struct console *con;

    for (con = console_drivers; con; con = con->next) {
        if ((con->flags & CON_ENABLED) && con->write &&
                (cpu_online(smp_processor_id()) ||
                (con->flags & CON_ANYTIME)))
            con->write(con, &LOG_BUF(start), end - start);
    }
}


   至此,整个printk的实现流程就已经结束了,并不复杂,流程比较清晰,嘿嘿。

   圣诞节一个人在公司加班写博客,oye!

 

posted on 2011-01-06 09:35  SunBo 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/sunyubo/archive/2011/01/06/2282076.html

  • 0
    点赞
  • 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、付费专栏及课程。

余额充值