精华内容
下载资源
问答
  • Linux串口上网的程序实现方法,Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-PeerProtocol,端对端协议)和SLIP(SerialLineInterfaceProtocol,非常老的串行线路接口协议)。这里所说的"上网"是指把...
  • 近日,研华科技宣布最新推出新一代8/16口串口上网服务器-EDG-4508+/4516+系列产品,它基于全新的EVA集成控制芯片和通讯技术,使这款产品在通讯转换、处理和安全技术方面都有新的突破,同时增加了市场同类产品所不...
  • linux 下串口上网实现

    2009-11-28 16:59:55
    用linux下串口上网,串口到串口上网程序。非常宝贵!!
  • 引言Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-Peer Protocol, 端对端协议)和SLIP(Serial Line Interface Protocol, 非常老的串行线路接口协议),这里所说的"上网"是指把串口当成一个网络接口,通过封装...

    引言

    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简单串口上网原理

    简单串口上网的实现原理如下图所示:

    b89c29b0293f7769d0d8ed0c6fb6f924.png

    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的加载和注销函数如下图所示:

    dc75feffe4f92facaaa2c47722539f02.png

    当我们需要加载模块的时候,我们只需要使用insmod命令,如果需要卸载模块,我们使用rmmod命令。比如加载ed_device模块,并且配置伪网络接口IP地址为192.168.5.1:

    [root@localhost test]insmod ed_device.o,[root@localhost test]ifconfig ed0 192.168.5.1 up

    这时可以在/proc/net/dev 文件中看到有ed0伪网络设备了。如果需要卸载ed_device模块,应先停止其网络数据发送和接收工作,然后卸载模块:

    [root@localhost test]ifconfig ed0 down[root@localhost test]rmmod ed_device

    如果我们设置另一台Linux box的伪网接口地址是192.168.5.2那么,我们可以用串口线直接连接两台终端并使用网络应用程序了,在两台终端上运行server守护程序,然后执行telnet:

    [root@localhost test]# telnet 192.168.5.2Trying 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 i686login:

    编写字符设备驱动程序

    用户空间的进程主要通过两种方式和内核空间模块打交道,一种是使用proc文件系统,另一种是使用字符设备。本文所描述的两个字符设备sending device 和receiving device事实上是内核空间和用户空间交换数据的缓存区,编写字符设备驱动实际上就是编写用户空间读写字符设备所需要的内核设备操作函数。

    在头文件中,我们定义ED_REC_DEVICE为receiving device,名字是ed_rec;定义ED_TX_DEVICE为sending device,名字是ed_tx。

    #define MAJOR_NUM_REC 200#define MAJOR_NUM_TX  201#define IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int)

    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]#mknod c 201 0 /dev/ed_tx

    设备建立后,编译好的模块就可以动态加载了:

    root@localhost]#insmod ed_device.o

    为了方便对设备编程,我们还需要一个字符设备管理的数据结构:

     struct ed_device{     int magic;     char name[8];        int busy;     unsigned char *buffer;     #ifdef LINUX_24 wait_queue_head_t rwait; #endif     int mtu;     spinlock_t lock;     int data_len;     int buffer_size;     struct file *file;     ssize_t (*kernel_write)(const char *buffer,size_t length,int buffer_size); };

    这个数据结构是用来保存字符设备的一些基本状态信息:

    • 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) {     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;     #ifdef LINUX_24     wake_up_interruptible(&ed[ED_TX_DEVICE].rwait);      #endif        return length; }

    字符设备的操作及其相关函数调用过程如下图所示:

    7f47356ab1572e8e460eac99a2bafdd8.png

    当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 ={ #ifdef LINUX_24     NULL, #endif     NULL,     device_read,     device_write,     NULL,     NULL,     device_ioctl,     NULL,     device_open,     NULL,     device_release,      };

    注意到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 *)     device_release: int (*release) (struct inode *, struct file *);device_read:  ssize_t (*read) (struct file *, char *, size_t, loff_t *);device_write: ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

    操作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()结构如下图所示:

    4740ac7e4198ae3c967fb5e698c76298.png

    从设备中读取数据(用户空间调用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);     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_t device_write(struct file *file,const char *buffer, size_t length,loff_t *offset)向设备写入数据。拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到内核空间,如下图所示:

    b6ff83cfbd31e05f7843d29acda78ace.png

    编写伪网络设备驱动程序

    伪网络驱动程序和字符设备驱动程序一样,也必须初始化和注册。网络驱动需记录其发送和接收数据量的统计信息,所以我们定义一个记录这些信息的数据结构:

    struct ednet_priv {#ifdef LINUX_24    struct net_device_stats stats;#else    struct enet_statistics stats;#endif    struct sk_buff *skb;    spinlock_t lock;};

    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:

    #ifdef LINUX_24struct net_device ednet_dev;#elsestruct device ednet_dev;#endif

    伪网络驱动程序的也需要初始化和注册。和字符设备的注册不同之处是,它使用的是register_netdev(net_device *) kernel API:

     int ednet_module_init(void) {     int err;     strcpy(ednet_dev.name, "ed0");     ednet_dev.init = ednet_init;     if ( (err = register_netdev(&ednet_dev)) )             printk("ednet: error %i registering pseudo network device "%s"",                    err, ednet_dev.name);      return err; }

    ednet_dev的name域是接口名,ednet_module_init()中赋予网络接口的名字为ed0,如果本网络设备被加载,使用ifconfig命令可以看到ed0:

    [root@localhost pku]# /sbin/ifconfiged0       Link encap:Ethernet  HWaddr 00:45:44:30:30:30          inet addr:192.168.3.9  Bcast:192.168.3.255  Mask:255.255.255.0          UP BROADCAST RUNNING NOARP MULTICAST  MTU:1500  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:100          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)

    我们看到我们的伪网络接口没有Interrupt和Base address,这是因为这个伪网络接口不和硬件打交道,也没有分配中断号和IO基址。否则,如果你看一个实实在在的网络接口(如下面的eth1),可以看到它的Interrupt号是11和IO Base address是0xa000:

    eth1      Link encap:Ethernet  HWaddr 50:78:4C:43:1D:012          inet addr:192.168.21.202  Bcast:192.168.21.255  Mask:255.255.255.03          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:14          RX packets:356523 errors:0 dropped:0 overruns:0 frame:05          TX packets:266 errors:0 dropped:0 overruns:0 carrier:06          collisions:0 txqueuelen:1007          RX bytes:21542043 (20.5 Mb)  TX bytes:19510 (19.0 Kb)8          Interrupt:11 Base address:0xa000

    ednet_dev的init域是一个函数指针,指向用户定义的ednet_init()例程。ednet_init()添充net_device结构,只有ednet_init()初始化成功后,系统才被加入到设备链表中。ednet_dev的初始化例程ednet_init()如下:

    #ifdef LINUX_24int ednet_init(struct net_device *dev)#elseint ednet_init(struct device *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;  #ifdef LINUX_24    dev->hard_header      = ednet_header;#endif    dev->rebuild_header    = ednet_rebuild_header;#ifdef LINUX_24    dev->tx_timeout        = ednet_tx_timeout;    dev->watchdog_timeo   = timeout;#endif    /* We do not need the ARP protocol. */    dev->flags           |= IFF_NOARP;#ifndef LINUX_20                            dev->hard_header_cache = NULL;      #endif #ifdef LINUX_24                                     SET_MODULE_OWNER(dev);#endif    dev->priv = kmalloc(sizeof(struct ednet_priv), GFP_KERNEL);    if (dev->priv == NULL)        return -ENOMEM;    memset(dev->priv, 0, sizeof(struct ednet_priv));    spin_lock_init(& ((struct ednet_priv *) dev->priv)->lock);    return 0;}

    ether_setup()填充一些以太网的缺省设置。dev->hard_header_cache=NULL表示不缓存向本网络接口回复的ARP网络数据包。IFF_NOARP的标志设置表明本网络接口不使用ARP。ARP的主要功能是获得通信对方的网络接口的硬件地址,本文的伪网络接口的物理地址是程序中设定的伪物理地址,所以我们不需要ARP协议。SET_MODULE_OWNER(dev)这个宏是设置dev结构中owner域(定义为struct module *owner;),使得它指向本模块本身。与字符设备一样,本网络设备也需要定义在其上的操作例程。下面就对ednet_init()中用户定义的设备操作函数做进一步说明。整个伪网络设备操作调用结构如下图所示:

    00a1020ed574e09e3ac7e9da12dc9575.png

    由上图我们看到,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(中有定义)中定义了指向以上函数的函数指针的原形:

     ednet_open:   int  (*open)(struct net_device *dev); ednet_release:  int  (*stop)(struct net_device *dev); ednet_tx:   int  (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev); ednet_stats:   struct net_device_stats* (*get_stats)(struct net_device *dev); ednet_change_mtu:int  (*change_mtu)(struct net_device *dev, int new_mtu); ednet_header:  int  (*hard_header) (struct sk_buff *skb,             struct net_device *dev,             unsigned short type,             void *daddr,             void *saddr,             unsigned len);

    操作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<<8)) 。

    因为伪网络接口没有使用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的程序流图如下图所示:

    f10d788584a3c7789c8757a2af29803d.png

    传送的frame按照SLIP定义的格式:数据的两头都是END字符(0300),如下图所示:

    5b2d3e7ad78b2b34ce1d02cc1229df48.png

    特殊控制字符的定义如下:

    define END             03002#define ESC             03333#define ESC_END         0334      4#define ESC_ESC         0335

    如果打包前的数据中有END这个字符,那么使用ESC_END代替,如果发现有ESC这个字符,那么使用ESC_ESC字符替换。在Linux环境下,串口名从ttyS0开始依次是ttyS1、ttyS2等。在本程序中,使用ttyS0作为通信串口。在打开ttyS0的时候,选项O_NOCTTY 表示不能把本串口当成控制终端,否则用户的键盘输入信息将影响程序的执行;O_NDELAY表示打开串口的时候,程序并不关心另一端的串口是否在使用中。在Linux中,打开串口设备和打开普通文件一样,使用的是open()系统调用。比如我么打开串口设备1也就是COM1,只需要:

    fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY );

    打开的串口设备有很多设置选项。本文中使用int setup_com(int fd)设置。在系统头文件中定义了终端控制结构struct termios,tcgetattr()和tcsetattr()两个系统函数获得和设置这些属性。结构struct termios中的域描述的主要属性包括:

    c_cflag  : 控制选项c_lflag  : 线选项c_iflag  : 输入选项c_oflag  :输出选项c_cc    :控制字符c_ispeed :输入数据波特率c_ospeed :输出数据波特率

    如果要设置某个选项,那么就使用"|="运算,如果关闭某个选项就使用"&="和"~"运算。本文使用的各个选项的意义定义如下:

     c_cflag: CLOCAL 本地模式,不改变端口的所有者          CREAD  表示使能数据接收器          PARENB  表示偶校验          PARODD 表示奇校验 CSTOPB  使用两个停止位 CSIZE    对数据的bit使用掩码 CS8      数据宽度是8bit c_lflag:  ICANON 使能规范输入,否则使用原始数据(本文使用) ECHO    回送(echo)输入数据 ECHOE   回送擦除字符 ISIG      使能SIGINTR,SIGSUSP, SIGDSUSP和 SIGQUIT 信号 c_iflag:  IXON     使能输出软件控制          IXOFF    使能输入软件控制          IXANY    允许任何字符再次开启数据流          INLCR    把字符NL(0A)映射到CR(0D)          IGNCR    忽略字符CR(0D)        ICRNL    把CR(0D)映射成字符NR(0A)     c_oflag: OPOST  输出后处理,如果不设置表示原始数据(本文使用原始数据)  c_cc[VMIN]:  最少可读数据 c_cc[VTIME]: 等待数据时间(10秒的倍数)

    根据以上设置的定义,串口端口设置函数setup_com()定义如下:

    int setup_com(int fd){    struct termios options;     tcgetattr(fd, &options);    /* Set the baud rates to 38400...*/    cfsetispeed(&options, B38400);    cfsetospeed(&options, B38400);    /* Enable the receiver and set local mode...*/    options.c_cflag |= (CLOCAL | CREAD);    /* Set c_cflag options.*/    options.c_cflag |= PARENB;    options.c_cflag &= ~PARODD;    options.c_cflag &= ~CSTOPB;    options.c_cflag &= ~CSIZE;    options.c_cflag |= CS8;        /* Set c_iflag input options */    options.c_iflag &=~(IXON | IXOFF | IXANY);    options.c_iflag &=~(INLCR | IGNCR | ICRNL);    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);    /* Set c_oflag output options */    options.c_oflag &= ~OPOST;       /* Set the timeout options */    options.c_cc[VMIN]  = 0;    options.c_cc[VTIME] = 10;    tcsetattr(fd, TCSANOW, &options);    return 1;}

    两个打包和拆包函数和SLIP协议定义的一样,拆包函数和打包相反,这里不列举了。

    小结

    本文描述的是一个非常简单的串口上网程序,如果需要可靠的通信,增加吞吐量,可在用户空间添加适当的网络控制协议,也可增加数据压缩算法

    本文涉及的参考源码如有需要请关注微信公众号“麻辣软硬件”,在后台回复关键词“串口”,谢谢!

    文章转载自:https://www.ibm.com/developerworks/cn/linux/l-serialnet/index.html

    展开全文
  • 引言Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-Peer Protocol, 端对端协议)和SLIP(Serial Line Interface Protocol, 非常老的串行线路接口协议),这里所说的"上网"是指把串口当成一个网络接口,通过封装...

    引言

    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简单串口上网原理

    简单串口上网的实现原理如下图所示:

    f71e8aa73f0997a13b4602cc96c884d7.png

    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的加载和注销函数如下图所示:

    99031d4c6db5a8f334ba57c695291cbf.png

    当我们需要加载模块的时候,我们只需要使用insmod命令,如果需要卸载模块,我们使用rmmod命令。比如加载ed_device模块,并且配置伪网络接口IP地址为192.168.5.1:

    [root@localhost test]insmod ed_device.o,[root@localhost test]ifconfig ed0 192.168.5.1 up

    这时可以在/proc/net/dev 文件中看到有ed0伪网络设备了。如果需要卸载ed_device模块,应先停止其网络数据发送和接收工作,然后卸载模块:

    [root@localhost test]ifconfig ed0 down[root@localhost test]rmmod ed_device

    如果我们设置另一台Linux box的伪网接口地址是192.168.5.2那么,我们可以用串口线直接连接两台终端并使用网络应用程序了,在两台终端上运行server守护程序,然后执行telnet:

    [root@localhost test]# telnet 192.168.5.2Trying 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 i686login:

    编写字符设备驱动程序

    用户空间的进程主要通过两种方式和内核空间模块打交道,一种是使用proc文件系统,另一种是使用字符设备。本文所描述的两个字符设备sending device 和receiving device事实上是内核空间和用户空间交换数据的缓存区,编写字符设备驱动实际上就是编写用户空间读写字符设备所需要的内核设备操作函数。

    在头文件中,我们定义ED_REC_DEVICE为receiving device,名字是ed_rec;定义ED_TX_DEVICE为sending device,名字是ed_tx。

    #define MAJOR_NUM_REC 200#define MAJOR_NUM_TX  201#define IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int)

    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]#mknod c 201 0 /dev/ed_tx

    设备建立后,编译好的模块就可以动态加载了:

    root@localhost]#insmod ed_device.o

    为了方便对设备编程,我们还需要一个字符设备管理的数据结构:

     struct ed_device{     int magic;     char name[8];        int busy;     unsigned char *buffer;     #ifdef LINUX_24 wait_queue_head_t rwait; #endif     int mtu;     spinlock_t lock;     int data_len;     int buffer_size;     struct file *file;     ssize_t (*kernel_write)(const char *buffer,size_t length,int buffer_size); };

    这个数据结构是用来保存字符设备的一些基本状态信息:

    • 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) {     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;     #ifdef LINUX_24     wake_up_interruptible(&ed[ED_TX_DEVICE].rwait);      #endif        return length; }

    字符设备的操作及其相关函数调用过程如下图所示:

    33440ea8ce057fe4bcb72a7e962e5828.png

    当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 ={ #ifdef LINUX_24     NULL, #endif     NULL,     device_read,     device_write,     NULL,     NULL,     device_ioctl,     NULL,     device_open,     NULL,     device_release,      };

    注意到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 *)     device_release: int (*release) (struct inode *, struct file *);device_read:  ssize_t (*read) (struct file *, char *, size_t, loff_t *);device_write: ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

    操作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()结构如下图所示:

    82b23fc4895f95a46cd59ca4a6775f74.png

    从设备中读取数据(用户空间调用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);     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_t device_write(struct file *file,const char *buffer, size_t length,loff_t *offset)向设备写入数据。拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到内核空间,如下图所示:

    46a568ff6b66d63761ac3e8c96ada15b.png

    编写伪网络设备驱动程序

    伪网络驱动程序和字符设备驱动程序一样,也必须初始化和注册。网络驱动需记录其发送和接收数据量的统计信息,所以我们定义一个记录这些信息的数据结构:

    struct ednet_priv {#ifdef LINUX_24    struct net_device_stats stats;#else    struct enet_statistics stats;#endif    struct sk_buff *skb;    spinlock_t lock;};

    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:

    #ifdef LINUX_24struct net_device ednet_dev;#elsestruct device ednet_dev;#endif

    伪网络驱动程序的也需要初始化和注册。和字符设备的注册不同之处是,它使用的是register_netdev(net_device *) kernel API:

     int ednet_module_init(void) {     int err;     strcpy(ednet_dev.name, "ed0");     ednet_dev.init = ednet_init;     if ( (err = register_netdev(&ednet_dev)) )             printk("ednet: error %i registering pseudo network device "%s"",                    err, ednet_dev.name);      return err; }

    ednet_dev的name域是接口名,ednet_module_init()中赋予网络接口的名字为ed0,如果本网络设备被加载,使用ifconfig命令可以看到ed0:

    [root@localhost pku]# /sbin/ifconfiged0       Link encap:Ethernet  HWaddr 00:45:44:30:30:30          inet addr:192.168.3.9  Bcast:192.168.3.255  Mask:255.255.255.0          UP BROADCAST RUNNING NOARP MULTICAST  MTU:1500  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:100          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)

    我们看到我们的伪网络接口没有Interrupt和Base address,这是因为这个伪网络接口不和硬件打交道,也没有分配中断号和IO基址。否则,如果你看一个实实在在的网络接口(如下面的eth1),可以看到它的Interrupt号是11和IO Base address是0xa000:

    eth1      Link encap:Ethernet  HWaddr 50:78:4C:43:1D:012          inet addr:192.168.21.202  Bcast:192.168.21.255  Mask:255.255.255.03          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:14          RX packets:356523 errors:0 dropped:0 overruns:0 frame:05          TX packets:266 errors:0 dropped:0 overruns:0 carrier:06          collisions:0 txqueuelen:1007          RX bytes:21542043 (20.5 Mb)  TX bytes:19510 (19.0 Kb)8          Interrupt:11 Base address:0xa000

    ednet_dev的init域是一个函数指针,指向用户定义的ednet_init()例程。ednet_init()添充net_device结构,只有ednet_init()初始化成功后,系统才被加入到设备链表中。ednet_dev的初始化例程ednet_init()如下:

    #ifdef LINUX_24int ednet_init(struct net_device *dev)#elseint ednet_init(struct device *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;  #ifdef LINUX_24    dev->hard_header      = ednet_header;#endif    dev->rebuild_header    = ednet_rebuild_header;#ifdef LINUX_24    dev->tx_timeout        = ednet_tx_timeout;    dev->watchdog_timeo   = timeout;#endif    /* We do not need the ARP protocol. */    dev->flags           |= IFF_NOARP;#ifndef LINUX_20                            dev->hard_header_cache = NULL;      #endif #ifdef LINUX_24                                     SET_MODULE_OWNER(dev);#endif    dev->priv = kmalloc(sizeof(struct ednet_priv), GFP_KERNEL);    if (dev->priv == NULL)        return -ENOMEM;    memset(dev->priv, 0, sizeof(struct ednet_priv));    spin_lock_init(& ((struct ednet_priv *) dev->priv)->lock);    return 0;}

    ether_setup()填充一些以太网的缺省设置。dev->hard_header_cache=NULL表示不缓存向本网络接口回复的ARP网络数据包。IFF_NOARP的标志设置表明本网络接口不使用ARP。ARP的主要功能是获得通信对方的网络接口的硬件地址,本文的伪网络接口的物理地址是程序中设定的伪物理地址,所以我们不需要ARP协议。SET_MODULE_OWNER(dev)这个宏是设置dev结构中owner域(定义为struct module *owner;),使得它指向本模块本身。与字符设备一样,本网络设备也需要定义在其上的操作例程。下面就对ednet_init()中用户定义的设备操作函数做进一步说明。整个伪网络设备操作调用结构如下图所示:

    7ba677d775628895a806b0c4e583d2d3.png

    由上图我们看到,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(中有定义)中定义了指向以上函数的函数指针的原形:

     ednet_open:   int  (*open)(struct net_device *dev); ednet_release:  int  (*stop)(struct net_device *dev); ednet_tx:   int  (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev); ednet_stats:   struct net_device_stats* (*get_stats)(struct net_device *dev); ednet_change_mtu:int  (*change_mtu)(struct net_device *dev, int new_mtu); ednet_header:  int  (*hard_header) (struct sk_buff *skb,             struct net_device *dev,             unsigned short type,             void *daddr,             void *saddr,             unsigned len);

    操作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<<8)) 。

    因为伪网络接口没有使用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的程序流图如下图所示:

    9d5f80ac5dd2189b75f3bb117baec7d4.png

    传送的frame按照SLIP定义的格式:数据的两头都是END字符(0300),如下图所示:

    33bee6432a99ec0728fa38b94d4cdb63.png

    特殊控制字符的定义如下:

    define END             03002#define ESC             03333#define ESC_END         0334      4#define ESC_ESC         0335

    如果打包前的数据中有END这个字符,那么使用ESC_END代替,如果发现有ESC这个字符,那么使用ESC_ESC字符替换。在Linux环境下,串口名从ttyS0开始依次是ttyS1、ttyS2等。在本程序中,使用ttyS0作为通信串口。在打开ttyS0的时候,选项O_NOCTTY 表示不能把本串口当成控制终端,否则用户的键盘输入信息将影响程序的执行;O_NDELAY表示打开串口的时候,程序并不关心另一端的串口是否在使用中。在Linux中,打开串口设备和打开普通文件一样,使用的是open()系统调用。比如我么打开串口设备1也就是COM1,只需要:

    fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY );

    打开的串口设备有很多设置选项。本文中使用int setup_com(int fd)设置。在系统头文件中定义了终端控制结构struct termios,tcgetattr()和tcsetattr()两个系统函数获得和设置这些属性。结构struct termios中的域描述的主要属性包括:

    c_cflag  : 控制选项c_lflag  : 线选项c_iflag  : 输入选项c_oflag  :输出选项c_cc    :控制字符c_ispeed :输入数据波特率c_ospeed :输出数据波特率

    如果要设置某个选项,那么就使用"|="运算,如果关闭某个选项就使用"&="和"~"运算。本文使用的各个选项的意义定义如下:

     c_cflag: CLOCAL 本地模式,不改变端口的所有者          CREAD  表示使能数据接收器          PARENB  表示偶校验          PARODD 表示奇校验 CSTOPB  使用两个停止位 CSIZE    对数据的bit使用掩码 CS8      数据宽度是8bit c_lflag:  ICANON 使能规范输入,否则使用原始数据(本文使用) ECHO    回送(echo)输入数据 ECHOE   回送擦除字符 ISIG      使能SIGINTR,SIGSUSP, SIGDSUSP和 SIGQUIT 信号 c_iflag:  IXON     使能输出软件控制          IXOFF    使能输入软件控制          IXANY    允许任何字符再次开启数据流          INLCR    把字符NL(0A)映射到CR(0D)          IGNCR    忽略字符CR(0D)        ICRNL    把CR(0D)映射成字符NR(0A)     c_oflag: OPOST  输出后处理,如果不设置表示原始数据(本文使用原始数据)  c_cc[VMIN]:  最少可读数据 c_cc[VTIME]: 等待数据时间(10秒的倍数)

    根据以上设置的定义,串口端口设置函数setup_com()定义如下:

    int setup_com(int fd){    struct termios options;     tcgetattr(fd, &options);    /* Set the baud rates to 38400...*/    cfsetispeed(&options, B38400);    cfsetospeed(&options, B38400);    /* Enable the receiver and set local mode...*/    options.c_cflag |= (CLOCAL | CREAD);    /* Set c_cflag options.*/    options.c_cflag |= PARENB;    options.c_cflag &= ~PARODD;    options.c_cflag &= ~CSTOPB;    options.c_cflag &= ~CSIZE;    options.c_cflag |= CS8;        /* Set c_iflag input options */    options.c_iflag &=~(IXON | IXOFF | IXANY);    options.c_iflag &=~(INLCR | IGNCR | ICRNL);    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);    /* Set c_oflag output options */    options.c_oflag &= ~OPOST;       /* Set the timeout options */    options.c_cc[VMIN]  = 0;    options.c_cc[VTIME] = 10;    tcsetattr(fd, TCSANOW, &options);    return 1;}

    两个打包和拆包函数和SLIP协议定义的一样,拆包函数和打包相反,这里不列举了。

    小结

    本文描述的是一个非常简单的串口上网程序,如果需要可靠的通信,增加吞吐量,可在用户空间添加适当的网络控制协议,也可增加数据压缩算法

    本文涉及的参考源码如有需要请关注微信公众号“麻辣软硬件”,在后台回复关键词“串口”,谢谢!

    文章转载自:https://www.ibm.com/developerworks/cn/linux/l-serialnet/index.html

    展开全文
  • Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-Peer Protocol, 端对端协议)和SLIP(Serial Line Interface Protocol, 非常老的串行线路接口协议),这里所说的"上网"是指把串口当成一个网络接口,通过封装...
  • Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-Peer Protocol, 端对端协议)和SLIP(Serial Line Interface Protocol, 非常老的串行线路接口协议),这里所说的"上网"是指把串口当成一个网络接口,通过封装...

    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所示。

    图 1
    图 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所示。

    图 2
    图 2

    当我们需要加载模块的时候,我们只需要使用insmod命令,如果需要卸载模块,我们使用rmmod命令。比如加载ed_device模块,并且配置伪网络接口IP地址为192.168.5.1

    [root@localhost test]insmod ed_device.o,
    [root@localhost test]ifconfig ed0 192.168.5.1 up

    这时可以在/proc/net/dev 文件中看到有ed0伪网络设备了。如果需要卸载ed_device模块,应先停止其网络数据发送和接收工作,然后卸载模块:

    [root@localhost test]ifconfig ed0 down
    [root@localhost test]rmmod 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。

    #define MAJOR_NUM_REC 200
    #define MAJOR_NUM_TX  201
    #define IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int)

    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]#mknod c 201 0 /dev/ed_tx

    设备建立后,编译好的模块就可以动态加载了:

    [root@localhost]#insmod ed_device.o

    为了方便对设备编程,我们还需要一个字符设备管理的数据结构:

    struct ed_device{
    	int magic;
    	char name[8]; 	
    	int busy;
    	unsigned char *buffer;
        #ifdef LINUX_24
    wait_queue_head_t rwait;
    #endif
    	int mtu;
    	spinlock_t lock;
    	int data_len;
        int buffer_size;
    	struct file *file;
        ssize_t (*kernel_write)(const char *buffer,size_t length,int buffer_size);
    };

    这个数据结构是用来保存字符设备的一些基本状态信息。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)
    {
        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;
        #ifdef LINUX_24
        wake_up_interruptible(&ed[ED_TX_DEVICE].rwait);	
        #endif   
        return length;
    }

    字符设备的操作及其相关函数调用过程如图3 所示。

    图 3
    图 3

    当ed_device模块被加载的时候,eddev_module_init()调用register_chrdev()内核API注册ed_tx和ed_rec两个字符设备。这个函数定义在<linux/fs.h>:

    int register_chdev(unsigned int major, const char *, struct fle_operations *fops)

    字符设备被注册成功后,内核把这两个字符设备加入到内核字符设备驱动表中。内核字符设备驱动表保留指向struct file_operations的一个数据指针。用户进程调用设备读写操作时,通过这个指针访问设备的操作函数, struct file_operations中的域大部分是指向函数的函数指针,指向用户自己编写的设备操作函数。

    struct file_operations ed_ops ={
    #ifdef LINUX_24
        NULL,
    #endif
        NULL,
        device_read,
        device_write,
        NULL,
        NULL,
        device_ioctl,
        NULL,
        device_open,
        NULL,
        device_release,    	
    };

    注意到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 *)     
         device_release: int (*release) (struct inode *, struct file *);
         device_read:  ssize_t (*read) (struct file *, char *, size_t, loff_t *);
         device_write: ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

    操作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
    图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);
        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_t device_write(struct file *file,const char *buffer, size_t length,loff_t *offset)向设备写入数据。拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到内核空间,如图5所示。

    图 5
    图 5

    编写伪网络设备驱动程序

    伪网络驱动程序和字符设备驱动程序一样,也必须初始化和注册。网络驱动需记录其发送和接收数据量的统计信息,所以我们定义一个记录这些信息的数据结构。

    struct ednet_priv {
    #ifdef LINUX_24
        struct net_device_stats stats;
    #else
        struct enet_statistics stats;
    #endif
        struct sk_buff *skb;
        spinlock_t lock;
    };

    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。

    #ifdef LINUX_24
    struct net_device ednet_dev;
    #else
    struct device ednet_dev;
    #endif

    伪网络驱动程序的也需要初始化和注册。和字符设备的注册不同之处是,它使用的是register_netdev(net_device *) kernel API。

    int ednet_module_init(void)
    {
        int err;
        strcpy(ednet_dev.name, "ed0");
        ednet_dev.init = ednet_init;
        if ( (err = register_netdev(&ednet_dev)) )
                printk("ednet: error %i registering pseudo network device \"%s\"\n",
                       err, ednet_dev.name);
            
        return err;
    }

    ednet_dev的name域是接口名,ednet_module_init()中赋予网络接口的名字为ed0,如果本网络设备被加载,使用ifconfig命令可以看到ed0。

    [root@localhost pku]# /sbin/ifconfig
    ed0       Link encap:Ethernet  HWaddr 00:45:44:30:30:30
              inet addr:192.168.3.9  Bcast:192.168.3.255  Mask:255.255.255.0
              UP BROADCAST RUNNING NOARP MULTICAST  MTU:1500  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:100
              RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)

    我们看到我们的伪网络接口没有Interrupt和Base address,这是因为这个伪网络接口不和硬件打交道,也没有分配中断号和IO基址。否则,如果你看一个实实在在的网络接口(如下面的eth1),可以看到它的Interrupt号是11和IO Base address是0xa000。

    eth1      Link encap:Ethernet  HWaddr 50:78:4C:43:1D:01
              inet addr:192.168.21.202  Bcast:192.168.21.255  Mask:255.255.255.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:356523 errors:0 dropped:0 overruns:0 frame:0
              TX packets:266 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:100
              RX bytes:21542043 (20.5 Mb)  TX bytes:19510 (19.0 Kb)
              Interrupt:11 Base address:0xa000

    ednet_dev的init域是一个函数指针,指向用户定义的ednet_init()例程。ednet_init()添充net_device结构,只有ednet_init()初始化成功后,系统才被加入到设备链表中。ednet_dev的初始化例程ednet_init()如下:

    #ifdef LINUX_24
    int ednet_init(struct net_device *dev)
    #else
    int ednet_init(struct device *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;  
    #ifdef LINUX_24
        dev->hard_header      = ednet_header;
    #endif
        dev->rebuild_header    = ednet_rebuild_header;
    #ifdef LINUX_24
        dev->tx_timeout        = ednet_tx_timeout;
        dev->watchdog_timeo   = timeout;
    #endif
        /* We do not need the ARP protocol. */
        dev->flags           |= IFF_NOARP;
    #ifndef LINUX_20                        
        dev->hard_header_cache = NULL;      
    #endif 
    #ifdef LINUX_24                                 
        SET_MODULE_OWNER(dev);
    #endif
        dev->priv = kmalloc(sizeof(struct ednet_priv), GFP_KERNEL);
        if (dev->priv == NULL)
            return -ENOMEM;
        memset(dev->priv, 0, sizeof(struct ednet_priv));
        spin_lock_init(& ((struct ednet_priv *) dev->priv)->lock);
        return 0;
    }

    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
    图 6

    由图6我们看到,ednet_rx()并不是网络设备的一个操作,而是模块中的一个函数。在实际的网卡驱动程序中,当网卡确实接收到数据的时候,由网络中断唤醒等待接收数据的用户进程,也就是说,ednet_rx()应该由那个网络中断处理例程调用。我们这里并没有中断,所以字符设备的device_write()可以看成是一个"中断例程",也就是说,用户空间往字符写操作的时候,也就调用了网络设备的数据接收内核例程ednet_rx()了。然后ednet_rx()会把原始的数据包发送到TCP/IP上层进行处理,这一切均依赖于内核API 函数netif_rx()。ednet_rx()就需要sk_buff数据结构(<linux/skbuff.h>中定义),用来存放从网络接口接收到的原始网络数据,分配后的sk_buff结构将在TCP/IP协议栈上被释放掉。

    下面介绍一下网络设备的主要操作例程ednet_open()、ednet_release()、ednet_tx()、ednet_stats ()、ednet_change_mtu()、ednet_header()。网络设备文件操作结构struct net_device(<linux/netdevice.h>中有定义)中定义了指向以上函数的函数指针的原形:

    ednet_open:   int  (*open)(struct net_device *dev);
      ednet_release:  int  (*stop)(struct net_device *dev);
      ednet_tx:   int  (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);
    ednet_stats:   struct net_device_stats* (*get_stats)(struct net_device *dev);
    ednet_change_mtu:int  (*change_mtu)(struct net_device *dev, int new_mtu);
      ednet_header:  int  (*hard_header) (struct sk_buff *skb,
                struct net_device *dev,
                unsigned short type,
                void *daddr,
                void *saddr,
                unsigned len);

    操作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<<8)) 。

    因为伪网络接口没有使用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的程序流图如图所示。

    图 7
    图 7

    传送的frame按照SLIP定义的格式:数据的两头都是END字符(0300),如图8所示。

    图 8
    图 8

    特殊控制字符的定义如下:

    #define END              0300
    #define ESC              0333
    #define ESC_END         0334            
    #define ESC_ESC         0335

    如果打包前的数据中有END这个字符,那么使用ESC_END代替,如果发现有ESC这个字符,那么使用ESC_ESC字符替换。在Linux环境下,串口名从ttyS0开始依次是ttyS1、ttyS2等。在本程序中,使用ttyS0作为通信串口。在打开ttyS0的时候,选项O_NOCTTY 表示不能把本串口当成控制终端,否则用户的键盘输入信息将影响程序的执行; O_NDELAY表示打开串口的时候,程序并不关心另一端的串口是否在使用中。在Linux中,打开串口设备和打开普通文件一样,使用的是open()系统调用。比如我么打开串口设备1也就是COM1,只需要:

    fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY );

    打开的串口设备有很多设置选项。本文中使用int setup_com(int fd)设置。在系统头文件<termios.h>中定义了终端控制结构struct termios,tcgetattr()和tcsetattr()两个系统函数获得和设置这些属性。结构struct termios中的域描述的主要属性包括:

    c_cflag  : 控制选项
    c_lflag  : 线选项
    c_iflag  : 输入选项
    c_oflag  :输出选项
    c_cc    :控制字符
    c_ispeed :输入数据波特率
    c_ospeed :输出数据波特率

    如果要设置某个选项,那么就使用"|="运算,如果关闭某个选项就使用"&="和"~"运算。本文使用的各个选项的意义定义如下:

    c_cflag: CLOCAL 本地模式,不改变端口的所有者
             CREAD  表示使能数据接收器
             PARENB  表示偶校验
             PARODD 表示奇校验
    CSTOPB  使用两个停止位
    CSIZE    对数据的bit使用掩码
    CS8      数据宽度是8bit
    c_lflag:  ICANON 使能规范输入,否则使用原始数据(本文使用)
    ECHO    回送(echo)输入数据
    ECHOE   回送擦除字符
    ISIG      使能SIGINTR,SIGSUSP, SIGDSUSP和 SIGQUIT 信号
    c_iflag:  IXON     使能输出软件控制
             IXOFF    使能输入软件控制
             IXANY    允许任何字符再次开启数据流
             INLCR    把字符NL(0A)映射到CR(0D)
             IGNCR    忽略字符CR(0D)
           ICRNL    把CR(0D)映射成字符NR(0A)
        c_oflag: OPOST  输出后处理,如果不设置表示原始数据(本文使用原始数据) 
    c_cc[VMIN]:  最少可读数据
    c_cc[VTIME]: 等待数据时间(10秒的倍数)

    根据以上设置的定义,串口端口设置函数setup_com()定义如下:

    int setup_com(int fd){
        struct termios options; 
        tcgetattr(fd, &options);
        /* Set the baud rates to 38400...*/
        cfsetispeed(&options, B38400);
        cfsetospeed(&options, B38400);
        /* Enable the receiver and set local mode...*/
        options.c_cflag |= (CLOCAL | CREAD);
        /* Set c_cflag options.*/
        options.c_cflag |= PARENB;
        options.c_cflag &= ~PARODD;
        options.c_cflag &= ~CSTOPB;
        options.c_cflag &= ~CSIZE;
        options.c_cflag |= CS8;    
        /* Set c_iflag input options */
        options.c_iflag &=~(IXON | IXOFF | IXANY);
        options.c_iflag &=~(INLCR | IGNCR | ICRNL);
        options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
        /* Set c_oflag output options */
        options.c_oflag &= ~OPOST;   
        /* Set the timeout options */
        options.c_cc[VMIN]  = 0;
        options.c_cc[VTIME] = 10;
        tcsetattr(fd, TCSANOW, &options);
        return 1;
    }

    两个打包和拆包函数和SLIP协议定义的一样,拆包函数和打包相反,这里不列举了。

    小结

    本文描述的是一个非常简单的串口上网程序,如果需要可靠的通信,增加吞吐量,可在用户空间添加适当的网络控制协议,也可增加数据压缩算法。

    参考资料

    • 本文的所有源代码可以在 这里下载
    • 1. 《Linux Programming White Papers》,(美) David A. Rusling
    • 2. 《Linux Device Driver》,(美)Alessandro Rubini
    • 3. 《Linux Device Driver-Second edition》,(美)Alessando Rubini
    转自:http://www.ibm.com/developerworks/cn/linux/l-serialnet/index.html
    展开全文
  • Linux串口上网的简单实现 <br />级别: 初级 李素科 (lisuke@infosec.pku.edu.cn )北京大学软件学院 <br />2003 年 12 月 01 日本文主要说明某些简易 Linux 环境或者嵌入式 Linux 中实现串口上网...

    Linux串口上网的简单实现


    级别: 初级

    李素科 (lisuke@infosec.pku.edu.cn )北京大学软件学院

    2003 年 12 月 01 日

    本文主要说明某些简易 Linux 环境或者嵌入式 Linux 中实现串口上网的简单实现,这在工业控制中有着广泛的应用。希望对实现无网卡设备上网的方法有抛砖引玉的作用。

    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所示。


    图 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所示。


    图 2

    当我们需要加载模块的时候,我们只需要使用insmod命令,如果需要卸载模块,我们使用rmmod命令。比如加载ed_device模块,并且配置伪网络接口IP地址为192.168.5.1

    [root@localhost test]insmod ed_device.o,
    [root@localhost test]ifconfig ed0 192.168.5.1 up

    这时可以在/proc/net/dev 文件中看到有ed0伪网络设备了。如果需要卸载ed_device模块,应先停止其网络数据发送和接收工作,然后卸载模块:

    [root@localhost test]ifconfig ed0 down
    [root@localhost test]rmmod 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。

    #define MAJOR_NUM_REC 200
    #define MAJOR_NUM_TX 201
    #define IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int)

    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]#mknod c 201 0 /dev/ed_tx

    设备建立后,编译好的模块就可以动态加载了:

    [root@localhost]#insmod ed_device.o

    为了方便对设备编程,我们还需要一个字符设备管理的数据结构:

    struct ed_device{
    int magic;
    char name[8];
    int busy;
    unsigned char *buffer;
    #ifdef LINUX_24
    wait_queue_head_t rwait;
    #endif
    int mtu;
    spinlock_t lock;
    int data_len;
    int buffer_size;
    struct file *file;
    ssize_t (*kernel_write)(const char *buffer,size_t length,int buffer_size);
    };

    这个数据结构是用来保存字符设备的一些基本状态信 息。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)
    {
    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;
    #ifdef LINUX_24
    wake_up_interruptible(&ed[ED_TX_DEVICE].rwait);
    #endif
    return length;
    }

    字符设备的操作及其相关函数调用过程如图3 所示。


    图 3

    当ed_device模块被加载的时候,eddev_module_init()调用register_chrdev()内核API注册ed_tx和ed_rec两个字符设备。这个函数定义在<linux/fs.h>:

    int register_chdev(unsigned int major, const char *, struct fle_operations *fops)

    字符设备被注册成功后,内核把这两个字符设备加入到 内核字符设备驱动表中。内核字符设备驱动表保留指向struct file_operations的一个数据指针。用户进程调用设备读写操作时,通过这个指针访问设备的操作函数, struct file_operations中的域大部分是指向函数的函数指针,指向用户自己编写的设备操作函数。

    struct file_operations ed_ops ={
    #ifdef LINUX_24
    NULL,
    #endif
    NULL,
    device_read,
    device_write,
    NULL,
    NULL,
    device_ioctl,
    NULL,
    device_open,
    NULL,
    device_release,
    };

    注意到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 *)     
    device_release: int (*release) (struct inode *, struct file *);
    device_read: ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    device_write: ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

    操作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()即可唤醒等待进程,这段

    代码如下:

        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_t device_write(struct file *file,const char *buffer, size_t length,loff_t *offset)向设备写入数据。拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到 内核空间,如图5所示。


    图 5





    回页首


    编写伪网络设备驱动程序

    伪网络驱动程序和字符设备驱动程序一样,也必须初始化和注册。网络驱动需记录其发送和接收数据量的统计信息,所以我们定义一个记录这些信息的数据结构。

    struct ednet_priv {
    #ifdef LINUX_24
    struct net_device_stats stats;
    #else
    struct enet_statistics stats;
    #endif
    struct sk_buff *skb;
    spinlock_t lock;
    };

    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。

    #ifdef LINUX_24
    struct net_device ednet_dev;
    #else
    struct device ednet_dev;
    #endif

    伪网络驱动程序的也需要初始化和注册。和字符设备的注册不同之处是,它使用的是register_netdev(net_device *) kernel API。

    int ednet_module_init(void)
    {
    int err;
    strcpy(ednet_dev.name, "ed0");
    ednet_dev.init = ednet_init;
    if ( (err = register_netdev(&ednet_dev)) )
    printk("ednet: error %i registering pseudo network device /"%s/"/n",
    err, ednet_dev.name);

    return err;
    }

    ednet_dev的name域是接口名,ednet_module_init()中赋予网络接口的名字为ed0,如果本网络设备被加载,使用ifconfig命令可以看到ed0。

    [root@localhost pku]# /sbin/ifconfig
    ed0 Link encap:Ethernet HWaddr 00:45:44:30:30:30
    inet addr:192.168.3.9 Bcast:192.168.3.255 Mask:255.255.255.0
    UP BROADCAST RUNNING NOARP MULTICAST MTU:1500 Metric:1
    RX packets:0 errors:0 dropped:0 overruns:0 frame:0
    TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
    collisions:0 txqueuelen:100
    RX bytes:0 (0.0 b) TX bytes:0 (0.0 b)

    我们看到我们的伪网络接口没有Interrupt和 Base address,这是因为这个伪网络接口不和硬件打交道,也没有分配中断号和IO基址。否则,如果你看一个实实在在的网络接口(如下面的eth1),可以 看到它的Interrupt号是11和IO Base address是0xa000。

    eth1      Link encap:Ethernet  HWaddr 50:78:4C:43:1D:01
    inet addr:192.168.21.202 Bcast:192.168.21.255 Mask:255.255.255.0
    UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
    RX packets:356523 errors:0 dropped:0 overruns:0 frame:0
    TX packets:266 errors:0 dropped:0 overruns:0 carrier:0
    collisions:0 txqueuelen:100
    RX bytes:21542043 (20.5 Mb) TX bytes:19510 (19.0 Kb)
    Interrupt:11 Base address:0xa000

    ednet_dev的init域是一个函数指针,指 向用户定义的ednet_init()例程。ednet_init()添充net_device结构,只有ednet_init()初始化成功后,系统才 被加入到设备链表中。ednet_dev的初始化例程ednet_init()如下:

    #ifdef LINUX_24
    int ednet_init(struct net_device *dev)
    #else
    int ednet_init(struct device *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;
    #ifdef LINUX_24
    dev->hard_header = ednet_header;
    #endif
    dev->rebuild_header = ednet_rebuild_header;
    #ifdef LINUX_24
    dev->tx_timeout = ednet_tx_timeout;
    dev->watchdog_timeo = timeout;
    #endif
    /* We do not need the ARP protocol. */
    dev->flags |= IFF_NOARP;
    #ifndef LINUX_20
    dev->hard_header_cache = NULL;
    #endif
    #ifdef LINUX_24
    SET_MODULE_OWNER(dev);
    #endif
    dev->priv = kmalloc(sizeof(struct ednet_priv), GFP_KERNEL);
    if (dev->priv == NULL)
    return -ENOMEM;
    memset(dev->priv, 0, sizeof(struct ednet_priv));
    spin_lock_init(& ((struct ednet_priv *) dev->priv)->lock);
    return 0;
    }

    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

    由 图6我们看到,ednet_rx()并不是网络设备的一个操作,而是模块中的一个函数。在实际的网卡驱动程序中,当网卡确实接收到数据的时候,由网络中断 唤醒等待接收数据的用户进程,也就是说,ednet_rx()应该由那个网络中断处理例程调用。我们这里并没有中断,所以字符设备的 device_write()可以看成是一个"中断例程",也就是说,用户空间往字符写操作的时候,也就调用了网络设备的数据接收内核例程 ednet_rx()了。然后ednet_rx()会把原始的数据包发送到TCP/IP上层进行处理,这一切均依赖于内核API 函数netif_rx()。ednet_rx()就需要sk_buff数据结构(<linux/skbuff.h>中定义),用来存放从网络 接口接收到的原始网络数据,分配后的sk_buff结构将在TCP/IP协议栈上被释放掉。

    下面介绍一下网络设备的主要操作例程ednet_open()、ednet_release()、ednet_tx ()、ednet_stats ()、ednet_change_mtu()、ednet_header()。网络设备文件操作结构struct net_device(<linux/netdevice.h>中有定义)中定义了指向以上函数的函数指针的原形:

    ednet_open:   int  (*open)(struct net_device *dev);
    ednet_release: int (*stop)(struct net_device *dev);
    ednet_tx: int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);
    ednet_stats: struct net_device_stats* (*get_stats)(struct net_device *dev);
    ednet_change_mtu:int (*change_mtu)(struct net_device *dev, int new_mtu);
    ednet_header: int (*hard_header) (struct sk_buff *skb,
    struct net_device *dev,
    unsigned short type,
    void *daddr,
    void *saddr,
    unsigned len);


    操作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<<8)) 。

    因为伪网络接口没有使用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的程序流图如图所示。


    图 7

    传送的frame按照SLIP定义的格式:数据的两头都是END字符(0300),如图8所示。


    图 8

    特殊控制字符的定义如下:

    #define END              0300
    #define ESC 0333
    #define ESC_END 0334
    #define ESC_ESC 0335

    如果打包前的数据中有END这个字符,那么使用 ESC_END代替,如果发现有ESC这个字符,那么使用ESC_ESC字符替换。在Linux环境下,串口名从ttyS0开始依次是ttyS1、 ttyS2等。在本程序中,使用ttyS0作为通信串口。在打开ttyS0的时候,选项O_NOCTTY 表示不能把本串口当成控制终端,否则用户的键盘输入信息将影响程序的执行; O_NDELAY表示打开串口的时候,程序并不关心另一端的串口是否在使用中。在Linux中,打开串口设备和打开普通文件一样,使用的是open()系 统调用。比如我么打开串口设备1也就是COM1,只需要:

    fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY );

    打开的串口设备有很多设置选项。本文中使用int setup_com(int fd)设置。在系统头文件<termios.h>中定义了终端控制结构struct termios,tcgetattr()和tcsetattr()两个系统函数获得和设置这些属性。结构struct termios中的域描述的主要属性包括:

    c_cflag  : 控制选项
    c_lflag : 线选项
    c_iflag : 输入选项
    c_oflag :输出选项
    c_cc :控制字符
    c_ispeed :输入数据波特率
    c_ospeed :输出数据波特率

    如果要设置某个选项,那么就使用"|="运算,如果关闭某个选项就使用"&="和"~"运算。本文使用的各个选项的意义定义如下:

    c_cflag: CLOCAL 本地模式,不改变端口的所有者
    CREAD 表示使能数据接收器
    PARENB 表示偶校验
    PARODD 表示奇校验
    CSTOPB 使用两个停止位
    CSIZE 对数据的bit使用掩码
    CS8 数据宽度是8bit
    c_lflag: ICANON 使能规范输入,否则使用原始数据(本文使用)
    ECHO 回送(echo)输入数据
    ECHOE 回送擦除字符
    ISIG 使能SIGINTR,SIGSUSP, SIGDSUSP和 SIGQUIT 信号
    c_iflag: IXON 使能输出软件控制
    IXOFF 使能输入软件控制
    IXANY 允许任何字符再次开启数据流
    INLCR 把字符NL(0A)映射到CR(0D)
    IGNCR 忽略字符CR(0D)
    ICRNL 把CR(0D)映射成字符NR(0A)
    c_oflag: OPOST 输出后处理,如果不设置表示原始数据(本文使用原始数据)
    c_cc[VMIN]: 最少可读数据
    c_cc[VTIME]: 等待数据时间(10秒的倍数)

    根据以上设置的定义,串口端口设置函数setup_com()定义如下:

    int setup_com(int fd){
    struct termios options;
    tcgetattr(fd, &options);
    /* Set the baud rates to 38400...*/
    cfsetispeed(&options, B38400);
    cfsetospeed(&options, B38400);
    /* Enable the receiver and set local mode...*/
    options.c_cflag |= (CLOCAL | CREAD);
    /* Set c_cflag options.*/
    options.c_cflag |= PARENB;
    options.c_cflag &= ~PARODD;
    options.c_cflag &= ~CSTOPB;
    options.c_cflag &= ~CSIZE;
    options.c_cflag |= CS8;
    /* Set c_iflag input options */
    options.c_iflag &=~(IXON | IXOFF | IXANY);
    options.c_iflag &=~(INLCR | IGNCR | ICRNL);
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    /* Set c_oflag output options */
    options.c_oflag &= ~OPOST;
    /* Set the timeout options */
    options.c_cc[VMIN] = 0;
    options.c_cc[VTIME] = 10;
    tcsetattr(fd, TCSANOW, &options);
    return 1;
    }

    两个打包和拆包函数和SLIP协议定义的一样,拆包函数和打包相反,这里不列举了。





    回页首


    小结

    本文描述的是一个非常简单的串口上网程序,如果需要可靠的通信,增加吞吐量,可在用户空间添加适当的网络控制协议,也可增加数据压缩算法。



    参考资料

    • 本文的所有源代码可以在 这里 下载

    • 1. 《Linux Programming White Papers》,(美) David A. Rusling



    • 2. 《Linux Device Driver》,(美)Alessandro Rubini



    • 3. 《Linux Device Driver-Second edition》,(美)Alessando Rubini




    关于作者

    李素科,北京大学软件学院北大摩托罗拉嵌入式系统联合实验室讲师,主要研究方向是:网络与信息安全、嵌入式系统、分布式计算。现在主要从事Linux和MPC860嵌入式系统的一些教学和开发工作。可以通过电子邮件地址 lisuke@infosec.pku.edu.cn 与他联系。

    展开全文
  • LM-805xNET串口服务器是一款智能协议转换网桥,为RS232/RS485串口到TCP/IP网络之间完成数据转换的通讯接口转换器。提供RS232/RS485终端串口与TCP/IP网络的数据双向透明传输,提供串口转网络功能,RS232/RS485串口转...
  • 本文主要说明某些简易 Linux 环境或者嵌入式 Linux 中实现串口上网的简单实现,这在工业控制中有着广泛的应用。希望对实现无网卡设备上网的方法有抛砖引玉的作用
  • LM-805xNET串口服务器是一款智能协议转换网桥,为RS232/RS485串口到TCP/IP网络之间完成数据转换的通讯接口转换器。提供RS232/RS485终端串口与TCP/IP网络的数据双向透明传输,提供串口转网络功能,RS232/RS485串口转...
  • Linux为串口上网提供了 丰富的支持,比如PPP(Peer-to-Peer Protocol, 端对端协议)和SLIP(Serial Line Interface Protocol, 非常老的串行线路接口协议),这里所说的"上网"是指把串口当成一个网络接口,通过封装...
  • LM-8051NET RS232/RS485串口网桥/串口设备联网服务器,美丽小巧的流线型外壳,拿到它,还没开始使用,就让您爱不释手,LM-8051NET RS232/RS485串口网桥/串口设备联网服务器不仅仅提供优异的功能,也十分注重产品的...
  • Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-Peer Protocol, 端对端协议)和SLIP(Serial Line Interface Protocol, 非常老的串行线路接口协议)。这里所说的"上网"是指把串口当成一个网络接口,通过封装网络...
  • Linux为串口上网提供了丰富的支持,比如PPP(Peer-to-Peer Protocol, 端对端协议)和SLIP(Serial Line Interface Protocol, 非常老的串行线路接口协议)。本文引用地址:http://www.eepw.com.cn/article/150775.htm这里...
  • 遇到一个恶心的问题要处理,PDA本来有wifi,但是客户现场不允许使用wifi,...最终目的,就是让pda可以插上网线,用浏览器访问网页,(实际应用是,通过网络,访问服务器上的sqlserver数据库)。 拜托各位大神了!!!
  • LM-8051NET 是一个非常美丽小巧的流线型外壳,一端是一个标准的九线制串口插座,可以通过电缆将串口接到LM-8051NET ,更可以直接将LM-8051NET插到设备的串口插座上。 另一端是一个标准的RJ45插座,这里是插入RJ45头...
  • lwip+freeRTOS+pppos拨号上网,能够实现普通4G,2G等模块上网
  • Linux串口编程

    2012-03-18 09:36:23
    时间:2011-05-03 Linux下串口编程入门 串口简介串口操作打开串口设置串口读写串口关闭串口例子 ...Linux简单串口上网原理Linux串口上网设备加载和注销形式编写字符设备驱动程序编写伪网络设备
  • 通过命令行的方式实现,支持枚举系统可用串口,支持加时间戳,支持拨号上网
  • 在ARM-LINUX平台实现4G模块PPP拨号上网#1.解压ppp程序包,交叉编译内核配置ppp安装PPP配置串口通信测试串口拨号USB拨号USB拨号 ##串口拨号 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 662
精华内容 264
关键字:

串口上网