51
第7第 第第第第第第第第第 第第第第第第第第第第第第 Linux 第 第第第第第第第 第第第第 第第第第第第第第第第第 TCP/IP 第第 第第第第第第第第第第第第第第第第 第第第第第 Socket 第第

第 7 章 嵌入式网络程序设计

  • Upload
    mauli

  • View
    88

  • Download
    0

Embed Size (px)

DESCRIPTION

第 7 章 嵌入式网络程序设计. 本章将学习如何开发嵌入式 Linux 系统的网络应用程序。  嵌入式系统处理的主要以太网协议和 TCP/IP 协议;  以太网驱动程序设计和硬件接口结构;  嵌入式下的 Socket 编程. 7.1 嵌入式以太网基础知识. - PowerPoint PPT Presentation

Citation preview

Page 1: 第 7 章   嵌入式网络程序设计

第 7 章 嵌入式网络程序设计

本章将学习如何开发嵌入式 Linux 系统的网络应用程序。 嵌入式系统处理的主要以太网协议和 TCP/IP 协议; 以太网驱动程序设计和硬件接口结构; 嵌入式下的 Socket 编程

Page 2: 第 7 章   嵌入式网络程序设计

7.1 嵌入式以太网基础知识目前大多数嵌入式系统还处于单独应用的阶段,以 MCU 为核心,与一些监测、

伺服、指示设备配合实现一定的功能。虽然在一些工业和汽车应用中,为了实现多个 MCU 之间的信息交流,利用 CAN 、 RS232 、 RS485 等总线将 MCU 组网,但这种网络的有效半径有限,有关的通信协议也比较少,并且一般是孤立于 Internet 以外的。 Internet 现已成为社会重要的基础信息设施之一,是信息流通的重要渠道,如果嵌入式系统能够连接到 Internet 上,则可以方便、低廉地将信息传送到几乎世界上的任何一个地方。

在 Internet 的众多协议中,以太网和 TCP/IP 协议已经成为使用最广泛的协议,它的高速、可靠、分层,以及可扩充性使得它在各个领域的应用越来越灵活。

Page 3: 第 7 章   嵌入式网络程序设计

以太网技术及其嵌入式应用以太网是由 Xeros 公司开发的一种基带局域网技术,最初使用同轴电缆作为网络媒

体,采用载波多路访问和碰撞检测( CSMA/CD )机制,数据传输速率达到 10Mbps 。虽然以太网是由 Xeros 公司早在 20 世纪 70 年代最先研制成功的,但是如今以太网一词更多的被用来指各种采用 CSMA/CD 技术的局域网。以太网被设计用来满足非持续性网络数据传输的需要, IEEE 802.3 规范则是基于最初的以太网技术于 1980 年制定。以太网版本 2.0 由 Digital Equipment Corporation 、 Intel 、和 Xeros 三家公司联合开发,与 IEEE 802.3 规范相互兼容。

在 Internet 网络中,以太网可以算是应用最广泛的数据链路层协议了。现在的操作系统均能够同时支持这种类型的协议格式。以太网和 IEEE 802.3 帧的基本结构如图 7.1 所示。

Page 4: 第 7 章   嵌入式网络程序设计

同步位:由 0 和 1 间隔代码组成,可以通知目标站做好接收准备。 IEEE 802.3 帧的同步位占用 7 个字节,紧随其后的是长度为 1 个字节的帧首定界符( SOF )。由于以太网帧把 SOF包含在同步位中,因此,同步位的长度扩大为 8 个字节。

帧首定界符( SOF ): IEEE 802.3 帧中的定界字节,以两个连续的代码 1 结尾,表示一帧实际开始。

目标和源地址:表示发送和接收帧的工作站的地址,各占 6 个字节。其中目标地址可以是单址,也可以是多点传送或广播地址。

类型:存在于以太网帧中,占用 2 个字节,指定接收数据的高层协议。 长度:存在于 IEEE 802.3 帧中,表示紧随其后的以字节为单位的数据段的长

度。 数据:数据段长度不能超过 1500字节,不能低于 46字节。如果数据段长度过

小,帧的总长度无法达到 46 个字节的最小值,那么相应软件将会自动填充数据段,以确保整个帧的长度不低于 46 个字节。

帧校验序列( FSC ):该序列包含长度为 4 个字节的循环冗余校验值( CRC ),由发送设备计算产生,在接收方被重新计算,以确定帧在传送过程中是否被损坏。

同步位、帧首定界符、数据段中的填充位,以及帧校验位一般都是由网卡自动产生的,目标地址、源地址、类型和数据字段的内容则由上层的软件控制产生。

以太网采用广播机制,所有与网络连接的工作站都可以看到网络上传递的数据。通过查看包含在帧中的目标地址,确定是否进行接收或放弃。如果证明数据确实是发给自己的,工作站将会接收数据并传递给高层协议进行处理。以太网采用 CSMA/CD媒体访问机制,任何工作站都可以在任何时间访问网络。在发送数据之前,工作站首先需要侦听网络是否空闲,如果网络上没有任何数据传送,工作站就会把所要发送的信息投放到网络中。否则,工作站只能等待网络下一次出现空闲的时候再进行数据的发送。

Page 5: 第 7 章   嵌入式网络程序设计

嵌入式系统中主要处理的网络协议

Page 6: 第 7 章   嵌入式网络程序设计

1. ARP ( Address Resolution Protocol )地址解析协议网络层用 32bit 的地址来标识不同的主机,而链路层使用 48bit 的物理( MA

C )地址来标识不同的以太网或令牌环网络接口。只知道目的主机的 IP 地址并不能发送数据帧给它,必须知道目的主机网络接口的 MAC 地址才能发送数据帧。

ARP 的功能就是实现从 IP 地址到对应物理地址的转换。源主机发送一份包含目的主机 IP 地址的 ARP请求数据帧给网上的每个主机,称做 ARP 广播,目的主机的 ARP收到这份广播报文后,识别出这是发送端在询问它的 IP 地址,于是发送一个包含目的主机 IP 地址及对应的 MAC 地址的 ARP 应答给源主机。

为了加快 ARP 协议解析的数据,每台主机上都有一个 ARP 高速缓存,存放最近的 IP 地址到硬件地址之间的映射记录。这样,当在 ARP 的生存时间之内连续进行 ARP解析的时候,就不需要反复发送 ARP请求了。

2. IP ( Internet Protocol )网际协议IP 工作在网络层,是 TCP/IP 协议族中最为核心的协议,其他的协议可以利

用 IP 协议来传输数据。 TCP 和 UDP 数据都以 IP 数据包格式传输, IP 信息封装在 IP 数据包中。每一个 IP 数据包都有一个 IP 数据头,其中包括源地址和目的地址,一个数据校验和,以及其他一些有关的信息,如图 7.3 所示。 IP 数据包最长可达 65535字节,其中包含 32bit 的报头、 32bit 的源 IP 地址和 32bit 的目的 IP 地址。 IP 数据包的大小随传输介质的不同而不同,例如,以太网的数据包要大于 PPP 的数据包。

Page 7: 第 7 章   嵌入式网络程序设计

IP提供不可靠、指无连接的数据包传送服务,但却具有高效灵活的特点。不可靠的意思是指其不能保证 IP 数据包能成功地到达目的地。如果发生某种错误, IP 有一个简单的错误处理算法:丢弃该数据包,然后发送消息报给信源端。任何要求的可靠性必须由上层来提供。无连接的意思是指 IP 并不维护任何关于后续数据包的状态信息。每个数据包的处理是相互独立的, IP 数据包可以不按发送顺序接收。因为每个数据包都是独立地进行路由选择,可能选择不同的路线,所以传送所需时间有所不同。

数据的传输依据路由选择来完成,源主机 IP 接收本地 TCP 和 UDP 数据,生成 IP 数据包,如果目的主机与源主机在同一个共享网络上,那么 IP 数据包就直接送到目的主机上。否则,把数据包发往一个默认的路由器上,由路由器来转发该数据包。目的地址的主机在接收数据包后,必须再将数据装配起来,然后传送给接收的应用程序。

Page 8: 第 7 章   嵌入式网络程序设计

3. TCP ( Transfer Control Protocol )传输控制协议TCP 协议是为人所熟知的协议,是基于连接的协议,是在需要通信的两个应用

程序之间建立起一条虚拟的连接线路,而在这条线路间可能会经过很多子网、网关和路由器。 TCP 协议保证在两个应用程序之间可靠地传送和接收数据,并且可以保证没有丢失的或者重复的数据包。

TCP 协议把发送方应用程序提交的数据分成合适的小块,并添加附加信息,包括顺序号,源和目的端口、控制、纠错信息等字段,称为 TCP 数据包,并将 TCP数据包交给下面的网络层处理。接受方确认接收到的 TCP 数据包,重组并将数据送往高层。当 TCP 协议使用 IP 协议传送它自己的数据包时, IP 数据包中的数据就是 TC

P 数据包本身。相互通信的主机中的 IP 协议层负责传送和接收 IP 数据包。每一个IP 数据头中都包括一个字节的协议标志符。当 TCP 协议请求 IP 协议层传送一个 IP 数据包时, IP 数据头中的协议标志符指明其中的数据包是一个 TCP 数据包。当应用程序使用 TCP/IP 通信时,它们不仅要指明目标计算机的 IP 地址,还要指明应用程序使用的端口地址。端口地址可以惟一地表示一个应用程序,标准的网络应用程序使用标准的端口地址,例如, Web 服务器使用端口 80 。已经登记的端口地址可以在 /etc/services 中查看。

Page 9: 第 7 章   嵌入式网络程序设计

4. UDP ( User Datagram Protocol )用户数据包协议UDP 协议是一种无连接、不可靠的传输层协议。使用该协议只是把应用程序传来的数

据加上 UDP头包括端口号、段长等字段,作为 UDP 数据包发送出去,但是并不保证数据包能到达目的地,其可靠性由应用层来提供。就像发送一封写有地址的一般信件,却不保证信件能到达一样。因为协议开销少,与 TCP 协议相比, UDP 更适用于应用在低端的嵌入式领域中。很多场合中,如网络管理 SNMP 、域名解析 DNS 、简单文件传输协议 TFTP ,大都使用 UDP 协议。

UDP具有 TCP 所望尘莫及的速度优势。虽然 TCP 协议中植入了各种安全保障功能,但是在实际执行的过程中会占用大量的系统开销,这无疑使速度受到严重的影响。而 UDP 将安全和排序等功能移交给上层应用来完成,极大地降低了执行时间,使速度得到了保证。

UDP 协议的最早规范于 1980 年发布,尽管时间已经很长,但是 UDP 协议仍然继续在主流应用中发挥着作用,包括视频电话会议系统在内的许多应用都证明了 UDP 协议的存在价值。因为相对于可靠性来说,这些应用更加注重实际性能,所以为了获得更好的使用效果(例如,更高的画面帧刷新速率)往往可以牺牲一定的可靠性(例如,会面质量)。这就是 UDP 和 TCP两种协议的权衡之处。根据不同的环境和特点,两种传输协议都将在今后的网络世界中发挥更加重要的作用。

图 7.3 所示为网络协议示意图。IP 协议层也可以使用不同的物理介质来传送 IP 数据包到其他的 IP 地址主机。这些介

质可以自己添加协议头。例如,以太网协议层、 PPP 协议层或者 SLIP 协议层。由于以太网可以同时连接很多个主机,每一个主机上都有一个惟一的以太网的地址,并且保存在以太网卡中。所以在以太网上传输 IP 数据包时,必须将 IP 数据包中的 IP 地址转换成主机的以太网卡中的物理地址。 Linux 系统使用 ARP 协议把 IP 地址翻译成主机以太网卡中的物理地址。希望把 IP 地址翻译成硬件地址的主机使用广播地址向网络中的所有节点发送一个包括 IP 地址的 ARP请求数据包。拥有此 IP 地址的目的计算机接收到请求以后,返回一个包括其物理地址的 ARP 应答。

Page 10: 第 7 章   嵌入式网络程序设计

7.2 以太网接口设计

Page 11: 第 7 章   嵌入式网络程序设计

网络设备驱动程序基本结构 从整体角度考虑, Linux 网络系统可以分为硬件层、设备驱动层、网络协议层

和应用层。其中,网络协议层得到的数据包通过设备驱动的发送函数被发送到具体的通信设备上,通信设备传来的数据也在设备驱动程序的接收函数中被解析,并组成相应的数据包传给网络协议层。要实现一个网络设备驱动程序的主要工作只是根据具体的硬件设备向它的高层提供服务而已,这与字符设备、块设备的思路都是一样的。整个网络设备驱动程序体系结构如图 7.4 所示。

在 Linux 中,整个网络接口驱动程序的框架可分为四层,从上到下分别为协议接口层、网络设备接口层、提供实际功能的设备驱动功能层,以及网络设备和网络媒介层。这个框架在内核网络模块中已经搭建好了,在设计网络驱动程序时,要做的主要工作就是根据上层网络设备接口层定义的 device 结构和底层具体的硬件特性,完成设备驱动的功能。

在 Linux 中,网络设备接口层是对所有网络设备的一个抽象,其提供了对所有网络设备的操作集合。所有对网络硬件的访问都是通过这一接口进行的,接口为所有类型的硬件提供了一个一致化的操作集合。任意一个网络接口均可看成一个发送和接收数据包的实体。在 Linux 中这个统一的接口就是数据结构 device 。一个设备就是一个对象,即 device 结构,其内部有自己的数据和方法。每一个设备的方法被调用时的第一个参数都是这个设备对象本身。这样,这个方法就可以存取自身的数据,类似面向对象程序设计时的 this引用。

Page 12: 第 7 章   嵌入式网络程序设计

1.网络驱动程序的基本方法一个网络设备最基本的方法有初始化、发送和接收。初始化程序完成硬件的初始化、

数据结构 device 中变量的初始化和系统资源的申请。发送程序是在驱动程序的上层协议层有数据要发送时自动调用的。一般驱动程序中不对发送数据进行缓存,而是直接使用硬件的发送功能把数据发送出去。接收数据一般是通过硬件中断来通知的。在中断处理程序里,把硬件帧信息填入一个 sk_buff 结构中,然后调用 netif_rx() 传递给上层处理。

( 1 )初始化( initialize )。驱动程序必须有一个初始化方法,在把驱动程序载入系统的时候会调用这个初始化

程序。可以完成以下几方面的工作: 检测设备,在初始化程序里可以根据硬件的特征检查硬件是否存在,然后决定

是否启动这个驱动程序。 配置和初始化硬件,在初始化程序里可以完成对硬件资源的配置,比如,即插

即用的硬件就可以在这个时候进行配置。 申请这些资源,配置或协商好硬件占用的资源以后,可以向系统申请这些资源。有些资源是可以和别的设备共享的,如中断。有些是不能共享的,如 IO 、 DMA 。

接下来要初始化 device 结构中的变量。最后,可以让硬件正式开始工作。( 2 )打开( open )。open函数在网络设备驱动程序中,在网络设备激活的时候被调用,即设备状态由 d

own转变为 up 。实际上,很多在初始化中的工作可以在这里做。比如,资源的申请、硬件的激活。如果 dev- >open返回非 0 ( error ),则硬件的状态还是 down 。 open 方法的另一个作用是,如果驱动程序作为一个模块被装入,则要防止模块卸载时设备处于打开状态。在 open 方法里要调用 MOD_INC_USE_COUNT宏。

Page 13: 第 7 章   嵌入式网络程序设计

( 3 )关闭( stop )。close 方法完成和 open 相反的工作,它可以释放某些资源以减少系统负担。

close 是在设备状态由 up转为 down 时被调用的。另外,如果是作为模块装入的驱动程序, close里应该调用 MOD_DEC_USE_COUNT ,减少设备被引用的次数,以使驱动程序可以被卸载。除此之外, close 方法必须返回成功( 0==success )。

( 4 )发送( hard_start_xmit )。所有的网络设备驱动程序都必须有这个发送方法。在系统调用驱动程序的

xmit 时,发送的数据放在一个 sk_buff 结构中。一般的驱动程序把数据传给硬件发出去,也有一些特殊的设备,比如, loopback把数据组成一个接收数据再回送给系统,或者 dummy 设备直接丢弃数据。

如果发送成功, hard_start_xmit 方法里释放 sk_buff ,返回 0 (发送成功)。如果设备暂时无法处理,比如,硬件忙,则返回 1 。这时如果 dev->tbusy置为非 0 ,则系统认为硬件忙,要等到 dev->tbusy置 0 以后才会再次发送。 tbusy 的置 0 任务一般由中断完成。硬件在发送结束后产生中断,同时可以把 tbusy置 0 ,然后用 mark_bh()调用通知系统可以再次发送。在发送不成功的情况下,也可以不置 dev->tbusy 为非 0 ,此时系统会不断尝试重发。可见,如果 hard_start_xmit 发送不成功,则不要释放 sk_buff 。

