oops && ksymoops && objdump

1. OOPS
什麼是OOPS呢? 如果寫過linux模塊或者linux驅動,對於OOPS並不陌生, 當模塊程序出現錯誤時, 終端會打印出一些讓人頭疼的寄存器和數據, 例如:
divide error: 0000
CPU: 0
EIP: 0010:[] Tainted: P
EFLAGS: 00010286
eax: c10b0048 ebx: d0064000 ecx: 00005ae5 edx: c10b0048
esi: 00000000 edi: 00000000 ebp: c770defc esp: c770deec
ds: 0018 es: 0018 ss: 0018
Process insmod.old (pid: 1160, stackpage=c770d000)
Stack: c0101d04 0f76a067 00067000 00000000 c770df1c d00640be d00640e4 00000212
00000060 d0064000 00000000 00000000 ffffffea c01165e1 00000000 08085a05
0000010d 00000060 00000060 00000005 c2ea95a0 c4145000 cc5ca000 d0066000
Call Trace: [] [] [] [] []

Code: f6 7d fb 88 45 fb 0f be 45 fb 50 68 e0 40 06 d0 e8 c1 16 0b
這些數據就是我們這裡要講的OOPS消息, 這些消息包含了出錯時的寄存器信息以及內存信息, 例如, EIP(0010:[]), 這就告訴了我們出錯時EIP的相對值是0010, 在運用objdump工具對源代碼進行反彙編, 就可以輕而易舉的找到錯誤點. 因此這些數據對於代碼錯誤分析相當重要. 但是, 對於這些只有機器才能明白的數據, 程序員恐怕很不喜歡.

2. Ksymoops

為了讓程序員明白它們的含義, 以及更好的使用這些」寶貴」的數據, 開發人員設計了ksymoops工具, 它就是講晦澀難懂的oops消息, 轉換成我們可以直接理解的信息.
這裡還是以上面數據為例, 首先要把數據存儲到一個文件中, 作為ksmoops的輸入數據. 這裡我把上面的數據放入文件oops.info中, 然後執行ksmoops數據, 看看有什麼結果.
#ksmoops < oops.info
>>EIP; d006408a <=====

>>eax; c10b0048
>>ebx; d0064000
>>edx; c10b0048
>>ebp; c770defc
>>esp; c770deec

Trace; d00640be
Trace; d00640e4
Trace; c01165e1
Trace; d0064060
Trace; c0108983

Code; d006408a
00000000 <_EIP>:
Code; d006408a <=====
0: f6 7d fb idivb 0xfffffffb(%ebp) <=====
Code; d006408d
3: 88 45 fb mov %al,0xfffffffb(%ebp)
Code; d0064090
6: 0f be 45 fb movsbl 0xfffffffb(%ebp),%eax
Code; d0064094
a: 50 push %eax
Code; d0064095
b: 68 e0 40 06 d0 push $0xd00640e0
Code; d006409a

2.1 Trace

很顯然, trace是模塊執行過程中對應的函數地址, 由於ksymoops 運行時的默認尋找模塊在/lib/modules的下面, 因為我所運行的模塊不在那個目錄下,所以結果是pg0+ … 一類的數據.

2.2 Code
Code行對應的是相應的錯誤發生時對應的執行代碼, 通過ksymoops的處理, 就變成了我們熟悉的彙編代碼:
Code; d006408a <=====
0: f6 7d fb idivb 0xfffffffb(%ebp) <=====
Code; d006408d
3: 88 45 fb mov %al,0xfffffffb(%ebp)
Code; d0064090
6: 0f be 45 fb movsbl 0xfffffffb(%ebp),%eax
Code; d0064094
a: 50 push %eax
Code; d0064095
b: 68 e0 40 06 d0 push $0xd00640e0
Code; d006409a

第二行就是錯誤發生的地方, 以下部分是將要執行的代碼.

3 objdump

Ksymoops只是給出了錯誤點的信息, 但是對於龐大的系統模塊, 我們必須準確的定位到那個錯誤點, 為此, 我們還可以利用另一個反彙編工具objdump繼續分析錯誤. 我的模塊名稱時hello.o, 為了瞭解它的源碼, 我們就可以使用該工具.
#objdump –d hello.o
hello.o: file format elf32-i386

Disassembly of section .text:

00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp)
d: 83 7d fc 63 cmpl $0x63,0xfffffffc(%ebp)
11: 7e 02 jle 15
13: eb 34 jmp 49
15: 83 ec 08 sub $0x8,%esp
18: 8b 45 fc mov 0xfffffffc(%ebp),%eax
1b: 03 45 08 add 0x8(%ebp),%eax
1e: 8a 00 mov (%eax),%al
20: 66 0f be d0 movsbw %al,%dx
24: c6 45 fb 00 movb $0x0,0xfffffffb(%ebp)
28: 89 d0 mov %edx,%eax
2a: f6 7d fb idivb 0xfffffffb(%ebp)
2d: 88 45 fb mov %al,0xfffffffb(%ebp)
30: 0f be 45 fb movsbl 0xfffffffb(%ebp),%eax
34: 50 push %eax
35: 68 00 00 00 00 push $0x0
3a: e8 fc ff ff ff call 3b
3f: 83 c4 10 add $0x10,%esp
42: 8d 45 fc lea 0xfffffffc(%ebp),%eax
45: ff 00 incl (%eax)
47: eb c4 jmp d
49: c9 leave
4a: c3 ret

0000004b :
4b: 55 push %ebp
4c: 89 e5 mov %esp,%ebp
4e: 83 ec 08 sub $0x8,%esp
51: 83 ec 0c sub $0xc,%esp
54: 68 04 00 00 00 push $0x4
59: e8 fc ff ff ff call 5a
5e: 83 c4 10 add $0x10,%esp
61: b8 00 00 00 00 mov $0x0,%eax
66: c9 leave
67: c3 ret

00000068 :
68: 55 push %ebp
69: 89 e5 mov %esp,%ebp
6b: 83 ec 08 sub $0x8,%esp
6e: 83 ec 0c sub $0xc,%esp
71: 68 19 00 00 00 push $0x19
76: e8 fc ff ff ff call 77
7b: 83 c4 10 add $0x10,%esp
7e: c9 leave
7f: c3 ret

這樣通過ksmoops的結果和objdump的數據就可以輕而易舉的找到發生錯誤的函數以及在函數內部的具體位置了:
2a: f6 7d fb idivb 0xfffffffb(%ebp)

4 總結

我給出的oops信息是在Linux內核版本為2.4.8的系統裡面執行的結果, 在2.6.*版本的內核中, oops信息中已經給出了調用函數的名稱. Kysmoops 一些具體的參數這裡也沒有介紹如何使用, 想具體瞭解它們, 可以參考文檔: /usr/src/linux/Documentation/oops-tracing.txt或者ksymoops手冊.

5 附件
Hello.c 源碼:
/*file: hello.c*/
#ifndef MODULE
#define MODULE
#endif
#include
#include "hello.h"


MODULE_AUTHOR("BUROC") ;
MODULE_DESCRIPTION("The test module") ;
MODULE_SUPPORTED_DEVICE("no_dev") ;

void print(char *str)
{
int i;
for(i = 0; i < 100; i++)
printk("%d\n",str[i]/(str[i]-str[i]));
}

static int __init hello_init(void){
print("Hello, I am coming.\n");

return 0;
}

static void __exit hello_exit(void){
print("Bye, I am leaving.\n");
}
module_init(hello_init);
module_exit(hello_exit);

wait queue

相信很多寫程序的人都寫過 socket 的程序。當我們 open 一個 socket 之後,接著去讀取這個 socket,如果此時沒有任何資料可供讀取,那 read 就會 block 住。(這是沒有加上 O_NONBLOCK 的情形),直到有資料可讀取才會傳回來。在 Linux kernel 裡有一個數據結構可以幫助我們做到這樣的功能。這個數據結構就是這裡要跟各位介紹的 wait queue。在 kernel 裡,wait_queue 的應用很廣,舉凡 device driver semaphore 等方面都會使用到 wait_queue 來 implement。所以,它算是 kernel 裡蠻基本的一個數據結構。

接下來,我要跟各位介紹一下 wait_queue 的用法,以及用一個例子來說明如何使用 wait_queue。最後,我會帶各位去 trace 一下 wait_queue 的原始程序代碼,看看 wait_queue 是如何做到的。

我想有件事要先提及的是 Linux 在 user space 跟在 kernel space 上的差異。我們知道 Linux 是 multi-tasking 的環境,同時可以有很多人執行很多的程序。這是從 user 的觀點來看的。如果就 kernel 的觀點來看,是沒有所謂的 multi-tasking 的。在 kernel 裡,只有 single-thread。也就是說,如果你的 kernel code 正在執行,那系統裡只有那部分在執行。不會有另一部分的 kernel code 也在運作。當然,這是指 single processor 的情況下,如果是 SMP 的話,那我就不清楚了。我想很多人都在 Windows 3.1 下寫過程序,在那種環境下寫程序,每一個程序都必須適當的將 CPU 讓給別的程序使用。如果有個程序裡面有一個

while (1);

的話,那保證系統就停在那裡了。這種的多任務叫做 non-preemptive。它多任務的特性是由各個程序相互合作而造成的。在 Linux 的 user space 下,則是所謂的 preemptive,各個 process 喜歡執行什麼就執行什麼,就算你在你的程序裡加上 while(1); 這一行也不會影響系統的運作。反正時間到了,系統自動就會將你的程序停住,讓別的程序去執行。這是在 user space 的情況下,在 kernel 這方面,就跟 Windows 3.1 程序是一樣的。在 kernel 裡,你必須適當的將 CPU 的執行權釋放出來。如果你在 kernel裡加入 while(1); 這一行。那系統就會跟 Windows 3.1 一樣。卡在那裡。當然啦,我是沒試過這樣去改 kernel,有興趣的人可以去試試看,如果有不同的結果,請記得告訴我。

假設我們在 kernel 裡產生一個 buffer,user 可以經由 read,write 等 system call 來讀取或寫資料到這個 buffer 裡。如果有一個 user 寫資料到 buffer 時,此時 buffer 已經滿了。那請問你要如何去處理這種情形呢 ? 第一種,傳給 user 一個錯誤訊息,說 buffer 已經滿了,不能再寫入。第二種,將 user 的要求 block 住,等有人將 buffer 內容讀走,留出空位時,再讓 user 寫入資料。但問題來了,你要怎麼將 user 的要求 block 住。難道你要用

while ( is_full );
write_to_buffer;

這樣的程序代碼嗎? 想想看,如果你這樣做會發生什麼事? 第一,kernel會一直在這個 while 裡執行。第二個,如果 kernel 一直在這個 while 裡執行,表示它沒有辦法去 maintain系統的運作。那此時系統就相當於當掉了。在這裡 is_full 是一個變量,當然,你可以讓 is_full 是一個 function,在這個 function裡會去做別的事讓 kernel 可以運作,那系統就不會當。這是一個方式。但是,如果我們使用 wait_queue 的話,那程序看起來會比較漂亮,而且也比較讓人瞭解,如下所示:


struct wait_queue *wq = NULL; /* global variable */
while ( is_full ) {
interruptible_sleep_on( &wq );
}
write_to_buffer();

interruptible_sleep_on( &wq ) 是用來將目前的 process,也就是要求寫資料到 buffer 的 process放到 wq 這個 wait_queue 裡。在 interruptible_sleep_on 裡,則是最後會呼叫 schedule() 來做 schedule 的動作,也就是去找另一個 process 來執行以維持系統的運作。當執行完 interruptible_sleep_on 之後,要求 write 的 process 就會被 block 住。那什麼時候會恢復執行呢 ? 這個 process 之所以會被 block 住是因為 buffer 的空間滿了,無法寫入。但是如果有人將 buffer 的資料讀取掉,則 buffer 就有空間可以讓人寫入。所以,有關於叫醒 process 的動作應該是在 read buffer 這方面的程序代碼做的。

extern struct wait_queue *wq;
if ( !is_empty ) {
read_from_buffer();
wake_up_interruptible( &wq );
}
....

以上的程序代碼應該要放在 read buffer 這部分的程序代碼裡,當 buffer 有多餘的空間時,我們就呼叫 wake_up_interruptible( &wq ) 來將掛在 wq 上的所有 process 叫醒。請記得,我是說將 wq 上的所有 process 叫醒,所以,如果如果有10個 process 掛在 wq 上的話,那這 10 個都會被叫醒。之後,至於誰先執行。則是要看 schedule 是怎麼做的。就是因為這 10 個都會被叫醒。如果 A 先執行,而且萬一很不湊巧的,A 又把 buffer 寫滿了,那其它 9 個 process 要怎麼辦呢? 所以在 write buffer 的部分,需要用一個 while 來檢查 buffer 目前是否滿了.如果是的話,那就繼續掛在 wq 上面.

上面所談的就是 wait_queue 的用法。很簡單不是嗎? 接下來,我會再介紹一下 wait_queue 提供那些 function 讓我們使用。讓我再重申一次。wait_queue 應設為 global variable,比方叫 wq,只要任何的 process 想將自己掛在上面,就可以直接叫呼叫 sleep_on 等 function。要將 wq 上的 process 叫醒。只要呼叫 wake_up 等 function 就可以了.

就我所知,wait_queue 提供4個 function 可以使用,兩個是用來將 process 加到 wait_queue 的:

sleep_on( struct wait_queue **wq );
interruptible_sleep_on( struct wait_queue **wq );

另外兩個則是將process從wait_queue上叫醒的。

wake_up( struct wait_queue **wq );
wake_up_interruptible( struct wait_queue **wq );

我現在來解釋一下為什麼會有兩組。有 interruptible 的那一組是這樣子的。當我們去 read 一個沒有資料可供讀取的 socket 時,process 會 block 在那裡。如果我們此時按下 Ctrl+C,那 read() 就會傳回 EINTR。像這種的 block IO 就是使用 interruptible_sleep_on() 做到的。也就是說,如果你是用 interruptible_sleep_on() 來將 process 放到 wait_queue 時,如果有人送一個 signal 給這個 process,那它就會自動從 wait_queue 中醒來。但是如果你是用 sleep_on() 把 process 放到 wq 中的話,那不管你送任何的 signal 給它,它還是不會理你的。除非你是使用 wake_up() 將它叫醒。sleep 有兩組。wake_up 也有兩組。wake_up_interruptible() 會將 wq 中使用 interruptible_sleep_on() 的 process 叫醒。至於 wake_up() 則是會將 wq 中所有的 process 叫醒。包括使用 interruptible_sleep_on() 的 process。

在使用 wait_queue 之前有一點需要特別的小心,呼叫 interruptible_sleep_on() 以及 sleep_on() 的 function 必須要是 reentrant。簡單的說,reentrant 的意思是說此 function不會改變任何的 global variable,或者是不會 depend on 任何的 global variable,或者是在呼叫 interruptible_sleep_on() 或 sleep_on() 之後不會 depend on 任何的 global variable。因為當此 function 呼叫 sleep_on() 時,目前的 process 會被暫停執行。可能另一個 process 又會呼叫此 function。若之前的 process 將某些 information 存在 global variable,等它恢復執行時要使用,結果第二行程進來了,又把這個 global variable 改掉了。等第一個 process 恢復執行時,放在 global variable 中的 information 都變了。產生的結果恐怕就不是我們所能想像了。其實,從 process 執行指令到此 function 中所呼叫的 function 都應該是要 reentrant 的。不然,很有可能還是會有上述的情形發生.

由於 wait_queue 是 kernel 所提供的,所以,這個例子必須要放到 kernel 裡去執行。我使用的這個例子是一個簡單的 driver。它會 maintain 一個 buffer,大小是 8192 bytes。提供 read跟 write 的功能。當 buffer 中沒有資料時,read() 會馬上傳回,也就是不做 block IO。而當 write buffer 時,如果呼叫 write() 時,空間已滿或寫入的資料比 buffer 大時,就會被 block 住,直到有人將 buffer 裡的資料讀出來為止。在 write buffer 的程序代碼中,我們使用 wait_queue 來做到 block IO 的功能。在這裡,我會將此 driver 寫成 module,方便加載 kernel。

第一步,這個 driver 是一個簡單的 character device driver。所以,我們先在 /dev 下產生一個 character device。major number 我們找一個比較沒人使用的,像是 54,minor number 就用 0。接著下一個命令.

mknod /dev/buf c 54 0

mknod 是用來產生 special file 的 command。/dev/buf 表示要產生叫 buf 的檔案,位於 /dev 下。 c 表示它是一個 character device。54 為其 major number,0 則是它的 minor number。有關 character device driver 的寫法。有機會我再跟各位介紹,由於這次是講 wait_queue,所以,就不再多提 driver 方面的東西.

第二步,我們要寫一個 module,底下是這個 module 的程序代碼:

buf.c
#define MODULE
#include
#include
#include
#include
#include
#define BUF_LEN 8192

int flag; /* when rp = wp,flag = 0 for empty,flag = 1 for
non-empty */
char *wp,*rp;
char buffer[BUF_LEN];
EXPORT_NO_SYMBOLS; /* don't export anything */

static ssize_t buf_read( struct file *filp,char *buf,size_t count,
loff_t *ppos )
{
return count;
}

static ssize_t buf_write( struct file *filp,const char *buf,size_t count,
loff_t *ppos )
{
return count;
}

static int buf_open( struct inode *inode,struct file *filp )
{
MOD_INC_USE_COUNT;
return 0;
}

static int buf_release( struct inode *inode,struct file *filp )
{
MOD_DEC_USE_COUNT;
return 0;
}

static struct file_operations buf_fops = {
NULL, /* lseek */
buf_read,
buf_write,
NULL, /* readdir */
NULL, /* poll */
NULL, /* ioctl */
NULL, /* mmap */
buf_open, /* open */
NULL, /* flush */
buf_release, /* release */
NULL, /* fsync */
NULL, /* fasync */
NULL, /* check_media_change */
NULL, /* revalidate */
NULL /* lock */
};

static int buf_init()
{
int result;

flag = 0;
wp = rp = buf;

result = register_chrdev( 54,"buf",&buf_fops );
if ( result < 0 ) {
printk( "<5>buf: cannot get major 54\n" );
return result;
}

return 0;
}

static void buf_clean()
{
if ( unregister_chrdev( 54,"buf" ) ) {
printk( "<5>buf: unregister_chrdev error\n" );
}
}

int init_module( void )
{
return buf_init();
}

void cleanup_module( void )
{
buf_clean();
}

有關 module 的寫法,請各位自行參考其它的文件,最重要的是要有 init_module()和 cleanup_module() 這兩個 function。我在這兩個 function 裡分別做 initialize 和 finalize 的動作。現在分別解釋一下。在 init_module() 裡,只有呼叫 buf_init() 而己。其實,也可以將 buf_init() 的 code 寫到 init_module() 裡。只是我覺得這樣比較好而已。

flag = 0;
wp = rp = buf;
result = register_chrdev( 54,"buf",&buf_fops );
if ( result < 0 ) {
printk( "<5>buf: cannot get major 54\n" );
return result;
}
return 0;

init_buf() 做的事就是去註冊一個 character device driver。在註冊一個 character device driver 之前,必須要先準備一個型別為 file_operations 結構的變量,file_operations 裡包含了一些 function pointer。driver 的作者必須自己寫這些 function。並將 function address 放到這個結構裡。如此一來,當 user 去讀取這個 device 時,kernel 才有辦法去呼叫對應這個 driver 的 function。其實,簡要來講。character device driver 就是這麼一個 file_operations 結構的變量。file_operations 定義在這個檔案裡。它的 prototype 在 kernel 2.2.1 與以前的版本有些微的差異,這點是需要注意的地方。

register_chrdev() 看名字就大概知道是要註冊 character device driver。第一個參數是此 device 的 major number。第二個是它的名字。名字你可以隨便取。第三個的參數就是一個 file_operations 變量的地址。init_module() 必須要傳回 0,module 才會被加載。

在 cleanup_module() 的部分,我們也是只呼叫 buf_clean() 而已。它做的事是 unregister 的動作。

if ( unregister_chrdev( 54,"buf" ) ) {
printk( "<5>buf: unregister_chrdev error\n" );
}

也就是將原本記錄在 device driver table 上的資料洗掉。第一個參數是 major number。第二個則是此 driver 的名稱,這個名字必須要跟 register_chrdev() 中所給的名字一樣才行。

現在我們來看看此 driver 所提供的 file_operations 是那些。

static struct file_operations buf_fops = {
NULL, /* lseek */
buf_read,
buf_write,
NULL, /* readdir */
NULL, /* poll */
NULL, /* ioctl */
NULL, /* mmap */
buf_open, /* open */
NULL, /* flush */
buf_release, /* release */
NULL, /* fsync */
NULL, /* fasync */
NULL, /* check_media_change */
NULL, /* revalidate */
NULL /* lock */
};

在此,我們只打算 implement buf_read(),buf_write(),buf_open,和 buf_release()等 function 而已。當 user 對這個 device 呼叫 open() 的時候,buf_open() 會在最後被 kernel 呼叫。相同的,當呼叫 close(),read(),和 write() 時,buf_release(),buf_read(),和 buf_write() 也都會分別被呼叫。首先,我們先來看看 buf_open()。

static int buf_open( struct inode *inode,struct file *filp )
MOD_INC_USE_COUNT;
return 0;
}

buf_open() 做的事很簡單。就是將此 module 的 use count 加一。這是為了避免當此 module 正被使用時不會被從 kernel 移除掉。相對應的,在 buf_release() 中,我們應該要將 use count 減一。就像開啟檔案一樣。有 open(),就應該要有對應的 close() 才行。如果 module 的 use count 在不為 0 的話,那此 module 就無法從 kernel 中移除了。

static int buf_release( struct inode *inode,struct file *filp )
{
MOD_DEC_USE_COUNT;
return 0;
}

接下來,我們要看一下buf_read()和buf_write()。

static ssize_t buf_read( struct file *filp,char *buf,size_t count,
loff_t *ppos )
{
return count;
}

static ssize_t buf_write( struct file *filp,const char *buf,
size_t count,loff_t *ppos )
{
return count;
}
在此,我們都只是回傳 user 要求讀取或寫入的字符數目而已。在此,我要說明一下這些參數的意義。filp 是一個 file 結構的 pointer。也就是指我們在 /dev 下所產生的 buf 檔案的 file 結構。當我們呼叫 read() 或 write() 時,必須要給一個 buffer 以及要讀寫的長度。Buf 指的就是這個 buffer,而 count 指的就是長度。至於 ppos 是表示目前這個檔案的 offset 在那裡。這個值對普通檔案是有用的。也就是跟 lseek() 有關係。由於在這裡是一個 drvice。所以 ppos 在此並不會用到。有一點要小心的是,上面參數 buf 是一個地址,而且還是一個 user space 的地址,當 kernel 呼叫 buf_read() 時,程序在位於 kernel space。所以你不能直接讀寫資料到 buf 裡。必須先切換 FS 這個 register 才行。

Makefile
P = buf
OBJ = buf.o
INCLUDE = -I/usr/src/linux/include/linux
CFLAGS = -D__KERNEL__ -DMODVERSIONS -DEXPORT_SYMTAB -O $(INCLUDE) \
-include /usr/src/linux/include/linux/modversions.h
CC = gcc

$(P): $(OBJ)
ld -r $(OBJ) -o $(P).o

.c.o:
$(CC) -c $(CFLAGS) $<

clean:
rm -f *.o *~ $(P)

加入上面這個 Makefile,打入 make 之後,就會產生一個 buf.o 的檔案。利用 insmod 將 buf.o 載到 kernel 裡。相信大家應該都用過 /dev/zero 這個 device。去讀取這個 device,只會得到空的內容。寫資料到這個 device 裡也只會石沈大海。現在你可以去比較 buf 和 zero 這兩個 device。兩者的行為應該很類似才是。

第三步,我們在第二步中 implement 一個像 zero 的 device driver。我們現在要經由修改它來使用 wait_queue。首先,我們先加入一個 global variable,write_wq,並把它設為 NULL。

struct wait_queue *write_wq = NULL;

然後,在 buf_read() 裡,我們要改寫成這個樣子。

static ssize_t buf_read( struct file *filp,char *buf,size_t count,
loff_t *ppos )
{
int num,nRead;
nRead = 0;
while ( ( wp == rp ) && !flag ) { /* buffer is empty */
return 0;
}

repeate_reading:
if ( rp < wp ) {
num = min( count,( int ) ( wp-rp ) );
}
else {
num = min( count,( int ) ( buffer+BUF_LEN-rp ) );
}
copy_to_user( buf,rp,num );
rp += num;
count -= num;
nRead += num;
if ( rp == ( buffer + BUF_LEN ) )
rp = buffer;
if ( ( rp != wp ) && ( count > 0 ) )
goto repeate_reading;
flag = 0;
wake_up_interruptible( &write_wq );
return nRead;
}

在前頭我有提到,buf 的地址是屬於 user space 的。在 kernel space 中,你不能像普通寫到 buffer 裡一樣直接將資料寫到 buf 裡,或直接從 buf 裡讀資料。Linux 裡使用 FS 這個 register 來當作 kernel space 和 user space 的切換。所以,如果你想手動的話,可以這樣做:

mm_segment_t fs;
fs = get_fs();
set_fs( USER_DS );
write_data_to_buf( buf );
set_fs( fs );

也就是先切換到 user space,再寫資料到 buf 裡。之後記得要切換回來 kernel space。這種自己動手的方法比較麻煩,所以 Linux 提供了幾個 function,可以讓我們直接在不同的 space 之間做資料的搬移。誠如各位所見,copy_to_user() 就是其中一個。

copy_to_user( to,from,n );
copy_from_user( to,from,n );

顧名思義,copy_to_user() 就是將資料 copy 到 user space 的 buffer 裡,也就是從 to 寫到 from,n 為要 copy 的 byte 數。相同的,copy_from_user() 就是將資料從 user space 的 from copy 到位於 kernel 的 to 裡,長度是 n bytes。在以前的 kernel 裡,這兩個 function 的前身是 memcpy_tofs() 和 memcpy_fromfs(),不知道為什麼到了 kernel 2.2.1之後,名字就被改掉了。至於它們的程序代碼有沒有更改就不太清楚了。至於到那一版才改的。我沒有仔細去查,只知道在 2.0.36 時還沒改,到了 2.2.1 就改了。這兩個 function 是 macro,都定義在裡。要使用前記得先 include 進來。

相信 buf_read() 的程序代碼應當不難瞭解才對。不知道各位有沒有看到,在buf_read() 的後面有一行的程序,就是

wake_up_interruptible( &write_wq );

write_wq 是我們用來放那些想要寫資料到 buffer,但 buffer 已滿的 process。這一行的程序會將掛在此 queue 上的 process 叫醒。當 queue 是空的時,也就是當 write_wq 為 NULL 時,wake_up_interruptible() 並不會造成任何的錯誤。接下來,我們來看看更改後的 buf_write()。

static ssize_t buf_write( struct file *filp,const char *buf,size_t count,loff_t *ppos )
{
int num,nWrite;
nWrite = 0;
while ( ( wp == rp ) && flag ) {
interruptible_sleep_on( &write_wq );
}

repeate_writing:
if ( rp > wp ) {
num = min( count,( int ) ( rp - wp ) );
}
else {
num = min( count,( int ) ( buffer + BUF_LEN - wp ) );
}
copy_from_user( wp,buf,num );
wp += num;
count -= num;
nWrite += num;
if ( wp == ( buffer + BUF_LEN ) ) {
wp = buffer;
}
if ( ( wp != rp ) && ( count > 0 ) ) {
goto repeate_writing;
}
flag = 1;
return nWrite;
}

我們把 process 丟到 write_wq 的動作放在 buf_write() 裡。當 buffer 已滿時,就直接將 process 丟到 write_wq 裡.

while ( ( wp == rp ) && flag ) {
interruptible_sleep_on( &write_wq );
}

好了。現在程序已經做了一些修改。再重新 make 一次,利用 insmod 將 buf.o 載到 kernel 裡就行了。接著,我們就來試驗一下是不是真正做到 block IO.

