接上篇 Lwip之PPP、PPPoE实现(一)_龙赤子的博客-CSDN博客
目录
三:文件结构
四:数据结构
4.1 PPP数据结构
4.1.1 PPP protent
4.1.2 PPPControl
4.1.3 PPP_settings
4.2 PPPoE数据结构
4.2.1 PPPoEPacketStruct
4.2.2 PPPoETagStruct
4.2.3 PPPoEConnectionStruct
4.2.4 PacketCriteria
4.3 维护的几个全局数据结构:
五:接口以及数据流程
5.1 PPP接口实现
5.2 PPPoE接口实现
5.3 底层数据流程
5.4 基于函数调用的流程
5.5 总的收发流程:
六:PPPoE&PPP的完善
6.1 lwip的定时机制
6.1.1 sys_timeout函数的实现
6.1.2 sys_mbox_fetch函数的实现及对定时的影响
6.1.3 sys_sem_wait函数的实现及对定时的影响
6.1.4 sys_msleep函数的实现及对定时的影响
6.2 PPPoE超时重传
6.3 链路的维护
6.4 PPP定时事件的维护
6.5 PPP任务的执行流程
三:文件结构
这部分主要介绍一下程序中各个文件的基本功能(所实现的模块)
头文件
C文件
PPPoE.c:实现PPPoE层的接口(包括输出函数接口、输入函数接口以及对PPPoE包格式的解析)
Discovery.c: discovery阶段具体处理的实现(包括发送padi/padr/padt接收pado/pads/padt以及对此阶段接收包格式的解析)
PPP.c: 实现PPP层的接口(包括PPP链路建立的启动、任务的创建以及PPP层的输入输出接口)
Sio.c: 基于串行链路输入输出接口的具体实现(实际上PPPoE.c实现了基于以太网链路的输入输出接口)
Lcp.c: 链路控制协议的实现(链路数据结构及链路控制状态实现)
Chap.c: 认证协议的实现
Pap.c: 认证协议的实现(认证相关数据结构以及状态控制实现)
Auth.c: 认证的高层接口,对上述两个认证协议有简单的封装
Ipcp.c: 链路控制协议的实现(网络控制结构以及状态转换的实现)
Fsm.c: PPP有限状态机的实现,主要衔接各个状态(根据不同的状态和动作进行状态转换)
四:数据结构
这部分介绍几个重要的数据结构
4.1 PPP数据结构
4.1.1 PPP protent
用于衔接PPP各个子模块
struct protent {
u_short protocol; /* PPP protocol number */
/* Initialization procedure */
void (*init) (int unit);
/* Process a received packet */
void (*input) (int unit, u_char *pkt, int len);
/* Process a received protocol-reject */
void (*protrej) (int unit);
/* Lower layer has come up */
void (*lowerup) (int unit);
/* Lower layer has gone down */
void (*lowerdown) (int unit);
/* Open the protocol */
void (*open) (int unit);
/* Close the protocol */
void (*close) (int unit, char *reason);
。。。
};
该数据结构提供了一个模型,可以说是一个模子,当我们在不同的阶段需要时会复制一份,并根据当前需要进行赋值。该结构体的protocol域用于指示当前具体的PPP协议,可能的选择有lcp、pap、chap、ipcp等等,用于表示不同的PPP阶段。之后的几个域是函数指针,根据名称我们可以对它们进行具体的赋值,比如如果将其用于LCP协议中,则init就可以使用lcp_init函数来注册,将其地址保存下来。在PPP中我们定义了一个该结体指针的数组,其中各个项就是各个结构体的地址,如下:
struct protent *PPP_protocols[] = {
&lcp_protent,
#if PAP_SUPPORT > 0
&pap_protent,
#endif
#if CHAP_SUPPORT > 0
&chap_protent,
#endif
&ipcp_protent,
NULL
};
最终就形成一个表,如下:
这样通过该数组我们就可以调用到各个模块的操作函数,从而方便的实现控制。
4.1.2 PPPControl
用于对PPP接口进行控制,包括链路的状态,当前网络接口以及回调函数。
typedef struct PPPControl_s {
char openFlag; /* True when in use. */
char oldFrame; /* Old framing character for fd. */
sio_fd_t fd; /* File device ID of port. */
int kill_link; /* Shut the link down. */
int sig_hup; /* Carrier lost. */
int if_up; /* True when the interface is up. */
int errCode; /* Code indicating why interface is down. */
struct pbuf *inHead, *inTail; /* The input packet. */
PPPDevStates inState; /* The input process state. */
char inEscaped; /* Escape next character. */
u16_t inProtocol; /* The input protocol code. */
u16_t inFCS; /* Input Frame Check Sequence value. */
int mtu; /* Peer's mru */
int pcomp; /* Does peer accept protocol compression? */
int accomp; /* Does peer accept addr/ctl compression? */
u_long lastXMit; /* Time of last transmission. */
ext_accm inACCM; /* Async-Ctl-Char-Map for input. */
ext_accm outACCM; /* Async-Ctl-Char-Map for output. */
struct netif netif;
struct PPP_addrs addrs;
void (*linkStatusCB)(void *ctx, int errCode, void *arg);
void *linkStatusCtx;
} PPPControl;
在该数据结构中我们通过设置一些标识来实现对PPP会话的控制,比如通过kill_link可以判读是否发生了人为的关闭链路的操作,通过sig_hup可以判断是否发生了信号丢失,netif结构体保存了接口信息,PPP_addrs结构体保存了地址信息,包括ip地址,子网掩码以及dns服务器地址。
4.1.3 PPP_settings
该结构用来配置PPP链路的配置选项
struct PPP_settings {
u_int disable_defaultip : 1; /* Don't use hostname for default IP addrs */
u_int auth_required : 1; /* Peer is required to authenticate */
u_int explicit_remote : 1; /* remote_name specified with remotename opt */
u_int refuse_pap : 1; /* Don't wanna auth. ourselves with PAP */
u_int refuse_chap : 1; /* Don't wanna auth. ourselves with CHAP */
u_int usehostname : 1; /* Use hostname for our_name */
u_int usepeerdns : 1; /* Ask peer for DNS adds */
u_short idle_time_limit; /* Shut down link if idle for this long */
int maxconnect; /* Maximum connect time (seconds) */
char user[MAXNAMELEN + 1];/* Username for PAP */
char passwd[MAXSECRETLEN + 1]; /* Password for PAP, secret for CHAP */
char our_name[MAXNAMELEN + 1]; /* Our name for authentication purposes */
char remote_name[MAXNAMELEN + 1]; /* Peer's name for authentication */
};
通过该结构体我们可以处理一些与认证相关的信息以及控制链路最大的连接时间和最大的空闲时间。
除了上面描述的PPP顶层的几个数据结构外,PPP不同的链路阶段也有相应的数据结构来控制,这里暂时就不介绍了。
4.2 PPPoE数据结构
下面给出几个与PPPoE密切相关的数据结构的定义及说明
4.2.1 PPPoEPacketStruct
该数据结构保存了一个完整的PPPoE包,其定义与PPPoE结构本身是一致的
typedef struct PPPoEPacketStruct {
struct eth_hdr *ethHdr;/* Ethernet header */
unsigned char ver_type; /* PPPoE Version&& PPPoE Type (must be 1) */
unsigned char code; /* PPPoE code */
unsigned short int session; /* PPPoE session */
unsigned short int length; /* Payload length */
unsigned char *payload; //[ETH_DATA_LEN]; /* A bit of room to spare */
} PPPoEPacket;
该结构体定义了PPPoE包的结构,包括头以及数据。其中需要注意的是PPPoE的version和type都必须是1。
4.2.2 PPPoETagStruct
描述PPPoE Tag的结构
typedef struct PPPoETagStruct {
unsigned int type:16; /* tag type */
unsigned int length:16; /* Length of payload */
unsigned char payload[ETH_DATA_LEN]; /* A LOT of room to spare */
} PPPoETag;
该结构体定义了PPPoE的tag的结构。它由类型、长度以及数据三项表示。在discovery阶段,PPPoE包中的数据都是由一个或多个tag构成的。
4.2.3 PPPoEConnectionStruct
PPPoE链路控制结构
typedef struct PPPoEConnectionStruct {
int discoveryState; /* Where we are in discovery */
int discoverySocket; /* Raw socket for discovery frames */
int sessionSocket; /* Raw socket for session frames */
unsigned char myEth[ETH_ALEN]; /* My MAC address */
unsigned char peerEth[ETH_ALEN]; /* Peer's MAC address */
UINT16_t session; /* Session ID */
char *ifName; /* Interface name */
char *serviceName; /* Desired service name, if any */
char *acName; /* Desired AC name, if any */
int synchronous; /* Use synchronous PPP */
int useHostUniq; /* Use Host-Uniq tag */
int printACNames; /* Just print AC names */
int skipDiscovery; /* Skip discovery */
int noDiscoverySocket; /* Don't even open discovery socket */
int killSession; /* Kill session and exit */
int numPADOs; /* Number of PADO packets received */
PPPoETag cookie; /* We have to send this if we get it */
PPPoETag relayId; /* Ditto */
int PADSHadError; /* If PADS had an error tag */
int discoveryTimeout; /* Timeout for discovery packets */
int padi_trans; /*number of padi's transmit*/
int padr_trans; /*number of padr's transmit*/
} PPPoEConnection;
该结构体是一个比较大的结构体,在其中我们保存了与PPPoE连接相关的许多信息,在程序中我们会将其定义为全局变量,从而可以在不同的地方和时刻都能够对当前的PPPoE会话进行控制。
4.2.4 PacketCriteria
struct PacketCriteria {
PPPoEConnection *conn;
int acNameOK;
int serviceNameOK;
int seenACName;
int seenServiceName;
};
使用该结构体判断是否发现了服务器的名字以及其所提供的服务。
4.3 维护的几个全局数据结构:
在PPPoE与PPP模块维护了几个全局变量和数据结构,通过它们我们能够方便的管理和控制数据流并获得系统当前所处的状态。(?对这些全局变量的访问是否需要保护---对于只可能有一个任务访问的全局变量,则不需要进行保护?)下面进行简单的介绍:
PPPoE_conn 抽象表示了PPPoE连接,保存了会话的状态信息,地址信息,以及操作信息。
PPP_settings 对PPP会话进行一些基本的设置,包括认证信息,链路需要持续的时间以及最大空闲时间等。
PPPcontrol 对PPP协议的运转进行控制,保存了包的一些填充信息和状态信息。
PPP_protocols 操作状态机需要使用该数组,因为通过该数组我们就可以找到PPP各个阶段所需的控制函数。
待续。。。
五:接口以及数据流程
在这一部分将给出总体的函数接口以及数据流程
5.1 PPP接口实现
Lwip本身所带的PPP代码是针对串行线路的,所以其中有许多处理转义字符序列的操作,并且数据的发送是一个字节一个字节进行的,其中读写调用接口函数为sio_read与sio_write。Sio_write由nput函数调用,而nput由PPPifoutput和PPP_write调用,其中PPPifoutput输出上层Ip传下来的数据,PPP_write输出PPP模块本身运转的输出。使用PPPoE后需要修改这种实现方式,输入采用消息邮箱机制,PPPoE将收到的数据放入邮箱中,PPP模块在需要的时候就从邮箱中取出数据。对于输出,Ip层的输出通过PPPifoutput调用PPPoE层的输出接口函数sendippacket完成;PPP模块本身的输出则通过PPPwrite调用PPPoE层的输出接口函数sendsessionpacket完成。整个流程以及操作方式的变化如上图所示。
5.2 PPPoE接口实现
在这里,PPPoE一方面通过获得接入服务器的MAC以及会话ID为PPP会话做准备,另一方面,提供PPP与网络设备通信的接口。
从上图可以看出,整个PPPoE的流程可以分为两个部分,即输入与输出。对于输入部分,底层上来的数据首先要经过判断,确定是PPPoE发现阶段的包还是PPPoE会话阶段的包,并将它们分发到不同的接口处理;对于输出部分,如果是IP包,则直接通过底层接口送出,否则通过会话和发现两部分的处理在送给底层发送。
5.3 底层数据流程
下图是dm9ks_netif与PPP_netif所注册函数的简单描述:
从上图可以看出有两点不同:其一,上层输入,PPP直接将数据送给了ip模块的输入进行处理;其二,下层输出,dm9ks对应的为正常的tcpip输出,所以要经过arp模块,而PPP部分则不需要经过arp模块,而是直接输出。
综合上述描述,添加PPP模块后的整个底层流程如下图所示:
按照所标的序号对上图进行简单的说明
@1:底层网卡数据送到网络接口
@2:网络接口数据送给底层网卡
@3:arp包由arp模块发送给网络接口,用于获得对方的MAC地址
@4:当输入的数据包为arp查询包时,送给arp模块进行处理
@5:如果输入的数据包为IP包,则直接交给IP模块,进入协议栈
@6:如果输入的数据包为PPPoE包,经由PPPoE接口进入PPP模块
@7:PPP模块产生的数据向下输出,这些数据包可能为PPPoE查询阶段的包,也可能为PPPoE会话阶段的包,这时就包括了IP包。
@8:PPP与PPPoE的数据交互
@9:PPPoE与PPP的数据交互
@10:协议栈产生的数据经过arp模块被添加MAC地址后发送
@11:协议栈产生的数据经由PPP通道发送
@12:进入PPP通道的IP数据包上传给协议栈
(在PPPoE会话过程结束后,是按照标准的PPP过程进行新的会话,所以PPP部分不会进行大的改动)
5.4 基于函数调用的流程
初始化:
PPPoE_init --> send_padi --> send discovery packet
输入:
输出:
上层发送 --> ip route --> ip output ànetif->output --> PPP if output --> send ip packet --> low level output
PPP write --> send session packet
5.5 总的收发流程:
基本的执行过程如下:
调用PPPoE_init初始化PPPoE连接,在PPPoE连接中我们维护了一个全局数据结构体PPPoEpacket来记录PPPoE连接过程中的信息,在初始化中我们对其中的一些变量(域)进行赋值操作,从而完成初始化。
在完成PPPoE init 之后,广播padi包,进入PPPoE的会话阶段
PPPoE_discovery_input函数处理PPPoE会话,该函数接受PPPoE discovery阶段的数据包,解析数据报,并根据类型的不同进行不同的处理。按照PPPoE discovery的正常流程,该函数首先收到pado包,由于padi是广播出去的,所以可能会收到不止一个pado包,这里我们只取第一个到达的pado包进行解析。之后我们发送pado包,如果有pads包到达,PPPoE_discovery_input函数会按照PPPoE标准进行处理,并发送padr包。最后接收pads包,确定会话id,完成discovery 过程,进入session阶段。
当收到pads包后会把其session id保存到全局数据结构PPPoEpacket中,以后都发送PPPoE session包,并使用该获得的唯一session id。
在完成PPPoE discovery之后,就进入PPP阶段,在处理了收到的pads包后,调用PPP_init进行PPP初始化过程,随后调用PPP_open进入PPP处理。这里的PPP处理过程与标准的串行线路上的PPP处理过程是一致的,所不同的是此时PPP包是承载在PPPoE包中,并在基于共享机制的以太网上的,并且去掉了PPP包头的串行线路控制信息,只保留了两个字节的协议类型字段。
在PPP_init中,初始化了PPP所经历的各个阶段,包括lcp pa ncp ipcp等,分别调用它们各自的初始化函数对其进行初始化。在PPP_open中,最重要的工作就是重新启动一个任务,进行链路维护,并接收输入的PPP阶段的包进行进一步的处理。这包括与服务器进行通行完成PPP状态机以及传送数据到ip层
PPP链路的建立主要包括三个阶段,lcp chap ncp ipcp,这些数据包都是在PPPoE中的,此时的PPPoE的状态是session。
PPPoE_session_input函数就如其名称所表达的那样,用于接收session阶段的数据包,当一个PPPoE session包到达后,会调用该函数,在该函数中将输入的包发送到了一个消息队列,PPP 任务函数PPP main会不断从该消息队列中读取包进行处理
到此,就完成了接收接口
对于发送,主要有两部分,一部分是PPPoE部分,另外就是PPP。在PPPoE discovery阶段,对应的padi padr包都通过函数senddiscoverypacket函数发送。PPP阶段的链路建立相关的过程lcp chap ncp ipcp等的相应包都通过函数sendsessionpacket函数发送,该函数主要作了两项额外的工作,一是加入PPPoE session标识,表明这是一个PPPoE session阶段的数据包,另外就是session id,即当前session所拥有和维护的唯一id;对于PPP链路建立后,从ip等上层传下来的数据包,经过IP路由,利用PPP对应的netif的output函数指针在PPP模块中调用PPPifoutput完成。该函数会调用PPPoE模块的函数sendippacket完成,该函数出了做与sendsessionpacket函数类似的工作外还要加入PPP_ip类型到PPP头,然后发送数据。
至此,就完成了发送接口。
需要在额外确定的几点事项:在收到pado后我们就可以确定服务器的mac地址了
在ip configure阶段,会进行协商并获取服务器给自己的ip地址和网关地址。在PPP部分会调用sifaddr完成这一工作,它的参数,也就是它所获得的数据是来自PPP状态机的。
在PPPInput函数中驱动状态机的运行,PPPInput函数在TcpipThread任务上下文中运行,因此,TcpipThread任务不仅维护了tcp ip协议栈的正常运行,其实也维护了PPP协议栈的正常运行。所有的处理最终都是在tcp ip 任务下完成的
总结起来,各个任务都是通过邮箱隔离的。
最后就是关于第四部分所提出的问题 :在PPP模块有许多的全局变量,不同任务对这些全局变量的访问是否需要增加保护机制?
六:PPPoE&PPP的完善
6.1 lwip的定时机制
Lwip的设计是基于进程模型,整个协议栈的处理是由一个进程来处理的,通常也说是一个任务。在lwip中没有使用任何操作系统相关的函数,取而代之的是提供了一个操作系统仿真层,操作系统仿真层提供唯一的一个接口给操作系统服务,这些服务包括定时器,进程同步以及消息传递机制。进程的同步使用信号量来实现,消息传递使用邮箱实现,而对于定时器的实现则是这一部分需要介绍的。
Lwip通过维护一个任务相关的定时链表来管理相应的定时事件。该链表的基本结构如下图所示。
定时事件总体结构:
通过上图可以看出,系统通过全局指针threads指向链表的开始,左边一列是与任务相关的,每个任务对应一个控制块,所有的任务通过next指针连在一起,右边则是该任务对应的定时事件,该结构体中包含了定时时间,对应捕获函数的参数以及地址,各个定时事件同样通过next指针域连在一起。所以,通过threads指针我们可以找到每个任务的每个定时事件。要添加一个定时事件到任务的定时链表中,则需要在任务的上下文环境中调用函数sys_timeout,关于该函数要做的工作在下面介绍:
6.1.1 sys_timeout函数的实现
@1:首先为当前定时事件timeout结构体分配内存并赋值 (timeout结构体)
@2:调用sys_arch_timeouts函数获得当前任务的定时队列(我们所添加的定时事件都是任务相关的,即该定时事件属于那个任务是确定的)
@3:如果当前任务的定时队列为空(如上图中的2与4),则将刚才分配的定时事件挂到任务的定时队列上,返回
@4:进入到这一步说明当前任务的定时队列如上图的1和3两种情况。此时,如果当前任务定时队列中的第一个定时事件的触发时间大于刚才新产生的定时事件的触发时间,则将其触发时间减去新产生的定时事件的触发时间,并将新的定时事件加入到其后;否则,进入下一步(比如,如果当前的定时为7à4à2,到达的定时事件的触发时间为5,则新的定时队列就为5à2à4à2,这样各个事件的新的触发时间就都是相对值了,相对的对象就是其前一事件的触发时间值)
@5:遍历当前任务的定时队列,每进入一次循环,执行下面的操作:
修改A的(A/B/C等的含义见后面说明)Atime,即减去Btime,如果没到队列头并且Ctime不大于Atime,则继续进行下次循环。否则,如果队列到头了,则直接将A添加到B之后,如果是相对值小于C与B的相对值,则说明Atime介于二者之间,此时就将其插入到二者之间,并进行Ctime - Atime操作。最后跳出循环。
(新产生定时事件——A; 当前指向的任务的定时事件(循环开始时为任务的定时队列上的第一个定时事件)——B; B之后的定时事件——C; A的触发时间Atime; B的触发时间Btime; C的触发时间Ctime)
对于第四步所举的实例,如果Atime为8,则最终的排序为5à2à1à3à2,如果Atime为7,则为5à2à2à4à2,若Atime为6,则排序为5à1à2à4à2,若以实际时间值排序,则三种情况下的对应的结果分别为:5à7à8à11à13; 5à7à7(Atime)à11à13; 5à6à7à11à13。
从上述结果可以看出,整个过程是一个按照升序进行排序的过程,只不过绝对的时间值的比较变成了相对时间值的比较。这里,sys_timeout函数不仅做了定时事件的添加,而且还有排序工作,这样就为后续的操作减轻了负担。如果我们使用绝对时间的话,该函数的实现是可以简单一些,但是,一旦第一个定时事件的触发时间到达后,则还需要遍历队列去修改后续定时事件的触发时间,这会影响到整个系统的效率。
这里介绍了定时事件的结构以及如何被添加到任务的定时队列中去,但是,在这里并没有按照我们所想象的那样有任何关于触发时间如何消耗以及定时捕获函数的执行等相关操作。我们将在后面通过对下面三个函数的介绍来对这一问题进行说明。
6.1.2 sys_mbox_fetch函数的实现及对定时的影响
该函数的执行流程如下:
首先,调用sys_arch_timeouts获取当前任务的定时队列
如果当前任务没有定时队列或者定时队列为空,则调用sys_arch_mbox_fetch函数获取消息,此时,该函数的时间参数设置为0,也就是说系统将处于死等状态,如果此时邮箱为空,系统将一直等待,直到邮箱中有消息。
否则,也就是说任务的定时队列不为空,将进行下面的所有操作:
如果Btime(含义见上)大于零,同样调用函数sys_arch_mbox_fetch函数获取消息,不过此时该函数的时间参数并不设置为0,而是Btime。同时,用一个变量保存其返回值。否则,说明应该触发该定时事件,执行其相应的捕获函数,因此,我们给时间变量赋值为全F。
对于sys_arch_mbox_fetch函数的调用,前面已经说明,如果时间参数为0,则就是死等,而如果参数设置为Btime,则我们至多等待Btime的时间,如果在次期间函数取得消息返回,则返回的时间为实际等待的时间,否则,就是Btime。
通过上面的操作,我们完成了对时间变量的设置,下面的操作就基于该时间变量的值进行。如果定时事件的触发时间到了,则将其从定时队列中取出,并执行其捕获函数。执行完后返回到开始,继续取消息。否则,说明在定时时间到达之前取得了消息,此时只需将Btime减去时间变量保存的返回值即可。
通过上面对从邮箱取消息操作的说明可以看出,只有当前的定时事件的定时还没有到达,并且从邮箱中取得了消息,该函数才会返回。
6.1.3 sys_sem_wait函数的实现及对定时的影响
该函数的操作与sys_mbox_fetch类似,只不过取邮箱操作变成了等待信号量,对应底层的操作由sys_arch_mbox_fetch变成了sys_arch_sem_wait。
6.1.4 sys_msleep函数的实现及对定时的影响
该函数的作用是使得系统进行毫秒级的等待,同时允许其他任务有执行的机会。基本的执行过程如下:
调用sys_sem_new创建一个信号量
调用sys_sem_wait_timeout进行sleep操作
调用函数sys_sem_free释放之前创建的信号量资源
下面对sys_sem_wait_timeout函数的实现进行简单的说明:
首先判断等待的时间,如果不大于零,则一直等待信号量,否则,添加一个用于释放信号量的定时事件,触发时间就是sleep时间。之后等待信号量的操作则如对sys_sem_wait函数的描述。从之前的描述中我们可以看出该操作会消耗系统时间。
根据以上描述,如果一个任务在其执行的整个过程中,如果没有直接或者间接的调用以上三个函数或其一,那么在其上下文中调用sys_timeout所添加的定时事件将永远不会得到执行。
下面将根据对lwip定时机制的描述来介绍PPPoE中超时重传的实现。
6.2 PPPoE超时重传
首先需要确定的是,要想让我们添加的定时器能够执行,必须在一个任务环境中创建它,同时,该任务还必须有调用上述三个能够触发定时的函数或其一的操作,否则,定时事件的处理函数将永远不会得到执行。根据这些限制,可以有三种选择,其一是将放在接收任务中来添加,其二是放在TcpIP任务中添加,最后就是放在PPP任务中添加。对于接收任务,我们虽然有等待信号量的操作,但是该操作是直接调用操作系统接口实现的,而不是lwip自身的接口完成,所以在该任务中添加的定时事件,其捕获函数不会得到执行。而对于PPP任务,它是在PPPoE会话建立起来后创建的,所以逻辑上不合适。最后选择在TCPIP任务中来实现。
具体的实现过程有两步,首先实现一个定时回调函数,该函数中调用sys_timeout添加相关的定时事件到任务队列;其次,就是定时捕获函数的实现,具体的实现逻辑在下面介绍。为了能够让定时事件添加到TCPIP任务环境中,我们使用函数tcpip_callback函数将之前创建的定时回调函数的地址发送到TCPIP的消息邮箱中,这样,TCPIP任务通过取邮箱并执行定时回调函数中的sys_timeout函数将PPPoE定时事件添加到TCPIP任务环境中。至于等邮箱以及信号量的操作,在TCPIP任务中是能够得到保证的。
PPPoE的超时重传都发生在Discovery阶段,有两种情况,其一是在发送padi包后确定时间内没有收到pado包;其二是在发送padr包后确定时间内没有收到pads包,具体的处理逻辑如下:
Discovery阶段,发送padi包,如果在确定时间内没有收到pado包,请求端需要重发padi包,并将等待时间加倍,这一过程可重复期望的次数
收到pado包,发送padr包,如果在确定的时间内没有收到pads包,采取类似padi包的重传机制,只不过这时重发的是padr包,重复执行期望的次数后仍然没有收到pads包,则重发padi包,重新请求服务
6.3 链路的维护
这一部分介绍与链路的建立以及断开相关的操作。
通过5.5节我们可以看出,PPP模块增加了两个新的资源,其一是PPP的邮箱,其二是PPP任务。根据已有的设计,PPP任务会在PPP断开连接后退出,在有新的连接时又会重新创建,所以希望在断开连接后将PPP任务资源释放。对于邮箱,由于我们在PPP初始化的过程中会去创建邮箱,也就是在PPP建立连接的时候创建邮箱,所以,类似于PPP任务,在断开连接时也释放其资源。
选择上述处理方法的另外一层原因在于,PPPoE过程是无连接的,即我们发起连接的时候首先进行的是PPPoE过程,如果失败,则不会进入PPP模块进行处理,这样,即使我们上层执行了建立连接的操作,但是该操作也很有可能悄无声息的退出,如果我们对PPP的上述资源在断开连接时保留,而在重新建立连接时通过设置状态条件进行相应的处理的话,该状态条件的设置将会是比较复杂的。
(任务的协调)通过5.5节我们还可以看出,PPP链路的协商以及数据的上传都是在TCPIP任务环境中来完成的,PPP任务只是用于从邮箱取PPP相关数据包并把它们交给TCPIP任务进行处理。各个任务是通过邮箱进行隔离的。对于PPP任务与TCPIP任务,二者会访问全局变量ppp_controls;对于全局变量使用的保护。。。
资源的释放是在PPP任务退出时通过发送回调函数给TCPIP任务,由TCPIP任务执行完成的。
目前,PPP链路可能由于一下四种原因断开:
- 用户执行主动的断开操作
- LCP的回波请求超过设置的次数时还没有得到相应
- 链路达到设置的最大空闲时间时仍没有任何数据接收到
- 连接达到设置的最大连接时间
6.4 PPP定时事件的维护
在这里将PPP的定时事件维护从链路维护中单独提出来是为了起突出强调作用
当前,在LWIP中维护了十二个定时事件,它们是:
tcp任务维护了其中的三个,它们是:
1: arp timer
2: tcp timer
3: ip timer
ppp任务维护了另外的九个,它们是:
1: lcp echo timer
2: check idle timer
3: max connect timer
4: chap rechallenge timer
5: chap response timer
6: chap challenge timer
7: fsm timer
8: upap request timer
9: upap timer
因为PPP的处理是在TCPIP任务环境中执行的,同样,这些定时事件也都是添加在TCPIP任务的定时队列上的,所以,如果我们在断开PPP连接的时候如果没有将它们都卸载,那么其中的一些定时事件还将继续执行,这在逻辑上是没有任何意义的,同时又会影响正常的PPP处理。所以在PPP连接断开时,我们要保证所有的PPP相关的定时事件都必须从TCPIP任务的定时队列上取下来。
通过实际的执行,在PPP由于自身原因退出时,lcp echo timer没有卸载,在lcp_close函数中添加了相应的卸载处理(这里没有卸载可能是由于不同的断开原因使得PPP自身没有执行到相关定时事件的卸载函数。)
基于操作系统关于时间操作的相关接口,完成了check idle timer的功能
对于max connect timer,PPP模块并没有卸载接口,在up_down函数中将其卸载
6.5 PPP任务的执行流程
PPPMain