由于从上层传送下来的 sk_buff 中的数据已经包含硬件需要的帧头。所以在发送方法里不需要再填充硬件帧头,数据可以直接提交给硬件发送。 sk_buff 是被锁住的( locked ),确保不会被其他程序存取。

Page 14: 第 7 章   嵌入式网络程序设计

( 5 )接收( reception )。一般设备收到数据后都会产生一个中断,在中断处理程序中驱动程序申请一块 sk_buff

( skb ),从硬件读出数据放置到申请好的缓冲区里,接下来填充 sk_buff 中的一些信息。skb->dev = dev ,判断收到帧的协议类型,填入 skb->protocol (多协议的支持)。把指针 skb->mac.raw 指向硬件数据然后丢弃硬件帧头( skb_pull )。还要设置 skb->pkt_type ,标明第二层(链路层)数据类型。可以是以下类型:

PACKET_BROADCAST :链路层广播; PACKET_MULTICAST :链路层组播; PACKET_SELF :发给自己的帧; PACKET_OTHERHOST :发给别人的帧,监听模式时会有这种帧。最后调用 netif_rx()把数据传送给协议层。 netif_rx()里数据放入处理队列然后返回,

真正的处理是在中断返回以后,这样可以减少中断时间。( 6 )硬件帧头( hard_header )。硬件一般都会在上层数据发送之前加上其硬件帧头,比如,以太网就有 14字节的帧头。

这个帧头是加在上层 ip 、 ipx 等数据包的前面的。驱动程序提供一个 hard_header 方法,协议层( ip 、 ipx 、 arp 等)在发送数据之前会调用这段程序。硬件帧头的长度必须填在dev->hard_header_len ,这样,协议层会在数据之前保留好硬件帧头的空间。这样, hard_header 程序只要调用 skb_push 然后正确填入硬件帧头就可以了。

在协议层调用 hard_header 时,传送 6 个参数:数据的 sk_buff 、 device 指针、 protocol 、 daddr 、 saddr 和 len 。数据长度不使用 sk_buff 中的参数,因为调用 hard_header 时数据可能还没完全组织好。 saddr 是 NULL 的话是使用默认地址( default )。 daddr是 NULL表明协议层不知道硬件目的地址。如果 hard_header完全填好了硬件帧头,则返回添加的字节数。如果硬件帧头中的信息还不完全,则返回负字节数。 hard_header返回负数的情况下,协议层会做进一步的 build header 的工作。

Page 15: 第 7 章   嵌入式网络程序设计

( 7 )地址解析( xarp )。有些网络有硬件地址,并且在发送硬件帧时需要知道目的硬件地址。这样就需

要上层协议地址( ip 、 ipx )和硬件地址的对应,这个对应是通过地址解析完成的。需要处理 arp 的设备在发送之前会调用驱动程序的 rebuild_header 。调用的主要参数包括指向硬件帧头的指针和协议层地址。如果驱动程序能够解析硬件地址,就返回 1 ,如果不能,返回 0 。对 rebuild_header 的调用在 net/core/dev.c 的 do_dev_queue_xmit()里。( 8 )参数设置和统计数据。在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般

只有超级用户( root )权限才能对设备参数进行设置。设置方法有: dev->set_mac_address() ,当用户调用 ioctl 类型为 SIOCSIFHWADDR 时

是要设置这个设备的 mac 地址。一般对mac 地址的设置没有太大意义的。 dev->set_config() ,当用户调用 ioctl 时类型为 SIOCSIFMAP 时,系统会调

用驱动程序的 set_config 方法。用户会传递一个 ifmap 结构,其包含需要的 I/O 、中断等参数。

dev->do_ioctl() ,如果用户调用 ioctl 时类型在 SIOCDEVPRIVATE 和 SIOCDEVPRIVATE +15 之间,系统会调用驱动程序的这个方法。一般是设置设备的专用数据。读取信息也是通过 ioctl调用进行。除此之外,驱动程序还可以提供一个 dev->g

et_stats 方法,返回一个 enet_statistics 结构,包含发送接收的统计信息。ioctl 的处理在 net/core/dev.c 的 dev_ioctl() 和 dev_ifsioc() 中。

Page 16: 第 7 章   嵌入式网络程序设计

2.数据结构在网络驱动程序部分主要有两个数据结构,一个是网络设备数据结构 device ,

另一个是套接安缓冲区 sk_buff , TCP/IP 中不同协议层间,以及与网络驱动程序之间数据包的传递都是通过这个 sk_buff 结构体来完成的,这个结构体主要包括传输层、网络层、连接层需要的变量,决定数据区位置和大小的指针,以及发送接收数据包所用到的具体设备信息等。根据网络应用的特点,对链表的操作主要是删除链表头的元素和添加元素到链表尾。该数据结构定义在 /include/linux/skbuff.h 中。