# cd /dev
# ls -l ~/WWW-HOWTO
-rw-r--r-- 1 root root 23910 Apr 14 16:50 /root/WWW-HOWTO
# cat ~/WWW-HOWTO > buf

執行到這裡,應該會被 block 住。現在,我們再開一個 shell 出來.

# cd /dev
# cat buf
..。( contents of WWW-HOWTO ) ..。skip ...

此時,WWW-HOWTO 的內容就會出現了。而且之前 block 住的 shell 也已經回來了。最後,試驗結束,可以下

# rmmod buf

將 buf 這個 module 從 kernel 中移除。以上跟各位介紹的就是 wait_queue 的使用。希望能對各位有所助益。

我想對某些人來講,會使用一個東西就夠了。然而對某些人來講,可能也很希望知道這項東西是如何做出來的。至少我就是這種人。在下面,我將為各位介紹 wait_queue 的 implementation。如果對其 implementation 沒興趣,以下這一段就可以略過不用看了。

wait_queue 是定義在 裡,我們可以先看看它的數據結構是怎麼樣:

struct wait_queue {
struct task_struct * task;
struct wait_queue * next;
};

很簡單是吧。這個結構裡面只有二個字段,一個是 task_struct 的 pointer,另一個則是 wait_queue 的 pointer。很明顯的,我們可以看出 wait_queue 其實就是一個 linked list,而且它還是一個 circular linked list。 其中 task_struct 就是用來指呼叫 sleep_on 等 function的 process。在 Linux 裡,每一個 process 是由一個 task_struct 來描敘。task_struct 是一個很大的的結構,在此我們不會討論。Linux 裡有一個 global variable,叫 current,它會指到目前正在執行的 process 的 task_struct 結構。這也就是為什麼當 process 呼叫 system call,切換到 kernel 時,kernel 會知道是那個 process 呼叫的。

好,我們現在來看看 interruptible_sleep_on() 和 sleep_on() 是如何做的。這兩個 function 都是位於 /usr/src/linux/kernel/sched.c 裡。

void interruptible_sleep_on(struct wait_queue **p)
{
SLEEP_ON_VAR
current->state = TASK_INTERRUPTIBLE;
SLEEP_ON_HEAD
schedule();
SLEEP_ON_TAIL
}

void sleep_on(struct wait_queue **p)
{
SLEEP_ON_VAR
current->state = TASK_UNINTERRUPTIBLE;
SLEEP_ON_HEAD
schedule();
SLEEP_ON_TAIL
}

各位有沒有發現這兩個 function 很類似。是的,它們唯一的差別就在於

current->state = ...

這一行而已。之前,我們有說過,interruptible_sleep_on() 可以被 signal 中斷,所以,其 current->state 被設為 TASK_INTERRUPTIBLE。而 sleep_on() 沒辦法被中斷,所以 current->state 設為 TASK_UNINTERRUPTIBLE。接下來,我們只看 interruptible_sleep_on() 就好了。畢竟它們兩的差異只在那一行而已。

在 sched.c 裡,SLEEP_ON_VAR 是一個 macro,其實它只是定義兩個區域變量出來而已。

#defineSLEEP_ON_VAR\
unsigned long flags;\
struct wait_queue wait;

剛才我也說過,current 這個變量是指到目前正在執行的 process 的 task_struct 結構。所以 current->state = TASK_INTERRUPTIBLE 會設定在呼叫 interruptible_sleep_on() 的 process 身上。至於 SLEEP_ON_HEAD 做的事,則是將 current 的值放到 SLEEP_ON_VAR 宣告的 wait 變量裡,並把 wait 放到 interruptible_sleep_on() 的參數所屬的 wait_queue list 中。

#defineSLEEP_ON_HEAD\
wait.task = current;\
write_lock_irqsave(&waitqueue_lock,flags);\
__add_wait_queue(p,&wait);\
write_unlock(&waitqueue_lock);

wait 是在 SLEEP_ON_VAR 中宣告的區域變量。其 task 字段被設成呼叫 interruptible_sleep_on() 的 process。至於 waitqueue_lock 這個變量是一個 spin lock。 waitqueue_lock 是用來確保同一時間只能有一個 writer。但同一時間則可以有好幾個 reader。也就是說 waitqueue_lock 是用來保證 critical section 的 mutual exclusive ACCESS。

unsigned long flags;
write_lock_irqsave(&waitqueue_lock,flags);
...critical section ...
write_unlock(&waitqueue_lock)

學過 OS 的人應該知道 critical section 的作用是什麼,如有需要,請自行參考 OS 參考書。在 critical section 裡只做一件事,就是將 wait 這個區域變量放到 p 這個 wait_queue list 中。 p 是 user 在呼叫 interruptible_sleep_on() 時傳進來的,它的型別是 struct wait_queue **。在此, critical section 只呼叫 __add_wait_queue()。

extern inline void __add_wait_queue(struct wait_queue ** p,
struct wait_queue * wait)
{
wait->next = *p ? : WAIT_QUEUE_HEAD(p);
*p = wait;
}

__add_wait_queue() 是一個inline function,定義在 中。WAIT_QUEUE_HEAD()是個很有趣的 macro,待會我們再討論。現在只要知道它會傳回這個 wait_queue 的開頭就可以了。所以,__add_wait_queue() 的意思就是要把 wait 放到 p 所屬的 wait_queue list 的開頭。但是,大家還記得嗎? 在上面的例子裡,一開始我們是把 write_wq 設為 NULL。也就是說 *p 是 NULL。所以,當 *p 是 NULL 時,


wait->next = WAIT_QUEUE_HEAD(p)

是什麼意思呢?

所以,現在,我們來看一下 WAIT_QUEUE_HEAD() 是怎麼樣的一個 macro,它是定義在裡。

#define WAIT_QUEUE_HEAD(x) ((struct wait_queue *)((x)-1))

x 型別是 struct wait_queue **,因為是一個 pointer,所以大小是 4 byte。因此,若 x 為 100 的話,那 ((x)-1) 就變成 96。如下圖所示。 WAIT_QUEUE_HEAD(x) 其實會傳回 96,而且將其轉型為 struct wait_queue*,各位可以看看。原本的 wait_queue* 只配製在 100-104 之間。現在 WAIT_QUEUE_HEAD(x) 卻直接傳回96,但是 96-100 這塊位置根本沒有被我們配置起來。更妙的事。由於 x 是一個 wait_queue list 的開頭,我們始終不會用到 96-100 這塊,我們只會直接使用到 100-104 這塊內存。這也算是 wait_queue 一項比較奇怪的 implementation 方式吧。下面有三張圖,第一張表示我們宣告了一個 wait_queue* 的變量,地址在 100。另外還有一個 wait_queue 的變量,名叫 wait。第二張圖是我們呼叫 interruptible_sleep_on() 之後得到的結果。第三張則是我們又宣告一個 wait_queue,名叫 ano_wait,將 ano_wait 放到 wait_queue list 後的結果就第三張圖所顯示的。http://linuxfab.cx/Columns/10/wqq.GIF

在 interruptible_sleep_on() 中,當呼叫完 SLEEP_ON_HEAD 之後,目前的 process 就已經被放到 wait_queue 中了。接下來會直接呼叫 schedule(),這個 function 是用來做 scheduling 用的。current 所指到的 process 會被放到 scheduling queue 中等待被挑出來執行。執行完 schedule() 之後,current 就沒辦法繼續執行了。而當 current 以後被 wake up 時,就會從 schedule() 之後,也就是從 SLEEP_ON_TAIL 開始執行。SLEEP_ON_TAIL 做的事剛好跟 SLEEP_ON_HEAD 相反,它會將此 process 從 wait_queue 中移除。

#defineSLEEP_ON_TAIL\
write_lock_irq(&waitqueue_lock);\
__remove_wait_queue(p,&wait);\
write_unlock_irqrestore(&waitqueue_lock,flags);

跟 SLEEP_ON_HEAD 一樣。SLEEP_ON_TAIL 也是利用 spin lock 包住一個 critical section。

extern inline void __remove_wait_queue(struct wait_queue ** p,struct
wait_queue * wait)
{
struct wait_queue * next = wait->next;
struct wait_queue * head = next;
struct wait_queue * tmp;
while ((tmp = head->next) != wait) {
head = tmp;
}
head->next = next;
}

__remove_wait_queue() 是一個 inline function,也是同樣定義在 裡。是用來將 wait 從 p 這個 wait_queue list 中移除掉。

現在,大家應該已經清楚了 interruptible_sleep_on() 和 sleep_on() 的做法,也應該比較清楚 wait_queue 是如何的做到 block IO。接下來,我們繼續看 wake_up_interruptible() 和 wake_up() 是如何 implement 的。wake_up_interruptible() 和 wake_up() 其實是兩個 macro,都定義在 裡。

#define wake_up(x) __wake_up((x),TASK_UNINTERRUPTIBLE | \
TASK_INTERRUPTIBLE)
#define wake_up_interruptible(x) __wake_up((x),TASK_INTERRUPTIBLE)

從這裡可以看出,兩個 macro 幾乎是一樣的,差別只在於傳給 __wake_up() 中的一個 flag 有所差異而已。其實,wake_up() 傳給 __wake_up() 的是 TASK_UNINTERRUPTIBLE|TASK_INTERRUPTIBLE,意思是說它會將 wait_queue list 中 process->state 是 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的所有 process 叫醒。而 wake_up_interruptible() 則只將 state是 TASK_INTERRUPTIBLE 的叫醒.

void __wake_up(struct wait_queue **q,unsigned int mode)
{
struct wait_queue *next;
read_lock(&waitqueue_lock);
if (q && (next = *q)) {
struct wait_queue *head;
head = WAIT_QUEUE_HEAD(q);
while (next != head) {
struct task_struct *p = next->task;
next = next->next;
if (p->state & mode)
wake_up_process(p);
}
}
read_unlock(&waitqueue_lock);
}

在 wake up 的過程中,我們不需要設定 write lock,但是仍要設定 read lock,這是為了避免有人在我們讀取 wait_queue 時去寫 wait_queue list 的內容,造成 inconsistent。在這段程序代碼中,是去 transverse 整個 list,如果 process 的 state 跟 mode 有吻合,則呼叫 wake_up_process() 將它叫醒。

void wake_up_process(struct task_struct * p)
{
unsigned long flags;
spin_lock_irqsave(&runqueue_lock,flags);
p->state = TASK_RUNNING;
if (!p->next_run) {
add_to_runqueue(p);
reschedule_idle(p);
}
spin_unlock_irqrestore(&runqueue_lock,flags);
}

在此,runqueue_lock 也是一個 spin lock,kernel 依然在此設一個 critical section 以方便更改 run queue。Run queue 是用來放可以執行的 process 用的。在放入 run queue 之前,會先將 process 的 state 設為 TASK_RUNNING。

wait_queue 其實是一個蠻好用的東西。相信只要各位有機會去修改 kernel 的話,都應該有機會用到它才對。希望對大家有點幫助.

Plurk語法

Plurk為國外一碎碎念相關web2.0服務

最大特色:
相較於其它的碎念服務如twitter, buboo, jaiku等直向訊息排列
(最新一筆在上面,由上往下且一排一則,排列訊息)
Plurk採用流水式橫向時間軸排列個別訊息
(最新一筆在左邊,由左而右且多則多道,排列訊息)

次要特色:
1.發文可選分類
2.於主畫面上可點選圖示進行訊息過濾
3.近似tumblr與soup可貼影片網址或圖檔網址
4.可針對單則發文進行隱私設定
5.可經由發文累積karma獲取表情圖示
6.發文可用wiki-like的語法

說了這堆拉里拉雜...似乎離題了。
這篇寫來是為整理Dogg大神的語法測試文:

1.粗體
語法 **文字**
例子 **這是粗體**
效果 **這是粗體**
解說 兩個「*」為一組,包圍文字左右兩側,不空格

2.斜體
語法 *文字*
例子 *這是斜體*
效果 *這是斜體*
解說 一個「*」為一組,包圍文字左右兩側,不空格

3.底線
語法 __文字__
例子 __這有底線__
效果 __這有底線__
解說 兩個「_」為一組,包圍文字左右兩側,不空格

4.網址連結
語法 網址 (連結說明)
例子 http://www.plurk.com/ (Plurk)
效果 Plurk
解說 網址,後面空半形格,緊接被半形括號包圍的網址說明

5.在發文裡連結user個人頁
語法 @user_id
例子 @orca
效果 orca
解說 @小老鼠後面緊接使用者id,可直接連結至其個人plurk頁

Restaurant City 每日 Food Quiz 答案

每日 Food Quiz 答案

問:What is the main ingredient of tofu? (問豆腐的主要成分)
答:Soybeans (黃豆)

問:The sirloin is found where on the cow? (西冷來自牛身體哪個部分)
答:Mid (中間)

問:The flank is found where on the cow? (牛腩來自牛身體哪個部分)
答:Mid (中間)

問:Granny Smith apples are? (問青蘋果是甚麼顏色)
答:Green (綠色)

問:A Coconut is a? (問椰子屬於哪一種食品)
答:Fruit (水果)

問:Honey comes from? (問蜜糖來自甚麼)
答:Bees (蜜蜂)

問:Ketchup is made from? (問茄汁是用甚麼造的)
答:Tomatoes (蕃茄)

問:What is the most expensive spice in the world? (問世上最昂貴的香料)
答:Saffron (番紅花) – 網上找到一篇不錯的番紅花介紹文章

問:What is hashi? (問 hashi 是甚麼)
答:Chopsticks (日文 hashi 是筷子)

問:Which of these fruits were originally called Chinese gooseberry? (問哪一種水果原稱中國鵝莓)
答:Kiwi fruit (奇異果)

問:Caviar is made from? (問魚子醬是用甚麼造的)
答:Fish eggs (魚卵)

問:Agar is a gelatinous substance derived from what? (問石花菜是源自甚麼的凝膠狀物)
答:Seaweed (海藻)

問:Saffron is a? (番紅花是一種…?)
答:Spice (香料)

問:In which restaurant would you typically find Sushi? (通常在甚麼餐廳可找到壽司)
答:Japanese (日本餐廳)

