本文也即《Linux Device Drivers》,LDD3的第四章Debuging Techniques的讀書筆記之三,但我們不限於此內容。
在上次我們使用了read_proc的方式通過/proc檔案讀取kernel module的資訊。作者給的例子他自己說是ugly。而我們在讀取大量資料時發現,受到使用者buffer大小的限制(page的大小),可能需要讀取多次,不僅需要記錄上次讀取的位置,而且由於每次讀取我們申請了訊號量,讀取完釋放,那麼如果多次讀取的間隔中,如果訊號量被寫所擷取就好出現混亂。linux kernel提供seq_file更好的方式來解決這個問題,除非我們確定讀取的資訊量非常少,能夠在page中返回,我們應使用seq_file的方式而不是read_proc 。
LDD3中介紹的方式,我覺得是典型的西方人和中國人思維方式的不同。在seq_file的介紹中,LDD3先從每個操作具體將其,然後到如何和proc檔案聯絡,最後到如何建立proc檔案,我喜歡反過來的方式,先建立proc,在一步步細化。老外是日月年,我們是年月日,嘿嘿。seq_file的處理方式開看有點發展,步驟有些多,但是安全,是規範的處理方式。
步驟一:建立proc檔案。
通過一個struct proc_dir_entry的元素,在/proc中建立檔案,如下:
struct proc_dir_entry * entry = create_proc_entry(“scullseq”,0,NULL)。參數的內容和read_proc,第一個參數表示檔案名稱,第二個參數表示檔案屬性,對於唯讀方式為0,第三個參數表示檔案路徑,NULL表示預設路徑,即/proc。
步驟二:關聯proc的操作。
需要對檔案進行操作,見過檔案和struct file_operations相關聯,我們注意到這個資料結構也用於模組操作關聯中。具體操作如下:
#ifdef SCULL_SEQ_FILE
/* 步驟二:2、定義proc檔案所關聯的檔案操作資料 */
static struct file_operations
scull_proc_ops
= {
.owner = THIS_MODULE,
.open = scull_proc_open, //open通常這是我們唯一需要重新定義的函數,需要和特定的seq_file關聯起來。
.read = seq_read, //採用系統的處理方式
.llseek = seq_lseek, //採用系統的處理方式
.release = seq_release, //採用系統的處理方式
};
/* 步驟二:4、在前面的步驟二1~3中我們建立了proc檔案,關聯了proc檔案和file_operations,並進一步關聯了seq_file,這裡我們具體定義被關聯的seq_file */
static struct seq_operations
scull_seq_ops = {
.start = scull_seq_start,
.next = scull_seq_next,
.stop = scull_seq_stop,
.show = scull_seq_show,
};
static struct proc_dir_entry * entry;
#endif
… …
static int __init scull_init(void)
{
… …
#ifdef SCULL_SEQ_FILE
/* 步驟一:建立proc檔案*/
entry = create_proc_entry
("scullseq", 0 ,NULL);
/* 步驟二:1、將proc檔案和對應的檔案操作關聯起來*/
if(entry)
entry -> proc_fops = & scull_proc_ops
;
#endif
}
... ...
static void __exit scull_exit(void)
{
#ifdef SCULL_SEQ_FILE
/* 我們在模組中建立的proc檔案,都應該模組cleanup模組的時候刪除,以防影響系統,另外,我們應該在刪除模組函數的開始執行這個操作,防止相關聯的資料已經刪除或者登出後再來處理,避免異常出現。
*/
remove_proc_entry("scullseq",NULL
);
#endif
if(is_get_dev < 0){
return ;
}else{
int i = 0;
for(i = 0; i < SCULL_DEV_NUM; i ++){
scull_trim(&mydev[i]);
cdev_del( & mydev[i].cdev );
}
unregister_chrdev_region(dev,SCULL_DEV_NUM);
WDEBUG(WEI_KERN_NOTICE,"Scull module exit/n");
}
}
/* 步驟二:3、具體實現proc檔案的open操作,目的與seq_file相關聯。*/
int scull_proc_open(struct inode * inode , struct file * file)
{
return seq_open(file, & scull_seq_ops
);
}
步驟三:處理seq_file操作過程。
seq_file操作定義了四個操作,格式如下:
void * start(struct seq_file * s, loff_t * v);
void * next (struct seq_file * s, void * v, loff_t * pos);
void stop (struct seq_file * s, void * v);
int show (struct seq_file * s, void * v);
其中loff_t表示位置,這是由我們自己程式控制的,初始為0,在scull中我們依次讀取scull0-3,因此使用該位移量來表示我們所讀取的裝置的序號。
我們利用void * v來記錄裝置的入口位置。start根據編譯量,即我們的裝置的序號,返回scullx的入口位置,無論下一操作是next,stop,還是show,這個傳回值會作為參數void *v輸入。next表示下一查詢,和start相似,只是多了void * v的輸入,同樣它的傳回值也作為下一操作的參數void *v輸入。show用於通過/proc檔案輸出。stop表示一次讀取的結束。雖然在seq_file中和read_proc不一樣,不需要考慮每次可以輸出的buff的大小,但是實際讀取不會連續一片很大的資料輸出,在例子後面,我們將討論這些操作的執行。
輸出方式非常簡單,一般可以使用seq_printf,另外還有seq_putc,seq_puts,seq_escape。例子如下:
#ifdef SCULL_SEQ_FILE
void * scull_seq_start
(struct seq_file * s, loff_t * pos)
{
printk("==scull_seq_start() enter %p %p %lli/n", s , pos , * pos);
if( * pos >= SCULL_DEV_NUM)
return NULL;
else
return mydev + * pos;
}
void * scull_seq_next
(struct seq_file * s, void * v, loff_t * pos)
{
printk("==scull_seq_next() enter %p %p %p %lli/n", s , v, pos , * pos);
(* pos ) ++;
return scull_seq_start(s, pos);
}
void scull_seq_stop
(struct seq_file * s, void * v)
{
printk("==scull_seq_stop() enter/n");
return ;
}
int scull_seq_show
(struct seq_file * s, void * v)
{
struct scull_dev * dev = (struct scull_dev *) v;
struct scull_qset * qs = NULL;
int j = 0;
printk("==scull_seq_show() enter/n");
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
seq_printf
(s, "/n Device Scull%d: qset %i, q %i sz %li/n", (int) (dev - mydev),dev->qset, dev->quantum, dev->size);
printk("/n Device Scull%d: qset %i, q %i sz %li/n", (int) (dev - mydev),dev->qset, dev->quantum, dev->size);
for(qs = dev->data ; qs ; qs=qs->next){
seq_printf
( s," item at %p, qset at %p/n", qs, qs->data);
printk(" item at %p, qset at %p/n", qs, qs->data);
if(qs->data ){
for(; j < dev->qset /* && qs->data[ j ] */ ;j++){
seq_printf
(s ,"/t%4i:%8p/n", j,qs->data[ j ]);
printk("/t%4i:%8p/n", j,qs->data[ j ]);
}
}
}
up(&dev->sem);
return 0;
}
我們描述一下處理的過程:當我們讀取proc檔案,例如cat /proc/scullseq時,我們假設scull0和scull1都有較多資訊輸入。
一開始調用start,位移量為0,返回scull0的入口,接著調用show,scull0的入口作為參數輸入,在show中,我們可以遍曆scull0的資料結構,通過seq_printf輸出。完成show後,由於輸出資訊多,進入stop,在例子中stop沒有實際操作,我們只是用來跟蹤處理的流程。
再次調用start,位移量步進1,即1,返回scull0的入口,接著調用show,scull1的入口作為參數輸入,在show中,我們可以遍曆scull1的資料結構,通過seq_printf輸出。完成show後,由於輸出資訊多,進入stop。
再次調用start,位移量步進1,即2,返回scull2的入口,接著調用show,scull2的入口作為參數輸入,在show中,我們可以遍曆scull2的資料結構,通過seq_printf輸出。完成show後,由於輸出資訊非常少,kernel認為可以繼續進行操作,而不需要stop,調用next(),在next參數中輸入的參數loff_t為2,next將其加一,為3,返回scull3的入口。接著調用show,scull3的入口作為參數輸入,在show中,我們可以遍曆scull3的資料結構,通過seq_printf輸出。完成show後,由於輸出資訊非常少,kernel認為可以繼續進行操作,而不需要stop,調用next()。由於已經全部資訊返回,在next中發現沒有資料,返回NULL。系統再次調用start,返回NULL,系統調用stop,結束這次輸出。
再次調用start,返回NULL,表示已經沒有資料輸出,調用stop,結束所有的輸出。
值得注意 seq_file 代碼在調用 start 和 stop 之間不睡眠或者進行其他非原子性任務. 你也肯定會看到在調用 start
後馬上有一個 stop 調用. 因此, 對你的 start 方法來說請求訊號量或自旋鎖是安全的. 只要你的其他 seq_file
方法是原子的, 調用的整個序列是原子的.(http://www.deansys.com/doc/ldd3/ch04s03.html)
相關技術文章:
我的與kernel module有關的文章