嵌入式Linux——kmsg:分析/proc/kmsg文件以及寫自己的/proc/mymsg【轉】


轉自:https://blog.csdn.net/w1107101310/article/details/80582314

簡介:

    本文主要分析/proc/kmsg文件的形成過程以及使用cat /proc/kmsg查看log_buf中的信息時所要經歷的代碼。並結合上面的分析寫自己的 /proc/mymsg和myprintk 。

 

 Linux內核:linux-2.6.22.6

 所用開發板:JZ2440 V3(S3C2440A)

聲明:

    本文主要是看完韋東山老師的視頻后,自己分析代碼所寫。同時我在寫這篇文章的時候也參考了一些網友的文章。我相信這樣可以讓我們的知識點變得更加全面。

分析/proc/kmsg:

    我們都知道當我們使用cat /proc/kmsg時,我們可以看到緩存在log_buf中的內容,那么/proc/kmsg文件是在什么時候創建的?同時我們是如何使用cat /proc/kmsg獲得log_buf中的信息,這就成了我們要問的問題了。下面我們會通過分析代碼來回答上面的問題。

    我先將上篇文章中的一幅圖引入這里,希望對大家有幫助:

 

    首先我們看/proc/kmsg文件是什么時候創建的。我們打開內核的文件我們會發現有fs(文件系統)文件,並在這個文件下面有proc文件,在proc文件下有proc_misc.c文件,我們看這個文件的入口函數:proc_misc_init

void __init proc_misc_init(void)
{
·······
#ifdef CONFIG_PRINTK
{
struct proc_dir_entry *entry;
entry = create_proc_entry("kmsg", S_IRUSR, &proc_root);
if (entry)
entry->proc_fops = &proc_kmsg_operations;
}
#endif
        ·······
}
    從上面看代碼可能的意思是,當我們定義了CONFIG_PRINTK時,我們就要在/porc目錄下創建一個kmsg的子條目,而子條目的文件操作函數為proc_kmsg_operations。下面我們慢慢分析代碼看是不是我說的這個意思。從上面程序看,先定義了一個proc_dir_entry的結構體。

/*
* 這個結構體不會完全的運用。但是他的想法是:
* 在內存中創建一個proc_dir_entries的樹(就像真實的/proc文件系統樹)
* 這樣我們就可以動態的添加一個新的文件到/proc
*/
struct proc_dir_entry {
unsigned int low_ino;
unsigned short namelen; //名字長度
const char *name; //名字
mode_t mode; //操作屬性
nlink_t nlink;
uid_t uid;
gid_t gid;
loff_t size;
const struct inode_operations *proc_iops;
const struct file_operations *proc_fops; //文件操作函數
get_info_t *get_info;
struct module *owner;
struct proc_dir_entry *next, *parent, *subdir; //下一個條目,父條目,子條目
void *data;
read_proc_t *read_proc;
write_proc_t *write_proc;
atomic_t count; /* use count */
int deleted; /* delete flag */
void *set;
};
    從上面proc_dir_entry的注釋我們可以知道,這個結構體就是要將我們注冊到/proc目錄下的子條目填寫到這個結構體中。而通過create_proc_entry函數來完成這個任務,同時create_proc_entry函數還會將這個子條目注冊到/proc目錄下,然后通過返回填好的proc_dir_entry結構體。我們現在看create_proc_entry函數:

struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent)
{
struct proc_dir_entry *ent;
nlink_t nlink;
        //對子條目的操作屬性進行檢查
if (S_ISDIR(mode)) {
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO | S_IXUGO;
nlink = 2;
} else {
if ((mode & S_IFMT) == 0)
mode |= S_IFREG;
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO;
nlink = 1;
}
        //創建一個名為name,屬性為mode,而他的父目錄為parent的子條目
ent = proc_create(&parent,name,mode,nlink);
if (ent) {
if (S_ISDIR(mode)) {
ent->proc_fops = &proc_dir_operations;
ent->proc_iops = &proc_dir_inode_operations;
}
if (proc_register(parent, ent) < 0) { //將這個子條目注冊進父目錄
kfree(ent);
ent = NULL;
}
}
return ent;
}
    在上面的代碼中我們看到了要設置這個子條目的操作屬性,那么他都有什么屬性那?我們看代碼:

#define S_IRWXU 00700 //用戶rwx:111
#define S_IRUSR 00400 //用戶rwx:100
#define S_IWUSR 00200 //用戶rwx:010
#define S_IXUSR 00100 //用戶rwx:001

#define S_IRWXG 00070 //同組用戶rwx:111
#define S_IRGRP 00040 //同組用戶rwx:100
#define S_IWGRP 00020 //同組用戶rwx:010
#define S_IXGRP 00010 //同組用戶rwx:001

#define S_IRWXO 00007 //其他用戶rwx:111
#define S_IROTH 00004 //其他用戶rwx:100
#define S_IWOTH 00002 //其他用戶rwx:010
#define S_IXOTH 00001 //其他用戶rwx:001
    從上面代碼看,他的屬性與我們在Linux中運用chmod命令來定義文件的操作屬性是一樣的,都是使用一個二進制位來表示讀寫可執行中的一個。

    而同時我們使用下面的代碼來判斷子條目的文件類型:

#define S_IFMT 00170000
#define S_IFSOCK 0140000 //是否是socket
#define S_IFLNK 0120000 //是否是連接
#define S_IFREG 0100000 //是否是寄存器
#define S_IFBLK 0060000 //是否是塊設備
#define S_IFDIR 0040000 //是否是文件
#define S_IFCHR 0020000 //是否是字符設備
#define S_IFIFO 0010000
    而通過對子條目的文件類型的判斷我們會為他加上合適的文件操作屬性。而通過上面的判斷我們的子條目為只讀屬性。

    而注冊完子條目,我們就要為他添加文件操作函數了。我們知道在Linux中一些皆文件。所以對這個子條目的操作其實就是要操作這個條目的文件操作函數:proc_kmsg_operations 。    

const struct file_operations proc_kmsg_operations = {
.read = kmsg_read,
.poll = kmsg_poll,
.open = kmsg_open,
.release = kmsg_release,
};
    我們看到上面的代碼是不是很熟悉啊,這就是我們以前寫字符驅動時的file_operations。而這里也是這個結構體。那么下面的講解就要簡單些了,因為道理都是相同的。我們在上面介紹了這個子條目為只讀屬性。所以我們主要在意的是使用 cat /proc/kmsg來對log_buf中的內容進行讀取。所以我們在這里主要看kmsg_read函數。

static ssize_t kmsg_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
if ((file->f_flags & O_NONBLOCK) && !do_syslog(9, NULL, 0)) //如果文件為非阻塞方式,並且log_buf中為空,我們返回錯誤
return -EAGAIN;
return do_syslog(2, buf, count); //否則我們調用do_syslog函數
}
    而同時我們發現其他幾個函數主要也是調用do_syslog函數。那么我們接下來最主要的就是對do_syslog函數進行分析了:

