Linux串口上网的程序实现方法Word文件下载.docx
《Linux串口上网的程序实现方法Word文件下载.docx》由会员分享,可在线阅读,更多相关《Linux串口上网的程序实现方法Word文件下载.docx(18页珍藏版)》请在冰豆网上搜索。
![Linux串口上网的程序实现方法Word文件下载.docx](https://file1.bdocx.com/fileroot1/2023-2/1/58254571-bd6e-4677-a314-1b860360183b/58254571-bd6e-4677-a314-1b860360183b1.gif)
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&
#39;
^]&
.
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-&
gt;
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;
wake_up_interruptible(&
amp;
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,
device_read,
device_write,
device_ioctl,
device_open,
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))
schedule();
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&
操作ssize_tdevice_write(structfile*file,constchar*buffer,size_tlength,loff_t*offset)向设备写入数据。
拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到内核空间,。
图5
编写伪网络设备驱动程序
伪网络驱动程序和字符设备驱动程序一样,也必须初始化和注册。
网络驱动需记录其发送和接收数据量的统计信息,所以我们定义一个记录这些信息的数据结构。
structednet_priv{
structnet_device_statsstats;
#else
structenet_statisticsstats;
structsk_buff*skb;
structednet_priv只有3个数据成员。
Linux2.4.x使用的网络数据状态统计结构是structnet_device_stats,而Linux2.2.x则使用的是structenet_statistics。
同样,对控制网络接口设备的设备结构也有不同的定义:
Linux2.4.x使用的是structnet_device,而Linux2.2.x却是structdevice。
structnet_deviceednet_dev;
structdeviceednet_dev;
伪网络驱动程序的也需要初始化和注册。
和字符设备的注册不同之处是,它使用的是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
inetaddr:
192.168.3.9Bcast:
192.168.3.255Mask:
255.255.255.0
UPBROADCASTRUNNINGNOARPMULTICASTMTU:
1500Metric:
1
RXpackets:
0errors:
0dropped:
0overruns:
0frame:
TXpackets:
0carrier:
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
192.168.21.202Bcast:
192.168.21.255Mask:
UPBROADCASTRUNNINGMULTICASTMTU:
356523errors:
266errors:
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()如下:
intednet_init(structnet_device*dev)
intednet_init(structdevice*dev)
{
ether_setup(dev);
dev-&
open=ednet_open;
stop=ednet_release;
hard_start_xmit=ednet_tx;
get_stats=ednet_stats;
change_mtu=ednet_change_mtu;
hard_header=ednet_header;
rebuild_header=ednet_rebuild_header;
tx_timeout=ednet_tx_timeout;
watchdog_timeo=timeout;
/*WedonotneedtheARPprotocol.*/
flags|=IFF_NOARP;
#ifndefLINUX_20
hard_header_cache=NULL;
#ifdefLINUX_