1. 程式人生 > >Linux PPP實現原始碼分析

Linux PPP實現原始碼分析

原文連結請參考: http://blog.csdn.net/osnetdev/article/details/8958058

©所有版權保留

轉載請保留作者署名,嚴禁用於商業用途 。

前言:

PPP(Point to Point Protocol)協議是一種廣泛使用的資料鏈路層協議,在國內廣泛使用的寬頻撥號協議PPPoE其基礎就是PPP協議,此外和PPP相關的協議PPTP,L2TP也常應用於VPN虛擬專用網路。隨著智慧手機系統Android的興起,PPP協議還被應用於GPRS撥號,3G/4G資料通路的建立,在嵌入式通訊裝置及智慧手機中有著廣泛的應用基礎。本文主要分析Linux中PPP協議實現的關鍵程式碼和基本資料收發流程,對PPP協議的詳細介紹請自行參考RFC和相關協議資料。

模組組成:


上圖為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:

第一階段:

pppd/main.c -> main():

……

new_phase(PHASE_INITIALIZE)//PPPD中的狀態機,目前是初始化階段

    /*

* Initialize magic number generator now so that protocols may

* use magic numbers in initialization.

*/

    magic_init();

    /*

* Initialize each protocol.

*/

    for(i=0;(protp=protocols[i])!= NULL;++i//protocols[]是全域性變數的協議陣列

        (*protp->init)(0)//初始化協議陣列中所有協議

    /*

* Initialize the default channel.

*/

    tty_init()//channel初始化,預設就是全域性的tty_channel,裡面包括很多TTY函式指標   

    if(!options_from_file(_PATH_SYSOPTIONS,!privileged,0,1)//解析/etc/ppp/options中的引數

       ||!options_from_user() 

       ||!parse_args(argc-1,argv+1)) //解析PPPD命令列引數

       exit(EXIT_OPTION_ERROR);

    devnam_fixed=1;       /* can no longer change device name */

    /*

* Work out the device name, if it hasn't already been specified,

* and parse the tty's options file.

*/

    if(the_channel->process_extra_options)

       (*the_channel->process_extra_options)()//實際上是呼叫tty_process_extra_options解析TTY 引數

    if(!ppp_available())//檢測/dev/ppp裝置檔案是否有效

       option_error("%s",no_ppp_msg);

       exit(EXIT_NO_KERNEL_SUPPORT);

    }

    /*

* Check that the options given are valid and consistent.

*/

    check_options()//檢查選項引數

    if(!sys_check_options()) //檢測系統引數,比如核心是否支援Multilink等

       exit(EXIT_OPTION_ERROR);

    auth_check_options()//檢查認證相關的引數

#ifdef HAVE_MULTILINK

mp_check_options();

#endif

    for(i=0;(protp=protocols[i])!= NULL;++i)

       if(protp->check_options!= NULL)

           (*protp->check_options)()//檢查每個控制協議的引數配置

    if(the_channel->check_options)

       (*the_channel->check_options)()//實際上是呼叫tty_check_options檢測TTY引數

……

    /*

* Detach ourselves from the terminal, if required,

* and identify who is running us.

*/

    if(!nodetach&&!updetach

       detach()//預設放在後臺以daemon執行,也可配置/etc/ppp/option中的nodetach引數放在前臺執行

……

    syslog(LOG_NOTICE,"pppd %s started by %s, uid %d",VERSION,p,uid)//熟悉的log,現在準備執行了

    script_setenv("PPPLOGNAME",p,0);

    if(devnam[0])

       script_setenv("DEVICE",devnam,1);

    slprintf(numbuf,sizeof(numbuf),"%d",getpid());

    script_setenv("PPPD_PID",numbuf,1);

    setup_signals()//設定訊號處理函式

    create_linkpidfile(getpid())//建立PID檔案

    waiting=0;

    /*

* If we're doing dial-on-demand, set up the interface now.

*/

    if(demand)//以按需撥號方式執行,可配置

       /*

 * Open the loopback channel and set it up to be the ppp interface.

 */

       fd_loop=open_ppp_loopback()//詳見下面分析

       set_ifunit(1)//設定IFNAME環境變數為介面名稱如ppp0

       /*

 * Configure the interface and mark it up, etc.

 */

       demand_conf();

}

(第二階段)……

PPP協議裡包括各種控制協議如LCP,PAP,CHAP,IPCP等,這些控制協議都有很多共同的地方,因此PPPD將每個控制協議都用結構protent表示,並放在控制協議陣列protocols[]中,一般常用的是LCP,PAP,CHAP,IPCP這四個協議。

/*

* PPP Data Link Layer "protocol" table.

* One entry per supported protocol.

* The last entry must be NULL.

*/

struct protent*protocols[]={

    &lcp_protent//LCP協議

    &pap_protent//PAP協議

    &chap_protent//CHAP協議

#ifdef CBCP_SUPPORT

&cbcp_protent,

#endif