問:Dulce de leche is prepared by heating? (焦糖牛奶由甚麼加熱而成)
答:Sweetened Milk (甜奶、煉奶)

問:To proof dough means to? (讓麵粉團發酵的意思是?)
答:Allow it to rise (讓它漲起來)

問:Naan bread is typically served with? (Naan bread 通常配甚麼一起吃?)
答:Indian food (印度菜) – Naan bread 一般譯為印度煎餅。

問:A crêpe is typically a? (叫作 crêpe 的一般是指…?)
答:Thinly cooked pancake (很薄的薄烤餅) – 法式的薄餅,一般是捲起來包裹著草莓和奶油。

問:To blanch a vegetable, means to? (要 blanch 一棵蔬菜,意思是指?)
答:Boil and cool off quickly (在沸水中稍為灼一下然後冷卻) – 廣東話裡應該是「飛水、出水」的意思吧?

問:Calamari is fried? (Calamari 是一種炸…?)
答:Squid (烏賊、墨魚、花枝) – 意大利文 calamari 就是指墨魚、烏賊。

090627205145

我來跟奶油獅合照一下

Trap Probe Example

Trap Probe Example

The following example shows the handling of traps.




"type" = "custom-snmp-trap"
"package" = "com.dartware"
"probe_name" = "snmp.trapdisplay"
"human_name" = "Trap Viewer"
"version" = "2.2"
"address_type" = "IP,AT"
"port_number" = "161"
"display_name" = "SNMP/Trap Viewer"




\GB\Trap Viewer Probe\P\

This probe listens for trap packets to arrive and displays the contents of the
trap in the Status Window. It does not actively poll the device, nor does it
take any action based on the trap contents.

You can view all the variables that have been parsed from the trap packet in the
device's Status Window. You can also use this as a prototype for making your own
trap probes.

\B\How the Trap Viewer Probe Works\p\

When a trap arrives, the probe parses the trap to get the values from the trap's
header as well as the first ten items in its Varbind List. It assigns all these
values to variables that can be used in the probe and displayed in the Status
Window.

To see how this probe works, you can configure your equipment to send traps to
InterMapper, or use the net-snmp \b\snmptrap\p\ command. Either way, the Status
Window will show the values present in any traps that arrive.

For more information on the \b\snmptrap\p\ command, read the net-snmp
documentation for the
\u2=http://www.net-snmp.org/tutorial/tutorial-4/commands/snmptrap.html\trap
tutorial\p0\ and the
\u2=http://www.net-snmp.org/docs/man/snmpinform.html\snmptrap command\0p\. The
remainder of this note shows how to send a trap with variables from the Dartware
MIB:

\i\SNMPv1 Traps\p\

a) Add a device to a map with the IP address \i\192.168.56.78\p\
b) Set it to use this probe
c) Issue the snmptrap command below from the command line (it should all be on
one line):

snmptrap -v 1 -c commString localhost
1.3.6.1.4.1.6306 192.168.56.78 6 123 4567890
1.3.6.1.4.1.6306.2.1.1.0 s "05/08 23:26:35"
1.3.6.1.4.1.6306.2.1.2.0 s Critical
1.3.6.1.4.1.6306.2.1.3.0 s "Big Router"
1.3.6.1.4.1.6306.2.1.4.0 s "Critical: High Traffic"
1.3.6.1.4.1.6306.2.1.5.0 s "127.0.0.1"
1.3.6.1.4.1.6306.2.1.6.0 s "SNMP Traffic Probe"

\i\SNMPv2c Traps\p\

a) Add a device to the map with an IP address of \i\localhost\p\
b) Set it to use this probe
c) Issue the snmptrap command below from the command line (it should all be on
one line)

snmptrap -v 2c -c commString localhost
4567890 1.3.6.1.4.1.6306
1.3.6.1.4.1.6306 192.168.56.78 6 123 4567890
1.3.6.1.4.1.6306.2.1.1.0 s "05/08 13:26:35"
1.3.6.1.4.1.6306.2.1.2.0 s Critical
1.3.6.1.4.1.6306.2.1.3.0 s "Big Router"
1.3.6.1.4.1.6306.2.1.4.0 s "Critical: High Traffic"
1.3.6.1.4.1.6306.2.1.5.0 s "127.0.0.1"
1.3.6.1.4.1.6306.2.1.6.0 s "SNMP Traffic Probe"




-- The parameters in this probe are unused, but could be used to
-- set thresholds for various alarms.


"MinValue" = "10"
"MaxValue" = "50"




-- TrapVariables are updated when a trap arrives.
-- This set of variables comes from the Dartware MIB
-- and would be sent in a trap from another copy of InterMapper.

trapTimeStamp, 1.3.6.1.4.1.6306.2.1.1.0, TRAPVARIABLE, "Timestamp"
DeviceStatus, 1.3.6.1.4.1.6306.2.1.2.0, TRAPVARIABLE, "Status"
DeviceDNS, 1.3.6.1.4.1.6306.2.1.3.0, TRAPVARIABLE, "DNS Name of Device"
DeviceCondition, 1.3.6.1.4.1.6306.2.1.4.0, TRAPVARIABLE, "Condition String"
TrapSourceAdrs, 1.3.6.1.4.1.6306.2.1.5.0, TRAPVARIABLE, "Source of trap"
ProbeType, 1.3.6.1.4.1.6306.2.1.6.0, TRAPVARIABLE, "Probe that generated
trap"

-- Variables from the trap packet itself

genericTrapVar, $GenericTrap, TRAPVARIABLE, "Generic Trap"
specificTrapVar, $SpecificTrap, TRAPVARIABLE, "Specific Trap"
timeStampVar, $TimeStamp, TRAPVARIABLE, "Timestamp"
enterpriseVar, $Enterprise, TRAPVARIABLE, "Enterprise"
commStringVar, $CommunityString, TRAPVARIABLE, "Community String"
trapOIDVar, $TrapOID, TRAPVARIABLE, "Trap OID"
agentAdrsVar, $AgentAddress, TRAPVARIABLE, "Agent Address"
senderAdrsVar, $SenderAddress, TRAPVARIABLE, "Sender Address"
snmpVersionVar, $SnmpVersion, TRAPVARIABLE, "SNMP Version"
varbindCountVar, $VarbindCount, TRAPVARIABLE, "Varbind Count"

-- Positional names of Varbind List items

vbVal1, $VarbindValue1, TRAPVARIABLE, "Value of Varbind1"
vbType1, $VarbindType1, TRAPVARIABLE, "Type of Varbind1"
vbOID1, $VarbindOID1, TRAPVARIABLE, "OID of Varbind1"
vbVal2, $VarbindValue2, TRAPVARIABLE, "Value of Varbind2"
vbType2, $VarbindType2, TRAPVARIABLE, "Type of Varbind2"
vbOID2, $VarbindOID2, TRAPVARIABLE, "OID of Varbind2"
vbVal3, $VarbindValue3, TRAPVARIABLE, "Value of Varbind3"
vbType3, $VarbindType3, TRAPVARIABLE, "Type of Varbind3"
vbOID3, $VarbindOID3, TRAPVARIABLE, "OID of Varbind3"
vbVal4, $VarbindValue4, TRAPVARIABLE, "Value of Varbind4"
vbType4, $VarbindType4, TRAPVARIABLE, "Type of Varbind4"
vbOID4, $VarbindOID4, TRAPVARIABLE, "OID of Varbind4"
vbVal5, $VarbindValue5, TRAPVARIABLE, "Value of Varbind5"
vbType5, $VarbindType5, TRAPVARIABLE, "Type of Varbind5"
vbOID5, $VarbindOID5, TRAPVARIABLE, "OID of Varbind5"
vbVal6, $VarbindValue6, TRAPVARIABLE, "Value of Varbind6"
vbType6, $VarbindType6, TRAPVARIABLE, "Type of Varbind6"
vbOID6, $VarbindOID6, TRAPVARIABLE, "OID of Varbind6"
vbVal7, $VarbindValue7, TRAPVARIABLE, "Value of Varbind7"
vbType7, $VarbindType7, TRAPVARIABLE, "Type of Varbind7"
vbOID7, $VarbindOID7, TRAPVARIABLE, "OID of Varbind7"
vbVal8, $VarbindValue8, TRAPVARIABLE, "Value of Varbind8"
vbType8, $VarbindType8, TRAPVARIABLE, "Type of Varbind8"
vbOID8, $VarbindOID8, TRAPVARIABLE, "OID of Varbind8"
vbVal9, $VarbindValue9, TRAPVARIABLE, "Value of Varbind9"
vbType9, $VarbindType9, TRAPVARIABLE, "Type of Varbind9"
vbOID9, $VarbindOID9, TRAPVARIABLE, "OID of Varbind9"
vbVal10, $VarbindValue10, TRAPVARIABLE, "Value of Varbind10"
vbType10, $VarbindType10, TRAPVARIABLE, "Type of Varbind10"
vbOID10, $VarbindOID10, TRAPVARIABLE, "OID of Varbind10"





\B5\Information about the Trap\0P\
\4\CommunityString:\0\ $commStringVar
\4\ TimeStamp:\0\ $timeStampVar
\4\ AgentAddress:\0\ $agentAdrsVar
\4\ SenderAddress:\0\ $senderAdrsVar
\4\ GenericTrap:\0\ $genericTrapVar \3IG\(v1 only) \P0M\
\4\ SpecificTrap:\0\ $specificTrapVar \3IG\(v1 only) \P0M\
\4\ Enterprise:\0\ $enterpriseVar \3IG\(v1 only) \P0M\
\4\ TrapOID:\0\ $trapOIDVar \3IG\(v2c only) \P0M\
\4\ SnmpVersion:\0\ $snmpVersionVar \3IG\(0=SNMPv1; 1=SNMPv2c) \P0M\
\4\ VarbindCount:\0\ $varbindCountVar \3IG\(total number of Varbinds) \P0M\

\B5\Varbind List Items parsed by OID\0P\
\4\ TimeStamp:\0\ $trapTimeStamp \3IG\ \P0M\
\4\ Device Status:\0\ $deviceStatus \3IG\ \P0M\
\4\ Device DNS:\0\ $deviceDNS \3IG\ \P0M\
\4\Condition String:\0\ $deviceCondition \3IG\ \P0M\
\4\Trap Source Adrs:\0\ $TrapSourceAdrs \3IG\ \P0M\
\4\ Probe Type:\0\ $ProbeType \3IG\ \P0M\

\B5\Varbind List Items by Position\0P\ \3IG\(Varbind Value / Varbind Type /
Varbind OID) \P0M\
\4\ VarBindList #1:\0\ $vbVal1 / $vbType1 / $vbOID1
\4\ VarBindList #2:\0\ $vbVal2 / $vbType2 / $vbOID2
\4\ VarBindList #3:\0\ $vbVal3 / $vbType3 / $vbOID3
\4\ VarBindList #4:\0\ $vbVal4 / $vbType4 / $vbOID4
\4\ VarBindList #5:\0\ $vbVal5 / $vbType5 / $vbOID5
\4\ VarBindList #6:\0\ $vbVal6 / $vbType6 / $vbOID6
\4\ VarBindList #7:\0\ $vbVal7 / $vbType7 / $vbOID7
\4\ VarBindList #8:\0\ $vbVal8 / $vbType8 / $vbOID8
\4\ VarBindList #9:\0\ $vbVal9 / $vbType9 / $vbOID9
\4\VarBindList #10:\0\ $vbVal10 / $vbType10 / $vbOID10
本文是笔者学习net-snmp开发包时的心得,由于那个官方网站的东西比较乱,所以写一个总结,希望后来者少走些弯路


一,trap的用途

TRAP是提供从代理进程到管理站的异步报告机制。

为了使管理站能够及时而又有效地对被管理设备进行监控,同时又不过分增加网络的通信负载,必须使用陷入(TRAP)制导的轮讯过程。代理进程负责在必要时 向管理站报告异常事件,得到异常事件的报告后,管理站可以查询有关的代理,以便得到更具体的信息,对事件的原因做进一步的分析

二,trap的工作流程

1,agent端:

A, 编写mib文件,确定好trap名称等信息。

B, 命令方式:发送各种trap命令(manager地址后面一定要加端口号162),在manager端看反应结果,在agent端无反应

以下都未实现

C, 自动触发:配置snmpd.conf设置触发trap,系统发生某类错误时会自动触发相应类型的trap,发送给manager

D, 程序方式:一部份trap需要写c语言程序,用相应的api(send_easy_trap 或 send_v2trap)发送

2,manager端:

A, 配置snmptrapd.conf文件,设置访问权限

B, 将mib导入到mibs文件夹中

C, 用perl等脚本语言编写处理trap的程序

D, 配置snmptrapd.conf文件,添加traphandler项,将不同的trap对应到不同的处理程序上

三,trap的环境配置

1, manager端

a, 在/etc/hosts.allow加入允许接受的网段snmptrapd: 192.168.

b, 建立/usr/share/snmp/snmptrapd.conf(我的机器上是这个,不同机器不同,可能有的放在/etc/snmp,/usr/local/share/snmp/下,视不同情况慢慢实验),加入以下一行:

authcommunity execute|log|net public

设置所有用户的访问权限:可执行,记录,传递

四,命令方式的过程

1,处理系统默认的trap

添加以下几行到snmptrapd.conf中

traphandle .1.3.6.1.6.3.1.5.1 page_me up

traphandle .1.3.6.1.4.1.2021.251.1 page_me up

traphandle .1.3.6.1.4.1.2021.251.2 page_me down

traphandle default log_it

用snmptrapd –d –f –Lo启动snmptrapd

然后在agent端输入snmptrap -v 2c -c public 192.168.213.64:162 "" UCD-SNMP-MIB::ucdStart

Manager端反应:

NET-SNMP version 5.3.0.1



Received 73 bytes from UDP: [192.168.213.64]:32807

0000: 30 47 02 01 01 04 06 70 75 62 6C 69 63 A7 3A 02 0G.....public.:.

0016: 04 58 92 A4 F0 02 01 00 02 01 00 30 2C 30 10 06 .X.........0,0..

0032: 08 2B 06 01 02 01 01 03 00 43 04 03 E7 18 95 30 .+.......C.....0

