PPD是用户态应用程序,负责PPP协议的具体配置,如MTU、拨号模式、认证方式、认证所需用户名/密码等
一 终端
终端介绍
终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。看下面一副图展示了计算机系统与终端之间得联系。
终端驱动程序的主要功能是在程序与相关设备之间进行数据传递。在一个LINUX内核自身的内部,终端基本上包括两个主要的软件部分:设备驱动程序和行规则(见下图)。
设备驱动程序是一个写到具体硬件上面的低级软件,该硬件允许计算机与终端进行交互。
行规则是执行一系列输入输出数据的逻辑处理,把一系列字符映射到其他的上面。
伪终端
伪终端(Pseudo Terminal)是成对的逻辑终端设备,在Linux系统上创建伪终端设备,使用了”pty master”方式,例如/dev/ptm3,它的对应端则会被自动地创建成/dev/pts/3。
伪终端从设备上的数据输入成为伪终端主设备的输入,反之亦然。
二 PPPOE 概述
PPPOE协议是基于PPP协议的协议,在PPPOE应用程序中并没有将PPP协议实现,PPP协议是由PPPD这个用户空间程序实现的,PPPOE程序只实现PPPOE协议部分代码,在适当的时候PPPD程序进行PPP协议。
在PPPOE用户程序与PPPOE服务器连接后,他将会建立一个PPP0设备,此设备是在PPPD程序中进行的,如果与服务器连接成功,他将会一直存在,此设备就与正常的网卡设备一样,但他是个虚拟设备,经过此设备的数据发出去时还是从真实存在的网卡设备接口发送出去的,但这已经在系统中做了转换。
三 PPPD
上文中说过PPPD是PPP协议的实现程序,PPP的实现由四部分组成
PPPD程序首先会进行一些初始化工作,读参数配置参数等,这部分工作非常重要,会在读代码时分析。
然后就开始走协议,代码流向见下:
main.c->main()
{
读参数
配置参数
从LCP协议开始,这是整个协议的开始部分,状态机开始发动了
lcp_lowerup(0);
lcp_open(0); /* Start protocol */
下面
while (phase != PHASE_DEAD) {
handle_events();
get_input();等待数据报文进入然后根据状态机处理报文
…………
…………
}
}
PPP协议与PPPOE有着重要的联系,但是PPP协议建立连接部分不是讨论重点,所以会着重讨论在pppoe连接成功后数据的报文流向,数据是如何被处理的。
四 PPPOE
PPPOE代码分析
PPPOE主文件为PPPOE.c,入口函数为main()
在main函数中主要做了两件事情
PPPOE协议中的DISCOVER阶段,处理函数为discovery(),此函数做的动作就是建立一个RAW SOCKET,然后发送报文,接着等待回应,继续处理,最后成功后设置状态进入SESSION阶段,代码片断见下:
do {
。。。。。。。。。。。
sendPADI(conn);发送报文
conn->discoveryState = STATE_SENT_PADI;
waitForPADO(conn, timeout);等待回应
。。。。。。。。。。。
} while (conn->discoveryState == STATE_SENT_PADI);
do {
。。。。。。。。。。。。。
sendPADR(conn); 发送报文
conn->discoveryState = STATE_SENT_PADR;
waitForPADS(conn, timeout); 等待回应
。。。。。。。。。。。。。
} while (conn->discoveryState == STATE_SENT_PADR);
conn->discoveryState = STATE_SESSION;设置状态为SESSION,接下来进入SESSION阶段
PPPOE协议中的SESSION阶段,处理函数为session(),此函数做的动作是首先建立一个RAW SOCKET,然后从PPPD中读入数据,在加上PPPOE发送出去,再接受到数据发送到PPPD程序中,让PPPD进行处理,处理完后,就建立PPPOE连接了,PPP0端口也建立了,并且从SERVER端分配到了IP。从上面的分析我们知道,PPPOE在这个阶段只是做了转发工作,在建立连接后,他就一直做着转发的工作。
for (;;) {
。。。。。。。。。。。。。。。。。。。。。。。。。
从PPPD那边收到数据,然后转发出去
if (FD_ISSET(0, &readable)) {
if (conn->synchronous) {
syncReadFromPPP(conn, &packet);
} else {
asyncReadFromPPP(conn, &packet);
}
}
从网卡端口收到数据后通过,发送到PPPD中去
if (FD_ISSET(conn->sessionSocket, &readable)) {
do {
if (conn->synchronous) {
syncReadFromEth(conn, conn->sessionSocket, optClampMSS);
} else {
asyncReadFromEth(conn, conn->sessionSocket, optClampMSS);
}
} while (BPF_BUFFER_HAS_DATA);
}
}
PPPOE建立连接后数据报文走向
如果从eth收到PPPOE数据:
首先数据被PPPOE中RAW SOCKET数据接收,PPPOE解开到达数据包的PPPOE头,然后将数据传送到伪终端主设备,也就是传送到了伪终端的从设备,然后在内核中数据跑到了PPP0接口,这是个设备,他会通过驱动将这个包处理后往上层传送;
如果有数据要发送出去:
首先根据路由知道数据要发送的端口在PPP0设备端口,数据就到达了PPP0端口,PPP0将数据传送到伪终端从设备,也就是传送到了伪终端主设备,然后到达PPPOE,PPPOE程序打上PPPOE包头,然后通过RAW SOCKET从ETH口发送出去。
pppd的main中get_pty (&pty_master, &pty_slave, ppp_devnam, uid)是获取主从伪终端设备,set_up_tty ( pty_slave, 1);设置从设备波特率,位数等。device_script ( ptycommand, pty_master, pty_master, 1 ) ,执行脚本start, 便于pppoe向pppd发送和接收( iRet = read ( 0, buf, READ_CHUNK
);是从pppd读取数据发送到eth,write ( 1, pppBuf, ( ptr-pppBuf )是从eth收到数据到pppoe然后在有pppoe写入到主设备)。 fd_ppp = tty_establish_ppp ( ttyfd );将从从设备的文件描述符ttyfd的通道与dev下面ppp设备的通道相连接。跳过一些协议的初始化,get_input();然后进行交互。
pppoe的mian中 discovery ( &conn );发送和接收报文包, session ( &conn );会话与网络和pppd进行交互数据。
以上是PPP连接建立后数据报文的走向。
五 PPPOE实现中理解疑难
pppoe建立连接后协议栈协议栈如下
数据报文理应这样,报文下来后先经过PPP封装,然后再PPPOE封装,然后加上以太头出去,这样的处理既直接又高效,但是我们PPPOE用户空间程序上面的分析画出来的数据流程图不是这样的,数据从内核中PPP0接口传送到从终端设备,再传送到PPPOE中打包,发出去,为什么不从PPP0中出来就到PPPOE中去,封装PPPOE报文出去,多好的事情啊,但这里事实不是这样,在PPPOE内核空间实现中将会是上面说的理想状态。在用户空间中,这样处理是为了不破坏LINUX内核中原有的终端实现的接口,因为LINUX内核中是将串行设备作为终端设备来驱动的。
接下来讨论一下PPP0端口与从设备终端之间是如何联系到一起的
1) ppp设备是指在点对点的物理链路之间使用PPP帧进行分组交换的内核网络接口设备,由于Linux内核将串行设备作为终端设备来驱动,于是引入PPP终端规程来实现终端设备与PPP设备的接口. 根据终端设备的物理传输特性的不同,PPP规程分为异步规程(N_PPP)和同步规程(N_SYNC_PPP)两种, 对于普通串口设备使用异步PPP规程.
2) 在PPP驱动程序中, 每一tty终端设备对应于一条PPP传输通道(chanell),每一ppp网络设备对应于一个PPP接口单元(unit).从终端设备上接收到的数据流通过PPP传输通道解码后转换成PPP帧传递到PPP网络接口单元,PPP接口单元再将PPP帧转换为PPP设备的接收帧. 反之, 当PPP设备发射数据帧时,发射帧通过PPP接口单元转换成PPP帧传递给PPP通道, PPP通道负责将PPP帧编码后写入终端设备.
从终端对应的文件描述符是ttyfd
ioctl(ttyfd, PPPIOCGCHAN, &chindex)
获取从终端的设备的CHANNEL
ppp_fd = open("/dev/ppp", O_RDWR);
ioctl(ppp_fd, PPPIOCATTCHAN, &chindex)
ppp_fd绑定到终端上去,就可以读写终端上的输入了
PPP0端口与ppp_dev_fd绑定到一起了,我们通过ppp_dev_fd读写PPP0的输出了
ioctl(ppp_fd, PPPIOCCONNECT, &ifunit)是将ppp_fd与ifunit所对应的接口连接在一起,这样后终端与设备就连接到一起了,从PPP0端口出来的报文就发送到从终端设备了,从终端设备上的数据也就传送到PPP0接口中去了
PPP (Point-to-Point)提供了一种标准的方法在点对点的连接上传输多种协议数据包,它最常见的用途可能是传统的拨号上网了(据说现在的宽带接入 也有采用PPPOE方式的)。在Linux Mobile Phone上,网络应用程序使用PPP作为与GSM模组之间的通信协议,最近遇到了一点关于PPP的麻烦,所以花了点时间去研究它。
PPP 协议肯定不是最复杂的网络协议,不过pppd、chat、tty、socket、ccp、chap、pap、eap、ecp、ipcp和很多其它概念搅在 一起之后,谁都会被搞得晕头转向。我关心的其实是PPP协议中各个实体之间的协作关系,而不是协议的状态转换或者服务的配置,由于没有找到这方面的资料, 只好去读RFC,内核、pppd和一些网络工具的代码。
PPP协议提供两个实体之间的数据链路连接的建立、维持和释放,负责流量和差错控制等等功能,所以它应该是属于数据链路层协议的。
PPP 协议之下是以太网和串口等物理层,之上是IP协议等网络层。这里,对于下层,我们只讨论串口的情况,对于上层,我们只讨论TCP/IP的情况。发送时, TCP/IP数据包经过PPP打包之后经过串口发送。接收时,从串口上来的数据经PPP解包之后上报给TCP/IP协议层。
网络协议是分层实现的,上层一般只需要知道其直接下层,只有在极少数据情况才使用间接下层的接口。比如,彩信、浏览器和邮件等应用程序使用socket接口编程,它们只需要知道TCP/IP协议,而无需要知道PPP协议的存在。这种分层设计简化了协议的实现和应用程序的开发。
问题来了:PPP 协议不只是提供了简单的数据链路层功能,它还提供了诸如鉴权(如PAP/CHAP),数据压缩/解压(如CCP)和数据加密/解密(如ECP)等扩展功 能。应用程序要求使用透明化,不关心这些扩展功能的存在,而反过来,PPP协议处理模块本身又无法处理这些策略性的东西,因为它不知道用户名/密码,不知 道是否要进行压缩,不知道是否要进行加密。
怎么办?如何在对应用程序透明的情况下使用扩展功能呢?上帝说,这个问题必须解决!于是pppd就出现了。pppd是一个后台服务进程(daemon),是一个用户空间的进程,所以把策略性的内容从内核的PPP协议处理模块移到pppd中是很自然的事了。pppd实现了所有鉴权、压缩/解压和加密/解密等扩展功能的控制协议。
pppd只是一个普通的用户进程,它如何扩展PPP协议呢?这就是pppd与内核中的PPP协议处理模块之间约定了,它们之间采用了最传统的内核空间与用户空间之间通信方式:设备文件。
设备文件名是/dev/ppp。 通过read系统调用,pppd可以读取PPP协议处理模块的数据包,当然,PPP协议处理模块只会把应该由pppd处理的数据包发给pppd。通过 write系统调用,pppd可以把要发送的数据包传递给PPP协议处理模块。通过ioctrl系统调用,pppd可以设置PPP协议的参数,可以建立/ 关闭连接。
在pppd 里,每种协议实现都在独立的C文件中,它们通常要实现protent接口,该接口主要用于处理数据包,和fsm_callbacks接口,该接口主要用于 状态机的状态切换。数据包的接收是由main.c: get_input统一处理的,然后根据协议类型分发到具体的协议实现上。而数据包的发送则是协议实现者根据需要调用output函数完成的。
chat是pppd所带一个辅助工具。呵,它和xchat不是一个类型的,xchat用来与人聊天,而chat用来与GSM模组建立会话。它的实现比较简单,它向串口发送AT命令,建立与GSM模组的会话,以便让PPP协议可以在串口上传输数据包。
应用程序通过socket 接口发送TCP/IP数据包,这些TCP/IP数据包如何流经PPP协议处理模块,然后通过串口发送出去呢?pppd在make_ppp_unit函数中 调用ioctrl(PPPIOCNEWUNIT)创建一个网络接口(如ppp0),内核中的PPP协议模块在处理PPPIOCNEWUNIT时,调用 register_netdev向内核注册ppp的网络接口,该网络接口的传输函数指向ppp_start_xmit。
当应用程序发送数据时,内核根据IP 地址和路由表,找到ppp网络接口,然后调用ppp_start_xmit函数,此时控制就转移到PPP协议处理模块了。ppp_start_xmit调 用函数ppp_xmit_process去发送队列中的所有数据包,ppp_xmit_process又调用ppp_send_frame去发送单个数据 包,ppp_send_frame根据设置,调用压缩等扩展处理之后,又经ppp_push调用pch->chan->ops-> start_xmit发送数据包。
pch ->chan->ops->start_xmit是什么?它就是具体的传输方式了,比如说对于串口发送方式,则是 ppp_async.c: ppp_asynctty_open中注册的ppp_async_send函数,ppp_async_send经ppp_async_push函数调用 tty->driver->write把数据发送串口。
用户数据发送过程如下图所示:
pppd的控制协议数据发送过程如下图所示:
反过来,接收数据的情形又是如何的?ppp_async.c在初始化时(ppp_async_init),调用tty_register_ldisc向tty注册了行规程处理接口,也就是一组回调函数,当串口tty收到数据时,它就会回调ppp_ldisc的ppp_asynctty_receive函数接收数据。ppp_asynctty_receive调用ppp_async_input把数据buffer转换成sk_buff,并放入接收队列ap->rqueue中。
ppp_async另外有一个tasklet(ppp_async_process)专门处理接收队列ap->rqueue中的数据包,ppp_async_process一直挂在接收队列ap->rqueue上,一旦被唤醒,它就调用ppp_input函数让PPP协议处理模块处理该数据包。
在ppp_input函数中,数据被分成两路,一路是控制协议数据包,放入pch->file.rqb队列,交给pppd处理。另外一路是用户数据包,经ppp_do_recv/ppp_receive_frame进行PPP处理之后,再由netif_rx提交给上层协议处理,最后经socket传递到应用程序。
数据接收过程如下图所示:
另一种说明:
(一)PPP驱动程序的基本原理
PPP 协议之下是以太网和串口等物理层,之上是IP协议等网络层。这里,对于下层,我们只讨论串口的情况,对于上层,我们只讨论TCP/IP的情况。发送时,TCP/IP数据包经过PPP打包之后经过串口发送。接收时,从串口上来的数据经PPP解包之后上报给TCP/IP协议层。
pppd是一个后台服务进程(daemon),是一个用户空间的进程,所以把策略性的内容从内核的PPP协议处理模块移到pppd中是很自然的事了。pppd实现了所有鉴权、压缩/解压和加密/解密等扩展功能的控制协议。
在移动终端向监控中心发送定位信息的过程中,移动终端上的 GPRS 通信程序通过 socket 接口发送 TCP/IP 数据包,内核根据 IP 地址和路由表,找到 PPP 网络接口,然后调用函数 ppp_start_xmit( ),此时控制权就转移到了 PPP 协议模块。函数 ppp_start_xmit( ) 调用函数 ppp_xmit_process( ) 去发送队列中的所有数据包,而函数 ppp_xmit_process( ) 会进一步调用函数ppp_send_frame( ) 去发送单个数据包。函数
ppp_send_frame( ) 根据前面 pppd 对 PPP 协议模块的设置调用压缩等扩展功能之后,又经函数 ppp_push( ) 调用函数pch->chan->ops->start_xmit( ) 发送数据包。函数pch->chan->ops->start_xmit( ) 是具体的传输方式,对于串口发送方式,则是ppp_async.c:ppp_asynctty_open 中注册的函数 ppp_async_send( ),函数
ppp_async_send( ) 经函数 ppp_async_push( ) 调用函数 tty->driver->write( )(定义在低层驱动程序中)把数据发送到串口 2(GPRS 通信模块接在串口 2 上)。
ppp_async.c 在初始化时(ppp_async_init),调用函数 tty_register_ldisc( ) 向
tty 注册了行规程 N_PPP 的处理接口,也就是一组回调函数。在移动终端接收监控中心指令的过程中,当 GPRS 通信模块收到数据时,就会回调 N_PPP 行规程中的函数 ppp_asynctty_receive( ) 来接收数据。函数ppp_asynctty_receive( ) 调用函数 ppp_async_input( ) 把数据 buffer 转换成 sk_buff,并放入接收队列 ap->rqueue 中。ppp_async
另外有一个 tasklet(ppp_async_process)专门处理接收队列 ap->rqueue 中的数据包,ppp_async_process 一直挂在接收队列 ap->rqueue 上,一旦被唤醒,它就调用函数 ppp_input( ) 让 PPP 协议模块处理该数据包。在函数 ppp_input( ) 中,数据被分成两路,一路是协议控制数据包,放入队列 pch->file.rqb 中,交给 pppd 处理。另外一路是用户数据包,经函数 ppp_do_recv( )、ppp_receive_frame(
) 进行 PPP 协议相关的处理后,再由函数 netif_rx( ) 提交给上层的 TCP/IP 协议模块进行处理,最后经 socket 接口传递给应用层的 GPRS 通信程序。
=====================
1) ppp设备是指在点对点的物理链路之间使用PPP帧进行分组交换的内核网络接口设备,
由于Linux内核将串行设备作为终端设备来驱动,
于是引入PPP终端规程来实现终端设备与PPP设备的接口.根据终端设备的物理传输特性的不同,
PPP规程分为异步规程(N_PPP)和同步规程(N_SYNC_PPP)两种,对于普通串口设备使用异步PPP规程.
2) 在PPP驱动程序中,每一tty终端设备对应于一条PPP传输通道(chanell),
每一ppp网络设备对应于一个PPP接口单元(unit).
从终端设备上接收到的数据流通过PPP传输通道解码后转换成PPP帧传递到PPP网络接口单元,
PPP接口单元再将PPP帧转换为PPP设备的接收帧.
反之,当PPP设备发射数据帧时,发射帧通过PPP接口单元转换成PPP帧传递给PPP通道,
PPP通道负责将PPP帧编码后写入终端设备.
在配置了多链路PPP时(CONFIG_PPP_MULTILINK),多个PPP传输通道可连接到同一PPP接口单元.
PPP接口单元将PPP帧分割成若干个片段传递给不同的PPP传输通道,反之,
PPP传输通道接收到的PPP帧片段被PPP接口单元重组成完整的PPP帧.
3) 在Linux-2.4中,应用程序可通过字符设备/dev/ppp监控内核PPP驱动程序.
用户可以用ioctl(PPPIOCATTACH)将文件绑定到PPP接口单元上,来读写PPP接口单元的输出帧,
也可以用ioctl(PPPIOCATTCHAN)将文件绑定到PPP传输通道上,来读写PPP传输通道的输入帧.
4) PPP传输通道用channel结构描述,系统中所有打开的传输通道在all_channels链表中.
PPP接口单元用ppp结构描述,系统中所有建立的接口单元在all_ppp_units链表中.
当终端设备的物理链路连接成功后,用户使用ioctl(TIOCSETD)将终端切换到PPP规程.
PPP规程初始化时,将建立终端设备的传输通道和通道驱动结构.对于异步PPP规程来说,
通道驱动结构为asyncppp,它包含通道操作表async_ops.
传输通道和接口单元各自包含自已的设备文件(/dev/ppp)参数结构(ppp_file).
/dev/ppp
设备文件/dev/ppp。通过read系统调用,pppd可以读取PPP协议处理模块的数据包,当然,PPP协议处理模块只会把应该由pppd处理的数据包发给pppd。通过write系统调用,pppd可以把要发送的数据包传递给PPP协议处理模块。通过ioctrl系统调用,pppd可以设置PPP协议的参数,可以建立/关闭连接。在pppd里,每种协议实现都在独立的C文件中,它们通常要实现protent接口,该接口主要用于处理数据包,和fsm_callbacks接口,该接口主要用于状态机的状态切换。数据包的接收是由main.c:get_input统一处理的,然后根据协议类型分发到具体的协议实现上。而数据包的发送则是协议实现者根据需要调用output函数完成的。
staticconst struct file_operationsppp_device_fops= {
.owner =THIS_MODULE,
.read =ppp_read,
.write =ppp_write,
.poll =ppp_poll,
.unlocked_ioctl = ppp_ioctl,
.open =ppp_open,
.release =ppp_release
};
()ppp_init(void)
err= register_chrdev(PPP_MAJOR,"ppp", &ppp_device_fops);
()ppp_async_init(void)
tty_register_ldisc(N_PPP,&ppp_ldisc);
(二)ppp相关数据结构
struct ppp{
structppp_file file; /* stuff for read/write/poll 0 */
structfile *owner; /* file that owns this unit 48 */
struct list_head channels;
/* list of attached channels 4c */
int n_channels; /* how many channels are attached 54 */
spinlock_t rlock; /*lock for receive side 58 */
spinlock_t wlock; /*lock for transmit side 5c*/
int mru; /*max receive unit 60 */
unsignedint flags; /* control bits 64 */
unsignedint xstate; /* transmit state bits 68 */
unsignedint rstate; /* receive state bits 6c */
int debug; /*debug flags 70 */
structslcompress *vj; /* state for VJheader compression */
enumNPmode npmode[NUM_NP]; /* what to do with each net proto 78 */
structsk_buff *xmit_pending; /* a packet ready to go out 88 */
structcompressor *xcomp; /* transmit packetcompressor 8c */
void *xc_state; /* its internal state 90 */
structcompressor *rcomp; /* receivedecompressor 94 */
void *rc_state; /* its internal state 98 */
unsignedlong last_xmit; /* jiffies when last pkt sent 9c */
unsignedlong last_recv; /* jiffies when last pkt rcvd a0 */
struct net_device *dev;
/* network interface device a4 */
int closing; /* is device closing down? a8 */
#ifdef CONFIG_PPP_MULTILINK
int nxchan; /* next channel to send something on */
u32 nxseq; /*next sequence number to send */
int mrru; /*MP: max reconst. receive unit */
u32 nextseq; /* MP: seq no of next packet */
u32 minseq; /*MP: min of most recent seqnos */
structsk_buff_head mrq; /* MP: receivereconstruction queue */
#endif /* CONFIG_PPP_MULTILINK */
#ifdef CONFIG_PPP_FILTER
structsock_filter *pass_filter; /* filterfor packets to pass */
structsock_filter *active_filter;/* filter for pkts to reset idle */
unsignedpass_len, active_len;
#endif /* CONFIG_PPP_FILTER */
struct net *ppp_net; /* the net we belong to */
};
struct channel{
struct ppp_file file; /*stuff for read/write/poll */
structlist_head list; /* link inall/new_channels list */
structppp_channel *chan; /* public channeldata structure */
structrw_semaphore chan_sem; /* protects`chan' during chan ioctl */
spinlock_t downl; /*protects `chan', file.xq dequeue */
struct ppp *ppp; /* ppp unit we're connected to */
structnet *chan_net; /* the net channel belongs to */
structlist_head clist; /* link inlist of channels per unit */
rwlock_t upl; /*protects `ppp' */
#ifdef CONFIG_PPP_MULTILINK
u8 avail; /* flag used in multilink stuff */
u8 had_frag; /* >= 1 fragments have been sent */
u32 lastseq; /* MP: last sequence # received */
int speed; /*speed of the corresponding ppp channel*/
#endif /* CONFIG_PPP_MULTILINK */
};
struct ppp_file {
enum{
INTERFACE=1,CHANNEL
} kind;
struct sk_buff_head xq; /*pppd transmit queue */ /*传输队列*/
struct sk_buff_head rq; /*receive queue for pppd */ /*发送队列*/
wait_queue_head_trwait; /* for poll on reading/dev/ppp */
atomic_t refcnt; /*# refs (incl /dev/ppp attached) */
int hdrlen; /* space to leave for headers */
int index; /*interface unit / channel number */
int dead; /*unit/channel has been shut down */
};
struct ppp_channel {
void *private; /* channel private data */
struct ppp_channel_ops *ops; /* operations for this channel */
int mtu; /*max transmit packet size */
int hdrlen; /* amount of headroom channel needs */
void *ppp; /*opaque to channel */
int speed; /*transfer rate (bytes/second) */
/*the following is not used at present */
int latency; /* overhead time in milliseconds */
};
struct ppp_channel_ops {
/*Send a packet (or multilink fragment) on this channel.
Returns 1 if it was accepted, 0 if not. */
int (*start_xmit)(struct ppp_channel *, struct sk_buff *);
/*Handle an ioctl call that has come in via /dev/ppp. */
int (*ioctl)(struct ppp_channel *, unsigned int, unsigned long);
};
static struct ppp_channel_ops async_ops = {
ppp_async_send,
ppp_async_ioctl
};
(三)ppp内核发送数据过程
应用程序通过socket接口发送TCP/IP数据包,这些TCP/IP数据包如何流经PPP协议处理模块,然后通过串口发送出去呢?pppd在make_ppp_unit函数调用ioctrl(PPPIOCNEWUNIT)创建一个网络接口(如ppp0),内核中的PPP协议模块在处理PPPIOCNEWUNIT时,调用register_netdev向内核注册ppp的网络接口,该网络接口的传输函数指向ppp_start_xmit。当应用程序发送数据时,内核根据IP地址和路由表,找到ppp网络接口,然后调用ppp_start_xmit函数,此时控制就转移到PPP协议处理模块了。ppp_start_xmit调用函数ppp_xmit_process去发送队列中的所有数据包,ppp_xmit_process又调用ppp_send_frame去发送单个数据包,
ppp_send_frame根据设置,调用压缩等扩展处理之后,又经ppp_push调用pch->chan->ops->start_xmit发送数据包。pch->chan->ops->start_xmit是什么?它就是具体的传输方式了,比如说对于串口发送方式,则是ppp_async.c:
ppp_asynctty_open中注册的ppp_async_send函数,ppp_async_send经ppp_async_push函数调用tty->driver->write把数据发送串口。
ppp_start_xmit(struct sk_buff *skb, structnet_device *dev)
ppp_xmit_process(struct ppp *ppp)
ppp_push(struct ppp *ppp)
pch->chan->ops->start_xmit(pch->chan,skb)
ppp_async_send(struct ppp_channel *chan,struct sk_buff *skb)
ppp_async_push(struct asyncppp *ap)
tty->ops->write(tty, ap->optr,avail)
(四)ppp内核接受数据过程
接收数据的情形又是如何的?ppp_async.c在初始化(ppp_async_init),调用tty_register_ldisc向tty注册了行规程处理接口,也就是一组回调函数,当串口tty收到数据时,它就会回调ppp_ldisc的
ppp_asynctty_receive函数接收数据。ppp_asynctty_receive调用ppp_async_input把数据buffer转换成sk_buff,并放入接收队列ap->rqueue中。ppp_async另外有一个tasklet(ppp_async_process)专门处理接收队列ap->rqueue中的数据包,ppp_async_process一直挂在接收队列ap->rqueue上,一旦被唤醒,它就调用ppp_input函数让PPP协议处理模块处理该数据包。
在ppp_input函数中,数据被分成两路,一路是控制协议数据包,放入pch->file.rqb队列,交给pppd处理。另外一路是用户数据包,经ppp_do_recv/ppp_receive_frame进行PPP处理之后,再由netif_rx提交给上层协议处理,最后经
socket传递到应用程序。
ppp_asynctty_receive(struct tty_struct*tty, const unsigned char *buf,
char *cflags, int count)
ppp_async_input(ap, buf, cflags, count);
ppp_async_process(unsigned long arg)
ppp_input(struct ppp_channel *chan, structsk_buff *skb)
{
if (!pch->ppp || proto >= 0xc000 ||proto == PPP_CCPFRAG) {
/*put it on the channel queue */
skb_queue_tail(&pch->file.rq, skb); //是控制协议数据包,放入pch->file.rqb队列,交给pppd处理。
/*drop old frames if queue too long */
while(pch->file.rq.qlen > PPP_MAX_RQLEN
&& (skb =skb_dequeue(&pch->file.rq)))
kfree_skb(skb);
wake_up_interruptible(&pch->file.rwait);
}
else {
ppp_do_recv(pch->ppp, skb, pch); //进行PPP处理之后,再由netif_rx提交给上层协议处理
}
}
(五)pppd代码整体框架流程
整个程序的主体实现是从主函数的LCP_OPEN()开始的,在这个函数里,调用了有限状态机FSM_OPEN(),而在FSM_OPEN()中,callback指针指向了starting,于是就到了LCP_STARTING()函数来实现一个OPEN事件从而使得PPP状态准备从DEAD到ESTABLISHED的转变。接下来,回到主函数,下面一步是调用START_LINK(),在此函数中会把一个串口设备作为PPP的接口,并把状态转变为ESTABLISHED,然后调用lcp_lowerup()来告诉上层底层已经UP,lcp_lowerup()中调用FSM_LOWERUP()来发送一个configure-request请求,再把当前状态设置为REQSENT状态,至此,第一个LCP协商的报文已经发送出去。
接下来的流程实现主要就是在这个while循环中实现了。之前说过了我们已经发送了第一个配置协商报文,所以handle_events()主要就是做等待接收数据包的时间处理了,在handle_events()里主要调用了两个函数一个是wait_input(),他的任务是等待并判断是否超时。还有一个是calltimeout()他主要是做超时的处理。当等待并未超时而且有数据包过来,则调用整个PPPD中最重要的函数get_input()函数。他主要接收过来的数据包并做出相应的动作。接下来就get_input()函数进行详细的说明,首先对包进行判断,丢弃所有不在LCP阶段和没有OPENED状态的包,然后protop指针指向当前协议的input函数。于是就进入了LCP_INPUT(),同理LCP_INPUT()调用了FSM_INPUT()对收到的包进行代码域的判断,判断收到的是什么包。假设比较顺利,我们收到的是CONFACK的包,于是调用fsm_rconack()函数,在此函数中根据当前自身的状态来决定下一步的状态如何改变,这里我们假设也很顺利,已经发送完了configure-ack,因此我们把FSM当前状态变成了OPENED状态,并把callback指针指向UP.所以我们马上就调用LCP_UP()在那里我们又调用了link_established()函数来进入认证的协商,或者如果没有认证则直接进入网络层协议。当然这里我们还是要认证的所有在LINK_ESTABLISHED()里我们选择是利用何种认证方式是PAP,还是EAP,还是CHAP.假设我们这里采用CHAP而且是选择CHAPWITH
PEER,意思是等待对端先发送CHALLENGE挑战报文。于是我们又调用了chap_auth_peer()函数,并等待接收挑战报文。于是从新又来到handle_events()等待接收。再利用get_input()来接收包,在get_input()里这次调用chap_input(),再调用FSM_INPUT(),在那里我们再对包的代码域进行判断,这次判断出是CHAP_CHALLENGE包,则我们要调用chap_respond()函数来回应对端,继续等待对方的报文,再次利用CHAP_INPUT(),FSM_INPUT()来判断,如果是SUCCESS,则调用chap_handle_status(),在这个函数里调用auth_withpeer_success函数,从而进入网络层阶段,调用network_phase()函数。网络层的互动是从start_networks()开始的,如果在网络层阶段同时有CCP协议(压缩控制协议)则进行压缩控制协议的协商,然后再进入正式的IPCP的协商,而