Linux上的核心訊息紀錄機制解析

Posted by Lawpig on 5月 30, 2017 | No comments
Linux上的核心訊息紀錄機制解析


一,前言

                     在linux的環境裡,系統訊息紀錄機制一直都是系統核心程式開發人員相當重要的參考資訊,了解核心訊息傳遞的機制也就成為要在Linux撰寫核心程式碼時,所需要去面對的課題,透過這篇文章筆者將為各位介紹Linux核心的訊息傳遞函式,並介紹一些簡單的方法,讓程式開發者可以更容易的控制核心訊息的導出.

                     這次的文章主要會使用到兩個Linux上的套件,分別為sysklogd-1.4.1.tar.gz與util-linux-2.11r.tar.gz,各位可以根據自己的需求下載不同的套件版本.

                     sysklogd套件主要包含兩個訊息紀錄程式,一個是klogd(Kernel Log Daemon),另一個為 syslogd(System Log Daemon),兩個工具主要的不同在於klogd是紀錄Linux 核心訊息與Linux核心模組訊息,每當核心程式呼叫printk時,就可以由這個User-Mode的klogd程式來負責把此時的核心訊息紀錄下來. 而syslogd則是負責User-Mode程式所需紀錄的系統訊息(例如紀錄在/var/log/messages的系統訊息).


                     Util-linux套件包含許多有用的系統工具軟體(例如,arch(取得硬體架構),ipcs(取得目前Process間通訊機制的資訊)..etc),我們主要使用的為dmesg,透過dmesg可以在User-Mode動態的查詢目前系統的核心訊息.


                     接下來要開始本次文章所要介紹的內容了,筆者所採用的Linux核新版本為2.4.17,在本篇文章的範圍中我將以此版本做為介紹的依據.


二,運作概念


核心紀錄訊息的Ring Buffer

                     Linux的核心訊息主要是透過一個環形的記憶體方式來儲存的,為了要維持這樣的記憶體空間,我們可以在 src/kernel/printk.c 當中看到以下幾個變數

(1)log_buf==>宣告Linux核心的訊息儲存空間,大小由參數LOG_BUF_LEN決定,在筆者使用的2.4.17版本中,預設的大小為32768kbytes.
(2)log_start==>指向下個要被System Call “sys_syslog” 讀出的字元.
(使用者程式可以透過sys_syslog取得新的核心訊息)
(3)con_start==>指向下個要秀出到console的字元. 在linux的環境中printk的訊息預設是會透過console裝置輸出的,也就是使用者在電腦前使用的console裝置,如果要在console以外取得核心訊息,就必需要透過sys_syslog呼叫來取得才行.
(4)log_end==>指向log_buf訊息紀錄的尾端,當下個訊息要被加入時,就會由此加入,log_end的位址就會變成(寫入訊息的長度+1+log_end),如果這個值超過log_buf所宣告的記憶體空間,就會重新回到log_buf的啟始點寫入.
(5)logged_chars==>紀錄在上回讀取或清除核心訊息(透過呼叫sys_syslog)之後,有多少新的核心訊息尚未處理.


                     如下圖(一)所示,Ring Buffer的運作為在log_buf所配置的記憶體空間裡,log_start紀錄著下一筆要透過sys_syslog讀出的訊息位址,con_start紀錄著下一筆要在使用者電腦console前輸出的訊息位址,log_end紀錄著可供下一筆訊息寫入的位址,如果使用者寫入log_buf的訊息超過了log_buf所配置的記憶體空間,就會從log_buf的起始位址繼續寫入. 不過由於log_start,log_end都是會不斷的增加,所以在log_buf中定址時,會與LOG_BUF_MASK作 “&” 運算,讓log_start,log_end的值只在0-LOG_BUF_LEN的範圍內.

                     如圖(二),為(log_end&LOG_BUF_MASK) 超過了log_buf的記憶體空間後,又由log_buf起始點開始寫入,如此就如同一個環形,可以不斷的把訊息寫入,並且維持一個最大為LOG_BUF_LEN大小的訊息儲存空間.
