源码克隆地址:
git://git.code.sf.net/p/linuxptp/code
项目官网文档:
https://linuxptp.nwtime.org/documentation/
关于linuxptp的相关配置可以参考以下博文:
linuxptp/ptp4l PTP时钟同步配置选项
ptp4l的main函数在ptp4l.c中,命令行解析使用的是 getopt_long ,具体使用方法可以百度,这个是现成的命令行解析API。
可以看到解析不同命令行参数后都是调用的 config_set_int 函数设置,linuxptp中配置一般都是保存在 config.c 中的 config_tab 中:
关于配置项所代表的含义可以参考上文推荐的博文。
命令行中比较重要的是 -i ,也就是添加interface:
创建接口使用的是网卡名称,比如 -i eth0,此时就会创建一个名字是eth0的接口,源码如下:
在 interface_create 中注意,除了名字(name)还有ts_label也被设置为传入的网卡名称:
除去配置参数和接口创建,其实功能主体就是创建clock,和轮询创建clock时添加的文件描述符:
在 clock_create 中只看几个关键的地方,第一个是软硬件时间戳相关:
在clock创建的初始你会看到基本都是初始化 c->dds 这个结构体相关的配置,这里在协议原文中有:
其实dds就是defaultDS,这几个数据集都是协议明文规定的数据集,linuxptp中在ds.h中有所定义,详细内容可以参照协议原文第8章节PTP data sets。
在配置比如使用软件时间戳还是硬件时间戳,是onestep还是twostep时,会先根据设置得到一个网卡预期需要支持的模式,然后根据前面创建的interface,获取网卡的信息,再判断网卡是否支持:
再下面是确定使用哪个PHC(ptp hardware clock)的逻辑:
还有UDS(unix domain sockets)的配置:
剩下的就是clock本身一些杂项初始化,在这个函数末尾有最重要的port添加与初始化:
在添加port的时候,可以看到每个port申请了多少个fd:
从上图可以看到clock的port个数=interface个数+2
从上图可以看到,当没有添加过port的时候port个数是两个uds,每次添加一个port,实际是加了3个port,也就是添加一个port的时候,一共有5个port,每个port有 N_CLOCK_PFD 个文件描述符,这些文件描述符就是后续需要轮询的。N_CLOCK_PFD是12,其中除了包含下面11个fd,还有一个处理错误状态的定时器fd。
回到刚才的函数,port_open中还有一些port的参数设置,其中比较重要的有:
以及通过 transport_create 创建了传输实例:
根据传输类型有UDS/ETHERNET/IPV4/IPV6可选,最终trp就是一组包含发送接收等的函数指针合集:
比如IPV4:
port_open 中还有有限状态机(fsm)的设置:
需要注意,状态机的各种状态也是协议中所明文规定的:
具体内容请参照协议原文9.2.5章节。
再有就是fault定时器也在这里被创建:
回到clock_create函数最后对port的初始化:
根据前面port_open中的源码,假如我们是E2E的OC,那么我们的 port_dispatch 函数是 bc_dispatch :
在 port_state_update 里我们根据 EV_INITIALIZE 事件对端口进行了初始化:
在 port_initialize 函数中,除了初始化一些参数配置,最重要的是创建了各种定时器fd:
拿IPv4来举例,319和320是固定的两个端口,它们就是通过 transport_open 函数打开:
这里event port用来接收event消息,general端口用来接收general消息:
详细信息可以参考协议原文7.3.3。
至此,所有配置都初始化完成了,后续就只剩下一直轮询之前添加的fd而已了。在main里有:
clock_poll 函数里面主要就是轮询fd,然后分发事件。
假如还按照之前举的例子,E2E的OC的话,port_event实际是 bc_event 函数。
比如我们是master的话,sync同步包发送定时器时间到了我们就会:
在处理完定时器fd事件后,紧跟着就是接收来自两个fd的数据:
然后根据接收到的事件做不同处理,同时更新状态机状态。
其实master要做的事情很简单:
1.发送Announce报文
2.发送Sync(FollowUp)报文
3.应答DelayReq报文,也就是回发DelayResp报文。
正常对时的两个设备报文如下:
ptp4l中是如何实现的呢?
首先,master是如何当上grandmaster的,这里我讲一下只有一个设备时,自身是怎么被选举为grandmaster的,在port初始化函数 port_initialize 中,有初始化两个参数:
在 port_open 函数中有初始化另一个参数:
这几个参数又在 port_initialize 中被使用:
上图函数中:
M = p->announceReceiptTimeout = 3
S = p->announce_span = 1
N = p->logAnnounceInterval = 1
所以设置的超时时间范围是6~8s
注意,周期性的报文的周期值最好不是一个定值。比如现在要以周期为2秒发送一个X报文,那最好的办法不是每隔2秒就发送一个X报文,而是在2秒的基础上加一个随机值(比如+1~-1之间的一个数),这样可以减少碰撞。
设置完 FD_ANNOUNCE_TIMER 后,当定时器超时后:
会返回一个Announce报文接收超时的状态,这个状态在clock_poll中有:
可以看到会改变clock的sde的值(state decision event,状态决策事件) ,当这个值置1后,会处理一次状态,还是在这个函数内:
其中历遍所有port的时候会用类似冒泡法去对比,当只有一个port的时候这个port自然会被选为master:
当选举完master clock后,会更新所有port的状态:
bmc_state_decision 中关于状态决策可以参考协议原文9.3.5中的相关描述。
然后生成了一个 EV_RS_GRAND_MASTER 事件,被分发到相关端口:
OC/BC使用的分发函数是 bc_dispatch 在这个函数中有更新port状态:
port_state_update 中使用的状态机 state_machine 是在 port_open 函数中初始化时赋值的:
这里master使用的是ptp_fsm,如果是slave则是ptp_slave_fsm。然后根据这个函数得到下个状态:
我们的当前状态是 PS_LISTENING,事件是 EV_RS_GRAND_MASTER ,所以下个状态就是 PS_GRAND_MASTER:
拿到这个状态后,根据选择的是E2E测量方式,所以会使用 port_e2e_transition 函数处理:
在这个函数中,当处于这个状态时,则会设置两个定时器:FD_MANNO_TIMER 和 FD_SYNC_TX_TIMER。注意之前 port_initialize 函数中有初始化 p->inhibit_announce = 0:
在这里第一次设置了 FD_MANNO_TIMER ,第一次设置的超时时间很短,后续设置是在 bc_event 中的:
默认情况下,超时时间是2秒:
而发送报文的内容可以查看 port_tx_announce 函数。
和设置 Announce 报文发送时间差不多,发送 Sync 报文的超时时间是1秒:
发送Sync报文的函数可以查看 port_tx_sync 。
一般指定当前设备为slave时使用 -s选项:
这里我主要讲一下slave收到报文后怎么计算offset和pathDelay的。
上面已经介绍过,对于接收报文的处理在 bc_event 函数内,对于E2E的slave主要是处理以下几种报文:
首先看对于sync报文的处理:
这里我们假如先收到的是Sync报文,则事件是 SYNC_MISMATCH ,在Sync和FollowUp报文处理状态机函数 port_syfufsm 里面有:
我们再看假如在Sync后收到FollowUp报文该如何处理:
至此当前状态是 SF_HAVE_SYNC ,处理的事件是 FUP_MATCH:
在函数 port_synchronize 中就可以拿到当前的t1、t2、c1(Sync的修正域)、c2(FollowUp),此时即可计算:
offset = t2-(t1+c1+c2) 代码中如下所示:
在函数 clock_synchronize 中有:
这里的 delay 也就是 filtered_delay 又是从何而来的呢?
在 bc_event 中对与发送DelayReq报文的定时器超时后会发送此报文:
port_delay_request 函数里关键的有以下处理:
还是在 bc_event 里收到 DelayResp 报文后 使用 process_delay_resp 函数处理:
原始pathdelay的计算过程如下:
上图中可以看到我添加了一些关键位置的打印,现在实际运行起来,slave打印如下: