加入收藏 | 设为首页 | 会员中心 | 我要投稿 PHP编程网 - 黄冈站长网 (http://www.0713zz.com/)- 数据应用、建站、人体识别、智能机器人、语音技术!
当前位置: 首页 > 运营中心 > 交互 > 正文

Linux PPP实现源码分析

发布时间:2016-10-30 09:17:55 所属栏目:交互 来源:站长网
导读:转自:http://blog.csdn.net/osnetdev/article/details/8958058 作者:kwest 版本:v0.7 所有版权保留 转载请保留作者署名,严禁用于商业用途 。 前言: PPP(Point to Point Protocol)协议是一种广泛使用的数据链路层协议,在国内广泛使用的宽带拨号协议PPPo
转自:http://blog.csdn.net/osnetdev/article/details/8958058

作者:kwest   版本:v0.7

©所有版权保留

转载请保留作者署名,严禁用于商业用途 。

 

前言:

PPP(Point to Point Protocol)协议是一种广泛使用的数据链路层协议,在国内广泛使用的宽带拨号协议PPPoE其基础就是PPP协议,此外和PPP相关的协议PPTP,L2TP也常应用于VPN虚拟专用网络。随着智能手机系统Android的兴起,PPP协议还被应用于GPRS拨号,3G/4G数据通路的建立,在嵌入式通信设备及智能手机中有着广泛的应用基础。本文主要分析Linux中PPP协议实现的关键代码和基本数据收发流程,对PPP协议的详细介绍请自行参考RFC和相关协议资料。

模块组成:

Linux PPP实现源码分析


上图为PPP模块组成示意图,包括:

PPPD:PPP用户态应用程序。

PPP驱动:PPP在内核中的驱动部分,kernel源码在/drivers/net/下的ppp_generic.c, slhc.c。

PPP线路规程*:PPP TTY线路规程,kernel源码在/drivers/net/下的ppp_async.c, ppp_synctty.c,本文只考虑异步PPP。

TTY核心:TTY驱动,线路规程的通用框架层。

TTY驱动:串口TTY驱动,和具体硬件相关,本文不讨论。

说明:本文引用的pppd源码来自于android 2.3源码包,kernel源码版本为linux-2.6.18。

Linux中PPP实现主要分成两大部分:PPPD和PPPK。PPPD是用户态应用程序,负责PPP协议的具体配置,如MTU、拨号模式、认证方式、认证所需用户名/密码等。 PPPK指的是PPP内核部分,包括上图中的PPP驱动和PPP线路规程。PPPD通过PPP驱动提供的设备文件接口/dev/ppp来对PPPK进行管理控制,将用户需要的配置策略通过PPPK进行有效地实现,并且PPPD还会负责PPP协议从LCP到PAP/CHAP认证再到IPCP三个阶段协议建立和状态机的维护。因此,从Linux的设计思想来看,PPPD是策略而PPPK是机制;从数据收发流程看,所有控制帧(LCP,PAP/CHAP/EAP,IPCP/IPXCP等)都通过PPPD进行收发协商,而链路建立成功后的数据报文直接通过PPPK进行转发,如果把Linux当做通信平台,PPPD就是Control Plane而PPPK是DataPlane。

在Linux中PPPD和PPPK联系非常紧密,虽然理论上也可以有其他的应用层程序调用PPPK提供的接口来实现PPP协议栈,但目前使用最广泛的还是PPPD。PPPD的源码比较复杂,支持众多类UNIX平台,里面包含TTY驱动,字符驱动,以太网驱动这三类主要驱动,以及混杂了TTY,PTY,Ethernet等各类接口,导致代码量大且难于理解,下文我们就抽丝剥茧将PPPD中的主干代码剥离出来,遇到某些重要的系统调用,我会详细分析其在Linux内核中的具体实现。

源码分析:

PPPD的主函数main:

第一阶段:

PPP协议里包括各种控制协议如LCP,PAP,CHAP,IPCP等,这些控制协议都有很多共同的地方,因此PPPD将每个控制协议都用结构protent表示,并放在控制协议数组protocols[]中,一般常用的是LCP,PAP,CHAP,IPCP这四个协议。

每个控制协议由protent结构来表示,此结构包含每个协议处理用到的函数指针:

在main()函数中会调用所有支持的控制协议的初始化函数init(),之后初始化TTY channel,解析配置文件或命令行参数,接着检测内核是否支持PPP驱动:

函数ppp_available会尝试打开/dev/ppp设备文件来判断PPP驱动是否已加载在内核中,如果此设备文件不能打开则通过uname判断内核版本号来区分当前内核版本是否支持PPP驱动,要是内核版本很老(2.3.x以下),则打开PTY设备文件并设置PPP线路规程。目前常用的内核版本基本上都是2.6以上,绝大多数情况下使用的内核都支持PPP驱动,因此本文不分析使用PTY的old driver部分。

接下来会检查选项的合法性,这些选项可以来自于配置文件/etc/ppp/options,也可以是命令行参数,PPPD里面对选项的处理比较多,这里不一一分析了。

后面是把PPPD以daemon方式执行或保持在前台运行并设置一些环境变量和信号处理函数,最后进入到第一个关键部分,当demand这个变量为1时,表示PPPD以按需拨号方式运行。

什么是按需拨号呢?如果大家用过无线路由器就知道,一般PPPoE拨号配置页面都会有一个“按需拨号”的选项,若没有到外部网络的数据流,PPP链路就不会建立,当检测到有流量访问外部网络时,PPP就开始拨号和ISP的拨号服务器建立连接,拨号成功后才产生计费。反之,如果在一定时间内没有访问外网的流量,PPP就会断开连接,为用户节省流量费用。在宽带网络普及的今天,宽带费用基本上都是包月收费了,对家庭宽带用户此功能意义不大。不过对于3G/4G网络这种按流量收费的数据访问方式,按需拨号功能还是有其用武之地。

PPP的按需拨号功能如何实现的呢?首先调用open_ppp_loopback:

全局变量new_style_driver,这个变量已经在ppp_avaliable函数里被设置为1了。接下来调用make_ppp_unit打开/dev/ppp设备文件并请求建立一个新的unit。

这里的unit可以理解为一个PPP接口,在Linux中通过ifconfig看到的ppp0就是通过ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit)建立起来的,unit number是可以配置的,不过一般都不用配置,传入-1会自动分配一个未使用的unit number,默认从0开始。这个ioctl调用的是PPPK中注册的ppp_ioctl:

 

 

