打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Linux PPPoE
Linux下的 PPPoE 
2013-04-16 15:08 858人阅读 评论(0)  举报
目录(?)[+]
一、 PPPoE 整体实现框架
图1:整体框架
PPPoE 程序
首先PPPoE完成PPPoE的发现阶段(即相互通知MAC地址),接着这个程序负责发送/接收所有通过ppp网络接口(如ppp0)的数据包。
在会话阶段,PPPoE从网口和stdin接收数据,向stdout发送数据。(PPPD程序在启动PPPoE程序时,将PPPoE程序的stdin和stdout都重定向到PTY的主设备)
PPPD 程序
与PPPoE 程序配合起来完成拨号上网的协商与维护。
/dev/ppp
创建了ppp设备后,PPP过程的数据包经过协议栈的分类,会被传送到该接口的队列内。PPPD从该接口读取PPP过程的数据包,然后交给相应的协议栈处理。
对于响应的数据包同样写入该设备,设备内会将数据包交给协议栈然后转发出去。
socket
PPPoE的会话与发现阶段数据包对应的以太网类型分别为0x8863和0x8864,内核中定义了这两种类型的socket。
PPP协议栈
主要负责PPP层的封装、压缩与解压。同时,它还对普通数据包和PPP过程的数据包进行了分流,将普通数据包提交给TCP/IP协议栈,而将PPP过程的数据包交给/dev/ppp设备队列中,等待PPPD去收取处理。
PTY设备
串行设备,PPP内核协议栈与PPPoE应用程序的中转站。因为PPP协议早多运行在串行链路上,所以在Linux内核中PPP协议栈与串行设备结合紧密。
伪终端的使用是成对出现的,分为 master 和 slaver 。写入主设备的信息,可以从从设备上读出;写入从设备的信息,可以从主设备读出。
二、 PPPoE的实现
客户端
rp-pppoe
PPPoE 背景
传统的PPP连接是基于点到点的,而在以太网是muti-accesss,即在以太网中的任一节点可以访问其他节点。以太网中的Frame包含着目的节点的MAC地址以找到目的节点。 所以在转换PPP frame之前,两个通信节点必须事先知道对方的MAC地址。
PPPoE的两个阶段
DISCOVERY阶段:以太网中的节点交换通知MAC地址,并建立一个Session ID供后续的包交换使用。处理函数为discovery()
SESSION阶段:当节点之间知道了相互的MAC地址后,就进入了SESSION阶段。处理函数为session()
三、 PPP的实现
图2: PPP协商过程
PPP主要由两类协议组成:链路控制协议族(LCP)和网络控制协议族(NCP)
LCP用于建立、拆除和监控PPP数据链路。
NCP主要用于协商在该数据链路上所传输的数据包的格式与类型。
同时,PPP还提供了用于网络安全方面的验证协议族(PAP和CHAP)。
PPPD的源代码实现
在pppd里,每种协议实现都在独立的c文件中,它们通常要实现protent接口(该接口主要用于处理数据包)和fsm_callbacks接口(该接口主要用于状态机的状态切换)。
数据包的接收是由main.c:get_input统一处理的,然后根据协议类型分发到具体的协议实现上。
数据包的发送是协议实现者根据需要调用output函数完成的。
四、 疑问
1. PPPD 程序中都是从/dev/ppp设备文件接收和发送数据,/dev/ppp和tty是如何关联的?
首先介绍一下tty discipline 。终端设备驱动架构一般分为三层。其中tty discipline是用来对接收或者发送的数据进行预处理。
图3:终端设备驱动架构
接下来就是pppd中,/dev/ppp 与tty 关联的代码片段
[cpp] view plaincopy
/*
*  pppd/src/sys-linux.c
*/
int tty_establish_ppp (int tty_fd)
{
......
......
// 设置线路规程为PPP Discipline
// 同时,tty驱动会回调ppp_ldisc的函数ppp_asynctty_open() 来使tty_fd 与channel_index 建立联系。
ppp_disc = (new_style_driver && sync_serial)? N_SYNC_PPP: N_PPP;
if (ioctl(tty_fd, TIOCSETD, &ppp_disc) < 0) {
if ( ! ok_error (errno) ) {
error("Couldn't set tty to PPP discipline: %m");
return -1;
}
}
ret_fd = generic_establish_ppp(tty_fd);
......
......
}
int generic_establish_ppp (int fd)
{
int x;
if (new_style_driver) {
int flags;
/* Open an instance of /dev/ppp and connect the channel to it */
if (ioctl(fd, PPPIOCGCHAN, &chindex) == -1) {
error("Couldn't get channel number: %m");
goto err;
}
dbglog("using channel %d", chindex);
// 打开/dev/ppp设备
fd = open("/dev/ppp", O_RDWR);
if (fd < 0) {
error("Couldn't reopen /dev/ppp: %m");
goto err;
}
(void) fcntl(fd, F_SETFD, FD_CLOEXEC);
// 建立/dev/ppp设备文件与channel_index的联系
// 这时向该设备文件写入数据,PPP内核驱动就会知道要使用到的tty设备
if (ioctl(fd, PPPIOCATTCHAN, &chindex) < 0) {
error("Couldn't attach to channel %d: %m", chindex);
goto err_close;
}
......
......
}
2. pppd 创建网络接口(如ppp0)如何与tty相关联?
pppd在make_ppp_unit函数中调用ioctrl(PPPIOCNEWUNIT)创建一个网络接口(如ppp0),内核中的PPP协议模块在处理PPPIOCNEWUNIT时,调用register_netdev向内核注册ppp的网络接口,该网络接口的传输函数指向ppp_start_xmit。
[cpp] view plaincopy
/*
* pppd/src/sys-linux.c
*/
int generic_establish_ppp (int fd)
{
......
......
if (!looped && !multilink) {
/*
* Create a new PPP unit.
*/
if (make_ppp_unit() < 0)
goto err_close;
}
if (looped)
modify_flags(ppp_dev_fd, SC_LOOP_TRAFFIC, 0);
if (!multilink) {
add_fd(ppp_dev_fd);
// 该fd = tty_fd
// 通过ifunit 与 tty设备建立连接,当应用程序通过网络接口(比如ppp0)发送数据时
// PPP内核驱动就知道最终数据要写往哪个tty设备
if (ioctl(fd, PPPIOCCONNECT, &ifunit) < 0) {
error("Couldn't attach to PPP unit %d: %m", ifunit);
goto err_close;
}
}
......
......
}
static int make_ppp_unit()
{
int x, flags;
if (ppp_dev_fd >= 0) {
dbglog("in make_ppp_unit, already had /dev/ppp open?");
close(ppp_dev_fd);
}
// 打开设备文件
ppp_dev_fd = open("/dev/ppp", O_RDWR);
if (ppp_dev_fd < 0)
fatal("Couldn't open /dev/ppp: %m");
flags = fcntl(ppp_dev_fd, F_GETFL);
if (flags == -1
|| fcntl(ppp_dev_fd, F_SETFL, flags | O_NONBLOCK) == -1)
warn("Couldn't set /dev/ppp to nonblock: %m");
ifunit = req_unit;
// 创建一个网络接口(如ppp0)
// 调用register_netdev向内核注册ppp的网络接口,该网络接口的传输函数指向ppp_start_xmit。
x = ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit);
if (x < 0 && req_unit >= 0 && errno == EEXIST) {
warn("Couldn't allocate PPP unit %d as it is already in use", req_unit);
ifunit = -1;
x = ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit);
}
if (x < 0)
error("Couldn't create new ppp unit: %m");
return x;
}
3. 普通应用程序通过socket接口发送TCP/IP数据包,这些TCP/IP数据包如何流经PPP协议处理模块,然后通过串口发送出去呢?
当应用程序发送数据时,内核根据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->ops->write把数据发送串口。
4. 普通应用程序接收数据的情形又是如何的?
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传递到应用程序。
五、 附录
1. 内核源代码
kernel/drivers/net/ppp/ppp_async.c
kernel/drivers/net/ppp/ppp_generic.c
kernel/drivers/tty/tty_io.c
kernel/drivers/tty/tty_ldisc.c
kernel/drivers/tty/tty_ioctl.c
2. 参考
https://zh.wikipedia.org/wiki/PPPoE
http://en.wikipedia.org/wiki/Point-to-point_protocol
http://en.wikipedia.org/wiki/Line_discipline
http://blog.csdn.net/absurd/article/details/1596496
http://blog.csdn.net/xuxinyl/article/details/6536281
http://blog.csdn.net/moazhen/article/details/1096849
http://www.linusakesson.net/programming/tty/
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
手把手实现tcp/ip用户态协议栈,帮你实践网络知识(网络必备,面试项目)
设计模式-半同步半异步(Half-Sync/Half-Async)
【转】Linux内核网络协议栈笔记4:接收网络数据包详细过程
PPP协议体系的实现
深入分析原始套接口
LwIP源代码文件目录解析
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服