kernel module編程(七):通過讀取proc檔案進行debug

來源:互聯網
上載者:User

  本文也即《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有關的文章

我的與編程思想相關的文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.