Linux為串口上網提供了豐富的支持,比如PPP(Peer-to-Peer Protocol, 端對端協議)和SLIP(Serial Line Interface Protocol, 非常老的串行線路接口協議),這里所說的"上網"是指把串口當成一個網絡接口,通過封裝網絡數據包(如IP包)以達到無網卡的終端可以通過串口進行網絡通信。但是使用這兩種協議必須得到內核的支持。例如,如果在沒有配置PPP的Linux環境中使用PPP,除了安裝PPP應用層軟件外,還必須重新編譯內核。SLIP是一個比較老的簡單的協議,現在的Linux內核缺省配置都支持,不需要重新編譯內核,盡管如此,其源代碼看上去有點"古怪而復雜"。在嵌入式Linux系統使用過程中,如果內核已經被燒入Flash中,而為了節省空間內核又沒有提供諸如PPP或者SLIP的支持,當然就沒有辦法在不重新燒寫Flash的情況下直接使用PPP或者SLIP了,事實上用戶必須動態加載PPP和SLIP的內核實現模塊。對某些嵌入式應用來說移植或者修改PPP源代碼變成了乏味和繁鎖的工作。這里介紹一種非常經濟而且實用的實現串口上網的簡單方法。 Linux簡單串口上網原理 簡單串口上網的實現原理如圖1所示。 Linux Box A 和 Linux Box B 是兩個安裝有Linux操作系統的終端(可以是PC,也可以是嵌入式設備),它們通過一條串口通信線(null modem cable line)連接。控制串口通信的服務進程server讀和寫兩個字符設備:發送字符設備sending device和接收字符設備receiving device。在內核空間,偽網絡設備驅動程序pseudo network driver可以直接讀寫發送字符設備和接收字符設備,事實上在內核空間它們之間的通信只是對共享緩存區的讀寫而已。偽網絡設備驅動程序具有大部分普通網卡驅動程序提供服務功能,只是沒有硬件部分代碼的實現而已。當用戶空間的進程要發送數據的時候,其首先讓數據經過Linux操作系統的TCP/IP處理層進行數據打包,然后把打包后的數據直接寫入sending device,等待server進程讀取,最后通過串口發送到另一個Linux Box的server進程;而當server進程發現有數據從串口傳送過來時就把數據寫入receiving device,偽網絡驅動程序發現receiving device設備有新數據的時候,就又把數據傳遞到TCP/IP層處理,最終網絡應用程序收到對方發來的數據。本文設計的源程序主要有三個,ed_device.c、ed_device.h、server.c。其中在ed_device.c是串口上網的內核部分的主程序,包含字符設備和偽網絡接口設備程序,server.c負責串口通信。主文件ed_device.c中包括的頭文件在源程序中,這里就不一一列舉了。 Linux串口上網設備加載和注銷形式 Linux串口上網程序的整個內核部分是以LKM(Loadable Kernel Module)形式實現的。LKM加載的時候完成偽網絡設備、發送字符設備、接收字符設備的初始化和注冊。注冊的目的是讓操作系統可以識別用戶進程所要操作的設備,并完成在其上的操作(比如read,write等系統調用)。Linux加載模塊,實際上就是模塊鏈表的插入;刪除模塊象是模塊鏈表成員的刪除。 初始化內核模塊入口函數init_module()中包括對字符設備的初始化入口 函數eddev_module_init()和偽網絡設備初始化入口函數ednet_module_init()。 在內核需要卸載的時候,必須進行資源釋放以及設備注銷, cleanup_module()完成這個任務。函數cleanup_module()中用eddev_module_cleanup()來釋放字符設備占用的資源(比如分配的緩存區等);有ednet_module_cleanup()來釋放偽網絡設備占用的資源。本文的內核部分模塊程序編譯后就是ed_device.o,加載后使用lsmod命令查看,模塊名就是ed_device。模塊ed_device的加載和注銷函數如圖2所示。 當我們需要加載模塊的時候,我們只需要使用insmod命令,如果需要卸載模塊,我們使用rmmod命令。比如加載ed_device模塊,并且配置偽網絡接口IP地址為192.168.5.1
這時可以在/proc/net/dev 文件中看到有ed0偽網絡設備了。如果需要卸載ed_device模塊,應先停止其網絡數據發送和接收工作,然后卸載模塊:
如果我們設置另一臺Linux box的偽網接口地址是192.168.5.2那么,我們可以用串口線直接連接兩臺終端并使用網絡應用程序了,在兩臺終端上運行server守護程序,然后執行telnet: [root@localhost test]# telnet 192.168.5.2 Trying 192.168.5.2... Connected to 192.168.5.2 (192.168.5.2). Escape character is '^]'. Red Hat Linux release 9 (Shrike) Kernel 2.4.20-8 on an i686 login: 編寫字符設備驅動程序 用戶空間的進程主要通過兩種方式和內核空間模塊打交道,一種是使用proc文件系統,另一種是使用字符設備。本文所描述的兩個字符設備sending device 和receiving device事實上是內核空間和用戶空間交換數據的緩存區,編寫字符設備驅動實際上就是編寫用戶空間讀寫字符設備所需要的內核設備操作函數。 在頭文件中,我們定義ED_REC_DEVICE為receiving device,名字是ed_rec;定義ED_TX_DEVICE為sending device,名字是ed_tx。
200和201分別代表receiving device 和 sending device的主設備號。在內核空間,驅動程序是根據主、次設備號識別設備的,而不是設備名;本文的字符設備的次設備號都是0,主設備號是用戶定義的且不能和系統已有的設備的主設備有沖突。IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int)是ioctl的操作函數定義(從用戶空間發送命令到內核空間),主要作用是使得每次在同一時間,同一字符設備上,只可進行一次操作。我們可以使用mknod來建立這兩個字符設備:
設備建立后,編譯好的模塊就可以動態加載了:
為了方便對設備編程,我們還需要一個字符設備管理的數據結構:
這個數據結構是用來保存字符設備的一些基本狀態信息。ssize_t (*kernel_write)(const char *buffer,size_t length,int buffer_size) 是一個指向函數的指針,它的作用是為偽網絡驅動程序提供寫字符設備數據的系統調用接口。magic字段主要是標志設備類型號的,這里沒有別的特殊意義;busy字段用來說明字符設備是否是處于忙狀態,buffer指向內核緩存區,用來存放讀寫數據;mtu保存當前可發送的網絡數據包最大傳輸單位,以字節為單位;lock的類型是自旋鎖類型spinlock_t,它實際以一個整數域作為鎖,在同一時刻對同一字符設備,只能有一個操作,所以使用內核鎖機制保護防止數據污染;data_len是當前緩存區內保存的數據實際大小,以字節為單位;file是指向設備文件結構struct file的一個指針,其作用主要是定位設備的私有數據 file-> private_data。定義字符設備struct ed_device ed[2],其中ed[ED_REC_DEVICE]就是receving device,ed[ED_TX_DEVICE]就是sending device。如果sending device ED_TX_DEVICE沒有數據,用戶空間的read調用將被阻塞,并把進程信息放于rwait隊列中。當有數據的時候,kernel_write()中的wake_up_interruptible()將喚醒等待進程。kernel_write()函數定義如下:
字符設備的操作及其相關函數調用過程如圖3 所示。 圖 3 當ed_device模塊被加載的時候,eddev_module_init()調用register_chrdev()內核API注冊ed_tx和ed_rec兩個字符設備。這個函數定義在:
字符設備被注冊成功后,內核把這兩個字符設備加入到內核字符設備驅動表中。內核字符設備驅動表保留指向struct file_operations的一個數據指針。用戶進程調用設備讀寫操作時,通過這個指針訪問設備的操作函數, struct file_operations中的域大部分是指向函數的函數指針,指向用戶自己編寫的設備操作函數。
注意到Linux2.4.x和Linux2.2.x內核中定義的struct file_operations是不一樣的。device_read()、device_write()、device_ioctl()、device_open()、device_release()就是需要用戶自己定義的函數操作了,這幾個函數是最基本的操作,如果需要設備驅動程序完成更復雜的任務,還必須編寫其他struct file_operations中定義的操作。eddev_module_init()除了注冊設備及其操作外,它還有初始化字符設備結構struct ed_device,分配內核緩存區所需要的空間的作用。在內核空間,分配內存空間的API函數是kmalloc()。 下面介紹一下字符設備的主要操作例程device_open()、device_release()、device_read()、devie_write()。字符設備文件操作結構ed_ops中定義的指向以上函數的函數指針的原形:
操作int device_open(struct inode *inode,struct file *file)是設備節點上的第一個操作,如果多個設備共享這一個操作函數,必須區分設備的設備號。我們使用inode->i_rdev >> 8 語句獲得設備的主設備號,本文中的接收設備主設備號是200,發送設備號是201。每個字符設備的file>private_data指向打開設備時候使用的file結構,private_data實際上可以指向用戶定義的任何結構,這里只指向我們自己定義的struct ed_device,用來保存字符設備的一些基本信息,比如設備名、內核緩存區等。 操作ssize_t device_read(struct file *file,char *buffer,size_t length, loff_t *offset)是讀取設備數據的操作。device_read()結構如圖4所示。 圖4 從設備中讀取數據(用戶空間調用read()系統調用)的時候,需要從內核空間把數據拷貝到用戶空間,copy_to_user()可完成此功能,它和memcpy()此類函數有本質的區別,memcpy()不能完成不同用戶空間數據的交換。如果需要數據臨界區的保護,使用spin_lock()內核API負責加鎖,spin_unlock()負責解鎖,防止數據污染。由于串口守候進程server需要不斷輪詢設備,以查詢是否有數據可讀,如果用戶進程不處于休眠狀態,在用戶空間查看進程使用資源情況,發現server占用了很多CPU資源。所以我們改進device_read(),使之在內核中輪詢,當發現當前設備沒有數據可讀取,那么就阻塞用戶進程,使用內核API add_wait_queue()可完成此功能,這時候用戶進程并沒有占用很多CPU資源,而是處于休眠狀態。當內核發現有數據可讀的時候,調用remove_wait_queue()即可喚醒等待進程,這段 代碼如下:
操作ssize_t device_write(struct file *file,const char *buffer, size_t length,loff_t *offset)向設備寫入數據。拷貝數據的copy_from_user()和copy_to_user()的功能恰恰相反,它是從用戶空間拷貝數據到內核空間,如圖5所示。 圖 5 [/td][/tr][/table] 編寫偽網絡設備驅動程序 偽網絡驅動程序和字符設備驅動程序一樣,也必須初始化和注冊。網絡驅動需記錄其發送和接收數據量的統計信息,所以我們定義一個記錄這些信息的數據結構。
struct ednet_priv只有3個數據成員。Linux2.4.x 使用的網絡數據狀態統計結構是struct net_device_stats,而Linux 2.2.x則使用的是struct enet_statistics。同樣,對控制網絡接口設備的設備結構也有不同的定義:Linux2.4.x使用的是struct net_device,而Linux2.2.x卻是struct device。
偽網絡驅動程序的也需要初始化和注冊。和字符設備的注冊不同之處是,它使用的是register_netdev(net_device *) kernel API。
ednet_dev的name域是接口名,ednet_module_init()中賦予網絡接口的名字為ed0,如果本網絡設備被加載,使用ifconfig命令可以看到ed0。
我們看到我們的偽網絡接口沒有Interrupt和Base address,這是因為這個偽網絡接口不和硬件打交道,也沒有分配中斷號和IO基址。否則,如果你看一個實實在在的網絡接口(如下面的eth1),可以看到它的Interrupt號是11和IO Base address是0xa000。
ednet_dev的init域是一個函數指針,指向用戶定義的ednet_init()例程。ednet_init()添充net_device結構,只有ednet_init()初始化成功后,系統才被加入到設備鏈表中。ednet_dev的初始化例程ednet_init()如下:
ether_setup()填充一些以太網的缺省設置。dev->hard_header_cache=NULL表示不緩存向本網絡接口回復的ARP網絡數據包。IFF_NOARP的標志設置表明本網絡接口不使用ARP。ARP的主要功能是獲得通信對方的網絡接口的硬件地址,本文的偽網絡接口的物理地址是程序中設定的偽物理地址,所以我們不需要ARP協議。SET_MODULE_OWNER(dev)這個宏是設置dev結構中owner域(定義為struct module *owner;),使得它指向本模塊本身。與字符設備一樣,本網絡設備也需要定義在其上的操作例程。下面就對ednet_init()中用戶定義的設備操作函數做進一步說明。整個偽網絡設備操作調用結構如圖6所示。 由圖6我們看到,ednet_rx()并不是網絡設備的一個操作,而是模塊中的一個函數。在實際的網卡驅動程序中,當網卡確實接收到數據的時候,由網絡中斷喚醒等待接收數據的用戶進程,也就是說,ednet_rx()應該由那個網絡中斷處理例程調用。我們這里并沒有中斷,所以字符設備的device_write()可以看成是一個"中斷例程",也就是說,用戶空間往字符寫操作的時候,也就調用了網絡設備的數據接收內核例程ednet_rx()了。然后ednet_rx()會把原始的數據包發送到TCP/IP上層進行處理,這一切均依賴于內核API 函數netif_rx()。ednet_rx()就需要sk_buff數據結構(中定義),用來存放從網絡接口接收到的原始網絡數據,分配后的sk_buff結構將在TCP/IP協議棧上被釋放掉。 下面介紹一下網絡設備的主要操作例程ednet_open()、ednet_release()、ednet_tx()、ednet_stats ()、ednet_change_mtu()、ednet_header()。網絡設備文件操作結構struct net_device(中有定義)中定義了指向以上函數的函數指針的原形:
操作int ednet_open(struct net_device *dev)的作用是打開偽網絡接口設備,獲得其需要的I/O端口、IRQ等,但是本網絡接口不需要和實際硬件打交道,所以不需要自動獲得或者賦予I/O端口值,也不需要IRQ中斷號,唯一需要程序指定的是其偽硬件地址(這個硬件地址是"0ED000",ifconfig可以看到其硬件地址是 00:45:44:30:30:30,struct net_device中的dev_addr域存放網絡接口的物理地址。操作ednet_open()必須調用netif_start_queue()內核API開啟網絡接口接收和發送數據隊列。 當接口關閉的時候,int ednet_release(struct net_device *dev)例程被系統調用,在ednet_release()中調用netif_stop_queque()將停止接收和發送隊列的工作。 偽網絡設備驅動的傳送例程int ednet_tx(struct sk_buff *skb, struct net_device *dev)將把要發送的網絡數據包寫入字符設備ed[ED_TX_DEVICE]。在發送完畢數據包的時候,dev_kfree_skb() Kernel API釋放由上層協議棧分配的sk_buff數據塊。偽網絡接口在進行硬件傳輸的時候,需要為網絡數據包打上時間戳。如果傳送數據包的時候超時,將調用超時處理例程ednet_tx_timeout()超時處理例程。例程ednet_tx()調用真正的"硬件"傳送例程ednet_hw_tx()在實際的網卡驅動程序中,就是真正向特定的網絡硬件設備寫數據的程序。我們看到,我們的"硬件"就是本文前面描述的字符設備,字符設備的操作例程.kernel_write()在ednet_hw_tx()將被調用。 如果我們希望使用ifconfig看到偽網絡接口的統計信息,那么系統就調用 struct net_device_stats *ednet_stats(struct net_device *dev)。我們看到,網絡接口的統計信息被放到設備的私有數據指針指向的內存。網絡數據信息的統計結構被放在內核結構struct net_device_stats中。 在TCP會話中,也許要協商MTU的大小,int ednet_change_mtu(struct net_device *dev, int new_mtu)可以隨時改變MTU的大小。比如在使用FTP協議的時候,在傳送數據庫的時候,MTU可能被協商為最大,以提高網絡傳送吞吐量。由于改變了MTU,存放網絡數據的字符設備初始化分配的緩存區就要重新被分配,并把已經存放數據的舊的緩存區的內容拷貝到新的緩存區中,所以,當MTU改變大小的時候,那么就要使用kmalloc(new_mtu ,GFP_KERNEL)重新分配緩存區。讀者可以根據自己的需要定義新的緩存區大小。kfree()是內核API,負責釋放內核空間的內存,它的使用方法和用戶空間的free()系統調用一致,這里就不列舉ed_realloc()函數的源程序了。 IP數據包在被網絡接口發送前,需要構建其以太網頭信息int ednet_header(struct sk_buff *skb,struct net_device *dev,unsigned short type,void *daddr,void *saddr,unsigned int len)例程完成此功能,我們看到網絡數據包的以太源、目的地址,都是從發送這個數據包的網絡接口設備數據結構struct net_device中得到的。源地址和目的地址信息是從網絡設備結構得到的。在編譯本程序的時候,如果發現htons()這個函數沒有定義,可以這樣定義htons()為:#define htons(x) ((x>>8) | (x因為偽網絡接口沒有使用ARP獲得硬件地址,所以我們可以把我們自己定義的偽硬件地址復制到數據包的以太網包頭。Linux2.4.x使用設備方法hard_header()代替設備 方法rebuild_header()。Linux2.x使用的rebuild_header()例程在本文的附加源程序中,這里不再說明。 編寫用戶空間串口通信程序 控制串口的server應用程序完成非常簡單的打包和拆包的工作,它沒有差錯控制,沒有重發機制,在實際應用中,需要加上適當的控制協議。server創建的子進程負責從串口讀取數據并把數據傳送到receiving device /dev/ed_rec;父進程則負責從sending device /dev/ed_tx 讀取需要發送的網絡數據包,然后從串口發送出去。子進程和父進程都是用輪詢方式讀取和寫入設備。Server的程序流圖如圖所示。 傳送的frame按照SLIP定義的格式:數據的兩頭都是END字符(0300),如圖8所示。 特殊控制字符的定義如下:
如果打包前的數據中有END這個字符,那么使用ESC_END代替,如果發現有ESC這個字符,那么使用ESC_ESC字符替換。在Linux環境下,串口名從ttyS0開始依次是ttyS1、ttyS2等。在本程序中,使用ttyS0作為通信串口。在打開ttyS0的時候,選項O_NOCTTY 表示不能把本串口當成控制終端,否則用戶的鍵盤輸入信息將影響程序的執行; O_NDELAY表示打開串口的時候,程序并不關心另一端的串口是否在使用中。在Linux中,打開串口設備和打開普通文件一樣,使用的是open()系統調用。比如我么打開串口設備1也就是COM1,只需要:
打開的串口設備有很多設置選項。本文中使用int setup_com(int fd)設置。在系統頭文件中定義了終端控制結構struct termios,tcgetattr()和tcsetattr()兩個系統函數獲得和設置這些屬性。結構struct termios中的域描述的主要屬性包括:
如果要設置某個選項,那么就使用"|="運算,如果關閉某個選項就使用"&="和"""運算。本文使用的各個選項的意義定義如下:
根據以上設置的定義,串口端口設置函數setup_com()定義如下:
兩個打包和拆包函數和SLIP協議定義的一樣,拆包函數和打包相反,這里不列舉了。 小結 本文描述的是一個非常簡單的串口上網程序,如果需要可靠的通信,增加吞吐量,可在用戶空間添加適當的網絡控制協議,也可增加數據壓縮算法。 |