    &ipcp_protent//IPCP協議,IPv4

#ifdef INET6

&ipv6cp_protent,//IPCP協議,IPv6

#endif

    &ccp_protent,

    &ecp_protent,

#ifdef IPX_CHANGE

&ipxcp_protent,

#endif

#ifdef AT_CHANGE

&atcp_protent,

#endif

    &eap_protent,

    NULL

};

每個控制協議由protent結構來表示,此結構包含每個協議處理用到的函式指標:

/*

* The following struct gives the addresses of procedures to call

* for a particular protocol.

*/

struct protent{

    u_short protocol;            /* PPP protocol number */

    /* Initialization procedure */

    void(*init)__P((int unit));  //初始化指標,在main()中被呼叫

    /* Process a received packet */

    void(*input)__P((int unit, u_char *pkt,int len))//接收報文處理

    /* Process a received protocol-reject */

    void(*protrej)__P((int unit));  //協議錯誤處理

    /* Lower layer has come up */

    void(*lowerup)__P((int unit));  //當下層協議UP起來後的處理

    /* Lower layer has gone down */

    void(*lowerdown)__P((int unit));  //當下層協議DOWN後的處理

    /* Open the protocol */

    void(*open)__P((int unit));  //開啟協議

    /* Close the protocol */

    void(*close)__P((int unit,char*reason))//關閉協議

    /* Print a packet in readable form */

    int (*printpkt)__P((u_char*pkt,int len,

                       void(*printer)__P((void*,char*,...)),

                       void*arg))//列印報文資訊,除錯用。

    /* Process a received data packet */

    void(*datainput)__P((int unit, u_char *pkt,int len))//處理已收到的資料包

    boolenabled_flag;         /* 0 iff protocol is disabled */

    char*name;                  /* Text name of protocol */

    char*data_name;          /* Text name of corresponding data protocol */

    option_t*options;        /* List of command-line options */

    /* Check requested options, assign defaults */

    void(*check_options)__P((void))//檢測和此協議有關的選項引數

    /* Configure interface for demand-dial */

    int (*demand_conf)__P((int unit));  //將介面配置為按需撥號需要做的 動作

    /* Say whether to bring up link for this pkt */

    int (*active_pkt)__P((u_char*pkt,int len))//判斷報文型別並激活鏈路

};

在main()函式中會呼叫所有支援的控制協議的初始化函式init(),之後初始化TTY channel,解析配置檔案或命令列引數,接著檢測核心是否支援PPP驅動:

pppd/sys_linux.c

main() -> ppp_avaiable():

intppp_available(void)

{

……

    no_ppp_msg=

       "This system lacks kernel support for PPP. This could be because\n"

       "the PPP kernel module could not be loaded, or because PPP was not\n"

       "included in the kernel configuration. If PPP was included as a\n"

       "module, try `/sbin/modprobe -v ppp'. If that fails, check that\n"

       "ppp.o exists in /lib/modules/`uname -r`/net.\n"

       "See README.linux file in the ppp distribution for more details.\n";

    /* get the kernel version now, since we are called before sys_init */

    uname(&utsname);

    osmaj=osmin=ospatch=0;

    sscanf(utsname.release,"%d.%d.%d",&osmaj,&osmin,&ospatch);

kernel_version=KVERSION(osmaj,osmin,ospatch);

    fd=open("/dev/ppp", O_RDWR);

    if(fd>=0){

       new_style_driver=1//支援PPPK

       /* XXX should get from driver */

       driver_version=2;

       driver_modification=4;

       driver_patch=0;

       close(fd);

       return1;

}

……

}

函式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:

pppd/sys-linux.c

main() -> open_ppp_loopback():

int

open_ppp_loopback(void)

{

    intflags;

    looped=1//設定全域性變數looped為1,後面會用到

    if(new_style_driver){

       /* allocate ourselves a ppp unit */

       if(make_ppp_unit()<0//建立PPP網路介面

           die(1);

       modify_flags(ppp_dev_fd,0, SC_LOOP_TRAFFIC)//通過ioctl設定SC_LOOP_TRAFFIC

       set_kdebugflag(kdebugflag);

       ppp_fd=-1;

       returnppp_dev_fd;

    }

……(下面是old driver,忽略)

}

全域性變數new_style_driver,這個變數已經在ppp_avaliable函式裡被設定為1了。接下來呼叫make_ppp_unit開啟/dev/ppp裝置檔案並請求建立一個新的unit。

pppd/sys-linux.c

main() -> open_ppp_loopback() -> make_ppp_unit():

staticintmake_ppp_unit()

{

       intx,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);  //開啟/dev/ppp

       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//傳入請求的unit number,可通過/etc/ppp/options配置

       x=ioctl(ppp_dev_fd, PPPIOCNEWUNIT,&ifunit)//請求建立一個新unit

       if(x<0&&