0048: 18 06 0A 2B 06 01 06 03 01 01 04 01 00 06 0A 2B ...+...........+

0064: 06 01 04 01 8F 65 81 7B 01 .....e.{.



192.168.213.64 [UDP: [192.168.213.64]:32807]: Trap , DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (65476757) 7 days, 13:52:47.57, SNMPv2-MIB::snmpTrapOID.0 = OID: UCD-SNMP-MIB::ucdStart

sh: page_me: command not found

说明收到trap ucdstart并且调用对应的脚本程序,这里由于系统没有page_me这个命令,所以返回命令找不到

2, 处理自定义trap(参考http://www.net-snmp.org/wiki/index.php/TUT:snmptrap)

编写两个mib文件,包括snmp1和snmp2两种trap

Snmp1的mib:TRAP-TEST-MIB.txt

TRAP-TEST-MIB DEFINITIONS ::= BEGIN

IMPORTS ucdExperimental FROM UCD-SNMP-MIB;

demotraps OBJECT IDENTIFIER ::= { ucdExperimental 990 }

demo-trap TRAP-TYPE

STATUS current

ENTERPRISE demotraps

VARIABLES { sysLocation }

DESCRIPTION "This is just a demo"

::= 17

END

Snmp2的mib:NOTIFICATION-TEST-MIB.txt

NOTIFICATION-TEST-MIB DEFINITIONS ::= BEGIN

IMPORTS ucdavis FROM UCD-SNMP-MIB;

demonotifs OBJECT IDENTIFIER ::= { ucdavis 991 }

demo-notif NOTIFICATION-TYPE

STATUS current

OBJECTS { sysLocation }

DESCRIPTION "Just a test notification"

::= { demonotifs 17 }

END

然后放入到mibs文件夹中

在manager端敲入命令:

snmptrap -v 2c -c public 192.168.213.64:162 "" NOTIFICATION-TEST-MIB::demo-notif SNMPv2-MIB::sysLocation.0 s "just here"

agent端输出:

Received 96 bytes from UDP: [192.168.213.64]:32808

0000: 30 5E 02 01 01 04 06 70 75 62 6C 69 63 A7 51 02 0^.....public.Q.

0016: 04 17 27 54 32 02 01 00 02 01 00 30 43 30 10 06 ..'T2......0C0..

0032: 08 2B 06 01 02 01 01 03 00 43 04 03 F0 3A 1A 30 .+.......C...:.0

0048: 18 06 0A 2B 06 01 06 03 01 01 04 01 00 06 0A 2B ...+...........+

0064: 06 01 04 01 8F 65 87 5F 11 30 15 06 08 2B 06 01 .....e._.0...+..

0080: 02 01 01 06 00 04 09 6A 75 73 74 20 68 65 72 65 .......just here



192.168.213.64 [UDP: [192.168.213.64]:32808]: Trap , DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (66075162) 7 days, 15:32:31.62, SNMPv2-MIB::snmpTrapOID.0 = OID: UCD-SNMP-MIB::ucdavis.991.17, SNMPv2-MIB::sysLocation.0 = STRING: just here

sh: log_it: command not found

其中just here就是我们想要的结果

3, 自己编写处理trap脚本

建立root/bin/traps文件,输入以下内容

#!/bin/sh

read host

read ip

vars=

while read oid val

do

if [ "$vars" = "" ]

then

vars="$oid = $val"

else

vars="$vars, $oid = $val"

fi

done

echo trap: $1 $host $ip $vars



在manager的snmptrapd.conf加入以下几行

traphandle SNMPv2-MIB::coldStart /root/bin/traps cold

traphandle SNMPv2-MIB::warmStart /root/bin/traps warm

traphandle IF-MIB::linkDown /root/bin/traps down

traphandle IF-MIB::linkUp /root/bin/traps up

traphandle SNMPv2-MIB::authenticationFailure /root/bin/traps auth

# this one is deprecated

traphandle .1.3.6.1.6.3.1.1.5.6 /root/bin/traps egp-neighbor-loss



重启snmptrapd:snmptrapd –d –f –Lo 161这里要它监听161端口

在agent端输入命令:

snmptrap -v 1 -c public 192.168.213.64 TRAP-TEST-MIB::demotraps 192.168.213.64 2 0 ""IF-MIB::ifIndex i 1



manager端的反应:

Received 63 bytes from UDP: [192.168.213.64]:32812

0000: 30 3D 02 01 00 04 06 70 75 62 6C 69 63 A4 30 06 0=.....public.0.

0016: 0A 2B 06 01 04 01 8F 65 0D 87 5E 40 04 C0 A8 D5 .+.....e..^@....

0032: 40 02 01 02 02 01 00 43 04 03 F1 9E 99 30 10 30 @......C.....0.0

0048: 0E 06 09 2B 06 01 02 01 02 02 01 01 02 01 01 ...+...........



2007-08-07 12:54:43 192.168.213.64(via UDP: [192.168.213.64]:32812) TRAP, SNMP v1, community public

UCD-SNMP-MIB::ucdExperimental.990 Link Down Trap (0) Uptime: 7 days, 15:47:44.25

IF-MIB::ifIndex = INTEGER: 1

trap: down 192.168.213.64 UDP: [192.168.213.64]:32812 DISMAN-EVENT-MIB::sysUpTimeInstance = 7:15:47:44.25, SNMPv2-MIB::snmpTrapOID.0 = IF-MIB::linkDown, IF-MIB::ifIndex = 1, SNMP-COMMUNITY-MIB::snmpTrapAddress.0 = 192.168.213.64, SNMP-COMMUNITY-MIB::snmpTrapCommunity.0 = "public", SNMPv2-MIB::snmpTrapEnterprise.0 = UCD-SNMP-MIB::ucdExperimental.990

4, 让agent自动产生trap

配置agent的snmpd.conf,加入以下几行:(未搞出来,可能是配置文件问题,manager端收不到任何trap)

# send v1 traps

trapsink 192.168.213.64:162 public

# also send v2 traps

trap2sink 192.168.213.64:162 secret

# send traps on authentication failures

authtrapenable 1

五,下一步

仔细研究snmpd.conf和snmptrapd.conf,调试出系统自动触发trap

学习mib结构,搞清楚如何写自定义trap的mib

搞清楚例子的意思

研究一下在程序中发送trap的c语言api

学习perl,python等一种脚本语言,学会编写trap处理程序


五,程序方式

思路:让一个程序监控设备,如果设备产生了发送trap的条件,那么就编写一条snmptrap命令,新建一个进程将其发送出去。

本demo模拟这一过程,每2秒钟发送一条trap,用到的是上面最后一条命令:

snmptrap -v 1 -c public localhost:162 TRAP-TEST-MIB::demotraps localhost 2 0 "" IF-MIB::ifIndex i 1

manager端trap处理程序不变,mib不变。

Mytrap.c:



#include

#include



int main()

{

while(1){

int pid ;

int status;

if( (pid = fork()) == 0){

execl("/usr/bin/snmptrap","snmptrap","-v","1","-c","public","localhost:162","TRAP-TEST-MIB::demotraps","localhost","2","0","\"\"","IF-MIB::ifIndex","i","1",NULL);



printf("snmptrap\n");

return 0;

}

else if (pid <0){

perror("fork:");

}

else{

printf("send a trap\n");

sleep(2);

}

}

return 0;

}
S3C4510上嵌入式Web服务器-boa在uclinux下的的移植 uClinux下,主要有三个Web Server:httpd、thttpd和Boa。Httpd是最简单的一个Web Server,它的功能最弱,不支持认证,不支持CGI。Thttpd和Boa都支持认证、CGI等,功能都比较全。为了实现动态Web技术,这里我们选择实现一个支持CGI的、非常适合于嵌入式系统的Boa Web Server。
Boa 是一个单任务的http服务器,源代码开放、性能高。目前,uClinux的代码中已经包含boa的源代码,在uClinux下实现 Boa,需要对Boa做一些配置和修改。这主要通过对boa.conf和mime.types文件进行修改来实现,需要改动的配置有以下几项。
(1)建目录
由于uClinux默认的根文件系统romfs是只读的,不能用mkdir等命令来新建目录,故应在编译内核前先建好要用到的目录,这通过修改 /

uclinux-samsung/vendors/Samsung/4510b/makefile
ROMFS_DIRS=bin dev etc home lib mnt proc usr var的后边增加home/web home/web/cgi-bin

3.7
然后在/uclinux- samsung/Vendors/Samsung/4510b/inittab里修改,
在:inet:unknown:/bin/inetd后加入: boa:unknown:/bin/boac/home。


(2)指定Web服务器的根目录路径(SERVER_ROOT)

/uclinux-samsung/user/boa/src/目录,
通过修改define.h文件中#define ERVER_ROOT“/home”语句来指定SERVER_ROOT。

(3)修改boa.conf文件
3.1
在./uclinux-samsung/user/boa/src/boa.conf里修改:
*将user obody改为User 0
Group ogroup改为Group 0;

3.2
*将DocumentRoot/var/www
改为DocumentRoot/home/web,

3.4
*将MimeTypes/etc/mime.types
改为MimeTypes/home/web/mime.types;

3.5
定义默认首页和CGI程序所在目录,即

DirectoryIndex index.html
ScriptAlias/cgi-bin/ /home/web/cgi-bin/


这样指定后,.index.htm为远程浏览客户访问Web Server所看到的首页默认值,用C编写的CGI程序编译成二进制文件,放到/cgi-bin/目录下,CGI程序能被正确地执行。

3.6
Boa.conf文件和mime.types文件必须放到Web服务器根目录下,所以把修改后的bao.conf和mime.types拷贝到 “./uclinux-samsung/romfs/home/”目录下。


3.8
make menuconfig------>kernel--->user setting---->miscellaneous configuration--->generic cgi& cgihtml

配置过程后,重新编译内核,编译时选中Boa选项。把编译好的内核下载到开发 板,启动uClinux,完成IP配置,启动Boa Web Server,然后就可以通过IE访问你的网页了。如果想启动uClinux时自动启动Boa Web Server,可以修改rc文件,进入uclinux-samsung/vendors/Samsung/4510B目录,在运行脚本rc中增加两行:

ifconfig eth0 192.168.168.101 up
boa-c /home/&


修改后重新编译内核,再下载到开发板运行。运行uClinux后,不需要配置就可以直接通过IE来访问你的网页了。

===============================================================
uclinux中boa web server的配置
-----------------------------------------------------------------------------

uclinux中boa web server的配置
一、环境准备
编译环境:uClinux-dist-20040408.tar.gz
编译工具:arm-elf-*
板子型号:SmartArm2200

二、编译配置
1、选择Kernel/Library/Defaults Selection -> Customize Vendor/User Settings -> Network Applications -> boa并编译
2、配置boa web server:
2.1、修改文件uClinux-dist/user/boa/src/defines.h
将#define SERVER_ROOT "/home/httpd"改成#define SERVER_ROOT "/etc/boa"
意思是设定boa web server的ServerRoot路径为/etc/boa
2.2、编辑配置文件uClinux-dist/user/boa/examples/boa.conf,如下
Port 80
User 0
Group 0
DocumentRoot /var/www
UserDir public_html
DirectoryIndex index.html
KeepAliveMax 1000
KeepAliveTimeout 10
DefaultType text/html
AddType application/x-httpd-cgi cgi
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
AddType image/jpeg jpg
关于配置文件的几点说明:
(1)所以的*Log选项都被注释掉了,否则启动boa的时候提示说不识别这些*Log标记;
(2)没有添加"mime.types"选项,原因详见boa测试图片显示乱码;
(3)配置文件boa.conf需拷贝到ServerRoot,可以手动,也可以通过修改Makefile文件(uClinux-dist/vendors/PHILIPS/lpc2200/Makefile)进行;
如果配置文件boa.conf的位置不正确,则会得到形如“Authentication password file for /cgi-bin/ not found!”之类的错误!

三、调试
1、如果现在运行boa & (或者运行boa -c /var/www &,但我们已指定DocumentRoot /var/www,所以运行外面的命令就行),会得到错误信息“kmod: failed to exec /sbin/modprobe -s -k net-pf-1, errno = 2”,此时boa web server还不可用!
2、增加编译Kernel/Library/Defaults Selection -> Customize Vendor/User Settings -> BusyBox -> modprobe并重新编译,此时运行boa &又会得到如下错误信息:
__alloc_pages: 6-order allocation failed (gfp=0x1f0/0)
Allocation of length 164704 from process 193 failed
Buffer memory: 52kB
Cache memory: 1076kB
Free pages: 876kB ( 0kB HighMem)
Zone:DMA freepages: 0kB
Zone:Normal freepages: 876kB
Zone:HighMem freepages: 0kB
( Active: 94, inactive: 188, free: 219 )
= 0kB)
25*4kB 21*8kB 12*16kB 5*32kB 2*64kB 1*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB = 876kB)
= 0kB)
Unable to mmap process text, errno 12
kmod: failed to exec /sbin/modprobe -s -k net-pf-1, errno = 12
__alloc_pages: 6-order allocation failed (gfp=0x1f0/0)
Allocation of length 164704 from process 195 failed
Buffer memory: 52kB
Cache memory: 1076kB
Free pages: 700kB ( 0kB HighMem)
Zone:DMA freepages: 0kB
Zone:Normal freepages: 700kB
Zone:HighMem freepages: 0kB
( Active: 47, inactive: 235, free: 175 )
= 0kB)
25*4kB 19*8kB 12*16kB 4*32kB 2*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB = 700kB)
= 0kB)
Unable to mmap process text, errno 12
munmap of non-mmaped memory by process 195 (insmod): 817fb000
munmap of non-mmaped memory by process 195 (insmod): 815863a0
Unhandled fault: external abort on linefetch (F4) at 0x00000001
fault-common.c(97): start_code=0x81380040, start_stack=0x815bff74)
pid 182: failed 256
好像是内存分配错误!但此时boa web server已经可以访问!
3、增加编译Kernel/Library/Defaults Selection -> Customize Kernel Settings -> Networking options -> Unix domain sockets并从新编译,则boa web server再启动时除了一个pid外,没任何多余的信息了!(此选项到底有什么作用,我还是不知道!)