/*
* Commands to do_syslog:
*
* 0 -- Close the log. Currently a NOP.
* 1 -- Open the log. Currently a NOP.
* 2 -- Read from the log.
* 3 -- Read all messages remaining in the ring buffer.
* 4 -- Read and clear all messages remaining in the ring buffer
* 5 -- Clear ring buffer.
* 6 -- Disable printk's to console
* 7 -- Enable printk's to console
* 8 -- Set level of messages printed to console
* 9 -- Return number of unread characters in the log buffer
* 10 -- Return size of the log buffer
*/
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: /* Close log */
break;
case 1: /* Open log */
break;
case 2: /* Read from log */
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;
}
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++;
spin_unlock_irq(&logbuf_lock);
error = __put_user(c,buf);
buf++;
i++;
cond_resched();
spin_lock_irq(&logbuf_lock);
}
spin_unlock_irq(&logbuf_lock);
if (!error)
error = i;
break;
case 4: /* Read/clear last kernel messages */
do_clear = 1;
/* FALL THRU */
case 3: /* Read last kernel messages */
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;
}
count = len;
if (count > log_buf_len)
count = log_buf_len;
spin_lock_irq(&logbuf_lock);
if (count > logged_chars)
count = logged_chars;
if (do_clear)
logged_chars = 0;
limit = log_end;
/*
* __put_user() could sleep, and while we sleep
* printk() could overwrite the messages
* we try to copy to user space. Therefore
* the messages are copied in reverse. <manfreds>
*/
for (i = 0; i < count && !error; i++) {
j = limit-1-i;
if (j + log_buf_len < log_end)
break;
c = LOG_BUF(j);
spin_unlock_irq(&logbuf_lock);
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) {
int offset = count-error;
/* buffer overflow during copy, correct user buffer. */
for (i = 0; i < error; i++) {
if (__get_user(c,&buf[i+offset]) ||
__put_user(c,&buf[i])) {
error = -EFAULT;
break;
}
cond_resched();
}
}
break;
case 5: /* Clear ring buffer */
logged_chars = 0;
break;
case 6: /* Disable logging to console */
console_loglevel = minimum_console_loglevel;
break;
case 7: /* Enable logging to console */
console_loglevel = default_console_loglevel;
break;
case 8: /* Set level of messages printed to console */
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: /* Number of chars in the log buffer */
error = log_end - log_start;
break;
case 10: /* Size of the log buffer */
error = log_buf_len;
break;
default:
error = -EINVAL;
break;
}
out:
return error;
}
    其中do_syslog實現了對log_buf的操作,而我們可以用圖表示為:

 

    syslog 調用(在內核中調用 ./linux/kernel/printk.c 的 do_syslog)是一個相對較小的函數,它能夠讀取和控制內核環緩沖區。

    type 參數是用於傳遞所執行的命令,它指定了可選的緩沖區長度。有一些命令(如清除環緩沖)是忽略 buf 和 len 這兩個參數的。雖然前面兩個命令類型不會對內核進行任何操作,但是其余命令則是用於讀取日志消息或控制日志。其中有三個命令是用於讀取日志消息的。SYSLOG_ACTION_READ(2) 用於阻塞操作,直至日志消息到達后才釋放該操作,然后將它們返回到所提供的緩沖區。這個命令會處理這些消息(舊的消息將不會出現在這個命令的后續調用中)。SYSLOG_ACTION_READ_ALL(3) 命令會從日志讀取最后 n 個字符(而 n 是在傳遞給 klogctl 的參數 'len' 中定義的)。SYSLOG_ACTION_READ_CLEAR (4)命令會先執行 SYSLOG_ACTION_READ_ALL(3)操作,然后執行SYSLOG_ACTION_CLEAR(5)命令(清除環緩沖區)。SYSLOG_ACTION_CONSOLE ON(7) 和 OFF(6) 可以將日志級別設置為激活或禁用日志消息輸出到控制台,而 SYSLOG_CONSOLE_LEVEL(8) 則允許調用者定義控制台所接受的日志消息級別。最后,SYSLOG_ACTION_SIZE_BUFFER (10)是用於返回內核環緩沖區大小,而 SYSLOG_ACTION_SIZE_UNREAD (9)則返回當前內核環緩沖區可讀取的字符數。下表顯示了 SYSLOG 命令的完整清單。

 