TIPS:这里还要解释一下PPPK中channel和unit的关系,一个channel相当于一个物理链路,而unit相当于一个接口。在Multilink PPP中,一个unit可以由多个channel组合而成,也就是说一个PPP接口下面可以有多个物理链路,这里的物理链路不一定是物理接口,也可以是一个物理接口上的多个频段(channel)比如HDLC channel。

PPPK中channel用结构channel表示,unit用结构ppp表示。

注意这两个结构体的第一个字段都是structppp_file,ppp_file的kind字段代表/dev/ppp的类型。

现在回到ppp_ioctl,它的执行要判定三种情况,没有任何绑定,绑定到PPP unit或绑定到PPP channel,在初始化时并没有任何绑定即file->private_data为空,因此这里会调用ppp_unattached_ioctl:

这个函数又会调用ppp_create_interface创建一个ppp网络接口:

 

OK,现在PPP网络接口已经创建起来了,例如建立的接口名为ppp0,这里的ppp0还只是一个“假接口”,其实到这里PPP的整个拨号过程根本就还没有开始,之所以建立这个接口只是为了让数据报文可以通过这个接口发送出去从而触发PPP拨号。

 接下来回到PPPD的open_ppp_loopback,make_ppp_unit这时候成功返回后,还会调用modify_flags函数来设置标志位SC_LOOP_TRAFFIC,这个函数其实调用的还是ioctl()->ppp_ioctl()来设置的flag。