struct sk_buff {struct sk_buff* next; /* next 指向 sk_buff双向链表的后缓冲区结点 */struct sk_buff* prev; /* Prev 指向前一个缓冲区结点 */struct sk_buff_head * list;struct sock *sk; /* 拥有的 Socket套接口 */struct timevalstamp; /* 到达时间 */struct net_device *dev; /* 涉及的设备 *//* 传输层数据包头 */union{

struct tcphdr *th;struct udphdr *uh;struct icmphdr *icmph;struct igmphdr *igmph;struct iphdr *ipiph;struct spxhdr *spxh;unsigned char *raw;

} h;

Page 17: 第 7 章   嵌入式网络程序设计

Union{struct iphdr *iph; /* 网络层数据包头 */struct ipv6hdr *ipv6h;struct arphdr *arph;struct ipxhdr *ipxh;unsigned char *raw;

} nh;union /* 链路层数据包头 */{ struct ethhdr *ethernet; unsigned char *raw;} mac;struct dst_entry *dst; /* 发送地址 */char cb[48]; /* 一般存放每层的控制指令和控制数据 */unsigned int len; /* 数据包长度 */unsigned int data_len;unsigned int csum; /* 校验和 */unsigned char __unused, /* 坏死区域,可以重新使用 */

cloned, pkt_type, ip_summed;

__u32 priority;atomic_t users;unsigned short protocol; /* 包协议 */unsigned short security; /* 包的安全等级 */unsigned int truesize; /* 缓冲区大小 */unsigned char *head; /* 缓冲区头 */unsigned char *data; /* 数据头指针 */unsigned char *tail; /* 数据尾指针 */unsigned char *end; /* 结束指针(地址) */void (*destructor)(struct sk_buff *); /*销毁器功能 */

#ifdef CONFIG_NETFILTER unsigned long nfmark;

__u32 nfcache;struct nf_ct_info *nfct;

#ifdef CONFIG_NETFILTER_DEBUG unsigned int nf_debug;#endif#endif /*CONFIG_NETFILTER*/#if defined(CONFIG_HIPPI)

union{__u32 ifield;

} private;#endif#ifdef CONFIG_NET_SCHED __u32 tc_index; /* 传输控制索引 */#endif};

Page 18: 第 7 章   嵌入式网络程序设计

通过以下方法可以控制 sk_buff : alloc_skb()申请一个 sk_buff 并对它初始化。返回的是申请到的 sk_buff 。 dev_alloc_skb() 类似 alloc_skb ,在申请好缓冲区后,保留 16字节的帧头空间。主要用在 Ethernet 驱动程序。 kfree_skb()释放一个 sk_buff 。 skb_clone()复制一个 sk_buff ,但不复制数据部分。 skb_copy()完全复制一个 sk_buff 。 skb_dequeue()从一个 sk_buff 链表里取出第一个元素。返回取出的 sk_buff ,如果链表空,则返回 NULL 。这是常用的一个操作。 skb_queue_head() 在一个 sk_buff 链表头放入一个元素。 skb_queue_tail() 在一个 sk_buff 链表尾放入一个元素。网络数据的处理主要是对一个先进先出队列的管理, skb_queue_tail() 和 skb_dequeue()完成这个工作。 skb_insert() 在链表的某个元素前插入一个元素。 skb_append() 在链表的某个元素后插入一个元素。一些协议(如 TCP )对没按顺序到达的数据进行重组时用到 skb_insert() 和 skb_append() 。 skb_reserve() 在一个申请好的 sk_buff 的缓冲区里保留一块空间。这个空间一般是用做下一层协议的头空间的。 skb_put() 在一个申请好的 sk_buff 的缓冲区里为数据保留一块空间。在 alloc_skb 以后,申请到的 sk_buff 的缓冲区都是处于空( free )状态,有一个 tail 指针指向 free空间,实际上开始时 tail就指向缓冲区头。 skb_reserve() 在 free空间里申请协议头空间, skb_put()申请数据空间。 skb_push()把 sk_buff缓冲区里数据空间往前移,即把 Head room 中的空间移一部分到 Data area 。 skb_pull()把 sk_buff缓冲区里 Data area 中的空间移一部分到 Head room 中。sk_buff 的控制方法都很短小以尽量减少系统负荷。

Page 19: 第 7 章   嵌入式网络程序设计

网络设备数据结构 device 是整个网络体系的中枢,定义了很多供系统访问和协议层调用的设备标准的方法,包括供设备初始化和向系统注册用的 init函数,打开和关闭网络设备的 open函数和 stop函数,处理数据包发送的 hard_start_xmit函数,以及中断处理函数,接口状态统计函数等。它的定义在 /include/linux/netdevice.h 中。数据结构 device 操作的数据对象——数据包是通过数据结构 sk_buff 来封装的。

struct net_device{char name[IFNAMSIZ];/* 网络设备特殊文件,每一个名字表示其备类型,以太网设备编号为 /dev/eth0 、 /dev/eth1 等。 */

unsigned long rmem_end; /* rmem 域段表示接收内存地址 */unsigned long rmem_start;unsigned long mem_end; /* mem 域段标示发送内存地址 */unsigned long mem_start;unsigned long base_addr; /* 设备 I/O 地址 */unsigned int irq; /* 设备 IRQ号 */unsigned char if_port; /* 选用 AUI, TP,..*/unsigned char dma; /* DMA 通道 */unsigned long state;struct net_device *next; /* 指向下一个网络设备 */int (*init)(struct net_device *dev); /* 初始化 */struct net_device *next_sched; /* 下一个调度 */int ifindex;int iflink;struct net_device_stats* (*get_stats)(struct net_device *dev);struct iw_statistics* (*get_wireless_stats)(struct net_device *dev);unsigned long trans_start; /* 记录最后一次发送成功的时间 */unsigned long last_rx; /* 记录最后一次接收成功的时间 */unsigned short flags; /* 接口标示 */unsigned short gflags;unsigned short priv_flags; unsigned short unused_alignment_fixer; unsigned mtu;unsigned short type; /* 物理硬件类型 */unsigned short hard_header_len;/* 指向驱动程序自己定义的一些参数 */void *priv; /* 私有区间指针 */struct net_device *master; /*

Page 20: 第 7 章   嵌入式网络程序设计

/* 接口地址信息 */unsigned char broadcast[MAX_ADDR_LEN]; /* 广播网地址 */unsigned char dev_addr[MAX_ADDR_LEN]; /* 硬件地址 */unsigned char addr_len; /* 地址长度 */struct dev_mc_list *mc_list; /* mac 地址列表 */int mc_count; /* 安装的网卡数目 */int promiscuity;int allmulti;int watchdog_timeo; /* 看门狗 */struct timer_list watchdog_timer; /* 定时链表 *//* 协议指针 */void *atalk_ptr; /* AppleTalk 链 */void *ip_ptr; /* IPv4专有数据 */void *dn_ptr; /* DECnet专有数据 */void *ip6_ptr; /* IPv6专有数据 */void *ec_ptr; /* Econet专有数据 */struct Qdisc *qdisc;struct Qdisc *qdisc_sleeping;struct Qdisc *qdisc_list;struct Qdisc *qdisc_ingress;unsigned long tx_queue_len;/* Max frames per queue allowed *//* 传输函数指针同步器 */spinlock_t xmit_lock;int xmit_lock_owner;spinlock_t queue_lock;atomic_t refcnt;int deadbeaf;int features;#define NETIF_F_SG 1 /* Scatter/gather IO.*/#define NETIF_F_IP_CSUM 2 /* IPv4 上 TCP/UDP 的检错 */#define NETIF_F_NO_CSUM 4 /* 回环时不检错 */#define NETIF_F_HW_CSUM 8 /* 检查所有包 */#define NETIF_F_DYNALLOC 16#define NETIF_F_HIGHDMA 32#define NETIF_F_FRAGLIST 64void (*uninit)(struct net_device *dev); /*释放网络设备时执行 */void (*destructor)(struct net_device *dev); /*撤销器 */int (*open)(struct net_device *dev); /*打开网络接口 */int (*stop)(struct net_device *dev); /*停止网络接口 */int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);

Page 21: 第 7 章   嵌入式网络程序设计

/* 硬件帧头 */int (*hard_header) (struct sk_buff *skb,

struct net_device *dev,unsigned short type,void *daddr,void *saddr,unsigned len);

int (*rebuild_header)(struct sk_buff *skb);/* 重建帧头 */#define HAVE_MULTICAST /* 设置多点传输地址链表 */void (*set_multicast_list)(struct net_device *dev);#define HAVE_SET_MAC_ADDR /* 设置设备 mac 地址 */int (*set_mac_address)(struct net_device *dev,void *addr);#define HAVE_PRIVATE_IOCTL /**/int (*do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd);#define HAVE_SET_CONFIG /**/int (*set_config)(struct net_device *dev,struct ifmap *map);#define HAVE_HEADER_CACHE /**/Int (*hard_header_cache)(struct neighbour *neigh,struct hh_cache *hh);void (*header_cache_update)(struct hh_cache *hh,

struct net_device *dev, unsigned char * haddr);

#define HAVE_CHANGE_MTU /**/int (*change_mtu)(struct net_device *dev, int new_mtu);#define HAVE_TX_TIMEOUT /* 传输超时 */void (*tx_timeout) (struct net_device *dev);int (*hard_header_parse)(struct sk_buff *skb,unsigned char *haddr);int (*neigh_setup)(struct net_device *dev, struct neigh_parms *);int (*accept_fastpath)(struct net_device *, struct dst_entry*);struct module *owner; /* 打开、释放、使用时生成 */struct net_bridge_port *br_port; /* 网桥接口 */#ifdef CONFIG_NET_FASTROUTE#define NETDEV_FASTROUTE_HMASK 0xF/* 半私有数据,保存在设备结构尾部 */rwlock_t fastpath_lock;struct dst_entry *fastpath[NETDEV_FASTROUTE_HMASK+1];#endif#ifdef CONFIG_NET_DIVERT/* 每个类型接口初始化时调用 */struct divert_blk *divert;#endif /* CONFIG_NET_DIVERT */

};

Page 22: 第 7 章   嵌入式网络程序设计

基于 CS8900A 的以太网接口设计

Page 23: 第 7 章   嵌入式网络程序设计

CS8900A 有两种工作模式: MEMORY MODE 和 I/O MODE 。其中在 MEMORY MODE 下编程操作较为简单,对任何寄存器都是直接操作,不过这需要硬件上多根地址线和网卡相连。 I/O MODE 则较为麻烦,因为这种模式下对任何寄存器操作均要通过 I/O端口0写入或读出,但这种模式在硬件上实现比较方便,而且这也是芯片的默认模式。在 I/O模式下, PacketPage存储器被映射到 CPU 的 8 个 16 位的 I/O端口上。在芯片被加电后,I/O 基地址的默认值被置为 300H 。这 8 个 16 位 I/O端口详细的功能和偏移地址如表 7.1所示。

Page 24: 第 7 章   嵌入式网络程序设计
Page 25: 第 7 章   嵌入式网络程序设计

主要工作寄存器(寄存器后括号内的数字为寄存器地址相对基址 300H 的偏移量)。 LINECTL ( 0112H ): LINECTL决定 CS8900 的基本配置和物理接口。 RXCTL ( 0104H ): RXCTL控制 CS8900 接收特定数据报。设置 RXTCL 的初始值为 0d05H ,接收网络上的广播或者目标地址同本地物理地址相同的正确数据报。 RXCFG ( 0102H ): RXCFG控制 CS8900 接收到特定数据报后会引发接收中断。RXCFG 可设置为 0103H ,这样,当收到一个正确的数据报后, CS8900 会产生一个接收中断。 BUSCT ( 0116H ): BUSCT 可控制芯片的 I/O 接口的一些操作。设置初始值为8017H ,打开 CS8900 的中断总控制位。 ISQ ( 0120H ): ISQ 是网卡芯片的中断状态寄存器,内部映射接收中断状态寄存器和发送中断状态寄存器的内容。 PORT0 ( 0000H ):发送和接收数据时, CPU 通过 PORT0 传递数据。 TXCMD ( 0004H ):发送控制寄存器,如果写入数据 00C0H ,那么网卡芯片在全部数据写入后开始发送数据。 TXLENG ( 0006H ):发送数据长度寄存器,发送数据时,首先写入发送数据长度,然后将数据通过 PORT0写入芯片。系统工作时,应首先对网卡芯片进行初始化,即写寄存器 LINECTL 、 RXCTL 、 RCCFG 、 BUSCT 。发数据时,写控制寄存器 TXCMD ,并将发送数据长度写入 TXLENG ,然后将数据依次写入 PORT0 口,如将第一个字节写入 300H ,第二个字节写入 301H ,第三个字节写入 300H ,依此类推。网卡芯片将数据组织为链路层类型并添加填充位和CRC校验送到网络。同样,处理器查询 ISO 的数据,当有数据来到后,读取接收到的数据帧。读数据时,处理器依次读地址 300H , 301H , 300H , 301H ,等等。