四、其它说明
1、我的uclinux的大部分配置都是“默认”的,但这里的“默认”可能是已被硬件厂商修改过的(我用的uClinux-dist-20040408.tar.gz是附带光盘里面的)!
2、web测试时,html文件应放在/var/www目录下,cgi程序应放在/var/www/cgi-bin目录下,这些在配置文件中都写的很明确!
3、也可以通过nfs进行web测试(这样会免去多次烧写的麻烦),nfs的配置详见http://wzc0066.blog.hexun.com/6582493_d.html中的第一点。

五、现存问题
现在cgi、javascript、图片浏览等测试都良好!
但在测mp3的时候,还不行,原因可能是还不支持大文件!
我看网上说uclinux好像默认每次分配的最大内存是128K,所以要是运行大于128K的文件就会出错,不知是不是这样!

我还看到一条信息说选择Kernel hacking中的一项配置后就可支持大文件了,但我现在的2.4.x内核中没找到网上说的那一项!
现在点击一mp3文件(二点几M大小)的链接时终端出现的错误信息为:
Allocation of length 2502723 from process 24 failed
Buffer memory: 56kB
Cache memory: 2744kB
Free pages: 2420kB ( 0kB HighMem)
Zone:DMA freepages: 0kB
Zone:Normal freepages: 2420kB
Zone:HighMem freepages: 0kB
( Active: 98, inactive: 602, free: 605 )
= 0kB)
7*4kB 9*8kB 11*16kB 7*32kB 4*64kB 1*128kB 0*256kB 1*512kB 1*1024kB 0*2048kB = 2420kB)
= 0kB)
munmap of non-mmaped memory by process 24 (boa): ffffffff

现在虽然还不完美,但一般的使用我想还是没什么问题的!这就是我的boa web server配置的一般过程!
uClinux下的Web Server(主要是boa服务器)

uCLinux 环境下,主要有三个Web 服务器:httpd、thttpd和Boa。httpd是最简单的一个Web 服务器,它的功能最弱,不支持认证,不支持CGI。如果Web 服务器仅需提供一些静态页面,例如简单的在线帮助、系统介绍等,完全可以用静态服务器httpd来实现。thttpd和Boa都支持认证、CGI等,功能都比较全。若需提高系统的安全性,或需要与用户进行交互,例如数据查询、实时状态查询等,则必须使用动态Web技术,可以选择这两种服务器之一来实现。
1. uCLinux下静态Web 服务器的实现

httpd 是最简单的一个web 服务器,只有几百行代码,功能较弱。它的一些简单配置需要在Makefile和httpd.c的内部进行,文件位于/bb40/uCLinux- dist/user/httpd/目录下。Makefile中定义了缺省的documentDir为/home/httpd,缺省主页面文件为 index.html。uCLinux20031103版本的httpd.c中的main函数有如下两行程序:

chroot(HTTPD_DOCUMENT_ROOT);

chdir("/");

其中第二行程序将影响到实际运行结果,建议将该行程序直接指向index.html文件所在目录。将页面文件复制到\bb40\uCLinux-dist\romfs\home\httpd下即可。

对uCLinux 进行重新配置,并在用户程序中选中httpd,编译uCLinux内核并下载到开发板,在uclinx启动后通过httpd &命令即可启动服务器,如需自动启动,在\uCLinux-dist\vendors\Samsung\S3C44B0目录下的rc文件中加入同样的命令即可。
2.动态服务器与CGI

目前实现动态Web页面有多种技术可供选择,CGI、ASP、PHP等技术在高端平台上都能很好地实现用户所需的功能。但在uCLinux下实现动态网页,目前只能采用CGI。

CGI,Common Gate Interface,即通用网关接口,是一个连接外部应用程序到服务器的标准。一个简单的静态HTML文档是没有交互后台程序,而CGI程序则可以实时执行并输出动态信息。

使用CGI 需要一个输入界面,一般就是一个包含了表单(FORM)的页面。FORM在CGI中是最常被使用的输入界面,它由一组标签所组成,目前的标准中,FORM 的标签可分为三大类:INPUT、SELECT以及TEXTAREA。在设置了一系列有关的标签后,每个FORM通常需要一个SUBMIT按钮用来发送表单内容。

当用户在客户端按下FORM上的SUBMIT按钮,浏览器(Browser)将客户端输入的参数传回服务器,服务器启动指定的程序并将封装的参数传入,后台程序依照传入的参数完成指定的工作。如果此时有需要传回结果的话,则程序会把结果传回给服务器并发送到浏览器。
3.用boa实现uCLinux下的动态Web 服务器( www.boa.org)

用Boa来实现动态Web 服务器的方法较为复杂,主要通过boa.conf和mime.types进行,具体过程如下:

Boa 的关键配置由boa.conf文件决定,该文件是一个文本文件,内部除了各项设置还包含了详细的注释和说明。Boa.conf文件的注释中首先指出,为了在用户访问Web时服务器能确定根目录的位置,需要指定服务器的根目录路径服务器_ROOT。\uCLinux-dist\user\boa\src\目录下的define.h定义了服务器的根目录路径,通过修改define.h文件中#define ERVER_ROOT /home语句来指定服务器_ROOT。在uCLinux启动后通过命令行也可以指定该路径,且命令行所指定的路径具有更高的优先级,命令参数为-c加上欲指向的根目录路径,例如该文件中提供的例子:boa -c /usr/local/boa。

下面来配置一个简单的boa服务器。在\uCLinux-dist\user\boa\src\boa.conf里进行如下修改:

l 将user root改为User 0;

l 将Group root改为Group 0;

l 将DocumentRoot/etc改为DocumentRoot/home;

l 将MimeTypes/etc/mime.types改为MimeTypes/home/mime.types;

l 将#AddType application/x-httpd-cgi cgi前面的#去掉使系统可以支持cgi方式;

l 定义CGI程序所在目录:ScriptAlias/cgi-bin/ /home/cgi-bin/;

这样配置后的boa服务器将/home目录作为服务器的根目录,cgi程序位于\hone\cgi-bin目录下,默认页面文件为\home\index.html。

Boa.conf文件的注释中指出,boa.conf文件和mime.types文件必须放到Web服务器根目录下,本例中把mime.types和修改后的bao.conf复制到\uCLinux-dist\romfs\home\目录下。

在编译uCLinux 内核前还要通过修改\uCLinux-dist\vendors\Samsung\S3C44B0\目录下的makefile文件来建立cgi二进制文件将要被存放到的目录。在ROMFS_DIRS=bin dev etc home lib mnt proc usr var的后边增加home/cgi-bin,这样修改后编译内核,编译后生成的根文件系统\bb40\uCLinux-dist\romfs中将增加一个 \home\cgi-bin目录。

对uCLinux 进行重新配置,并在用户程序中选中boa项,编译uCLinux内核并下载到开发板,在uclinx启动后,通过boa -c/home/&命令即可启动服务器,如需自动启动,在rc文件中加入同样的命令即可。由于尚未放入cgi程序,此时仅能看到放在home目录下的页面,不会有任何动态响应。通过下面的例子将了解如何实现页面到CGI程序的关联。
4.一个通过动态Web页面访问远程温度传感器的例子

下面通过一个例子来说明如何实现uCLinux下的动态Web页面技术。

首先建立一个简单的表单页面:



输入需要访问的温度传感器号码,并单击“确定”查看。







CGI 规定,GET方式下的表单被发送到服务器后,表单中的数据被保存在QUERY_STRING环境变量中。这种表单的处理相对简单,只要读取环境变量就可以了。在CGI程序中使用库函数getenv来把环境变量的值作为一个字符串来读取,在取得了字符串中的数据后,就可以对数据进行需要的处理。CGI程序完成处理后的输出被重定向到客户浏览器,用户通过浏览器就可以看到相关结果。请注意,在ACTION后面所指向的CGI文件并没有扩展名。

下面就是处理这个表单的CGI程序mycgi.c:

#include

#include

