以下內容主要摘錄自《Linux安全體系分析與編程》
1、基本原理
(1)在UBOOT裡設置console=ttySAC0或者console=tty1內核就會根據命令行參數來找到對應的硬件操作函數,並將信息通過對應的硬件終端打印出來!
2、printk及控制台的日志級別
函數printk的使用方法和printf相似,用於內核打印消息。printk根據日志級別(loglevel)對消息進行分類。
相似的在android中有使用Log函數進行調試信息的打印。
日志級別用宏定義,日志級別宏展開為一個字符串,在編譯時由預處理器將它和消息文本拼接成一個字符串,因此printk 函數中日志級別宏和格式字符串間不能有逗號。
下面是兩個printk的例子,一個用於打印調試信息,另一個用於打印臨界條件信息。
printk(KERN_DEBUG "Here I am: %s:%i/n", _ _FILE_ _, _ _LINE_ _); printk(KERN_CRIT "I'm trashed; giving up on %p/n", ptr);printk的日志級別定義如下(在linux2.6和3.14/include/linux/kernel.h中):
#defineKERN_EMERG"<0>"/*緊急事件消息,系統崩潰之前提示,表示系統不可用*/ #defineKERN_ALERT"<1>"/*報告消息,表示必須立即采取措施*/ #defineKERN_CRIT"<2>"/*臨界條件,通常涉及嚴重的硬件或軟件操作失敗*/ #defineKERN_ERR"<3>"/*錯誤條件,驅動程序常用KERN_ERR來報告硬件的錯誤*/ #defineKERN_WARNING"<4>"/*警告條件,對可能出現問題的情況進行警告*/ #defineKERN_NOTICE"<5>"/*正常但又重要的條件,用於提醒。常用於與安全相關的消息*/ #defineKERN_INFO"<6>"/*提示信息,如驅動程序啟動時,打印硬件信息*/ #defineKERN_DEBUG"<7>"/*調試級別的消息*/ extern int console_printk[]; #define console_loglevel (console_printk[0]) #define default_message_loglevel (console_printk[1]) #define minimum_console_loglevel (console_printk[2]) #define default_console_loglevel (console_printk[3])
/*沒有定義日志級別的printk使用下面的默認級別*/ #define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING 警告條件*/
/* 顯示比這個級別更重發的消息*/
#define MINIMUM_CONSOLE_LOGLEVEL 1 /*可以使用的最小日志級別*/
#define DEFAULT_CONSOLE_LOGLEVEL 7 /*比KERN_DEBUG 更重要的消息都被打印*/
int console_printk[4] = {
DEFAULT_CONSOLE_LOGLEVEL,/*控制台日志級別,優先級高於該值的消息將在控制台顯示*/
/*默認消息日志級別,printk沒定義優先級時,打印這個優先級以上的消息*/
DEFAULT_MESSAGE_LOGLEVEL,
/*最小控制台日志級別,控制台日志級別可被設置的最小值(最高優先級)*/
MINIMUM_CONSOLE_LOGLEVEL,
DEFAULT_CONSOLE_LOGLEVEL,/* 默認的控制台日志級別*/
};
#cat /proc/sys/kernel/printk 7 4 1 7
# echo "4 4 1 7" > /proc/sys/kernel/printk這裡有個問題需要說一下,在一些開發板提供的內核源碼printk的默認打印級別都是未經修改的,例如飛凌的開發板提供的內核,在實際使用時有一些驅動會輸出一些調試信息,在實際使用時這些信息會影響正常的用戶使用,比如飛凌開發板連接網線後串口終端會定時輸出以下內容:
eth0: link down eth0: link up, 100Mbps, full-duplex, lpa 0x4DE1以上信息並不是錯誤,只是在打印調試信息,說明網絡正常連接,但這樣會干擾串口輸入,所以在編譯內核時,修改printk.c中的調試級別如下即可解決這個問題:
int console_printk[4] = {
DEFAULT_MESSAGE_LOGLEVEL,/*控制台日志級別,優先級高於該值的消息將在控制台顯示*/
/*默認消息日志級別,printk沒定義優先級時,打印這個優先級以上的消息*/
DEFAULT_MESSAGE_LOGLEVEL,/*最小控制台日志級別,控制台日志級別可被設置的最小值(最高優先級)*/
MINIMUM_CONSOLE_LOGLEVEL,
DEFAULT_CONSOLE_LOGLEVEL,/* 默認的控制台日志級別*/
};
環形緩沖區__log_buf在使用之前就是已定義好的全局變量,緩沖區的長度為1 << CONFIG_LOG_ BUF_SHIFT。變量CONFIG_LOG_BUF_SHIFT在內核編譯時由配置文件定義,對於i386平台,其值定義如下(在 linux2.6/arch/i386/defconfig中):
CONFIG_LOG_BUF_SHIFT=18
在內核編譯時,編譯器根據配置文件的設置,產生如下的宏定義:
#define CONFIG_LOG_BUF_SHIFT 18
環形緩沖區__log_buf定義如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/printk.c中):
#define __LOG_BUF_LEN(1 << CONFIG_LOG_BUF_SHIFT) //定義環形緩沖區的長度,i386平台為 static char __log_buf[__LOG_BUF_LEN]; //printk的環形緩沖區 static char *log_buf = __log_buf; static int log_buf_len = __LOG_BUF_LEN; /*互斥鎖logbuf_lock保護log_buf、log_start、log_end、con_start和logged_chars */ static DEFINE_SPINLOCK(logbuf_lock);
為了指明環形緩沖區__log_buf字符讀取位置,定義了下面的位置變量:
static unsigned long log_start;/*系統調用syslog讀取的下一個字符*/
static unsigned long con_start;/*送到控制台的下一個字符*/
static unsigned long log_end;/*最近已寫字符序號加1 */
static unsigned long logged_chars; /*自從上一次read+clear 操作以來產生的字符數*/
任何地方的內核調用都可以調用函數printk打印調試、安全、提示和錯誤消息。函數printk嘗試得到控制台信號量(console_sem),如果得到,就將信息輸出到環形緩沖區__log_buf中,然後函數release_console_sem()在釋放信號 量之前把環形緩沖區中的消息送到控制台,調用控制台驅動程序顯示打印的信息。如果沒得到信號量,就只將信息輸出到環形緩沖區後返回。
函數printk列出如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/printk.c中):
asmlinkage int printk(const char *fmt, ...)
{
va_list args;
int r;
va_start(args, fmt);
r = vprintk(fmt, args);
va_end(args);
return r;
}
asmlinkage int vprintk(const char *fmt, va_list args)
{
unsigned long flags;
int printed_len;
char *p;
static char printk_buf[1024];
static int log_level_unknown = 1;
preempt_disable(); //關閉內核搶占
if (unlikely(oops_in_progress) && printk_cpu == smp_processor_id())
/*如果在printk運行時,這個CPU發生崩潰,
確信不能死鎖,10秒1次初始化鎖logbuf_lock和console_sem,留時間
給控制台打印完全的oops信息*/
zap_locks();
local_irq_save(flags); //存儲本地中斷標識
lockdep_off();
spin_lock(&logbuf_lock);
printk_cpu = smp_processor_id();
/*將輸出信息發送到臨時緩沖區printk_buf */
printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args);
/*拷貝printk_buf數據到循環緩沖區,如果調用者沒提供合適的日志級別,插入默認值*/
for (p = printk_buf; *p; p++) {
if (log_level_unknown) {
/* log_level_unknown signals the start of a new line */
if (printk_time) {
int loglev_char;
char tbuf[50], *tp;
unsigned tlen;
unsigned long long t;
unsigned long nanosec_rem;
/*在時間輸出之前強制輸出日志級別*/
if (p[0] == '<' && p[1] >='0' &&
p[1] <= '7' && p[2] == '>') {
loglev_char = p[1]; //獲取日志級別字符
p += 3;
printed_len -= 3;
} else {
loglev_char = default_message_loglevel
+ '0';
}
t = printk_clock();//返回當前時鐘,以ns為單位
nanosec_rem = do_div(t, 1000000000);
tlen = sprintf(tbuf,
"<%c>[%5lu.%06lu] ",
loglev_char,
(unsigned long)t,
nanosec_rem/1000);//寫入格式化後的日志級別和時間
for (tp = tbuf; tp < tbuf + tlen; tp++)
emit_log_char(*tp); //將日志級別和時間字符輸出到循環緩沖區
printed_len += tlen;
} else {
if (p[0] != '<' || p[1] < '0' ||
p[1] > '7' || p[2] != '>') {
emit_log_char('<');
emit_log_char(default_message_loglevel
+ '0'); //輸出字符到循環緩沖區
emit_log_char('>');
printed_len += 3;
}
}
log_level_unknown = 0;
if (!*p)
break;
}
emit_log_char(*p);//將其他printk_buf數據輸出到循環緩沖區
if (*p == '/n')
log_level_unknown = 1;
}
if (!down_trylock(&console_sem)) {
/*擁有控制台驅動程序,降低spinlock並讓release_console_sem()打印字符 */
console_locked = 1;
printk_cpu = UINT_MAX;
spin_unlock(&logbuf_lock);
/*如果CPU准備好,控制台就輸出字符。函數cpu_online檢測CPU是否在線,
函數have_callable_console()檢測是否
有注冊的控制台啟動時就可以使用*/
if (cpu_online(smp_processor_id()) || have_callable_console()) {
console_may_schedule = 0;
release_console_sem();
} else {
/*釋放鎖避免刷新緩沖區*/
console_locked = 0;
up(&console_sem);
}
lockdep_on();
local_irq_restore(flags); //恢復本地中斷標識
} else {
/*如果其他進程擁有這個驅動程序,本線程降低spinlock,
允許信號量持有者運行並調用控制台驅動程序輸出字符*/
printk_cpu = UINT_MAX;
spin_unlock(&logbuf_lock);
lockdep_on();
local_irq_restore(flags); //恢復本地中斷標識
}
preempt_enable(); //開啟搶占機制
return printed_len;
}
void release_console_sem(void)
{
unsigned long flags;
unsigned long _con_start, _log_end;
unsigned long wake_klogd = 0;
for ( ; ; ) {
spin_lock_irqsave(&logbuf_lock, flags);
wake_klogd |= log_start - log_end;
if (con_start == log_end)
break;/* 沒有需要打印的數據*/
_con_start = con_start;
_log_end = log_end;
con_start = log_end;/* Flush */
spin_unlock_irqrestore(&logbuf_lock, flags);
//調用控制台driver的write函數寫入到控制台
call_console_drivers(_con_start, _log_end);
}
console_locked = 0;
console_may_schedule = 0;
up(&console_sem);
spin_unlock_irqrestore(&logbuf_lock, flags);
if (wake_klogd && !oops_in_progress && waitqueue_active(&log_wait))
wake_up_interruptible(&log_wait);//喚醒在等待隊列上的進程
}
函數_call_console_drivers將緩沖區中從start到end - 1的數據輸出到控制台進行顯示。在輸出數據到控制台之前,它檢查消息的日志級別。只有日志級別小於控制台日志級別console_loglevel的消 息,才能交給控制台驅動程序進行顯示。
static void _call_console_drivers(unsigned long start,
unsigned long end, int msg_log_level)
{
//日志級別小於控制台日志級別的消息才能輸出到控制台
if ((msg_log_level < console_loglevel || ignore_loglevel) &&
console_drivers && start != end) {
if ((start & LOG_BUF_MASK) > (end & LOG_BUF_MASK)) {
/* 調用控制台驅動程序的寫操作函數 */
__call_console_drivers(start & LOG_BUF_MASK, log_buf_len);
__call_console_drivers(0, end & LOG_BUF_MASK);
} else {
__call_console_drivers(start, end);
}
}
}
static void __call_console_drivers(unsigned long start, unsigned long 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); //調用驅動程序的寫操作函數
}
}
4、sys_syslog系統調用
asmlinkage long sys_syslog(int type, char __user * buf, int len)
{
return do_syslog(type, buf, len);
}
int do_syslog(int type, char __user *buf, int len)
{
unsigned long i, j, limit, count;
int do_clear = 0;
char c;
int error = 0;
error = security_syslog(type); //檢查是否調用這個函數的權限
if (error)
return error;
switch (type) {
case 0:/* 關閉日志 */
break;
case 1:/* 打開日志*/
break;
case 2:/*讀取日志信息*/
error = -EINVAL;
if (!buf || len < 0)
goto out;
error = 0;
if (!len)
goto out;
if (!access_ok(VERIFY_WRITE, buf, len)) { //驗證是否有寫的權限
error = -EFAULT;
goto out;
}
//當log_start - log_end為0時,表示環形緩沖區無數據可讀,把當前進程放入
等待隊列log_wait
error = wait_event_interruptible(log_wait, (log_start - log_end));
if (error)
goto out;
i = 0;
spin_lock_irq(&logbuf_lock);
while (!error && (log_start != log_end) && i < len) {
c = LOG_BUF(log_start); //從環形緩沖區得到讀取位置log_start
log_start++;
spin_unlock_irq(&logbuf_lock);
error = __put_user(c,buf); //將c地址的字符傳遞到用戶空間的buf中
buf++;
i++;
cond_resched(); //條件調度,讓其他進程有運行時間
spin_lock_irq(&logbuf_lock);
}
spin_unlock_irq(&logbuf_lock);
if (!error)
error = i;
break;
case 4:/* 讀/清除上一次內核消息*/
do_clear = 1;
/* FALL THRU */
case 3:/*讀取上一次內核消息*/
error = -EINVAL;
if (!buf || len < 0)
goto out;
error = 0;
if (!len) //讀取長度為0
goto out;
if (!access_ok(VERIFY_WRITE, buf, len)) { //驗證有寫權限
error = -EFAULT;
goto out;
}
count = len;
if (count > log_buf_len)
count = log_buf_len;
spin_lock_irq(&logbuf_lock);
if (count > logged_chars) // logged_chars是上次讀/清除以來產生的日志字符數
count = logged_chars;
if (do_clear)
logged_chars = 0;
limit = log_end;
/* __put_user() 可以睡眠,當__put_user睡眠時,printk()可能覆蓋寫正在
拷貝到用戶空間的消息,因此,這些消息被反方向拷貝,將buf覆蓋部分的數據重寫到buf的起始位置*/
for (i = 0; i < count && !error; i++) { //讀取count個字符
j = limit-1-i;
if (j + log_buf_len < log_end)
break;
c = LOG_BUF(j); //從環形緩沖區得到讀取位置j
spin_unlock_irq(&logbuf_lock);
//將c位置的字符傳遞到用戶空間的buf中,如果發生錯誤,將發生錯誤的c位置給error
error = __put_user(c,&buf[count-1-i]);
cond_resched();
spin_lock_irq(&logbuf_lock);
}
spin_unlock_irq(&logbuf_lock);
if (error)
break;
error = i;
if (i != count) { //表示__put_user沒有拷貝完成
int offset = count-error;
/* 拷貝期間緩沖區溢出,糾正用戶空間緩沖區*/
for (i = 0; i < error; i++) {
if (__get_user(c,&buf[i+offset]) ||
__put_user(c,&buf[i])) { //將覆蓋部分的數據
重寫到buf的起始位置
error = -EFAULT;
break;
}
cond_resched();
}
}
break;
case 5:/* 清除環形緩沖區*/
logged_chars = 0;
break;
case 6:/*關閉向控制台輸出消息*/
console_loglevel = minimum_console_loglevel;
break;
case 7:/*開啟向控制台輸出消息*/
console_loglevel = default_console_loglevel;
break;
case 8:/* 設置打印到控制台的日志級別*/
error = -EINVAL;
if (len < 1 || len > 8)
goto out;
if (len < minimum_console_loglevel)
len = minimum_console_loglevel;
console_loglevel = len;
error = 0;
break;
case 9:/* 得到日志消息所占緩沖區的大小*/
error = log_end - log_start;
break;
case 10:/*返回環形緩沖區的大小*/
error = log_buf_len;
break;
default:
error = -EINVAL;
break;
}
out:
return error;
}