标志位SC_LOOP_TRAFFIC相当的重要,当通过ppp0接口发送数据时,PPPK才会唤醒PPPD进程去建立真正的PPP连接。之前在内核中创建ppp接口时会注册一个接口数据包发送函数ppp_start_xmit,当网络程序通过ppp0接口发送数据时,TCP/IP协议栈最终会调用到此函数。这个函数的call trace为ppp_start_xmit() -> ppp_xmit_process()-> ppp_send_frame():

很显然,只要ppp->flags中SC_LOOP_TRAFFIC置位,就要做点特殊处理:把发送的数据包放在接收队列ppp->file.rq中而不是平常的发送队列,这是为啥呢?留待过会分解。唤醒PPPD进程进行处理,并没有将数据发送出去哦。

返回主函数main()中,当open_ppp_loopback调用返回后,其返回值同时被赋值给fd_loop代表/dev/ppp的文件描述符。此时,网络接口ppp0已创建好并注册到TCP/IP协议栈中,当然只有 ppp0接口还不够,我们还需要对ppp0接口做些配置,接着调用demand_conf:

这个函数设置ppp0的MTU和MRU,然后调用每个控制协议的demand_conf函数。对于LCP,PAP,CHAP协议protp->demand_conf都为空, 只有IPCP协议有初始化这个函数指针:

上面提到在按需拨号模式下,要让数据报文通过ppp0接口发送才会触发PPP连接的建立。所以这里,IPCP协议块提供的ip_demand_conf函数就为ppp0配置了两个假的IP地址:本端IP地址为10.64.64.64,对端IP地址为10.112.112.112,并设置对端IP为默认网关。这样,当用户访问外部网络时,Linux路由子系统会选择ppp0接口发送数据包,从而触发PPP链路的建立。

第二阶段:

回到主函数main()中,接下来是最外层的for(;;)循环进行事件处理,

 

如果是demand拨号模式,PPPD状态机进入PHASE_DORMANT, 主要包含两个部分:

1.     调用add_fd将/dev/ppp的文件描述符fd_loop加入in_fds中:

2.     在嵌套的for(;;)死循环里调用handle_events函数进行事件处理。

这个函数里面重点是调用了wait_input对前面加入的/dev/ppp文件描述符调用select监听事件。

还记得吗,/dev/ppp在前面的make_ppp_unit函数中已经被设置为非阻塞,因此当没有事件发生时select调用不会一直阻塞下去,当超时时间到时wait_input会很快返回,calltimeout函数会被调用以处理注册的timer函数。这些timer函数是各控制协议及其fsm状态机需要用到的,从这里可以看出/dev/ppp被设置为非阻塞方式的必要性。

这个嵌套的for(;;)循环什么时候能跳出呢,这里有两个可能:

1.     变量asked_to_quit置为1。参考handle_events中对信号的处理,当收到SIGTERM时,表示用户想主动退出PPPD。

2.     函数get_loop_output调用返回1。下面分析一下这个函数:

首先调用read_packet读取数据到inpacket_buf中:

这个函数很简单,实际上就是调用标准的文件读函数read()读取/dev/ppp设备文件,其实就是调用到PPPK中的ppp_read:

这个函数要把PPPD进程加入到等待队列中,若pf->rq队列不为空,则读取队列中的第一个数据包并立即返回。注意哦,上面提到当网络程序通过ppp0接口发送数据时,最终会调用内核函数ppp_send_frame,发送的数据则放在了该函数的ppp->file.rq队列中,这个队列就是这里的pf->rq队列,这就意味着ppp_read读取的数据其实就是刚才网络程序发送的数据。

 

