全部博文(92)
分类: LINUX
2007-10-30 17:26:33
FROM
http://hi.baidu.com/wangy0919/blog/item/201f2f3ebe907c3f70cf6cc1.html
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源代 码变成了乏味和繁锁的工作。这里介绍一种非常经济而且实用的实现串口上网的简单方法。
简单串口上网的实现原理如图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 串口上网程序的整个内核部分是以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
[root@localhost test]insmod ed_device.o, |
这时可以在/proc/net/dev 文件中看到有ed0伪网络设备了。如果需要卸载ed_device模块,应先停止其网络数据发送和接收工作,然后卸载模块:
[root@localhost test]ifconfig ed0 down |
如果我们设置另一台Linux box的伪网接口地址是192.168.5.2那么,我们可以用串口线直接连接两台终端并使用网络应用程序了,在两台终端上运行server守护程序,然后执行telnet:
[root@localhost test]# telnet 192.168.5.2 |
|
用 户空间的进程主要通过两种方式和内核空间模块打交道,一种是使用proc文件系统,另一种是使用字符设备。本文所描述的两个字符设备sending device 和receiving device事实上是内核空间和用户空间交换数据的缓存区,编写字符设备驱动实际上就是编写用户空间读写字符设备所需要的内核设备操作函数。
在头文件中,我们定义ED_REC_DEVICE为receiving device,名字是ed_rec;定义ED_TX_DEVICE为sending device,名字是ed_tx。
#define MAJOR_NUM_REC 200 |
200和201分别代表receiving device 和 sending device的主设备号。在内核空间,驱动程序是根据主、次设备号识别设备的,而不是设备名;本文的字符设备的次设备号都是0,主设备号是用户定义的且不 能和系统已有的设备的主设备有冲突。IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int)是ioctl的操作函数定义(从用户空间发送命令到内核空间),主要作用是使得每次在同一时间,同一 字符设备上,只可进行一次操作。我们可以使用mknod来建立这两个字符设备:
[root@localhost]#mknod c 200 0 /dev/ed_rec |
设备建立后,编译好的模块就可以动态加载了:
[root@localhost]#insmod ed_device.o |
为了方便对设备编程,我们还需要一个字符设备管理的数据结构:
struct ed_device{ |
这个数据结构是用来保存字符设备的一些基本状态信息。 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()函数定义如下:
ssize_t kernel_write(const char *buffer,size_t length,int buffer_size) |
字符设备的操作及其相关函数调用过程如图3 所示。
当ed_device模块被加载的时候,eddev_module_init()调用register_chrdev()内核API注册ed_tx和ed_rec两个字符设备。这个函数定义在
int register_chdev(unsigned int major, const char *, struct fle_operations *fops) |
字符设备被注册成功后,内核把这两个字符设备加入到内 核字符设备驱动表中。内核字符设备驱动表保留指向struct file_operations的一个数据指针。用户进程调用设备读写操作时,通过这个指针访问设备的操作函数, struct file_operations中的域大部分是指向函数的函数指针,指向用户自己编写的设备操作函数。
struct file_operations ed_ops ={ |
注意到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中定义的指向以上函数的函数指针的原形:
device_open: int(*open)(struct inode *,struct file *) |
操作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所示。
从 设备中读取数据(用户空间调用read()系统调用)的时候,需要从内核空间把数据拷贝到用户空间,copy_to_user()可完成此功能,它和 memcpy()此类函数有本质的区别,memcpy()不能完成不同用户空间数据的交换。如果需要数据临界区的保护,使用spin_lock()内核 API负责加锁,spin_unlock()负责解锁,防止数据污染。由于串口守候进程server需要不断轮询设备,以查询是否有数据可读,如果用户进 程不处于休眠状态,在用户空间查看进程使用资源情况,发现server占用了很多CPU资源。所以我们改进device_read(),使之在内核中轮 询,当发现当前设备没有数据可读取,那么就阻塞用户进程,使用内核API add_wait_queue()可完成此功能,这时候用户进程并没有占用很多CPU资源,而是处于休眠状态。当内核发现有数据可读的时候,调用 remove_wait_queue()即可唤醒等待进程,这段
代码如下:
DECLARE_WAITQUEUE(wait,current); |
操作ssize_t device_write(struct file *file,const char *buffer, size_t length,loff_t *offset)向设备写入数据。拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到 内核空间,如图5所示。