命令/操作代碼 作用
SYSLOG_ACTION_CLOSE (0) 關閉日志(未實現)
SYSLOG_ACTION_OPEN (1) 打開日志(未實現)
SYSLOG_ACTION_READ (2) 從日志讀取
SYSLOG_ACTION_READ_ALL (3) 從日志讀取所有消息(非破壞地)
SYSLOG_ACTION_READ_CLEAR (4) 從日志讀取並清除所有消息
SYSLOG_ACTION_CLEAR (5) 清除環緩沖區
SYSLOG_ACTION_CONSOLE_OFF (6) Disable printks to the console
SYSLOG_ACTION_CONSOLE_ON (7) 激活控制台 printk
SYSLOG_ACTION_CONSOLE_LEVEL (8) 將消息級別設置為控制接受
SYSLOG_ACTION_SIZE_UNREAD (9) 返回日志中未讀取的字符數
SYSLOG_ACTION_SIZE_BUFFER (10) 返回內核環緩沖區大小
     文件 /proc/kmsg 實現了少數等同於內部 do_syslog 的文件操作。在內部,open 調用與 SYSLOG_ACTION_OPEN 有關,而 SYSLOG_ACTION_CLOSE 則與 release 有關(每一個調用都實現為一個 No Operation Performed [NOP])。這個輪循操作會等待文件活動的完成,然后才調用 SYSLOG_ACTION_SIZE_UNREAD 確定可以讀取的字符數。最后,read 操作會被映射到 SYSLOG_ACTION_READ,以處理可用的日志消息。

    上面描述來自:內核日志:API 及實現

    我們現在分析程序,我們主要是分析type為2時,從log_buf中讀取信息,程序為;

case 2: /* Read from log */
error = -EINVAL;
if (!buf || len < 0) //當用戶buf不存在或者要讀取的數據小於0時,表示出錯
goto out;
error = 0;
if (!len)
goto out;
if (!access_ok(VERIFY_WRITE, buf, len)) {
error = -EFAULT;
goto out;
}
error = wait_event_interruptible(log_wait,(log_start - log_end));//當log_start與log_end相等時,線程休眠,等待喚醒
if (error)
goto out;
i = 0;
spin_lock_irq(&logbuf_lock);
while (!error && (log_start != log_end) && i < len) { //當log_start與log_end不相等並且線程被喚醒
c = LOG_BUF(log_start); //從log_buf中讀出一字節數據
log_start++; //讀位置加一
spin_unlock_irq(&logbuf_lock);
error = __put_user(c,buf); //將讀出的數據放到buf中,然后發送到用戶空間,相當於copy_to_user函數
buf++;
i++;
cond_resched();
spin_lock_irq(&logbuf_lock);
}
spin_unlock_irq(&logbuf_lock);
if (!error)
error = i;
break
    上面就是一個讀數據的過程了,即先對要讀取的范圍等進行判斷,然后就是將log_buf中的數據讀出並發送到用戶空間。而do_syslog函數中的其他選項則主要是對log_start和log_end的判斷和操作了。

    我現在為大家解釋在cat /proc/dmsg 時我們為什么可以將log_buf中的信息打印出來。

 

    從上面圖中我們知道cat /proc/dmsg就相當於是:調用kmsg_open函數,然后在while循環中不斷調用kmsg_read函數。而有上面的分析我們知道這其實就是從log_buf中讀取數據。由於我們讀完一次后log_start指到與log_end相等的位置。所以我們只顯示一次的結果。

    而關於log_buf環形緩存區的知識我建議大家看:Linux-分析並制作環形緩沖區。

寫自己的/proc/mymsg:   

    好了,有了上面的知識我們就可以寫自己的/proc/mymsg了。我先在這里說一下我們的目標。我們的目標是:仿照printk建一個自己的環形緩沖區:mylog_buf,並在/proc下創建一個自己的mymsg子條目。當我們在其他驅動中加myprintk函數時,會將myprintk中要打印的信息放到mylog_buf中。如果想要看這些信息可以使用cat /proc/mymsg來查看存在mylog_buf中的信息。

    我們按着上面分析程序時的步驟來寫代碼。所以我們同樣要先申請注冊一個proc_dir_entry結構體。程序為:

struct proc_dir_entry *myentry;