反之,如果pf->rq队列为空,表示没有数据包需要通过ppp0接口发送,此函数直接返回-EAGAIN。也就是说,用户态函数 read_packet立即返回<0导致get_loop_output返回0,嵌套for(;;)循环不能退出继续循环等待事件处理。

 

考虑有数据通过ppp0发送,read_packet返回读取的数据长度,这时loop_frame会被调用:

这里实际上最后是调用IPCP协议块的ip_active_pkt函数来检查数据包有效性,这里就不具体分析了。如果发送数据是合法的IP报文,后面会保存这些数据包,并暂时放在pend_qtail队列中,留待PPP链路建立后重新发送。

第三阶段:

如果是demand拨号模式,并且假设有数据通过ppp0发送且是合法IP报文,第二阶段中的嵌套for(;;)循环会被跳出,接下来的代码和正常拨号模式就一样了,真是殊途同归啊。

再次回到主函数main() 中,我们要开始建立真正的PPP链路了:

 

第一步:调用lcp_open(0)建立LCP链路。

调用fsm_open打开LCP状态机:

初始化状态,实际调用lcp_starting()-> link_required():

这个函数的主要作用从函数命名上就能看出,就是将需要的物理链路都带起来,现在PPPD状态机进入PHASE_SERIALCONN阶段。

1.     调用connect_tty打开串口TTY驱动并配置TTY参数,变量ppp_devnam是串口驱动的设备文件如/dev/ttyS0,/dev/ttyUSB0,/dev/ttyHDLC0等,具体可以参考相关的串口TTY驱动,这里不作具体分析。

 

2.     然后调用tty_establish_ppp:

 

分成两部分来具体深入分析:

 

2.1 首先调用ioctl(tty_fd, TIOCSETD, &ppp_disc)将TTY驱动绑定到PPP线路规程,这里的ioctl是对TTY文件描述符的操作,实际上是调用了内核中的tty_ioctl() -> tiocsetd():

      

这个tiocsetd函数是个wrapper函数,只是把用户态传入的int参数放在内核态的ldisc中,再调用tty_set_ldist设置线路规程:

这个函数为TTY驱动绑定N_PPP线路规程,绑定后调用线路规程的open()函数,对于N_PPP实际上是调用ppp_asynctty_open:

此函数分配并初始化struct asyncppp结构来表示一个异步PPP,并将tty结构的disc_data指向该结构。另外调用ppp_register_channel注册了一个异步PPP channel:

OK,到此ioctl(tty_fd, TIOCSETD, &ppp_disc)在内核中的实现就分析完了。

 

2.2 返回tty_establish_ppp,继续调用generic_establish_ppp创建PPP接口:

这个函数可以分成4个主要部分:

1)     获取TTY中已注册的channel的索引值。

2)     将注册的channel绑定到/dev/ppp文件描述符,并保存到ppp_fd。

3)     对于正常拨号,调用make_ppp_unit创建ppp0网络接口并将此接口绑定,绑定后的/dev/ppp文件描述符保存在ppp_dev_fd。

4)     将ppp_dev_fd加入到select的fds,并连接channe到PPP unit。

 

第1部分:对TTY fd调用 ioctl(fd, PPPIOCGCHAN, &chindex),实际上调用内核中的tty_ioctl():

异步PPP线路规程已经在内核文件ppp_async.c中初始化了,并且在前面已经设置TTY的异步PPP线路规程,因此这里的ld->ioctl实际指向的是ppp_asynctty_ioctl:

ppp_channel_index实现:

 

第2部分:对/dev/ppp调用ioctl(fd, PPPIOCATTCHAN, &chindex),实际上调用ppp_ioctl -> ppp_unattached_ioctl:

 

这个ioctl返回后,/dev/ppp文件描述符绑定了索引值为chindex的channel。然后通过set_ppp_fd(fd)保存在全局变量ppp_fd中。

 

第3部分:会判断是否是demand模式,对正常拨号会调用make_ppp_unit创建ppp0接口,而demand模式在第二阶段已经调用过make_ppp_unit了,这里就直接忽略。具体参见第二阶段中对make_ppp_unit的详细分析。

 

