Linux串口上网的程序实现方法.docx
《Linux串口上网的程序实现方法.docx》由会员分享,可在线阅读,更多相关《Linux串口上网的程序实现方法.docx(18页珍藏版)》请在冰豆网上搜索。
Linux串口上网的程序实现方法
Linux串口上网的程序实现方法
Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-PeerProtocol,端对端协议)和SLIP(SerialLineInterfaceProtocol,非常老的串行线路接口协议)。
这里所说的"上网"是指把串口当成一个网络接口,通过封装网络数据包(如IP包)以达到无网卡的终端可以通过串口进行网络通信。
但是使用这两种协议必须得到内核的支持。
例如,如果在没有配置PPP的Linux环境中使用PPP,除了安装PPP应用层软件外,还必须重新编译内核。
SLIP是一个比较老的简单的协议,现在的Linux内核缺省配置都支持,不需要重新编译内核,尽管如此,其源代码看上去有点"古怪而复杂"。
在嵌入式Linux系统使用过程中,如果内核已经被烧入Flash中,而为了节省空间内核又没有提供诸如PPP或者SLIP的支持,当然就没有办法在不重新烧写Flash的情况下直接使用PPP或者SLIP了,事实上用户必须动态加载PPP和SLIP的内核实现模块。
对某些嵌入式应用来说移植或者修改PPP源代码变成了乏味和繁锁的工作。
这里介绍一种非常经济而且实用的实现串口上网的简单方法。
Linux简单串口上网原理
简单串口上网的实现原理。
图1
LinuxBoxA和LinuxBoxB是两个安装有Linux操作系统的终端(可以是PC,也可以是嵌入式设备),它们通过一条串口通信线(nullmodemcableline)连接。
控制串口通信的服务进程server读和写两个字符设备:
发送字符设备sendingdevice和接收字符设备receivingdevice。
在内核空间,伪网络设备驱动程序pseudonetworkdriver可以直接读写发送字符设备和接收字符设备,事实上在内核空间它们之间的通信只是对共享缓存区的读写而已。
伪网络设备驱动程序具有大部分普通网卡驱动程序提供服务功能,只是没有硬件部分代码的实现而已。
当用户空间的进程要发送数据的时候,其首先让数据经过Linux操作系统的TCP/IP处理层进行数据打包,然后把打包后的数据直接写入sendingdevice,等待server进程读取,最后通过串口发送到另一个LinuxBox的server进程;而当server进程发现有数据从串口传送过来时就把数据写入receivingdevice,伪网络驱动程序发现receivingdevice设备有新数据的时候,就又把数据传递到TCP/IP层处理,最终网络应用程序收到对方发来的数据。
本文设计的源程序主要有三个,ed_device.c、ed_device.h、server.c。
其中在ed_device.c是串口上网的内核部分的主程序,包含字符设备和伪网络接口设备程序,server.c负责串口通信。
主文件ed_device.c中包括的头文件在源程序中,这里就不一一列举了。
Linux串口上网设备加载和注销形式
Linux串口上网程序的整个内核部分是以LKM(LoadableKernelModule)形式实现的。
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
[root@localhosttest]insmoded_device.o,
[root@localhosttest]ifconfiged0192.168.5.1up
这时可以在/proc/net/dev文件中看到有ed0伪网络设备了。
如果需要卸载ed_device模块,应先停止其网络数据发送和接收工作,然后卸载模块:
[root@localhosttest]ifconfiged0down
[root@localhosttest]rmmoded_device
如果我们设置另一台Linuxbox的伪网接口地址是192.168.5.2那么,我们可以用串口线直接连接两台终端并使用网络应用程序了,在两台终端上运行server守护程序,然后执行telnet:
[root@localhosttest]#telnet192.168.5.2
Trying192.168.5.2...
Connectedto192.168.5.2(192.168.5.2).
Escapecharacteris'^]'.
RedHatLinuxrelease9(Shrike)
Kernel2.4.20-8onani686
login:
编写字符设备驱动程序用户空间的进程主要通过两种方式和内核空间模块打交道,一种是使用proc文件系统,另一种是使用字符设备。
本文所描述的两个字符设备sendingdevice和receivingdevice事实上是内核空间和用户空间交换数据的缓存区,编写字符设备驱动实际上就是编写用户空间读写字符设备所需要的内核设备操作函数。
在头文件中,我们定义ED_REC_DEVICE为receivingdevice,名字是ed_rec;定义ED_TX_DEVICE为sendingdevice,名字是ed_tx。
#defineMAJOR_NUM_REC200
#defineMAJOR_NUM_TX201
#defineIOCTL_SET_BUSY_IOWR(MAJOR_NUM_TX,1,int)
200和201分别代表receivingdevice和sendingdevice的主设备号。
在内核空间,驱动程序是根据主、次设备号识别设备的,而不是设备名;本文的字符设备的次设备号都是0,主设备号是用户定义的且不能和系统已有的设备的主设备有冲突。
IOCTL_SET_BUSY_IOWR(MAJOR_NUM_TX,1,int)是ioctl的操作函数定义(从用户空间发送命令到内核空间),主要作用是使得每次在同一时间,同一字符设备上,只可进行一次操作。
我们可以使用mknod来建立这两个字符设备[root@localhost]#mknodc2000/dev/ed_rec
[root@localhost]#mknodc2010/dev/ed_tx
设备建立后,编译好的模块就可以动态加载了:
[root@localhost]#insmoded_device.o
为了方便对设备编程,我们还需要一个字符设备管理的数据结构:
structed_device{
intmagic;
charname[8];
intbusy;
unsignedchar*buffer;
#ifdefLINUX_24
wait_queue_head_trwait;
#endif
intmtu;
spinlock_tlock;
intdata_len;
intbuffer_size;
structfile*file;
ssize_t(*kernel_write)(constchar*buffer,size_tlength,intbuffer_size);
};
这个数据结构是用来保存字符设备的一些基本状态信息。
ssize_t(*kernel_write)(constchar*buffer,size_tlength,intbuffer_size)是一个指向函数的指针,它的作用是为伪网络驱动程序提供写字符设备数据的系统调用接口。
magic字段主要是标志设备类型号的,这里没有别的特殊意义;busy字段用来说明字符设备是否是处于忙状态,buffer指向内核缓存区,用来存放读写数据;mtu保存当前可发送的网络数据包最大传输单位,以字节为单位;lock的类型是自旋锁类型spinlock_t,它实际以一个整数域作为锁,在同一时刻对同一字符设备,只能有一个操作,所以使用内核锁机制保护防止数据污染;data_len是当前缓存区内保存的数据实际大小,以字节为单位;file是指向设备文件结构structfile的一个指针,其作用主要是定位设备的私有数据file->private_data。
定义字符设备structed_deviceed[2],其中ed[ED_REC_DEVICE]就是recevingdevice,ed[ED_TX_DEVICE]就是sendingdevice。
如果sendingdeviceED_TX_DEVICE没有数据,用户空间的read调用将被阻塞,并把进程信息放于rwait队列中。
当有数据的时候,kernel_write()中的wake_up_interruptible()将唤醒等待进程。
kernel_write()函数定义如下:
ssize_tkernel_write(constchar*buffer,size_tlength,intbuffer_size)
{
if(length>buffer_size)
length=buffer_size;
memset(ed[ED_TX_DEVICE].buffer,0,buffer_size);
memcpy(ed[ED_TX_DEVICE].buffer,buffer,buffer_size);
ed[ED_TX_DEVICE].tx_len=length;
#ifdefLINUX_24
wake_up_interruptible(&ed[ED_TX_DEVICE].rwait);
#endif
returnlength;
}
字符设备的操作及其相关函数调用过程。
图3当ed_device模块被加载的时候,eddev_module_init()调用register_chrdev()内核API注册ed_tx和ed_rec两个字符设备。
这个函数定义在:
intregister_chdev(unsignedintmajor,constchar*,structfle_operations*fops)
字符设备被注册成功后,内核把这两个字符设备加入到内核字符设备驱动表中。
内核字符设备驱动表保留指向structfile_operations的一个数据指针。
用户进程调用设备读写操作时,通过这个指针访问设备的操作函数,structfile_operations中的域大部分是指向函数的函数指针,指向用户自己编写的设备操作函数。
structfile_operationsed_ops={
#ifdefLINUX_24
NULL,
#endif
NULL,
device_read,
device_write,
NULL,
NULL,
device_ioctl,
NULL,
device_open,
NULL,
device_release,
};
注意到Linux2.4.x和Linux2.2.x内核中定义的structfile_operations是不一样的。
device_read()、device_write()、device_ioctl()、device_open()、device_release()就是需要用户自己定义的函数操作了,这几个函数是最基本的操作,如果需要设备驱动程序完成更复杂的任务,还必须编写其他structfile_operations中定义的操作。
eddev_module_init()除了注册设备及其操作外,它还有初始化字符设备结构structed_device,分配内核缓存区所需要的空间的作用。
在内核空间,分配内存空间的API函数是kmalloc()。
下面介绍一下字符设备的主要操作例程device_open()、device_release()、device_read()、devie_write()。
字符设备文件操作结构ed_ops中定义的指向以上函数的函数指针的原形:
device_open:
int(*open)(structinode*,structfile*)
device_release:
int(*release)(structinode*,structfile*);
device_read:
ssize_t(*read)(structfile*,char*,size_t,loff_t*);
device_write:
ssize_t(*write)(structfile*,constchar*,size_t,loff_t*);操作intdevice_open(structinode*inode,structfile*file)是设备节点上的第一个操作,如果多个设备共享这一个操作函数,必须区分设备的设备号。
我们使用inode->i_rdev>>8语句获得设备的主设备号,本文中的接收设备主设备号是200,发送设备号是201。
每个字符设备的file>private_data指向打开设备时候使用的file结构,private_data实际上可以指向用户定义的任何结构,这里只指向我们自己定义的structed_device,用来保存字符设备的一些基本信息,比如设备名、内核缓存区等。
操作ssize_tdevice_read(structfile*file,char*buffer,size_tlength,loff_t*offset)是读取设备数据的操作。
device_read()结构。
图4从设备中读取数据(用户空间调用read()系统调用)的时候,需要从内核空间把数据拷贝到用户空间,copy_to_user()可完成此功能,它和memcpy()此类函数有本质的区别,memcpy()不能完成不同用户空间数据的交换。
如果需要数据临界区的保护,使用spin_lock()内核API负责加锁,spin_unlock()负责解锁,防止数据污染。
由于串口守候进程server需要不断轮询设备,以查询是否有数据可读,如果用户进程不处于休眠状态,在用户空间查看进程使用资源情况,发现server占用了很多CPU资源。
所以我们改进device_read(),使之在内核中轮询,当发现当前设备没有数据可读取,那么就阻塞用户进程,使用内核APIadd_wait_queue()可完成此功能,这时候用户进程并没有占用很多CPU资源,而是处于休眠状态。
当内核发现有数据可读的时候,调用remove_wait_queue()即可唤醒等待进程,这段代码如下:
DECLARE_WAITQUEUE(wait,current);
add_wait_queue(&edp->rwait,&wait);
for(;;){
set_current_state(TASK_INTERRUPTIBLE);
if(file->f_flags&O_NONBLOCK)
break;
/*其他代码*/
if(signal_pending(current))
break;
schedule();
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&edp->rwait,&wait);
操作ssize_tdevice_write(structfile*file,constchar*buffer,size_tlength,loff_t*offset)向设备写入数据。
拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到内核空间,。
图5
编写伪网络设备驱动程序
伪网络驱动程序和字符设备驱动程序一样,也必须初始化和注册。
网络驱动需记录其发送和接收数据量的统计信息,所以我们定义一个记录这些信息的数据结构。
structednet_priv{
#ifdefLINUX_24
structnet_device_statsstats;
#else
structenet_statisticsstats;
#endif
structsk_buff*skb;
spinlock_tlock;
};
structednet_priv只有3个数据成员。
Linux2.4.x使用的网络数据状态统计结构是structnet_device_stats,而Linux2.2.x则使用的是structenet_statistics。
同样,对控制网络接口设备的设备结构也有不同的定义:
Linux2.4.x使用的是structnet_device,而Linux2.2.x却是structdevice。
#ifdefLINUX_24
structnet_deviceednet_dev;
#else
structdeviceednet_dev;
#endif
伪网络驱动程序的也需要初始化和注册。
和字符设备的注册不同之处是,它使用的是register_netdev(net_device*)kernelAPI。
intednet_module_init(void)
{
interr;
strcpy(ednet_dev.name,"ed0");
ednet_dev.init=ednet_init;
if((err=register_netdev(&ednet_dev)))
printk("ednet:
error%iregisteringpseudonetworkdevice"%s"n",
err,ednet_dev.name);
returnerr;
}
ednet_dev的name域是接口名,ednet_module_init()中赋予网络接口的名字为ed0,如果本网络设备被加载,使用ifconfig命令可以看到ed0。
[root@localhostpku]#/sbin/ifconfig
ed0Linkencap:
EthernetHWaddr00:
45:
44:
30:
30:
30
inetaddr:
192.168.3.9Bcast:
192.168.3.255Mask:
255.255.255.0
UPBROADCASTRUNNINGNOARPMULTICASTMTU:
1500Metric:
1
RXpackets:
0errors:
0dropped:
0overruns:
0frame:
0
TXpackets:
0errors:
0dropped:
0overruns:
0carrier:
0
collisions:
0txqueuelen:
100
RXbytes:
0(0.0b)TXbytes:
0(0.0b)
我们看到我们的伪网络接口没有Interrupt和Baseaddress,这是因为这个伪网络接口不和硬件打交道,也没有分配中断号和IO基址。
否则,如果你看一个实实在在的网络接口(如下面的eth1),可以看到它的Interrupt号是11和IOBaseaddress是0xa000。
eth1Linkencap:
EthernetHWaddr50:
78:
4C:
43:
1D:
01
inetaddr:
192.168.21.202Bcast:
192.168.21.255Mask:
255.255.255.0
UPBROADCASTRUNNINGMULTICASTMTU:
1500Metric:
1
RXpackets:
356523errors:
0dropped:
0overruns:
0frame:
0
TXpackets:
266errors:
0dropped:
0overruns:
0carrier:
0
collisions:
0txqueuelen:
100
RXbytes:
21542043(20.5Mb)TXbytes:
19510(19.0Kb)
Interrupt:
11Baseaddress:
0xa000
ednet_dev的init域是一个函数指针,指向用户定义的ednet_init()例程。
ednet_init()添充net_device结构,只有ednet_init()初始化成功后,系统才被加入到设备链表中。
ednet_dev的初始化例程ednet_init()如下:
#ifdefLINUX_24
intednet_init(structnet_device*dev)
#else
intednet_init(structdevice*dev)
#endif
{
ether_setup(dev);
dev->open=ednet_open;
dev->stop=ednet_release;
dev->hard_start_xmit=ednet_tx;
dev->get_stats=ednet_stats;
dev->change_mtu=ednet_change_mtu;
#ifdefLINUX_24
dev->hard_header=ednet_header;
#endif
dev->rebuild_header=ednet_rebuild_header;
#ifdefLINUX_24
dev->tx_timeout=ednet_tx_timeout;
dev->watchdog_timeo=timeout;
#endif
/*WedonotneedtheARPprotocol.*/
dev->flags|=IFF_NOARP;
#ifndefLINUX_20
dev->hard_header_cache=NULL;
#endif
#ifdefLINUX_