Page 26: 第 7 章   嵌入式网络程序设计

网络驱动程序实例 1.初始化函数初始化函数完成设备的初始化功能,由数据结构 device 中的 init函数指针来调用。加

载网络驱动模块后,就会调用初始化过程。首先通过检测物理设备的硬件特征来检测网络物理设备是否存在,之后配置设备所需要的资源。比如,中断。这些配置完成之后就要构造设备的数据结构 device ,用检测到的数据初始化 device 中的相关变量,最后向 Linux内核中注册该设备并申请内存空间。函数定义为:

static int __init init_cs8900a_s3c2410(void) { struct net_local *lp; int ret = 0; dev_cs89x0.irq = irq; dev_cs89x0.base_addr = io; dev_cs89x0.init = cs89x0_probe; dev_cs89x0.priv = kmalloc(sizeof(struct net_local), GFP_KERNEL); if (dev_cs89x0.priv == 0) { printk(KERN_ERR "cs89x0.c: Out of memory.\n"); return -ENOMEM; } memset(dev_cs89x0.priv, 0, sizeof(struct net_local)); lp = (struct net_local *)dev_cs89x0.priv; request_region(dev_cs89x0.base_addr, NETCARD_IO_EXTENT, "cs8900a"); spin_lock_init(&lp->lock); /* boy, they'd better get these right */ if (!strcmp(media, "rj45")) lp->adapter_cnf = A_CNF_MEDIA_10B_T | A_CNF_10B_T; else if (!strcmp(media, "aui")) lp->adapter_cnf = A_CNF_MEDIA_AUI | A_CNF_AUI; else if (!strcmp(media, "bnc")) lp->adapter_cnf = A_CNF_MEDIA_10B_2 | A_CNF_10B_2; else lp->adapter_cnf = A_CNF_MEDIA_10B_T | A_CNF_10B_T;

if (duplex==1) lp->auto_neg_cnf = AUTO_NEG_ENABLE; if (io == 0) { printk(KERN_ERR "cs89x0.c: Module autoprobing not allowed.\n"); printk(KERN_ERR "cs89x0.c: Append io=0xNNN\n"); ret = -EPERM; goto out; } if (register_netdev(&dev_cs89x0) != 0) { printk(KERN_ERR "cs89x0.c: No card found at 0x%x\n", io); ret = -ENXIO; goto out; }out: if (ret) kfree(dev_cs89x0.priv); return ret;}

Page 27: 第 7 章   嵌入式网络程序设计

在这个网络设备驱动程序中,设备的数据结构 device就是 dev_cs89x0 。探测网络物理设备是否存在,利用 cs89x0_probe函数实现,通过调用 register_netdrv ( struct net_device*dev )函数进行注册。

与 init函数相对应的 cleanup函数在模块卸载时运行,主要完成资源的释放工作,如取消设备注册、释放内存、释放端口等。函数定义为:

static void __exit cleanup_cs8900a_s3c2410(void) { if (dev_cs89x0.priv != NULL) { /* Free up the private structure, or leak memory :-) */ unregister_netdev(&dev_cs89x0); outw(PP_ChipID, dev_cs89x0.base_addr + ADD_PORT); kfree(dev_cs89x0.priv); dev_cs89x0.priv = NULL; /* gets re-allocated by cs89x0_probe1 */ /* If we don't do this, we can't re-insmod it later. */ release_region(dev_cs89x0.base_addr, NETCARD_IO_EXTENT); }}并在其中调用取消网络设备注册的函数 :unregister_netdrv ( struct net_device*dev )。

Page 28: 第 7 章   嵌入式网络程序设计

2.打开函数打开函数在网络设备驱动程序中是在网络设备被激活时调用,即设备状态由 d

own至 up 。函数定义为:static int net_open(struct net_device *dev){ struct net_local *lp = (struct net_local *)dev->priv; int ret; writereg(dev, PP_BusCTL, readreg(dev, PP_BusCTL) & ~ENABLE_IRQ); ret = request_irq(dev->irq, &net_interrupt, SA_SHIRQ, "cs89x0", dev); if (ret) { printk("%s: request_irq(%d) failed\n", dev->name, dev->irq); goto bad_out; } if (lp->chip_type == CS8900) writereg(dev, PP_CS8900_ISAINT, 0); else writereg(dev, PP_CS8920_ISAINT, 0); writereg(dev, PP_BusCTL, MEMORY_ON); lp->linectl = 0; writereg(dev, PP_LineCTL,

readreg(dev, PP_LineCTL) | SERIAL_RX_ON | SERIAL_TX_ON); lp->rx_mode = 0; writereg(dev, PP_RxCTL, DEF_RX_ACCEPT); lp->curr_rx_cfg = RX_OK_ENBL | RX_CRC_ERROR_ENBL; if (lp->isa_config & STREAM_TRANSFER) lp->curr_rx_cfg |= RX_STREAM_ENBL; writereg(dev, PP_RxCFG, lp->curr_rx_cfg); writereg(dev, PP_TxCFG,

TX_LOST_CRS_ENBL | TX_SQE_ERROR_ENBL | TX_OK_ENBL | TX_LATE_COL_ENBL | TX_JBR_ENBL | TX_ANY_COL_ENBL | TX_16_COL_ENBL);

writereg(dev, PP_BufCFG, READY_FOR_TX_ENBL | RX_MISS_COUNT_OVRFLOW_ENBL | TX_COL_COUNT_OVRFLOW_ENBL | TX_UNDERRUN_ENBL);

writereg(dev, PP_BusCTL, readreg(dev, PP_BusCTL) | ENABLE_IRQ); enable_irq(dev->irq); netif_start_queue(dev); DPRINTK(1, "cs89x0: net_open() succeeded\n"); return 0; bad_out: return ret;}

Page 29: 第 7 章   嵌入式网络程序设计

打开函数中对寄存器操作使用了两个函数: readreg 和 writereg 。 readreg函数用来读取寄存器内容, writereg函数用来写寄存器。函数定义为:

inline int readreg(struct net_device *dev, int portno) {outw(portno, dev->base_addr + ADD_PORT);return inw(dev->base_addr + DATA_PORT);

}inline void writereg(struct net_device *dev, int portno, int value) {

outw(portno, dev->base_addr + ADD_PORT);outw(value, dev->base_addr + DATA_PORT);

}3.关闭函数关闭函数释放资源减少系统负担,设备状态有 up转为 down 时被调用。函数定义为:static int net_close(struct net_device *dev){

netif_stop_queue(dev);

writereg(dev, PP_RxCFG, 0);writereg(dev, PP_TxCFG, 0);writereg(dev, PP_BufCFG, 0);writereg(dev, PP_BusCTL, 0);free_irq(dev->irq, dev);/* Update the statistics here. */return 0;

}

Page 30: 第 7 章   嵌入式网络程序设计

4.发送函数首先,在网络设备驱动加载时,通过 device 域中的 init函数指针调用网络设备的初始

化函数对设备进行初始化,如果操作成功,就可以通过 device 域中的 open函数指针调用网络设备的打开函数打开设备,再通过 device 域中的包头函数指针 hard_header 来建立硬件包头信息。最后,通过协议接口层函数 dev_queue_xmit调用 device 域中的 hard_start_xmit函数指针来完成数据包的发送。

如果发送成功, hard_start_xmit释放 sk_buff ,返回 0 。如果设备暂时无法处理,比如,硬件忙,则返回 l 。此时如果 dev->tbusy置为非 0 ,则系统认为硬件忙,要等到 dev->tbusy置 0 以后才会再次发送。 tbusy 的置 0 任务一般由中断完成。硬件在发送结束会产生中断,这时可以把 tbusy置 0 ,然后用 mark_bh()调用通知系统可以再次发送。