注意:ppp_dev_fd文件描述符代表的是一个unit,ppp_fd文件描述符代表的是一个channel。

第4部分:对绑定了channel的ppp_fd调用ioctl(fd, PPPIOCCONNECT, &ifunit)将channel连接到unit。

ppp_connect_channel实现:

 

3.     函数link_required中前两步已经配置好了链路接口,接下来该做正事了:PPPD状态机进入PHASE_ESTABLISH阶段,然后用lcp_lowerup(0)发送LCP报文去建立连接。

实际调用lcp_lowerup() -> fsm_lowerup() -> fsm_sconfreq()-> fsm_sdata() -> output():

数据的发送要分两种情况:

1.     LCP控制帧用ppp_fd发送。

2.     数据帧用ppp_dev_fd发送。

不管是ppp_fd还是ppp_dev_fd打开的设备文件都是/dev/ppp,因此调用的都是同一个函数ppp_write:

继续对这个函数分析,现在要发送LCP帧去建立连接,因此调用ppp_channel_push来进行发送:

实际调用ppp_async_send发送LCP帧:

实际调用ppp_async_push:

       到此,LCP Configure Request帧就发送出去了。

现在,再次返回到PPPD中的主函数main()中:

第二步:PPPD状态机循环进行事件处理

 

调用handle_events处理事件,见demand模式下对此函数的分析。注意:这里等待事件处理的fds中包含有ppp_dev_fd。接下来调用get_input处理收到的报文:

此函数调用read_packet读取接收报文到inpacket_buf缓冲区,再提取出收到报文的协议号(LCP为0xC021),然后根据协议号匹配调用对应协议块的input和datainput函数。在第二阶段分析demand模式时已经分析了read_packet函数,这里就不啰嗦了。

至此,PPPD建立连接所需的数据收发基本流程就勾画出来了,这里我们看到的PPPD收发的数据包都是PPP控制帧如LCP,那像IP数据包这种数据流也都要经过PPPD吗?如果连数据流都经过PPPD那效率岂不是很低?

首先来看数据流的接收:

当底层TTY驱动收到数据时会产生一个中断,并在中断处理函数(硬中断或软中断BH)中调用TTY所绑定的线路规程的receive_buf函数指针。

对于本文中绑定了N_PPP线路规程的TTY驱动来讲,就是调用ppp_asynctty_receive:

1.     这个函数首先调用ppp_async_input:

函数ppp_asyc_input读取buf中的数据放在sk_buff中,然后调用process_input_packet把收到的数据包放在接收队列中:

 

2.     再用tasklet_schedule(&ap->tsk)调度tasklet来处理,这个ap->tsk在哪个地方初始化的呢?前面分析过的ppp_asynctty_open已经初始化了tasklet。这时tasklet函数ppp_async_process会被执行:

饶了一个圈,实际上是调用ppp_input来处理接收数据包:

函数ppp_input分两种情况分发报文:

1.     对控制流,放在channel的接收队列中并唤醒PPPD进程读取。

2.     对数据流,调用ppp_do_recv:

由于PPP unit上已建立了ppp0网络接口,这里会调用ppp_receive_frame:

对于非多链路PPP调用ppp_receive_nonmp_frame:

对于数据流,最终还是调用netif_rx(skb)将数据包放入Linux协议栈去处理。


结论:在PPP连接成功建立之前,为建立连接而传输的控制流都要通过PPPD进行报文解析并根据各控制协议的状态机和用户配置进行报文的收发、超时及状态迁移等处理。 当PPP连接经过三阶段LCP->PAP/CHAP->IPCP成功建立之后, 经过ppp0接口的数据流就直接通过Linux内核进行处理而不必经过PPPD,实现了控制路径与数据路径,策略与机制的有效分离。

(编辑:PHP编程网 - 黄冈站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读