int main(void){

char *data;

long m;

printf("Content-Type:text/html%c%c",10,10);

printf("温度传感器状态 ");

printf("

房间温度

");

data = getenv("QUERY_STRING");

if(sscanf(data,"m=%ld",&m)!=1)

printf("

错误!输入数据非法。表单中必须输入1~10的数字。");

else

printf("

%ld号房间的温度是:%ld度。",m,readtempr(m));

return 0;

}

其中的readtempr()是读取温度传感器输出的函数。在cygwin下使用arm-elf-gcc对该cgi源程序进行编译,获得mycgi.exe,将该程序复制到\uCLinux-dist\romfs\home\cgi-bin\目录下,确保前面的表单页面文件index.html位于\uCLinux-dist\romfs\home\目录里。编译内核并下载,下面两图是运行结果。
uClinux下的Web Server - raindy - 雨吧



运行结果说明:左图是访问IP为192.168.0.128的嵌入式服务器的结果,该服务器默认页面为index.html,浏览器显示为一个简单的表单。右图是在该表单输入传感器号5并按下“确定”按钮后,CGI程序执行后返回的结果。

==========================================

嵌入式Linux webserver: Boa+CGI程序设计技术[转]

-----------------------------------------

摘要:在详细介绍一种嵌入式Web服务器BOA的实现与配置方法的基础上,以一个Web在线远程监控GPIO(通用输入/输出)的程序为实例,介绍嵌入式Linux系统下CPU程序设计技术。

关键词:嵌入式系统Linux BOA CGI GPIO

1 概述

随着互联网应用的普及,越来越多的信息化产品需要接入互联网通过Web页面进行远程访问。嵌入式Web系统提供了一种经济、实用的互联网嵌入式接入方案。这里结合一种嵌入式Web Server BOA来介绍嵌入式Linux系统下的CGI程序设计技术。

2 Web Server BOA的实现与配置

2.1 uClinux下,主要有三个Web Server:HTTPD、THTTPD和BOA。HTTPD是最简单的一个Web Server,它的功能最弱,不支持认证,不支持CGI。THTTPD和BOA都支持认证、CGI等,功能都比较全。BOA是一个单任务的小型HTTP服务器,源代码开放、性能优秀,特别适合应用在嵌入式系统中。目前的uClinux的代码中已经包含BOA的源代码。在uClinux下实现BOA,只需要对BOA做一些配置和修改。以下是配置的过程。

(1)编译BOA到内核

首先,需要把BOA编译到内核,即执行make menuconfig,在应用程序选单中network application项下面选择boa。该操作需要重新编译内核。

(2)编制配置文件boa.conf

在Linux操作系统下,应用程序的配置都是以配置文件的形式提供的,一般都是放在目标板/etc/目录下或者/etc/config目录下。但boa的配置文件boa.conf一般都喜欢放在目标板/home/httpd/目录下。

例如,一个典型的boa.conf文件格式如下:

ServerName Samsung-ARM

DocumentRoot/home/httpd

ScriptAlias/cgi-bin/home/httpd/cgi-bin/

ScriptAlias/index.html/home/httpd/index.html

它指定了HTML页面必须放到/home/httpd目录下,cgi外部扩展程序必须放到/home/httpd/cgi-bin目录下。

(3)编译烧写内核

重新编译内核后,通过烧写工具烧写内核,就可以在PC上通过IE浏览器访问开发板上的 Web Server。例如,输入开发板的IP地址http://192.168.0.101,即可访问到自己做的网页index.html了。并且,通过编写 CGI外部扩展程序,可以实现动态Web技术,下面将详细介绍。

2.2 具有MMU平台的Linux下B0A的实现与配置

对于有MMU(内存管理单元)的平台,如armlinux和ppclinux,可以到网上下载一个主流版本的boa发行包。因为是运行在目标系统,所以要用交叉编译工具编译,即需要修改boa/src/Makefile里面的编译器。例如:

CC=/LinuxPPC/CDK/bin/powerpc-linux-gcc

CPP=/LinuxPPC/CDK/bin/powerpc-linux-g++

然后直接在boa/src目录下执行make,即可生成BOA可执行文件;将其编译入内核,并烧写到存储设备,就可以实现访问BOA服务器。

3 CGI程序设计技术

CGI(Common Gateway Interface)是外部应用扩展应用程序与WWW服务器交互的一个标准接口。按照CGI标准编写的外部扩展应用程序可以处理客户端浏览器输入的数据,从而完成客户端与服务器的交互操作。而CGI规范就定义了Web服务器如何向扩展应用程序发送消息,在收到扩展应用程序的信息后又如何进行处理等内容。通过CGI可以提供许多静态的HTML网页无法实现的功能,比如搜索引擎、基于Web的数据库访问等等。

3.1 工作原理

(1)WWW和CGI的工作原理

HTTP协议是WWW的基础,它基于客户/服务器模型,一个服务器可以为分布在网络中处的客户提供服务;它是建立在 TCP/IP协议之上的“无连接”协议,每次连接只处理一个请求。在服务器上,运行产着一个守护进程对端口进行监听,等待来自客户的请求。当一个请求到来时,将创建一个子进程为用户的连接服务。根据请求的不同,服务器返回HTML文件或者通过CGI调用外部应用程序,返回处理结果。服务器通过CGI与外部程序和脚本之间进行交互,根据客户端在进行请求时所采取的方法,服务器会收集客户所提供的信息,并将该部分信息发送给指定的 CGI扩展程序。CGI扩展程序进行信息处理并将结果返回服务器,然后服务器对信息进行分析,并将结果发送回客户端。

外部CGI程序与WWW服务器进行通信、传递有关参数和处理结果是通过环境变量、命令行参数和标准输入来进行的。服务器提供了客户端(浏览器)与CGI扩展程序之间的信息交换的通道。CGI的标准输入是服务器的标准输出,而CGI的标准输出是服务器的标准输入。客户的请求通过服务器的标准输出传送给CGI的标准输入,CGI对信息进行处理后,将结果发送到它的标准输入,然后由服务器将处理结果发送给客户端。

(2)URL编码

客户端浏览器向服务器发送数据采用编码的形式进行。该编码就是CRL编码。编码的主要工作是表单域的名字和值的转义,具体的做法为:每一对域和值里的空格都会被替换为一个加号(+)字符,不是字母或数字的字符将被替换为它们的十六进制数字形式,格式为%HH。HH是该字符的 ASCII十六进制值。
标签将被替换为“%0D%0A”。

信息是按它们在表单里出现的顺序排列的。数据域的名字和数据域的值通过等号(=)字符连在一起。各对名/值再通过“&”字符连接在一起。经过这些编码处理之后,表单信号就整个成为一个连续的字符流,里面包含着将被送往服务器的全部信息。

因为表单输入信息都是经过编码后传递给脚本程序的,所以CGI扩展程序在使用这些参数之前必须对它们进行解码。

3.2 CGI外部扩展程序编制

服务器程序可以通过三种途径接收信息:环境变量、命令行和标准输入。具体使用哪一种方法要由

标签的METHOD属性来决定。

在“METHOD=GET”时,向CGI程序传递表单编码信息的正常做法是通过命令来进行的。大多数表单编码信息都是通过 QUERY_STRING的环境变量来传递的。如果“METHOD=POST”,表单信息将通过标准输入来读取。还有一种不使用表单就可以向CGI传送信息的方法,那就是把信息直接追回在URL地址后面,信息和URL之间用问号(?)来分隔。

下面结合Web远程监控ARM芯片的GPIO(通用输入/输出)的应用实例详细介绍。

(1)GET方法

GET方法是对数据的一个请求,被用于获得静态文档。当使用GET方法时,CGI程序将会从环境变量 QUERY_STRING获取数据。为了处理客户端的请求,CGI必须对QUERY_STRING中的字符串进行分析。当需要从服务器获取数据并且不改变服务器上的数据时,应该选用GET方法;但是如果请求中包含的字符串超过了一定长度,一般是1024字节,那么就只能选用POST方法。GET 方法通过附加在URL后面的参数发送请求信息。这些参数将被放在环境变量QUERY_STRING中传给CGI程序。GET方法的表单格式和CGI解码程序可以参考POST方法的实现。

(2)POST方法

当浏览器将数据从一个填写的表单传给服务器时一般采用POST方法,而且在发送的数据超过 1024字节时也必须采用POST方法。当使用POST方法时,Web服务器向CGI程序的标准输入STDIN传送数据。发送的数据长度存在环境变量 CONTENT_LENGTH中,并且,POST方法的数据格式为:

variable1=value1&variable2=value2&etc

CGI程序必须检查REQUEST_METHOD环境变量以确定是否采用了POST方法,并决定是否要读取STDIN。POST方法在HTML文档中定义的表单如下:



Operate P0

Operate P1

Operate P2

NAME="cancel"TYPE=reset value="RESET">

它调用的服务器脚本程序是/cgi/bin/cgi_gpio.cgi。CGI扩展程序中form表单的解码可参考如下程序:

/*function getPOSTvars*/

char **getPOSTvars(){

int i;

int content_length;

char **postvars;

char *postinput;

char **pairlist;

int paircount=0;

chr *nvpair;

char *eqpos;

postinput=getenv("CONTENT_LENGTH");//获取传送给程序数据的字节数

if(!postinput)

exit();

if(!content_length=atoi(postinput))) //获取信息长度

exit(1);

if(!(postinput=(char*)malloc(content_length+1)))

exit(1);

if(!fread(postinput,content_length,1,stadin))

exit(1);

postinput[content_length]='0';

for(i=0;postinput[i];i++)

if(postinput[i]=='+')

postinput[i]=''; //对加易进行处理

pairlist=(char **)malloc(256*sizeof(char **));

paircount=0;

nvpair=strtok(postinput,"&");//从出现“&”字符的位置把信息分段,然后对结果依次处理

while (nvpair){

pairlist[paircount++]=strdup(nvpair);

if(!(paircount%256))

pairlist=(char**)realloc(pairlist,(paircount+256)*sizeof(char**));

nvpair=strtok(NULL,"&");

}

pairlist[paircount]=0;

postvars=(char**)malloc((paircount*2+1)*sizeof(char **));

for(i=0;i

if(eqpos=strchr(pairlist[i],'=')){

*eqpos='0';

unescape_url(postvars[i*2+1]=strdup(eqpos+1));//调用unescape_url函数继续解码

}else{

unescape_url(postvars[i*2+1])=strdup(""));

}

postvars[paircount*2]=0;

for(i=0;pairlist[i];i++)

free(pairlist[i]);

free(pairlist);

free(postinput);

return postvars;

}

其中,unescape_url函数再调用x2c函数,把(不是字节或数字的)特殊字符从其%HH表示方式解码为文本字符。

/*unescape_url function*/

static void unescape_url(char *url){

int x,y;

for(x=0,y=0;url[y];++x,++y){

if((url[x]=url[y])=='%'){

url[x]=x2c(&url[y+1]);

y+=2;

}

}

url[x]='0';

}

(3)直接URL加参数传递方法

这是一种不使用表单就可以向CGI传送信息的方法。它把信息直接追加在URL地址后面,信息和URL之间用号号(?)来分隔。例如,对于一个cgi_gpio.cgi的脚本,可以从如下的链接启动:



/*cgi-bin/cgi_gpio.cgi?flag=1 Operate P1



.

.

.

CGI扩展程序中可使用如下代码接收信息:char *get_input;//用于接收环境变量

.

.

.

get_input=getenv(“QUERY_STRING”);

if(get_input){

get_input=strdup(get_input);

printf("QUERY_STRING if %s",get_input);

}

/*判断flag=x信息*/

if(!strcmp(get_input,"flag=0")

...//Operate p0

else if(!strcmp(get_input,"flag=1")

...//Operate P1

else

...//Operate P2

对于上述三种方法,可以根据不同的应用场合和应用要求进行选取。

结语

嵌入式Web Server系统方案可以广泛应用在许多领域,如自动化设备的远程监控、嵌入式GSM短消息 平台以及远程家庭医疗等。并且,随着互联网应用领域的不断深入,嵌入式Internet技术将得到更为广泛的应用和发展。
嵌入式WEB服务器BOA和CGI
嵌入式WEB服务器常见的有
lighttpd
shttpd
thttpd
boa
mathopd
minihttpd
appweb
goahead
========================================================================
嵌入式WEB服务器BOA的移植方法(一)2007-2-26 16:20:00
随着Internet技术的兴起,在嵌入式设备的管理与交互中,基于Web方式的应用成为目前的主流,
这种程序结构也就是大家非常熟悉的B/S结构,即在 嵌入式设备上运行一个支持脚本或CGI功能的Web服务器,
能够生成动态页面,在用户端只需要通过Web浏览器就可以对嵌入式设备进行管理和监控,非常方 便实用。
本节主要介绍这种应用的开发和移植工作。
用户首先需要在嵌入式设备上成功移植支持脚本或CGI功能的Web服务器,然后才能进行应用程序的开发。
1、 嵌入式Web服务器移植 由于嵌入式设备资源一般都比较有限,并且也不需要能同时处理很多用户的请求,
因此不会使用Linux下最常用的如Apache 等服务器,而需要使用一些专门为嵌入式设备设计的Web服务器,
这些Web服务器在存贮空间和运行时所占有的内存空间上都会非常适合于嵌入式应用场合。
典型的嵌入式Web服务器有Boa (www.boa.org)和thttpd (http://www.acme.com/software/thttpd/)等,
它们和Apache等高性能的Web服务器主要的区别在于它们一般是 单进程服务器,只有在完成一个用户请求后才能响应
另一个用户的请求,而无法并发响应,但这在嵌入式设备的应用场合里已经足够了。
我们绍比较常用的Boa服务器的移植。
Boa是一个非常小巧的Web服务器,可执行代码只有约60KB。它是一个单任务Web服务器,只能依次完成用户的请求,
而不会fork出新的进程来处理 并发连接请求。但Boa支持CGI,能够为CGI程序fork出一个进程来执行。Boa的设计目标
是速度和安全,在其站点公布的性能测试中,Boa的性能 要好于Apache服务器。
第一步完成Boa程序的移植。从www.boa.org下载Boa源码,当前最新版本为0.94.13,将其解压并进入源码目录的src
子目录
# tar xzf boa-0.94.13.tar.gz
# cd boa-0.94.13/src
生成Makefile文件
# ./configure
修改Makefile文件,找到CC=gcc,将其改成CC = arm-linux-gcc,再找到CPP = gcc –E,
将其改成CPP = arm-linux-gcc –E,并保存退出。
然后运行make进行编译,得到的可执行程序为boa,将调试信息剥去,得到的最后程序只有约60KB大小。
# make
# arm-linux-strip boa
第二步完成Boa的配置,使其能够支持CGI程序的执行。Boa需要在/etc目录下建立一个boa目录,里面放入Boa的主要
配置文件boa.conf。在Boa源码目录下已有一个示例boa.conf,可以在其基础上进行修改,下面解释一下该文件的含义:
#监听的端口号,缺省都是80,一般无需修改
Port 80
# bind调用的IP地址,一般注释掉,表明绑定到INADDR_ANY,通配于服务器的所有IP地址
#Listen 192.68.0.5
#作为哪个用户运行,即它拥有该用户的权限,一般都是nobody,需要/etc/passwd中有
#nobody用户
User nobody
#作为哪个用户组运行,即它拥有该用户组的权限,一般都是nogroup,需要在/etc/group文
#件中有nogroup组
Group nogroup
#当服务器发生问题时发送报警的email地址,目前未用,注释掉
#ServerAdmin root@localhost
#错误日志文件。如果没有以/开始,则表示从服务器的根路径开始。如果不需要错误日志,
则用#/dev/null。在下面设置时,注意一定要建立/var/log/boa目录
ErrorLog /var/log/boa/error_log
#访问日志文件。如果没有以/开始,则表示从服务器的根路径开始。如果不需要错误日志,
则用#/dev/null或直接注释掉。在下面设置时,注意一定要建立/var/log/boa目录
#AccessLog /var/log/boa/access_log
#是否使用本地时间。如果没有注释掉,则使用本地时间。注释掉则使用UTC时间
#UseLocaltime
#是否记录CGI运行信息,如果没有注释掉,则记录,注释掉则不记录
#VerboseCGILogs
#服务器名字
ServerName www.hyesco.com
#是否启动虚拟主机功能,即设备可以有多个网络接口,每个接口都可以拥有一个虚拟的Web服
#务器。一般注释掉,即不需要启动
#VirtualHost
#非常重要,HTML文档的主目录。如果没有以/开始,则表示从服务器的根路径开始。
DocumentRoot /var/www
#如果收到一个用户请求的话,在用户主目录后再增加的目录名
UserDir public_html
#HTML目录索引的文件名,也是没有用户只指明访问目录时返回的文件名
DirectoryIndex index.html
#当HTML目录没有索引文件时,用户只指明访问目录时,boa会调用该程序生成索引文件然后
#返回给用户,因为该过程比较慢最好不执行,可以注释掉或者给每个HTML目录加上#DirectoryIndex指明的文件
#DirectoryMaker /usr/lib/boa/boa_indexer
#如果DirectoryIndex不存在,并且DirectoryMaker被注释,那么就用Boa自带的索引
#生成程序来生成目录的索引文件并输出到下面目录,该目录必须是Boa能读写
# DirectoryCache /var/spool/boa/dircache
#一个连接所允许的HTTP持续作用请求最大数目,注释或设为0都将关闭HTTP持续作用
KeepAliveMax 1000
#HTTP持续作用中服务器在两次请求之间等待的时间数,以秒为单位,超时将关闭连接
KeepAliveTimeout 10
#指明mime.types文件位置。如果没有以/开始,则表示从服务器的根路径开始。可以注释掉
#避免使用mime.types文件,此时需要用AddType在本文件里指明
MimeTypes /etc/mime.types
#文件扩展名没有或未知的话,使用的缺省MIME类型
DefaultType text/plain
#提供CGI程序的PATH环境变量值
CGIPath /bin:/usr/bin:/usr/local/bin
#将文件扩展名和MIME类型关联起来,和mime.types文件作用一样。如果用mime.types
#文件,则注释掉,如果不使用mime.types文件,则必须使用
#AddType application/x-httpd-cgi cgi
#指明文档重定向路径
#Redirect /bar http://elsewhere/feh/bar
#为路径加上别名
Alias /doc /usr/doc
#非常重要,指明CGI脚本的虚拟路径对应的实际路径。一般所有的CGI脚本都要放在实际路径
#里,用户访问执行时输入站点+虚拟路径+CGI脚本名
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
用户可以根据自己需要,对boa.conf进行修改,但必须要保证其他的辅助文件和设置必须和boa.conf里的配置相符,
不然Boa就不能正常工作。 在上面的例子中,我们还需要创建日志文件所在目录/var/log/boa,
创建HTML文档的主目录/var/www,将mime.types文件拷贝 到/etc目录,
创建CGI脚本所在目录/var/www/cgi-bin/。mime.types文件用来指明不同文件扩展名对应的MIME类型,
一般 可以直接从Linux主机上拷贝一个,大部分也都是在主机的/etc目录下。
===================================================================

===================host test=======================================
1.进入 boa-0.94.13/src
./configure
make
2.在etc/下建立boa目录并将boa.conf拷贝到该目录下.更改boa.conf
Group nogroup ===》Group 0
3.在 /var/log/下建立boa目录,该目录下可以查看boa服务器的日志
4.其它的一些路径
默认是/var/www下的内容可以访问 (DocumentRoot /var/www)
默认cgi :ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ (cgi可执行程序放在 /usr/lib/cgi-bin/目录下)
例子http://201.201.201.249/cgi-bin/cgi-test.cgi
CGIPath /bin:/usr/bin:/usr/local/bin
只有这些目录下的命令可以被调用,如果要root的权限(如ifconfig配置ip)需要加上/sbin



=====================================================================
cgi例子
=====================================================================
//pass.c
#include
#include
#include

char* getcgidata(FILE* fp, char* requestmethod);
int main()
{
char *input;
char *req_method;
char name[64];
char pass[64];
int i = 0;
int j = 0;

// printf("Content-type: text/plain; charset=iso-8859-1\n\n");
printf("Content-type: text/html\n\n");
printf("The following is query reuslt:

");

req_method = getenv("REQUEST_METHOD");
input = getcgidata(stdin, req_method);

// 我们获取的input字符串可能像如下的形式
// Username="admin"&Password="aaaaa"
// 其中"Username="和"&Password="都是固定的
// 而"admin"和"aaaaa"都是变化的,也是我们要获取的

// 前面9个字符是UserName=
// 在"UserName="和"&"之间的是我们要取出来的用户名
for ( i = 9; i < (int)strlen(input); i++ )
{
if ( input[i] == '&' )
{
name[j] = '\0';
break;
}
name[j++] = input[i];
}

// 前面9个字符 + "&Password="10个字符 + Username的字符数
// 是我们不要的,故省略掉,不拷贝
for ( i = 19 + strlen(name), j = 0; i < (int)strlen(input); i++ )
{
pass[j++] = input[i];
}
pass[j] = '\0';

printf("Your Username is %s
Your Password is %s
\n", name, pass);

return 0;
}

char* getcgidata(FILE* fp, char* requestmethod)
{
char* input;
int len;
int size = 1024;
int i = 0;

if (!strcmp(requestmethod, "GET"))
{
input = getenv("QUERY_STRING");
return input;
}
else if (!strcmp(requestmethod, "POST"))
{
len = atoi(getenv("CONTENT_LENGTH"));
input = (char*)malloc(sizeof(char)*(size + 1));

if (len == 0)
{
input[0] = '\0';
return input;
}

while(1)
{
input[i] = (char)fgetc(fp);
if (i == size)
{
input[i+1] = '\0';
return input;
}

--len;
if (feof(fp) || (!(len)))
{
i++;
input[i] = '\0';
return input;
}
i++;

}
}
return NULL;
}


/*
* gcc -o pass.cgi pass.c
*/
//pass.html

用户登陆验证
















用户名
密  码




随着Internet技术的兴起,在嵌入式 设备的管理与交互中,基于Web方式的应用成为目前的主流,这种程序结构也就是大家非常熟悉的B/S结构,即在嵌入式设备上运行一个支持脚本或CGI功能 的Web服务器,能够生成动态页面,在用户端只需要通过Web浏览器就可以对嵌入式设备进行管理和监控,非常方便实用。本节主要介绍这种应用的开发和移植 工作。
用户首先需要在嵌入式设备上成功移植支持脚本或CGI功能的Web服务器,然后才能进行应用程序的开发。
1、嵌入式Web服务器移植由于嵌入式设备资源一般都比较有限,并且也不需要能同时处理很多用户的请求,因此不会使用Linux下最常用的如Apache 等服务器,而需要使用一些专门为嵌入式设备设计的Web服务器,这些Web服务器在存贮空间和运行时所占有的内存空间上都会非常适合于嵌入式应用场合。
典型的嵌入式Web服务器有Boa (www.boa.org)和thttpd (http://www.acme.com/software/thttpd/)等,它们和Apache等高性能的Web服务器主要的区别在于它们一般是 单进程服务器,只有在完成一个用户请求后才能响应另一个用户的请求,而无法并发响应,但这在嵌入式设备的应用场合里已经足够了。
我们绍比较常用的Boa服务器的移植。
Boa是一个非常小巧的Web服务器,可执行代码只有约60KB。它是一个单任务Web服务器,只能依次完成用户的请求,而不会fork出新的进程来处理 并发连接请求。但Boa支持CGI,能够为CGI程序fork出一个进程来执行。Boa的设计目标是速度和安全,在其站点公布的性能测试中,Boa的性能 要好于Apache服务器。
第一步完成Boa程序的移植。从www.boa.org下载Boa源码,当前最新版本为0.94.13,将其解压并进入源码目录的src子目录
# tar xzf boa-0.94.13.tar.gz
# cd boa-0.94.13/src
生成Makefile文件
# ./configure
修改Makefile文件,找到CC=gcc,将其改成CC = arm-linux-gcc,再找到CPP = gcc –E,将其改成CPP = arm-linux-gcc –E,并保存退出。
然后运行make进行编译,得到的可执行程序为boa,将调试信息剥去,得到的最后程序只有约60KB大小。
# make
# arm-linux-strip boa
第二步完成Boa的配置,使其能够支持CGI程序的执行。Boa需要在/etc目录下建立一个boa目录,里面放入Boa的主要配置文件 boa.conf。在Boa源码目录下已有一个示例boa.conf,可以在其基础上进行修改,下面解释一下该文件的含义:
#监听的端口号,缺省都是80,一般无需修改
Port 80
# bind调用的IP地址,一般注释掉,表明绑定到INADDR_ANY,通配于服务器的所有IP地址
#Listen 192.68.0.5
#作为哪个用户运行,即它拥有该用户的权限,一般都是nobody,需要/etc/passwd中有
nobody用户
User nobody
#作为哪个用户组运行,即它拥有该用户组的权限,一般都是nogroup,需要在/etc/group文
件中有nogroup组
Group nogroup
#当服务器发生问题时发送报警的email地址,目前未用,注释掉
#ServerAdmin root@localhost
#错误日志文件。如果没有以/开始,则表示从服务器的根路径开始。如果不需要错误日志,则用#/dev/null。在下面设置时,注意一定要建立/var/log/boa目录
ErrorLog /var/log/boa/error_log
#访问日志文件。如果没有以/开始,则表示从服务器的根路径开始。如果不需要错误日志,则用#/dev/null或直接注释掉。在下面设置时,注意一定要建立/var/log/boa目录
#AccessLog /var/log/boa/access_log
#是否使用本地时间。如果没有注释掉,则使用本地时间。注释掉则使用UTC时间
#UseLocaltime
#是否记录CGI运行信息,如果没有注释掉,则记录,注释掉则不记录
#VerboseCGILogs
#服务器名字
ServerName www.hyesco.com
#是否启动虚拟主机功能,即设备可以有多个网络接口,每个接口都可以拥有一个虚拟的Web服
#务器。一般注释掉,即不需要启动
#VirtualHost
#非常重要,HTML文档的主目录。如果没有以/开始,则表示从服务器的根路径开始。
DocumentRoot /var/www
#如果收到一个用户请求的话,在用户主目录后再增加的目录名
UserDir public_html
#HTML目录索引的文件名,也是没有用户只指明访问目录时返回的文件名
DirectoryIndex index.html
#当HTML目录没有索引文件时,用户只指明访问目录时,boa会调用该程序生成索引文件然后
返回给用户,因为该过程比较慢最好不执行,可以注释掉或者给每个HTML目录加上#DirectoryIndex指明的文件
#DirectoryMaker /usr/lib/boa/boa_indexer
如果DirectoryIndex不存在,并且DirectoryMaker被注释,那么就用Boa自带的索引
生成程序来生成目录的索引文件并输出到下面目录,该目录必须是Boa能读写
# DirectoryCache /var/spool/boa/dircache
#一个连接所允许的HTTP持续作用请求最大数目,注释或设为0都将关闭HTTP持续作用
KeepAliveMax 1000
#HTTP持续作用中服务器在两次请求之间等待的时间数,以秒为单位,超时将关闭连接
KeepAliveTimeout 10
#指明mime.types文件位置。如果没有以/开始,则表示从服务器的根路径开始。可以注释掉,
避免使用mime.types文件,此时需要用AddType在本文件里指明
MimeTypes /etc/mime.types
#文件扩展名没有或未知的话,使用的缺省MIME类型
DefaultType text/plain
#提供CGI程序的PATH环境变量值
CGIPath /bin:/usr/bin:/usr/local/bin
#将文件扩展名和MIME类型关联起来,和mime.types文件作用一样。如果用mime.types
文件,则注释掉,如果不使用mime.types文件,则必须使用
#AddType application/x-httpd-cgi cgi
#指明文档重定向路径
#Redirect /bar http://elsewhere/feh/bar
#为路径加上别名
Alias /doc /usr/doc
#非常重要,指明CGI脚本的虚拟路径对应的实际路径。一般所有的CGI脚本都要放在实际路径
里,用户访问执行时输入站点+虚拟路径+CGI脚本名
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
用户可以根据自己需要,对boa.conf进行修改,但必须要保证其他的辅助文件和设置必须和boa.conf里的配置相符,不然Boa就不能正常工作。 在上面的例子中,我们还需要创建日志文件所在目录/var/log/boa,创建HTML文档的主目录/var/www,将mime.types文件拷贝 到/etc目录,创建CGI脚本所在目录/var/www/cgi-bin/。mime.types文件用来指明不同文件扩展名对应的MIME类型,一般 可以直接从Linux主机上拷贝一个,大部分也都是在主机的/etc目录下。

第三步就是测试Boa能否正常工作,静态HTML页面能否正常访问,CGI脚本能否正常运行,一般采用NFS方式来进行测试工作,可以将嵌入式目标系统上的/etc目录拷贝到主机的NFS共享目录下,然后将NFS共享目录下的etc目录重新NFS mount为目标系统上的/etc目录。这样就可以在主机上对etc目录下的各种配置文件,如进行修改而立刻在目标系统上生效。
假设主机/nfs目录为共享目录,在其下面建立一个www子目录作为Web站点的主目录,其内容如下:

# ls

cgi-bin images index.html

index.html为测试主页面,images为存放各种图片的子目录,cgi-bin为CGI脚本的存放目录。根据示例boa.conf的配置,目前 HTML文档的主目录为/var/www,CGI脚本目录为/var/www/cgi-bin,则运行以下命令将主机的/nfs/www目录mount成目标板上的/var/www目录。然后就可以运行boa了:
# mount -t nfs 192.168.67.1:/nfs/www /var/www -o nolock
# boa
在工作站上运行浏览器进行测试,在地址栏输入目标系统IP,即http://192.168.67.16 ,可以看到相关页面,表示静态HTML页面测试通过。
接下来进行CGI脚本的测试,我们需要一个测试用的CGI脚本。可以写个最简单的Hello World程序,示例代码如下

#include
void main() {
printf("Content-type: text/html\n\n") ;
printf("\n") ;
printf("CGI Output\n") ;
printf("\n") ;
printf("

Hello, world.

\n") ;
printf("\n") ;
printf("\n") ;
exit(0) ;
}
然后进行交叉编译,将得到的helloworld.cgi拷贝到主机的/nfs/www/cgi-bin目录下。
#arm-linux-gcc -o helloworld.cgi helloworld.c
#cp helloworld.cgi /nfs/www/cgi-bin
在浏览器地址栏输入http://192.168.67.16/cgi-bin/helloworld.cgi,可以看到相关页面,表示CGI脚本测试通过。
现在我们已经可以让Boa在嵌入式目标系统上正常工作了,嵌入式Web服务器移植成功。

在以上的移植过程中,最好设定boa.conf中的错误日志文件ErrorLog,允许Boa记录错误信息;
测试静态HTML页面和CGI脚本时,不管结果如何,最好都查看错误日志文件;
CGI脚本测试很容易发生权限不够的错误,要保证Boa访问的主目录、CGI脚本目录以及临时文件目录(如果没有设置TMP环境变量时,缺省是/tmp目录),都必须能被Boa运行时所代表的用户完全访问,该用户由boa.conf中的User指出。
輸出 / 入重新導向 (Redirection)

基本上,Linux 將標準 I/O分為三種,分別是:標準輸入 (stdin, Standard Input)、 標準輸出 (stdout, Standard Output) 及標準錯誤輸出 (stderr, Standard Error)。標準輸入就是指標準輸入設備鍵盤的輸入,或是一般指令後面所加的參數,例如:ls dir1,dir1 就是標準輸入。標準輸出則是指輸出到螢幕,例如:ls dir1 顯示在螢幕上的結果。標準錯誤輸出是指指令執行錯誤時,產生的錯誤訊息,例如:ls dir1結果沒有 dir1 這個目錄時,顯示在螢幕上的錯誤訊息。

輸出導向 :>

 

# ls dir1 > file1 執行結果會儲存到檔案 file1 中,而不是顯示在螢幕上
# cat file2 file3 > file4 將 file2 與 file3 兩個檔案合併後存到檔案 file4
# cat > file5 可以在鍵盤上輸入資料,直到按下 Ctrl+d 結束,所有輸入的資料即為新建檔案 file5 的內容
# exefile > /dev/null 執行檔 exefile 的執行結果將會消失不見

 

※ 上述輸出導向指令在執行之前,會先將右邊參數所指的檔案 (e.g., file1, file4 etc.) 內容清空

附加輸出導向 :>>

# cat file2 >> file1 將 file2 的內容附加到檔案 file1 的後面

輸入導向 :<

# cat <> 結果與「cat file1」相同
# mail user1 <> 將檔案 file2 的內容直接寄給使用者 user1

附加輸入導向 :<<

附加輸入導向「<< string」是指輸入會一直持續到遇到字串 "string" 為止,例如:

cat > file1 <<>

> This is a simple input test!

> You can do the input until eof appear.

> eof

 

錯誤輸出導向 :2>

 

# ls dir1 2> err.out 執行錯誤訊息將會存到檔案 err.out
# ls dir1 > file1 2>&1 將正確執行結果與錯誤輸出都寫入同一檔案 file1
# ls dir1 &> file1 同時將標準輸出與標準錯誤輸出導向到檔案 file1
# ls dir1 >& file1 同時將標準輸出與標準錯誤輸出導向到檔案 file1

管線 (pipe)|

管線的功用就是將程式的輸出引導為另一個程式的輸入,例如:

# ls -l dir1 | more 將執行結果一頁一頁的顯示

雙向重導向tee

last | tee ~/last.list 顯示所有登入過系統的使用者清單,並儲存一份至檔案 last.list

關於 sscanf

一般使用

char str[512] = {0};

sscanf("123456 ", "%s", str);

printf("str=%s\n", str);


取出指定長度字串,例如:取4個字元

sscanf("123456 ", "%4s", str);

printf("str=%s\n", str);


取出到指字字元為止的字串,例如:取出遇到空格為止前的字串

sscanf("123456 abcdedf", "%[^ ]", str);

printf("str=%s\n", str);


取出包含指定字元的字串,例如:取出包含1到9與小寫字母的字串


sscanf("123456abcdedfBCDEF", "%[1-9a-z]", str);

printf("str=%s\n", str);


取出到指字字元為止的字串,例如:取出遇到大寫字母為止的字串


sscanf("123456abcdedfBCDEF", "%[^A-Z]", str);

printf("str=%s\n", str);


Blogger Templates by Blog Forum