在 CS8900A 驱动程序中,网络设备的传输函数 dev->hard_start__xmit 定义为 net_send_ packet :static int net_send_packet(struct sk_buff *skb, struct net_device *dev){ struct net_local *lp = (struct net_local *)dev->priv; writereg(dev, PP_BusCTL, 0x0); writereg(dev, PP_BusCTL, readreg(dev, PP_BusCTL) | ENABLE_IRQ); DPRINTK(3, "%s: sent %d byte packet of type %x\n",

dev->name, skb->len, (skb->data[ETH_ALEN+ETH_ALEN] << 8) | (skb->data[ETH_ALEN+ETH_ALEN+1]));

spin_lock_irq(&lp->lock); netif_stop_queue(dev); /* initiate a transmit sequence */ writeword(dev, TX_CMD_PORT, lp->send_cmd); writeword(dev, TX_LEN_PORT, skb->len); /* Test to see if the chip has allocated memory for the packet */ if ((readreg(dev, PP_BusST) & READY_FOR_TX_NOW) == 0) { spin_unlock_irq(&lp->lock); DPRINTK(1, "cs89x0: Tx buffer not free!\n"); return 1; } /* Write the contents of the packet */ writeblock(dev, skb->data, skb->len); spin_unlock_irq(&lp->lock); dev->trans_start = jiffies; dev_kfree_skb (skb); return 0;}

Page 31: 第 7 章   嵌入式网络程序设计

5.中断处理和接收函数网络设备接收数据通过中断实现,当数据收到后,产生中断,在中断处理程序中驱

动程序申请一块 sk_buff ( skb ),从硬件读出数据放置到申请好的缓冲区里。接下来,填充 sk_buff 中的一些信息。处理完后,如果是获得数据包,则执行数据接收子程序,该函数被中断服务程序调用。函数定义:

static void net_rx(struct net_device *dev) { struct net_local *lp = (struct net_local *)dev->priv; struct sk_buff *skb; int status, length; int ioaddr = dev->base_addr; status = inw(ioaddr + RX_FRAME_PORT); if ((status & RX_OK) == 0) { count_rx_errors(status, lp); return; } length = inw(ioaddr + RX_FRAME_PORT); /* Malloc up new buffer. */ skb = dev_alloc_skb(length + 2); if (skb == NULL) { lp->stats.rx_dropped++; return; } skb_reserve(skb, 2); /* longword align L3 header */ skb->len = length; skb->dev = dev; readblock(dev, skb->data, skb->len); DPRINTK(3, "%s: received %d byte packet of type %x\n", dev->name, length, (skb->data[ETH_ALEN+ETH_ALEN] << 8) | skb->data[ETH_ALEN+ETH_ALEN+1]); skb->protocol=eth_type_trans(skb,dev); netif_rx(skb); dev->last_rx = jiffies; lp->stats.rx_packets++; lp->stats.rx_bytes += length;}

Page 32: 第 7 章   嵌入式网络程序设计

在 net_rx()函数中调用 netif_rx()把数据传送到协议层。 netif_rx()函数把数据放入处理队列,然后返回,真正的处理是在中断返回以后,这样可以减少中断时间。调用 netif_rx()后,驱动程序不能再存取数据缓冲区 skb 。 netif_rx()函数在 net/core/dev.c 中定义为:

int netif_rx(struct sk_buff *skb){int this_cpu = smp_processor_id();struct softnet_data *queue;unsigned long flags;if (skb->stamp.tv_sec == 0) do_gettimeofday(&skb->stamp);queue = &softnet_data[this_cpu];local_irq_save(flags);netdev_rx_stat[this_cpu].total++;if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {

if (queue->input_pkt_queue.qlen) {if (queue->throttle) goto drop;

enqueue: dev_hold(skb->dev);__skb_queue_tail(&queue->input_pkt_queue,skb);cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ);local_irq_restore(flags);

#ifndef OFFLINE_SAMPLEget_sample_stats(this_cpu);

#endifreturn softnet_data[this_cpu].cng_level; }

if (queue->throttle) { queue->throttle = 0;#ifdef CONFIG_NET_HW_FLOWCONTROL

if (atomic_dec_and_test(&netdev_dropping)) netdev_wakeup();#endif

}goto enqueue; }

if (queue->throttle == 0) {queue->throttle = 1;netdev_rx_stat[this_cpu].throttled++;

#ifdef CONFIG_NET_HW_FLOWCONTROLatomic_inc(&netdev_dropping);

#endif}

drop: netdev_rx_stat[this_cpu].dropped++;local_irq_restore(flags);kfree_skb(skb);return NET_RX_DROP; }

Page 33: 第 7 章   嵌入式网络程序设计

中断函数 net_interrupt 在打开函数中申请,中断发生后,首先驱动中断管脚为高电平,然后主机读取 CS8900A 中的中断申请序列 ISQ值,以确定事件类型,根据事件类型做出响应。函数定义为:

static void net_interrupt(int irq, void *dev_id, struct pt_regs * regs){ struct net_device *dev = dev_id; struct net_local *lp; int ioaddr, status; ioaddr = dev->base_addr; lp = (struct net_local *)dev->priv; while ((status = readword(dev, ISQ_PORT))) { DPRINTK(4, "%s: event=%04x\n", dev->name, status); switch(status & ISQ_EVENT_MASK) { case ISQ_RECEIVER_EVENT:

/* Got a packet(s). */net_rx(dev);break;

case ISQ_TRANSMITTER_EVENT:lp->stats.tx_packets++;netif_wake_queue(dev); /* Inform upper layers. */if ((status & ( TX_OK |

TX_LOST_CRS | TX_SQE_ERROR |TX_LATE_COL | TX_16_COL)) != TX_OK) {

if ((status & TX_OK) == 0) lp->stats.tx_errors++; if (status & TX_LOST_CRS) lp->stats.tx_carrier_errors++; if (status & TX_SQE_ERROR) lp->stats.tx_heartbeat_errors++; if (status & TX_LATE_COL) lp->stats.tx_window_errors++; if (status & TX_16_COL) lp->stats.tx_aborted_errors++;}break;

case ISQ_BUFFER_EVENT:if (status & READY_FOR_TX) { netif_wake_queue(dev); /* Inform upper layers. */}if (status & TX_UNDERRUN) { DPRINTK(1, "%s: transmit underrun\n", dev->name); lp->send_underrun++; if (lp->send_underrun == 3) lp->send_cmd = TX_AFTER_381; else if (lp->send_underrun == 6) lp->send_cmd = TX_AFTER_ALL; netif_wake_queue(dev); /* Inform upper layers. */}

break; case ISQ_RX_MISS_EVENT:

lp->stats.rx_missed_errors += (status >>6);break;

case ISQ_TX_COL_EVENT:lp->stats.collisions += (status >>6);break;

} }}

Page 34: 第 7 章   嵌入式网络程序设计

7.3 Linux 网络编程实现

UDP 和 TCP 是协议层中两个最重要的协议,主要区别是两者在实现信息的可靠传递方面不同。 TCP 协议中包含了专门的传递保证机制,当数据接收方收到发送方传来的信息时,会自动向发送方发出确认消息;发送方只有在接收到该确认消息之后才继续传送其他信息,否则将一直等待,直到收到确认信息为止。

与 TCP不同, UDP 协议并不提供数据传送的保证机制。如果在从发送方到接收方的传递过程中出现数据报的丢失,协议本身并不能做出任何检测或提示。

这两种协议都有其存在的价值,本节中以 TCP 和 UDP两种协议为例,介绍 Linux 的网络编程。

Page 35: 第 7 章   嵌入式网络程序设计

socket 基本函数 Linux 系统是通过提供套接口( socket )来进行网络编程的。网络程序通过 socket 和

其他几个系统调用,返回一个通信的文件描述符,可以将这个文件描述符看成普通的文件描述符来操作,这也正是设备无关性的好处。可以通过读写描述符实现网络之间的数据交流。

有两种最常用的 Internet套接口:数据流套接口和数据报套接口。数据流套接口采用的 TCP 协议是可靠的双向连接的通信数据流,是一种高质量的数据传输。数据报套接口有时也叫做“无连接的套接口”,使用 UDP 协议,数据报的顺序是没有保障的。数据报是按一种应答的方式进行数据传输的。

1. socket系统调用 socket() 的用法如下:#include <sys/types.h>#include <sys/socket.h>int socket(int domain, int type, int protocol);其中,参数 domain 设置 AF _ INET ,以允许远程主机之间通信。参数 type 是套接口

的类型: SOCK_STREAM或 SOCK_DGRAM 。 SOCK_STREAM表明使用的是 TCP 协议, SOCK_DGRAM表明使用的是 UDP 协议。参数 protocol 设置为 0 。

系统调用 socket()返回一个套接口描述符来获得文件描述符,如果出错,则返回 1 。

Page 36: 第 7 章   嵌入式网络程序设计

2. bind有了一个套接口以后,下一步就是把套接口绑定到本地计算机的某一个端口上。可

以使用 bind() 来实现。系统调用 bind() 的用法如下:int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

其中,参数 sockfd 是由 socket()调用返回的套接口文件描述符。参数 my_addr 是指向数据结构 sockaddr 的指针。数据结构 sockaddr 中包括了关于地址、端口和 IP 地址的信息。参数 addrlen 是 sockaddr 结构的长度,可以设置成 sizeof ( struct sockaddr )。

bind函数将本地的端口同 socket返回的文件描述符捆绑在一起,如果出错, bind()也返回 1 。

数据结构 struct sockaddr 中保存着套接口的地址信息,定义如下:struct sockaddr { unsigned short sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */};

sa_family 中可以是其他的很多值,但在这里我们把它赋值为“ AF_INET” 。 sa_data包括一个目的地址和一个端口地址。

为了使系统具有良好的兼容性也可以使用另一个数据结构 sockaddr_in ,如下所示:struct sockaddr_in { short int sin_family; /* Address family */ unsigned short int sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char sin_zero[8]; /* Same size as struct sockaddr */};

这个数据结构使得使用其中的各个元素更为方便。需要注意的是, sin_zero 应该使用 bzero()或者memset()而设置为全 0 。另外,一个指向 sockaddr_in 数据结构的指针可以投射到一个指向数据结构 sockaddr 的指针,反之亦然。

Page 37: 第 7 章   嵌入式网络程序设计

下面是一个具体例子。#include <string.h>#include <sys/types.h>#include <sys/socket.h>#define MYPORT 3490main (){ int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */ my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = inet_addr("132.241.5.10"); bzero( & (my _addr.sin_zero), 8); /* zero the rest of the struct */ bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); ...

3. connect系统调用 connect() 的用法如下:#include <sys/types.h>#include <sys/socket.h>int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);其中,参数 sockfd 是套接口文件描述符,它是由系统调用 socket ()返回的。参

数 serv_addr 是指向数据结构 sockaddr 的指针,其中包括目的端口和 IP 地址。参数 addrlen 可以使用 sizeof ( struct sockaddr )而获得。

connect函数是实现客户端与服务端的连接,成功时返回 0 ,如果出错, connect() 将会返回 1 。

Page 38: 第 7 章   嵌入式网络程序设计

4. listen如果不连接到远程的主机,而是等待一个进入的连接请求,然后再做处理,可以通过

首先调用 listen() ,然后再调用 accept() 来实现。系统调用 listen() 的形式如下:int listen(int sockfd, int backlog);其中,参数 sockfd 是系统调用 socket ()返回的套接口文件描述符。参数 backlog 是进

入队列中允许连接的个数,当由多个客户端程序和服务端相连时,可以用其表示可以接受的排队长度。大多数系统的默认设置为 2 0 。当出错时, listen () 将会返回 1值。在使用系统调用 listen() 之前,需要调用 bind ()

绑定到需要的端口。5. accept远程主机可能试图使用 connect() 连接使用 listen()正在监听的端口。但此连接将会在

队列中等待,直到使用 accept() 处理它。调用 accept() 之后,将会返回一个全新的套接口文件描述符来处理这个单个的连接。这样,对于同一个连接来说,就有了两个文件描述符。原先的一个文件描述符正在监听指定的端口,新的文件描述符可以用来调用 send() 和 recv() 。调用的例子如下:#include <sys/socket.h>int accept(int sockfd, void *addr, int *addrlen);其中,参数 sockfd 是正在监听端口的套接口文件描述符。参数 add 是指向本地的数据

结构 sockaddr_in 的指针。参数 addrlen 同样可以使用 sizeof ( struct sockaddr_in )来获得。

如果出错, accept () 也将返回 1 。

Page 39: 第 7 章   嵌入式网络程序设计

#include <string.h>#include <sys/types.h>#include <sys/socket.h>#define MYPORT 3490 /* the port users will be connecting to */#define BACKLOG 10 /* how many pending connections queue will hold */main(){ int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */ struct sockaddr_in my_addr; /* my address information */ struct sockaddr_in their_addr; /* connector's address information */ int sin_size; sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */ my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */ /* don't forget your error checking for these calls: */ bind(sockfd,(struct sockaddr *)&my_addr, sizeof(struct sockaddr)); listen(sockfd, BACKLOG); sin_size = sizeof(struct sockaddr_in); new_fd = accept(sockfd, &their_addr, &sin_size); ...

Page 40: 第 7 章   嵌入式网络程序设计

6. send 和 recv系统调用 send() 的用法如下:int send(int sockfd, const void *msg, int len, int flags);其中,参数 sockfd 是发送数据的套接口文件描述符,可以通过 socket() 系统

调用返回。参数 *msg 指向发送数据的指针。参数 len 是数据的字节长度。参数 flags 标志设置为 0 。

系统调用 send() 发送数据,并返回实际发送的字节数。当 send()出错时,将返回 1 。

下面是一个简单的例子:char *msg = "Beej was here!";int len, bytes_sent;...len = strlen(msg);bytes_sent = send(sockfd, msg, len, 0);...系统调用 recv () 的使用方法与 send () 类似:int recv(int sockfd, void *buf, int len, unsigned int flags);其中,参数 sockfd 是要读取的套接口文件描述符。参数 buf 是保存读入信息

的地址,参数 len 是缓冲区的最大长度,参数 flags 设置为 0 。系统调用 recv()返回实际读取到缓冲区的字节数,如果出错则返回 1 。

使用系统调用 send() 和 recv() ,可以通过数据流套接口来发送和接受信息。

Page 41: 第 7 章   嵌入式网络程序设计

7. sendto 和 recvfrom因为数据报套接口并不连接到远程的主机上,所以在发送数据包之前,我们必须首

先给出目的地址,调用形式如下:int sendto(int sockfd, const void *msg, int len, unsigned int flags,const struct sock

addr *to, int tolen);除了两个参数以外,其他的参数和系统调用 send() 时相同。参数 to 是指向包含目

的 IP 地址和端口号的数据结构 sockaddr 的指针。参数 tolen 可以设置为 sizeof ( struct sockaddr )。系统调用 sendto()返回实际发送的字节数,如果出错则返回 1 。

系统调用 recvfrom() 的使用方法也和 recv() 的使用方法十分近似:int recvfrom(int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, i

nt *fromlen);参数 from 是指向本地计算机中包含源 IP 地址和端口号的数据结构 sockaddr 的指

针。参数 fromlen 设置为 sizeof ( struct sockaddr )。系统调用 recvfrom ()返回接收到的字节数,如果出错则返回 1 。

8. close 和 shutdown可以使用 close()调用关闭连接的套接口文件描述符: close(sockfd);这样,就不能再对此套接口做任何的读写操作了。使用系统调用 shutdown() ,可有更多的控制权。它允许在某一个方向切断通信,

或者切断双方的通信: int shutdown(int sockfd, int how);其中参数 sockfd 是所希望切断通信的套接口文件描述符。参数 how值如下: 0表示断开远程接收; 1表示断开远程发送; 2表示断开远程发送和接收。shutdown() 如果成功则返回 0 ,如果失败则返回 1 。

Page 42: 第 7 章   嵌入式网络程序设计

9. getpeername 和 gethostname系统调用 getpeername 可以返回另一端的信息:#include <sys/socket.h>int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);其中,参数 sockfd 是连接的数据流套接口文件描述符。参数 addr 是指向

包含另一端的信息的数据结构 sockaddr 的指针,参数 addrlen 可以设置为 sizeof ( struct sockaddr )。

如果出错,系统调用将返回 1 。一旦获得了另一端的的地址,就可以使用 inet_ntoa()或者 gethostbyaddr()

来得到更多的信息。系统调用 gethostname() 比系统调用 getpeername() 还简单。其返回程序

正在运行的计算机的名字。系统调用 gethostbyname() 可以使用这个名字来决定机器的 IP 地址。

下面是一个例子:#include <unistd.h>int gethostname(char *hostname, size_t size);如果成功, gethostname 将返回 0 。如果失败,它将返回 1 。

Page 43: 第 7 章   嵌入式网络程序设计

TCP 编程实例网络通信大部分是在客户机 / 服务器模式下进行的,例如, telnet 。使用 telnet 连接到

远程主机的端口时,主机就开始运行 telnet 的程序,用来处理所有进入的 telnet 连接,设置登录提示符等。

应当注意的是,客户机 / 服务器模式可以使用 SOCK_STREAM 、 SOCK_DGRAM ,或者任何其他的方式。例如, telnet/telnetd 、 ftp/ftpd 和 bootp/bootpd 。

1. TCP 服务器程序TCP 服务器通过如下步骤建立:( 1 )通过函数 socket()建立一个套接口。( 2 )通过函数 bind()绑定一个地址,包括 IP 地址和端口地址。这一步确定了服务器

的位置,使客户端知道如何访问。( 3 )通过函数 listen() 监听端口的新的连接请求。( 4 )通过函数 accept() 接受新的连接。此服务器程序通过一个数据流连接发送字符串“ Hello, World!\n” 。可以在一个窗口上

运行此程序,然后在另一个窗口使用 telnet 得到字符串。 #include <stdio.h>#include <stdlib.h>#include <errno.h>#include <string.h>#include <sys/types.h>#include <netinet/in.h>#include <sys/socket.h>#include <sys/wait.h>#define MYPORT 3490 /* the port users will be connecting to */#define BACKLOG 10 /* how many pending connections queue will hold */

Page 44: 第 7 章   嵌入式网络程序设计

main(){ int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */ struct sockaddr_in my_addr; /* my address information */ struct sockaddr_in their_addr; /* connector's address information */ int sin_size; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == 1) { perror( "socket"); exit(1); } my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero(&(my_addr.sin_zero),8); /* zero the rest of the struct */ if(bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))== 1){ perror("bind") ; exit(1); } if (listen(sockfd, BACKLOG) == 1) { perror("listen"); exit(1); } while(1) { /* main accept() loop */ sin_size = sizeof(struct sockaddr_in); if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == 1) { perror("accept"); continue; } printf("server: got connection from %s\n", inet_ntoa(their_addr.sin_addr)); if (!fork()) { /* this is the child process */ if (send(new_fd, "Hello, world!\n", 14, 0) == 1) perror("send"); close(new_fd); exit(0); } close(new_fd); /* parent doesn't need this */ while(waitpid(1,NULL,WNOHANG) > 0); /* clean up child processes */ }}

Page 45: 第 7 章   嵌入式网络程序设计

2. TCP客户端程序一个典型的 TCP客户端程序需要先建立 socket文件描述符,接着连接服务器,然后

便可以写进或读取数据。这个过程重复到写入和读取完所需信息后,才关闭连接。客户机所做的是连接到主机的 3490端口。它读取服务器发送的字符串。下面是客户机程序的代码:

#define PORT 3490 /* the port client will be connecting to */#define MAXDATASIZE 100 /* max number of bytes we can get at once */int main(int argc, char *argv[]){ int sockfd, numbytes; char buf[MAXDATASIZE] ; struct hostent *he; struct sockaddr_in their_addr; /* connector's address information */ if (argc != 2) { fprintf( stderr,"usage: client hostname\n"); exit(1) ; } if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */ herror("gethostbyname" ) ; exit(1) ; } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == 1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; /* host byte order */ their_addr.sin_port = htons(PORT); /* short, network byte order */ their_addr.sin_addr = *((struct in_addr *)he->h_addr); bzero(&(their_addr.sin_zero),8); /* zero the rest of the struct */ if(connect(sockfd, (struct sockaddr *)&their_addr, \ sizeof(struct sockaddr)) == 1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == 1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Received: %s",buf); close(sockfd); return 0;}

在运行客户端程序之前,需要保证服务器端程序已经正常运行。如果在运行服务器程序之前运行客户机程序,则将会得到一个“ Connection refused” 的信息。

Page 46: 第 7 章   嵌入式网络程序设计

综合训练之 UDP 编程实现

Page 47: 第 7 章   嵌入式网络程序设计

UDP 协议的每个发送和接收的数据报都包含了发送方和接收方的地址信息。在发送和接收数据之前,先要建立一个数据报方式的套接口,该 socket 的类型为 SOCK_ DGRAM ,用如下的调用产生:

sockfd=socket(AF_INET, SOCK_DGRAM, 0); 由于不需要建立连接,因此产生 socket后就可以直接发送和接收了。当然,要

接收数据报也必须绑定一个端口,否则发送方无法得知要发送到哪个端口。 sendto和 recvfrom两个系统调用分别用于发送和接收数据报。使用无连接方式通信的基本过程如图 7.7 所示。

图 7.7描述的是通信双方都绑定自己地址端口的情形,但在某些情况下,也可能有一方不用绑定地址和端口。不绑定的一方的地址和端口由内核分配。由于对方无法预先知道不绑定的一方的端口和 IP 地址,因此只能由不绑定的一方先发出数据报,对方根据收到的数据报中的来源地址可以确定回送数据报所需要的发送地址。显然,在这种情况下,对方必须绑定地址和端口,并且通信只能由非绑定方发起。

与 read() 和 write() 相似,进程阻塞在 recvfrom() 和 sendto() 中也会发生。但与TCP 方式不同的是,接收到一个字节数为 0 的数据报是有可能的,应用程序完全可以将 sendto() 中的 msg 设为 NULL ,同时将 len 设为 0 。

Page 48: 第 7 章   嵌入式网络程序设计

/*****udptalk.c****/#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #define BUFLEN 255 int main(int argc, char **argv) { struct sockaddr_in peeraddr, /*存放谈话对方 IP 和端口的 socket 地址 */ localaddr;/* 本端 socket 地址 */ int sockfd; char recmsg[BUFLEN+1]; int socklen, n; if(argc!=5){ printf("%s <dest IP address> <dest port> <source IP address> <source port>\n", argv[0]); exit(0); } sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd<0){ printf("socket creating err in udptalk\n"); exit(1); } socklen = sizeof(struct sockaddr_in); memset(&peeraddr, 0, socklen); peeraddr.sin_family=AF_INET; peeraddr.sin_port=htons(atoi(argv[2])); if(inet_pton(AF_INET, argv[1], &peeraddr.sin_addr)<=0){ printf("Wrong dest IP address!\n"); exit(0); } memset(&localaddr, 0, socklen); localaddr.sin_family=AF_INET; if(inet_pton(AF_INET, argv[3], &localaddr.sin_addr)<=0){ printf("Wrong source IP address!\n"); exit(0); } localaddr.sin_port=htons(atoi(argv[4])); if(bind(sockfd, &localaddr, socklen)<0){ printf("bind local address err in udptalk!\n"); exit(2); }

if(fgets(recmsg, BUFLEN, stdin) == NULL) exit(0); if(sendto(sockfd, recmsg, strlen(recmsg), 0, &peeraddr, socklen)<0){ printf("sendto err in udptalk!\n"); exit(3); } for(;;){ /*recv&send message loop*/ n = recvfrom(sockfd, recmsg, BUFLEN, 0, &peeraddr, &socklen); if(n<0){ printf("recvfrom err in udptalk!\n"); exit(4); }else{ /* 成功接收到数据报 */ recmsg[n]=0; printf("peer:%s", recmsg); } if(fgets(recmsg, BUFLEN, stdin) == NULL) exit(0); if(sendto(sockfd, recmsg, strlen(recmsg), 0, &peeraddr, socklen)<0){ printf("sendto err in udptalk!\n"); exit(3); } } }

Page 49: 第 7 章   嵌入式网络程序设计

将 udptalk.c 编译好后就可以运行了,首先编写Makefile 指定两个编译目标可执行文件,一个用于在主机端的 x86-udptalk ,一个是用于 SBC2410X 的 arm-udptalk ,运行make命令将把这两个程序一起编译出来。

将 arm-udptalk 使用前面介绍的方法下载到 SBC2410X 中,假设主机的 IP 地址为 192.168.0.1 , SBC2410X 的 IP 地址为 192.168.0.230 。

在主机的终端上输入:#./x86-udptalk 192.168.0.230 2000 192.168.0.1 2000 在 SBC2410X 上的终端输入:#arm-udptalk 192.168.0.1 2000 192.168.0.230 2000 则运行结果分别如图 7.8 、图 7.9 所示。

Page 50: 第 7 章   嵌入式网络程序设计
Page 51: 第 7 章   嵌入式网络程序设计