圖(一),核心訊息Ring Buffer說明-1
圖(二),核心訊息Ring Buffer說明-2



klogd與dmesg運作

                     klogd與dmesg主要用到兩個System Call,分別為sys_syslog(80號中斷,ah=103)與sys_get_kernel_syms(80號中斷,ah=130).這兩個Linux System Call會定義在src/arch/i386/kernel/entry.S中,如下所示

.long SYMBOL_NAME(sys_syslog)  /* 103 */

.long SYMBOL_NAME( sys_get_kernel_syms )  /* 130 */

                     其中sys_syslog被實作在src/kernel/printk.c中(sys_syslog主要是呼叫函式do_syslog來完成的),在筆者使用的2.4.17核心當中,系統紀錄訊息的空間大小是由printk.c中的變數LOG_BUF_LEN所決定的,筆者編譯的預設值為32768Kbytes,執行dmesg指令時預設傳回的大小為16392Kbytes,各位可以透過 “dmesg -s” 來指定所要取得的核心訊息大小,但每次查詢訊息時,至少都會傳回4Kbytes大小的核心訊息,只要是核心模組透過printk()所秀出的訊息,都可以透過sys_syslog來取得. 如下所示,為指令dmesg的部份程式碼,它會檢查是否查詢的訊息大小小於4Kbytes,如果小於則設為4Kbytes,大於4Kbytes則繼續執行,之後透過malloc配置記憶體,再執行klogctl(也就是透過glibc再去呼叫sys_syslog)取得核心訊息

if (bufsize < 4096)
   bufsize = 4096;
buf = (char*)malloc(bufsize);
n = klogctl( cmd, buf, bufsize );
if (n < 0)
   {
   perror( “klogctl” );
   exit( 1 );
   }


                     sys_get_kernel_syms則是實作在src/kernel/module.c中,主要的功能是取得所有核心模組的訊息符號表,包括所有Linux核心模組的名稱和所提供的函式名稱.取得的機制為核心透過變數module_list得到核心模組串列的首位(mod=module_list),再依序 “mod=mod->next”的取得所有的核心模組的資訊,核心模組間彼此參考的資料結構如下圖(三)所示

圖(三),核心模組的資料結構示意圖

                     透過module_list我們可以取的Linux環境中所有核心模組的資料串列,進而取得所有與核心模組有關的訊息. 實際上module_list所串起的為一連串的 “struct module”,透過這些接續的module結構,就可以找出現在Linux核心當中有使用那一些核心模組與核心符號資訊.

                 有關於Linue 核心模組的相關知識,各位可以參考我過去的文章”Linux 動態載入 Module 介紹”<http://www.linuxfab.cx/indexColumnData.php?CID=84&FIRSTHIT=1>,裡面有詳細的介紹.

                    


                     接下來筆者把重點放在klogd部份的說明,既然klogd主要是負則核心訊息的紀錄截取工作,我們也就有必要進一步的透過了解klogd來知悉獲許核心訊息的方式. Klogd啟始後,會呼叫函式InitKsyms(),讀取/boot/System.map檔案,取得目前正在運作的Linux核心所有的符號表,並且與現在系統正在運作的Linux核心版本作一個比較,如果說/boot/System.map 所屬的Linux核心版本與目前系統正在運作的核心版本並不一致的話,那就會再去其它路徑尋找System.map檔案(例如:/System.map),直到找到System.map核心版本與目前正在運作的核心一致,才會把該符號資訊檔案載入,不然的話就會產生錯誤訊息”Cannot find map file.”.

                     接下來透過InitMsyms()函式呼叫Linux System Call “sys_get_kernel_syms”,透過這個System Call我們可以得到所有的Module Name以及 Module所包含的所有Symbol Name.

                     由於 sys_get_kernel_syms 只定義了一個變數 ” struct kernel_sym *table “,所以說當User-Mode的程式呼叫這個System Call時,首先會把table設為0來呼叫,當table為0時,它會執行第一個for迴圈

for (mod = module_list, i = 0; mod; mod = mod->next)
                            {
                            /* include the count for the module name! */
                            i += mod->nsyms + 1;
                            }

                     算出目前系統中所有已載入記憶體的核心模組與每個核心模組所包含的符號數目(所謂的符號指的是核心模組的函式名稱與全域變數名稱),再由User-Mode的程式根據這些核心模組的數目來配置對應的記憶體空間,例如

(rtn=sys_get_kernel_syms所傳回的核心模組符號總數)

ksym_table = (struct kernel_sym *) malloc(rtn *  sizeof(struct kernel_sym));

                     之後再次呼叫sys_get_kernel_syms時,就會把配置好的table記憶體空間傳入,讓函式sys_get_kernel_syms把得到的核心模組名稱與所包含的核心模組符號傳給User-Mode的程式.

  
asmlinkage long sys_get_kernel_syms(struct kernel_sym *table)
{
                            struct module *mod;
                            int i;
                            struct kernel_sym ksym;
                           
                            lock_kernel();  
                           
                            for (mod = module_list, i = 0; mod; mod = mod->next)
                                                        {
                                                       /* include the count for the module name! */
                                                       i += mod->nsyms + 1;
                                                       }
                           
                            if (table == NULL)
                                                       goto out;
                            /* So that we don’t give the user our stack content */
                            memset (&ksym, 0, sizeof (ksym));
                            for (mod = module_list, i = 0; mod; mod = mod->next)
                                                       {
                                                       struct module_symbol *msym;
                                                       unsigned int j;
                                                       if (!MOD_CAN_QUERY(mod))
                                                                                   continue;
                                                       /* magic: write module info as a pseudo symbol */
                                                       ksym.value = (unsigned long)mod;
                                                       ksym.name[0] = ‘#';
                                                       strncpy(ksym.name+1, mod->name, sizeof(ksym.name)-1);
                                                       ksym.name[sizeof(ksym.name)-1] = ‘\0′;
                                                       //Copy those data to user mode memory
                                                       if (copy_to_user(table, &ksym, sizeof(ksym)) != 0)
                                                                                   goto out;                                       
                                                       ++i, ++table;
                                                       if (mod->nsyms == 0)
                                                                                   continue;
                                                       for (j = 0, msym = mod->syms; j < mod->nsyms; ++j, ++msym)
                                                                                   {
                                                                                   ksym.value = msym->value;
                                                                                   strncpy(ksym.name, msym->name, sizeof(ksym.name));
                                                                                   ksym.name[sizeof(ksym.name)-1] = ‘\0′;
                                                                                   //Copy those data to user mode memory
                                                                                   if (copy_to_user(table, &ksym, sizeof(ksym)) != 0)
                                                                                                               goto out;
                                                                                   ++i, ++table;
                                                                                   }
                                                       }
out:
                            unlock_kernel();                         
                            return i;
}


                     之後,klogd會進入一個While(1)迴圈,如下圖(四)所示

圖(四),klogd Daemon的While迴圈運作示意圖


                     klogd常駐在記憶體後會透過這個while迴圈持續的執行工作,如下所述為迴圈內主要的函式說明

(1)LogKernelLine==>呼叫Linux System Call “sys_syslog”(80號中斷,ah=103),使用參數2,為讀取最新透過printk()所秀出的訊息,如下圖(五)所示 ksyslog 被宣告為 sys_syslog,或是klogctl(是glibc提供的函式,同樣為呼叫sys_syslog)來完成.


圖(五),ksyslog的宣告

(2)LogProcLine==>在這個函式裡,會透過讀取/proc/kmsg 檔案來取得每次透過printk()所秀出的訊息,也就是說,如果各位在自己登入的終端機前執行”more /proc/kmsg” 就可以把每次核心中呼叫printk()所顯示的訊息秀出在自己的中端機前.

                     “/proc/kmsg”的實作程式碼是在 src/fs/proc/kmsg.c,這段程式碼實作了以下四個函式”kmsg_read,kmsg_poll,kmsg_open,kmsg_release”,分別所實作的功能為

kmsg_read==>如以下程式碼,會透過呼叫do_syslog (所選擇的參數型態為2,也就是讀取新產生的核心訊息log),如果現在沒有新的核心訊息產生就會等待,直到有新的核心訊息產生,就會返回傳回新的核心訊息.

static ssize_t kmsg_read(struct file * file, char * buf, size_t count, loff_t *ppos)
{
                     return do_syslog(2,buf,count);
}

kmsg_poll==>如以下程式碼,會透過呼叫do_syslog (所選擇的參數型態為9,也就是傳回目前有多少未讀取的核心訊息),

static unsigned int kmsg_poll(struct file *file, poll_table * wait)
{
                     poll_wait(file, &log_wait, wait);
                     if (do_syslog(9, 0, 0))
                                          return POLLIN | POLLRDNORM;
                     return 0;
}

kmsg_open==>如以下程式碼,呼叫do_syslog,參數型態為1,為開啟核心訊息的紀錄.

static int kmsg_open(struct inode * inode, struct file * file)
{
                     return do_syslog(1,NULL,0);
}

kmsg_release==>如以下程式碼,呼叫do_syslog,參數型態為0,為關閉核心訊息的紀錄.

static int kmsg_release(struct inode * inode, struct file * file)
{
                     (void) do_syslog(0,NULL,0);
                     return 0;
}


(3)pause==>在這個函式中,會暫停程式的執行,直到程式接收到系統所傳送的signal為止.

                     因此,我們可以知道klogd主要可以透過兩種方式來取得核心訊息,一個為/proc/kmsg檔案,一個為System Call sys_syslog,在運作時klogd會選擇這兩個之一作為取得訊息的機制,首先會檢查/proc/kmsg是否存在,若無就會採使用sys_syslog,當然我們也可以透過加入 “-s” 參數,執行”klogd -s”讓klogd選擇透過sys_syslog取得核心訊息,而不採用/proc/kmsg.

訊息的導出


                     相信寫過核心模組的人一定都遇過同樣的問題,那就是printk()所秀出的訊息總是在Console前才可以看到,如果今天是透過遠端登入Linux環境想要寫核心模組的程式,就會面對看不到訊息的困擾,所以筆者在此提供一個小技巧,可以讓使用者很輕易的把訊息導到任何一個Character Device,只要執行以下的指令

klogd -f /dev/pts/0

                     這行的指令意義就是說,把klogd所紀錄到的核心訊息都寫入到指定的檔案中,而每個Telnet連線或是在X Windows上終端機其實都會對應到一個pts目錄下的檔案,因此透過這樣的方式,我們可以建立一個Telnet連線或是一個X Winows終端機後,再透過指令who去查看目前所在的pts號碼,執行klogd -f 把資料導到該檔案後,就可以在指定的終端機前秀出所有Kernel 的訊息了. 如下圖(六)所示

圖(六),透過klogd把訊息轉出


                            同理,我們既然知道klogd也是透過/proc/kmsg的檔案來取得核心訊息的,我們也可以如法泡製,當我們連進遠端主機時,如果想要知道核心訊息時也可以執行
“more /proc/kmsg” 這樣的話,就可以在遠端針對Linux主機進行Kernel Module的修改與除錯了.



三,結語

                    

                    
                     透過這次的文章,各位可以了解到Linux核心訊息使用機制,相對的也可以依據自己的需求把訊息轉換到所要顯現的裝置,不過我相信不是所有人都會對這些比較細節的部份感到興趣的,撰寫本文的目的就是希望可以提供一些個人認為值得了解的技術,讓日後從事不同方面程式的撰寫時,可以交互應用這些資訊,整合出更好的軟體產品.

                     如果對這篇文章有任何疑問的話,歡迎隨時來信與我聯繫.

                     我的E-Mail為hlchou@mail2000.com.tw.


四,參考資料
1,Linux動態載入Module介紹,
2,util-linux,http://www.kernel.org/pub/linux/utils/util-linux




                                                                                                                            




0 意見:

張貼留言