static int mymsg_init(void)
{
/* 創建一個名為mymsg,屬性為S_IRUSR,而父類為proc_root的proc_dir_entry結構體,並注冊 */
myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
if (myentry)
myentry->proc_fops = &proc_mymsg_operations; /* 創建成功,加文件操作函數 */

return 0;
}
    我們既然定義了proc_dir_entry結構體的文件處理函數proc_mymsg_operations,我們就要完善它,所以:

static struct file_operations proc_mymsg_operations = {
.read = mymsg_read, //read函數
};
    我們現在本應該要完善mymsg_read函數,但是我們知道read函數是要操作到mylog_buf上的,而這里我們還沒有定義這個環形緩沖區,並且沒有寫他相應函數。所以我們先定義環形緩沖區:

#define MYLOG_BUF_LEN 1024 //定義環形緩沖區的大小為1024

static char mylog_buf[MYLOG_BUF_LEN];
static char tmp_buf[MYLOG_BUF_LEN];

static int mylog_r=0; //定義讀指針
static int mylog_w=0; //定義寫指針
/* 判斷環形緩沖區是否為空 */
static int is_mylog_empty(void)
{
return (mylog_w == mylog_r);
}
/* 判斷環形緩沖區是否滿 */
static int is_mylog_full(void)
{
return (((mylog_w+1)%MYLOG_BUF_LEN) == (mylog_r%MYLOG_BUF_LEN));
}
/* 向環形緩沖區添加一個字符 */
static void mylog_putc(char c)
{
if(is_mylog_full()){ /* 如果環形緩沖區滿,將讀指針加一,實現對數據的覆蓋 */
mylog_r = (mylog_r+1)%MYLOG_BUF_LEN;
}
}

mylog_buf[mylog_w] = c; /* 將數據放到環形緩沖區 */
mylog_w = (mylog_w+1)%MYLOG_BUF_LEN; /* 寫指針加一 */

wake_up_interruptible(&mylog_waitq);
}
/* 從環形緩沖區讀取一個字符 */
static int mylog_getc(char *p)
{
if(is_mylog_empty()){ /* 如果環形緩沖區為空,返回 */
return 0;
}

*p = mylog_buf[mylog_r]; /* 從環形緩沖區讀出數據 */
mylog_r = (mylog_r+1)%MYLOG_BUF_LEN; /* 讀指針加一 */
return 1;
}

    完成這些之后我們要完成myprintk函數,來將要打印的信息放到mylog_buf中,這里我們參考sprintf函數。將myprintk函數寫為:

int myprintk(const char *fmt,...)
{
va_list args;
int i,j;
va_start(args,fmt);
i = vsnprintf(tmp_buf,INT_MAX,fmt, args); /* 參考sprintf函數,這里返回值i為放入的值的個數 */
va_end(args);
for(j=0;j<i;j++){
mylog_putc(tmp_buf[j]);
}

return i;
}
    完成這里之后我們就可以寫讀函數mymsg_read了:

static ssize_t mymsg_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
int error = 0;
int i = 0;
char c;

/* 把mylog_buf的數據copy_to_user,然后return */
if ((file->f_flags & O_NONBLOCK) && !is_mylog_empty_for_read())
return -EAGAIN;
error = wait_event_interruptible(mylog_waitq, !is_mylog_empty_for_read());

while(!error && mylog_getc_for_read(&c) && i<count){
error = __put_user(c, buf);
buf++;
i++;
}

if(!error)
error = i;

return error;
}
    好了,講到這里我們關於寫自己的mymsg就介紹完了。而我將代碼放到了:做自己的mymsg

參考文章:

內核日志:API 及實現
35.Linux-分析並制作環形緩沖區
Linux內核調試技術之自構proc
Linux修改內核使得普通用戶可以打印kmsg內容
————————————————
版權聲明:本文為CSDN博主「moxue10」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/w1107101310/article/details/80582314


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM