本文也即《Linux Device Drivers》,LDD3的第四章Debuging Techniques的讀書筆記之二,但我們不限於此內容。
在linux中,例如讀取CPU,可以使用cat /proc/cpuinfo,通過這個我們可以在程式中採用讀檔案的方式擷取CPU,這種大容量高效能的服務中非常常用,例如在cpu大於60%的時候,我們將拒絕所有的業務請求,直至cpu恢複到40%一下。我們可以根據此進行多級CPU過載保護,這在電信基本的系統中非常常用。由於採用讀取CPU的方式,也滿足JAVA程式的擷取。如果我們開發一個長期啟動並執行穩定的業務系統,過載保護是應當給予考慮。
【編程思想1:CPU過載保護】
/proc是特殊的檔案系統,通過軟體建立,由kernel給出資訊。我們之前通過printk給出調測資訊,雖然通過宏定義,在編譯的時候覺得是否給出printk或者給出某部分的printk,但是一旦模組載入後,printk也就定下來。不能夠做到需要時給出,不需要是不要去寫/var/log/messages檔案。使用/proc檔案系統可以滿足這個要求。我們在需要的時候去擷取資訊。但是這樣做是有風險的,例如我們在讀取的同時卸載模組,有或者兩個不同模組使用同一/proc/filename來進行資訊輸出。而書中的作者更是給了一個ugly的例子(作者自己原話:somewhat ugly),為了跑通,花費了我很多的時間,而examples(可以通過Google檢索ldd3 examples得到隨書光碟片的例子)有誤導,讓我兜了很多圈子。
我們通過下面函數建立和刪除/proc下面檔案,檔案名稱為scullmems,他們分別在載入和卸載模組時候調用。
create_proc_read_entry
("scullmem"/* 檔案名稱 */
,
0 /* default mode */
,
NULL /* parent dir,NULL表示預設在/proc路徑下 */
,
scull_read_procmem/* 讀取proc檔案時調用的函數 */
,
NULL /* client data */
);
remove_proc_entry
("scullmem", NULL /* parent dir */
);
關鍵是如何寫scull_read_procmem,他的結構是int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
其中傳回值是char ** start和int *eof,void *data是建立檔案時攜帶的參數,kernel不處理,但會在出發調用時傳遞,我們可以利用來攜帶某些資訊,其他的都是輸入值。
如果核心模組返回的資訊,大於允許存放的最大空間,就好出現cat /proc/scullmems是觸發多次該函數的調用,這一點不僅麻煩,而且需要非常小心,LDD3建議使用self_file(還沒看到)而不是這種方式,這可能就是作者自嘲ugly的原因。但是我覺得真正ugly的是example中的sculld的例子,將buff前向放內容,然後轉為後向存放資料,搞得我迷惑了很久。在scull的例子中,我的機器page大小為1024位元組,所有資訊不能一次傳遞完成,需要多次,而文章對這部分的處理說明語焉不詳。下面是我實踐得到的經驗,可能描述得不一定十分準確,但是管用。來看一個實驗例子,我們不去處理那個複雜的scull數組結構先
,只是希望輸出一定的資訊,增加了printk用來進行跟蹤:
static int my_index = 0,total_index = 10;
int scull_read_procmem(char * buf, char ** start, off_t offset, int count, int * eof, void *data)
{
int i ,len = 0;
int limit = count - 80;
printk("==============buf %p *start %p offset %li len %d eof %d count %i limit %i /n",
buf,*start,offset,len, * eof, count,limit);
if(my_index >= total_index){
printk("=_= return len = %i limit = %d eof = %d /n",len,limit, * eof);
return 0;
}
/* if(offset > count /2){
* start = buf;
}else{
buf = buf + offset;
* start = buf;
limit = count - offset -80;
}*/
if(offset > 0)
* start = buf;
printk("===buf %p *start %p offset %li len %d eof %d count %i limit %i /n",
buf,*start,offset,len, * eof, count,limit);
for(i = my_index; i< total_index && len <= limit; i ++){
printk( "%03d len=%d 12345678901234567890123456789901234567890/n",i,len);
len += sprintf(buf + len,"%03d len=%d 12345678901234567890123456789901234567890/n",
i,len);
my_index ++;
}
* eof = 1;
printk("=== return len = %i limit = %d eof = %d index = %d /n",len,limit, * eof, my_index);
return len;
}
第一個參數char * buf,給出的輸出資訊的buf位置,count表示表示buf空間的大小,通常是根據page來進行分配,在我的機器中,count=1024。
關鍵之一是我們需要告訴使用者,是否這次已經讀完,還是需要繼續讀,這個在LDD3中說明不太清楚。這和傳回值以及返回參數* eof有關。*eof的預設輸入值為0,如果我們需要告訴使用者資訊尚未讀完,仍需要進一步讀取,我們設定*eof為1,並且返回本次讀出的內容長度。如果這次讀取能夠讀到資訊,即返回有效長度,在我的實驗中無論eof設定為何值,都會進行下一次調用。只要我們設定* eof = 0並return 0,表示已無進一步的資訊。在我們的實驗中,有以下的規律
- 連續兩次傳回值為0,不再讀取
- 當* eof 且和上次有效返回是的數值不一樣(且上次不能為0),並傳回值為0,不再讀取。
- 如果有效進行讀取,我們應設定*eof為一個正整數。
offset的理解有些意思,我開始理解為buf中的位移量,但是我們是一次一次地輸出資訊,如果上一次的輸出如果還佔據buf的空間,我們就無法有足夠的空間給出新的資訊。對於多次輸出,offset實際是已經有效讀取的內容長度,他是一個累計的數值。作為的位移量不是指讀取buf的
位移量,而是指我們輸出資訊的位移量,即已完成讀取的長度。
在上面的例子中,為了保證存放在有效buf空間內,我們假定每一行的輸出不會超過80位元組,因此我們每次寫入(sprintf)的時候都要判斷,是否有足夠的空間。
char ** start用於大量資料,需要多次讀取,當offset不為0的時候,即表示不是第一次讀時,我們應該指出資料存放的起始位置,即* start。一般來講,系統會使用同一個buf來讀取,在非第一次的讀取中,我們可以選擇在buf+offset的位置上開始存放,直至buf滿,我們也可以自由設定我們存放資料的起始位置。例如在上面的例子中,可以修改為每次只有一條資訊就返回,而不是試圖讀多次。簡單來講在非第一次讀取時,我們都應該設定* start,指明初始位置。對於第一次讀取,預設從buf的第一個位元組開始,可能設定也可以把不設定*start的值。
下面是total_index = 100的dmesg的部分輸入結果:
==============buf effb1000 *start 00000000 offset 0 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 0 len 0 eof 0 count 1024 limit 944
000 len=0 12345678901234567890123456789901234567890
001 len=52 12345678901234567890123456789901234567890
002 len=105 12345678901234567890123456789901234567890
003 len=159 12345678901234567890123456789901234567890
004 len=213 12345678901234567890123456789901234567890
005 len=267 12345678901234567890123456789901234567890
006 len=321 12345678901234567890123456789901234567890
007 len=375 12345678901234567890123456789901234567890
008 len=429 12345678901234567890123456789901234567890
... ...
017 len=915 12345678901234567890123456789901234567890
=== return len = 969 limit = 944 eof = 1 index = 18
==============buf effb1000 *start 00000000 offset 969 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 969 len 0 eof 0 count 1024 limit 944
018 len=0 12345678901234567890123456789901234567890
... ....
035 len=915 12345678901234567890123456789901234567890
=== return len = 969 limit = 944 eof = 1 index = 36
==============buf effb1000 *start 00000000 offset 1938 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 1938 len 0 eof 0 count 1024 limit 944
036 len=0 12345678901234567890123456789901234567890
... ....
089 len=915 12345678901234567890123456789901234567890
=== return len = 969 limit = 944 eof = 1 index = 90
==============buf effb1000 *start 00000000 offset 4845 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 4845 len 0 eof 0 count 1024 limit 944
090 len=0 12345678901234567890123456789901234567890
... ...
099 len=483 12345678901234567890123456789901234567890
=== return len = 537 limit = 944 eof = 1 index = 100
==============buf effb1000 *start 00000000 offset 5382 len 0 eof 0 count 1024 limit 944
=_= return len = 0 limit = 944 eof = 0
我們下面給出scull記憶體資訊的例子,如果裝置SCULLx的scull_qset中含有資料,我們就在qset隊列中每一個quantum的位置顯示出來,包括沒有分配的quantum(顯示0,這樣一般都需要多次讀取)。對於多次讀取,最大的問題是,我們需要記住上次已經讀取了多少資訊,即這次應當從那個開始讀取。在LDD3的上一章中說到goto語句在核心程式是比較常見的,這和我們在學習編程的時候,將goto罵得狗血淋頭的情況不一樣,這和kernel的事件觸發機制有關,在這個例子中當本次讀取buffer快滿的時候,我們通過goto給出統一的出口,這樣整個程式顯得整潔和易讀。
static int store_dev_num = 0, store_qs_num = 0 , store_quan_num = 0;
int scull_read_procmem(char * buf, char ** start, off_t offset, int count, int * eof, void *data)
{
int i, j, len = 0, k = 0;
int limit = count - 80;
/*如果已經全部讀完,store_dev_num將等於SCULL_DEV_NUM,我們使用下面這三個參數,分別記錄正在讀取哪個裝置,讀到哪個qset,以及qset中的哪個quantum*/
if(store_dev_num >= SCULL_DEV_NUM){
store_dev_num = 0;
store_qs_num = 0;
store_quan_num = 0;
return 0;
}
/* 用於多次讀取*/
if(offset > 0){
* start = buf ;
}
for(i = store_dev_num ; i < SCULL_DEV_NUM && len <= limit ; i ++){
struct scull_dev * dev = & mydev[i];
struct scull_qset * qs = dev->data;
if( len > limit)
goto buffer_full;
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
if(! store_qs_num && ! store_quan_num ){
len += sprintf(buf + len, "/n Device Scull%d: qset %i, q %i sz %li/n",
i,dev->qset, dev->quantum, dev->size);
}
k = 0;
for(; qs ; qs=qs->next, k++){
if(k < store_qs_num)
continue;
if(len > limit){
up(&dev->sem);
goto buffer_full;
}
if(!store_quan_num ){
len += sprintf(buf + len ," item at %p, qset %d at %p/n", qs, k++, qs->data);
}
if(qs->data ){
j = store_quan_num ;
/*下面注釋的部分用於全部顯示隊列中的quantum位置,已保證輸出資訊足夠,
* 而最後程式將恢複,只顯示有效部分。*/
for(; j < dev->qset /* && qs->data[ j ] */
;j++){
if(len > limit){
up(&dev->sem);
goto buffer_full;
}
len += sprintf(buf + len, "/t%4i:%8p/n", j,qs->data[ j ]);
store_quan_num ++;
}
store_quan_num = 0;
}
store_qs_num ++;
}
up(&dev->sem);
store_dev_num ++;
store_qs_num = 0;
}
buffer_full:
* eof = 1;
return len;
}
從上面代碼看,有一個潛在的危險,就是讀取資訊過程中,如果正在對scull裝置進行寫操作。在一次讀寫完成後,我們釋放了訊號量,等待下一次讀寫,在這個過程中,訊號量可能會被寫操作所佔有。這個例子,我們只是讀取位置資訊,即使出現問題,也不會有太多的影響,但是在實際應用過程中,我們應當在一次訊號量持有中完成對某裝置的操作
,例如考慮緩衝沒有讀取的資訊,等待下次讀取等等,或者盡量減少不必要的資訊。【編程思想2:訊號量持有和操作】
相關技術文章:
我的與kernel module有關的文章
我的與編程思想相關的文章