天天看點

Linux PPP實作源碼分析-good

轉自 http://blog.csdn.net/osnetdev/article/details/8958058

Linux PPP實作源碼分析

作者:kwest <[email protected]>  版本:v0.7

©所有版權保留

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

前言:

PPP(Point to Point Protocol)協定是一種廣泛使用的資料鍊路層協定,在國内廣泛使用的寬帶撥号協定PPPoE其基礎就是PPP協定,此外和PPP相關的協定PPTP,L2TP也常應用于VPN虛拟專用網絡。随着智能手機系統Android的興起,PPP協定還被應用于GPRS撥号,3G/4G資料通路的建立,在嵌入式通信裝置及智能手機中有着廣泛的應用基礎。本文主要分析Linux中PPP協定實作的關鍵代碼和基本資料收發流程,對PPP協定的詳細介紹請自行參考RFC和相關協定資料。

子產品組成:

Linux PPP實作源碼分析-good

上圖為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中的狀态機,目前是初始化階段

    magic_init();

    for(i=0;(protp=protocols[i])!= NULL;++i) //protocols[]是全局變量的協定數組

        (*protp->init)(0); //初始化協定數組中所有協定

    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;       

    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_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參數

……

    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(demand){ //以按需撥号方式運作,可配置

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

       set_ifunit(1); //設定IFNAME環境變量為接口名稱如ppp0

       demand_conf();

}

(第二階段)……

PPP協定裡包括各種控制協定如LCP,PAP,CHAP,IPCP等,這些控制協定都有很多共同的地方,是以PPPD将每個控制協定都用結構protent表示,并放在控制協定數組protocols[]中,一般常用的是LCP,PAP,CHAP,IPCP這四個協定。

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結構來表示,此結構包含每個協定處理用到的函數指針:

struct protent{

    u_short protocol;            

    void(*init)__P((int unit));  //初始化指針,在main()中被調用

    void(*input)__P((int unit, u_char *pkt,int len)); //接收封包處理

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

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

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

    void(*open)__P((int unit));  //打開協定

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

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

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

                       void*arg)); //列印封包資訊,調試用。

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

    boolenabled_flag;         

    char*name;                  

    char*data_name;          

    option_t*options;        

    void(*check_options)__P((void)); //檢測和此協定有關的選項參數

    int (*demand_conf)__P((int unit));  //将接口配置為按需撥号需要做的 動作

    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";

    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

       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){

       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&&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");

       returnx;

}

這裡的unit可以了解為一個PPP接口,在Linux中通過ifconfig看到的ppp0就是通過ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit)建立起來的,unit number是可以配置的,不過一般都不用配置,傳入-1會自動配置設定一個未使用的unit number,預設從0開始。這個ioctl調用的是PPPK中注冊的ppp_ioctl:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> open_ppp_loopback() -> make_ppp_unit() -> ioctl(ppp_dev_fd,PPPIOCNEWUNIT,&ifunit) -> ppp_ioctl():

staticintppp_ioctl(struct inode *inode,struct file*file,

                   unsignedintcmd,unsignedlongarg)

{

       struct ppp_file*pf=file->private_data;

……

       if(pf==0)

              returnppp_unattached_ioctl(pf,file,cmd,arg);

TIPS:這裡還要解釋一下PPPK中channel和unit的關系,一個channel相當于一個實體鍊路,而unit相當于一個接口。在Multilink PPP中,一個unit可以由多個channel組合而成,也就是說一個PPP接口下面可以有多個實體鍊路,這裡的實體鍊路不一定是實體接口,也可以是一個實體接口上的多個頻段(channel)比如HDLC channel。

PPPK中channel用結構channel表示,unit用結構ppp表示。

linux-2.6.18/drivers/net/ppp_generic.c

struct ppp{

       struct ppp_file     file;        

       struct file     *owner;        

       struct list_headchannels;   

       int          n_channels;   

       spinlock_t    rlock;            

       spinlock_t    wlock;           

       int          mru;             

       unsignedint   flags;            

       unsignedint   xstate;          

       unsignedint   rstate;           

       int          debug;          

       struct slcompress*vj;        

       enumNPmode    npmode[NUM_NP];

       struct sk_buff      *xmit_pending;     

       struct compressor*xcomp;

       void       *xc_state;     

       struct compressor*rcomp; 

       void       *rc_state;      

       unsignedlonglast_xmit;      

       unsignedlonglast_recv;      

       struct net_device*dev;             

#ifdef CONFIG_PPP_MULTILINK

       int          nxchan;         

       u32         nxseq;          

       int          mrru;            

       u32         nextseq; 

       u32         minseq;         

       struct sk_buff_head mrq;   

#endif

       struct net_device_statsstats;     

#ifdef CONFIG_PPP_FILTER

       struct sock_filter*pass_filter;    

       struct sock_filter*active_filter;

       unsigned pass_len, active_len;

#endif

};

struct channel{

       struct ppp_file     file;        

       struct list_headlist;            

       struct ppp_channel*chan;  

       struct rw_semaphorechan_sem;

       spinlock_t    downl;           

       struct ppp    *ppp;            

       struct list_headclist;          

       rwlock_t      upl;        

#ifdef CONFIG_PPP_MULTILINK

       u8           avail;            

       u8           had_frag;      

       u32         lastseq;  

#endif

};

struct ppp_file{

       enum{

              INTERFACE=1,CHANNEL

       }           kind; //代表打開的/dev/ppp類型是channel還是unit

       struct sk_buff_headxq;          

       struct sk_buff_headrq;          

       wait_queue_head_t rwait;

       atomic_t       refcnt;          

       int         hdrlen;         

       int         index;          

       int         dead;           

};

注意這兩個結構體的第一個字段都是structppp_file,ppp_file的kind字段代表/dev/ppp的類型。

現在回到ppp_ioctl,它的執行要判定三種情況,沒有任何綁定,綁定到PPP unit或綁定到PPP channel,在初始化時并沒有任何綁定即file->private_data為空,是以這裡會調用ppp_unattached_ioctl:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> open_ppp_loopback() -> make_ppp_unit() -> ioctl(ppp_dev_fd,PPPIOCNEWUNIT,&ifunit) -> ppp_ioctl() –> ppp_unattached_ioctl():

staticintppp_unattached_ioctl(struct ppp_file *pf,struct file*file,

                            unsignedintcmd,unsignedlongarg)

{

       intunit,err=-EFAULT;

       struct ppp*ppp;

       struct channel*chan;

       int__user*p=(int__user*)arg;

       switch(cmd){

       casePPPIOCNEWUNIT:

              if(get_user(unit, p))

                     break;

              ppp=ppp_create_interface(unit,&err); //建立ppp網絡接口

              if(ppp==0)

                     break;

              file->private_data=&ppp->file; //注意:現在綁定到了PPP unit,指向的是struct ppp_file結構

              ppp->owner=file;

              err=-EFAULT;

              if(put_user(ppp->file.index,p))

                     break;

              err=0;

              break;

這個函數又會調用ppp_create_interface建立一個ppp網絡接口:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> open_ppp_loopback() -> make_ppp_unit() -> ioctl(ppp_dev_fd,PPPIOCNEWUNIT,&ifunit) -> ppp_ioctl() –> ppp_unattached_ioctl()-> ppp_create_interface():

staticstruct ppp*

ppp_create_interface(intunit,int*retp)

{

       struct ppp*ppp;

       struct net_device*dev=NULL;

       intret=-ENOMEM;

       inti;

       ppp=kzalloc(sizeof(struct ppp), GFP_KERNEL); //配置設定struct ppp即建立一個unit

       if(!ppp)

              gotoout;

       dev=alloc_netdev(0,"",ppp_setup); //配置設定net_device,這個結構表示一個網絡接口

       if(!dev)

              gotoout1;

       ppp->mru=PPP_MRU; //初始化MRU(最大接收單元)

       init_ppp_file(&ppp->file,INTERFACE); //初始化ppp_file結構,類型為INTERFACE

       ppp->file.hdrlen=PPP_HDRLEN-2;   

       for(i=0;i<NUM_NP;++i)

              ppp->npmode[i]=NPMODE_PASS;

       INIT_LIST_HEAD(&ppp->channels); //PPP接口中的channel連結清單

       spin_lock_init(&ppp->rlock); //接收隊列的鎖

       spin_lock_init(&ppp->wlock); //發送隊列的鎖

#ifdef CONFIG_PPP_MULTILINK

       ppp->minseq=-1;

       skb_queue_head_init(&ppp->mrq);

#endif

       ppp->dev=dev; //指向配置設定的net_device結構

       dev->priv=ppp; //ppp網絡接口的私有結構就是struct ppp

       dev->hard_start_xmit=ppp_start_xmit; //ppp網絡接口資料包發送函數,被TCP/IP協定棧調用

       dev->get_stats=ppp_net_stats; //收發資料包統計

       dev->do_ioctl=ppp_net_ioctl; //對ppp網絡接口調用ioctl()時使用

       ret=-EEXIST;

       mutex_lock(&all_ppp_mutex); //互斥操作,由于從程序進來的,要用可睡眠的mutex

       if(unit<0) //unit傳入-1表示自動配置設定未使用的unit number

              unit=cardmap_find_first_free(all_ppp_units);

       elseif(cardmap_get(all_ppp_units,unit)!=NULL)

              gotoout2;   

       ppp->file.index=unit; //這裡就是傳送中的unit number

       sprintf(dev->name,"ppp%d",unit); //ppp後面加上unit number就是ppp接口名,如ppp0

       ret=register_netdev(dev); //注冊ppp網絡接口,這時ifconfig才能看到這個接口

       if(ret!=0){

              printk(KERN_ERR"PPP: couldn't register device %s (%d)\n",

                     dev->name, ret);

              gotoout2;

       }

       atomic_inc(&ppp_unit_count);

       ret=cardmap_set(&all_ppp_units,unit,ppp);

       if(ret!=0)

              gotoout3;

       mutex_unlock(&all_ppp_mutex); //互斥操作,解鎖

       *retp=0;

       returnppp;

……

}

OK,現在PPP網絡接口已經建立起來了,例如建立的接口名為ppp0,這裡的ppp0還隻是一個“假接口”,其實到這裡PPP的整個撥号過程根本就還沒有開始,之是以建立這個接口隻是為了讓資料封包可以通過這個接口發送出去進而觸發PPP撥号。

 接下來回到PPPD的open_ppp_loopback,make_ppp_unit這時候成功傳回後,還會調用modify_flags函數來設定标志位SC_LOOP_TRAFFIC,這個函數其實調用的還是ioctl()->ppp_ioctl()來設定的flag。

pppd/sys-linux.c

main() -> open_ppp_loopback() –> modify_flags():

staticintmodify_flags(intfd,intclear_bits,intset_bits)

{

       intflags;

       if(ioctl(fd, PPPIOCGFLAGS,&flags)==-1)

              gotoerr;

       flags=(flags&~clear_bits)|set_bits;

       if(ioctl(fd, PPPIOCSFLAGS,&flags)==-1)

              gotoerr;

       return0;

标志位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():

linux-2.6.18/drivers/net/ppp_generic.c

staticvoid

ppp_send_frame(struct ppp*ppp,struct sk_buff*skb)

{

……

       if(ppp->flags&SC_LOOP_TRAFFIC){

              if(ppp->file.rq.qlen>PPP_MAX_RQLEN)

                     gotodrop;

              skb_queue_tail(&ppp->file.rq,skb); //發送的資料包放在rq接收對列而不是發送隊列!

              wake_up_interruptible(&ppp->file.rwait); //喚醒PPPD程序!!!

              return;

       }

……

}

很顯然,隻要ppp->flags中SC_LOOP_TRAFFIC置位,就要做點特殊處理:把發送的資料包放在接收隊列ppp->file.rq中而不是平常的發送隊列,這是為啥呢?留待過會分解。喚醒PPPD程序進行處理,并沒有将資料發送出去哦。

傳回主函數main()中,當open_ppp_loopback調用傳回後,其傳回值同時被指派給fd_loop代表/dev/ppp的檔案描述符。此時,網絡接口ppp0已建立好并注冊到TCP/IP協定棧中,當然隻有 ppp0接口還不夠,我們還需要對ppp0接口做些配置,接着調用demand_conf:

pppd/demand.c

main() -> demand_conf():

void

demand_conf()

{

    inti;

    struct protent*protp;

……

    netif_set_mtu(0,MIN(lcp_allowoptions[0].mru,PPP_MRU)); //設定ppp0接口的MTU

    if(ppp_send_config(0,PPP_MRU,(u_int32_t)0,0,0)<0

       ||ppp_recv_config(0,PPP_MRU,(u_int32_t)0,0,0)<0)

           fatal("Couldn't set up demand-dialled PPP interface: %m");

……

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

       if(protp->enabled_flag&&protp->demand_conf!= NULL)

           if(!((*protp->demand_conf)(0))) //調用每個控制協定的demand_conf函數

              die(1);

}

這個函數設定ppp0的MTU和MRU,然後調用每個控制協定的demand_conf函數。對于LCP,PAP,CHAP協定protp->demand_conf都為空, 隻有IPCP協定有初始化這個函數指針:

pppd/ipcp.c

main() -> demand_conf()-> ip_demand_conf():

staticint

ip_demand_conf(u)

    intu;

{

    ipcp_options*wo=&ipcp_wantoptions[u];

    if(wo->hisaddr==0){

       wo->hisaddr=htonl(0x0a707070+ifunit); //對端位址

       wo->accept_remote=1;

    }

    if(wo->ouraddr==0){

       wo->ouraddr=htonl(0x0a404040+ifunit); //本端位址

       wo->accept_local=1;

       ask_for_local=0;     

    }

    if(!sifaddr(u,wo->ouraddr,wo->hisaddr,GetMask(wo->ouraddr))) //在ppp0接口上配置本端位址和對端位址及子網路遮罩

       return0;

    if(!sifup(u)) //将ppp0接口設定為UP,接口類型為點對點。

       return0;

    if(!sifnpmode(u,PPP_IP,NPMODE_QUEUE))

       return0;

    if(wo->default_route)

       if(sifdefaultroute(u,wo->ouraddr,wo->hisaddr)) //設定ppp0為預設網關接口

           default_route_set[u]=1;

    if(wo->proxy_arp)

       if(sifproxyarp(u,wo->hisaddr))

           proxy_arp_set[u]=1;

    notice("local IP address %I",wo->ouraddr);

    notice("remote IP address %I",wo->hisaddr);

    return1;

}

上面提到在按需撥号模式下,要讓資料封包通過ppp0接口發送才會觸發PPP連接配接的建立。是以這裡,IPCP協定塊提供的ip_demand_conf函數就為ppp0配置了兩個假的IP位址:本端IP位址為10.64.64.64,對端IP位址為10.112.112.112,并設定對端IP為預設網關。這樣,當使用者通路外部網絡時,Linux路由子系統會選擇ppp0接口發送資料包,進而觸發PPP鍊路的建立。

第二階段:

回到主函數main()中,接下來是最外層的for(;;)循環進行事件處理,

pppd/main.c -> main():

(第一階段)……

    do_callback=0;

    for(;;){ 

……

       doing_callback=do_callback;

       do_callback=0;

       if(demand&&!doing_callback){//按需撥号

           new_phase(PHASE_DORMANT); //PPPD狀态機

           demand_unblock();

           add_fd(fd_loop); //将fd_loop即/dev/ppp的檔案描述符加入select的fds中

           for(;;){ //嵌套for(;;)循環

              handle_events(); //select事件處理

              if(asked_to_quit)

                  break;

              if(get_loop_output()) //發送資料有效就跳出循環

                  break;

           }

           remove_fd(fd_loop); //注意:要把/dev/ppp檔案描述符從fds中remove掉,後面還會再加入

           if(asked_to_quit)

              break;

           demand_block();

           info("Starting link");

       }

(第三階段)……

    }

如果是demand撥号模式,PPPD狀态機進入PHASE_DORMANT, 主要包含兩個部分:

1.     調用add_fd将/dev/ppp的檔案描述符fd_loop加入in_fds中:

pppd/sys-linux.c:

main() -> add_fd():

voidadd_fd(intfd)

{

    if(fd>= FD_SETSIZE)

       fatal("internal error: file descriptor too large (%d)",fd);

    FD_SET(fd,&in_fds);

    if(fd>max_in_fd)

       max_in_fd=fd;

}

2.     在嵌套的for(;;)死循環裡調用handle_events函數進行事件處理。

pppd/main.c:

main() -> handle_events():

staticvoid

handle_events()

{

    struct timevaltimo;

    kill_link=open_ccp_flag=0;

    if(sigsetjmp(sigjmp,1)==0){

       sigprocmask(SIG_BLOCK,&signals_handled, NULL);

       if(got_sighup||got_sigterm||got_sigusr2||got_sigchld){

           sigprocmask(SIG_UNBLOCK,&signals_handled, NULL);

       }else{

           waiting=1;

           sigprocmask(SIG_UNBLOCK,&signals_handled, NULL);

           wait_input(timeleft(&timo)); //調用select進行I/O多路複用

       }

    }

    waiting=0;

calltimeout(); //調用注冊的timer函數

    if(got_sighup){

       info("Hangup (SIGHUP)");

       kill_link=1;

       got_sighup=0;

       if(status!=EXIT_HANGUP)

           status=EXIT_USER_REQUEST;

    }

    if(got_sigterm){ //收到SIGTERM信号時退出

       info("Terminating on signal %d",got_sigterm);

       kill_link=1;

       asked_to_quit=1; //注意

       persist=0;

       status=EXIT_USER_REQUEST;

       got_sigterm=0;

    }

    if(got_sigchld){

       got_sigchld=0;

       reap_kids(); 

    }

    if(got_sigusr2){

       open_ccp_flag=1;

       got_sigusr2=0;

    }

}

這個函數裡面重點是調用了wait_input對前面加入的/dev/ppp檔案描述符調用select監聽事件。

pppd/sys-linux.c:

main() -> handle_events()-> wait_input():

voidwait_input(struct timeval*timo)

{

    fd_set ready,exc;

    intn;

    ready=in_fds; //in_fds中包含有/dev/ppp的檔案描述符

    exc=in_fds;

    n=select(max_in_fd+1,&ready, NULL,&exc,timo); //主菜在這裡,調用select監聽事件!!!

    if(n<0&& errno != EINTR)

       fatal("select: %m");

}

還記得嗎,/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。下面分析一下這個函數:

pppd/sys-linux.c:

main() -> get_loop_output():

int

get_loop_output(void)

{

    intrv=0;

    intn;

    if(new_style_driver){

       while((n=read_packet(inpacket_buf))>0) //有資料通過ppp0發送

           if(loop_frame(inpacket_buf,n))//發送資料合法時為真

              rv=1;//傳回1,導緻嵌套的for(;;)循環退出

       returnrv;

    }

……

}

首先調用read_packet讀取資料到inpacket_buf中:

pppd/sys-linux.c:

main() -> get_loop_output() -> read_packet():

intread_packet(unsignedchar*buf)

{

    intlen,nr;

    len=PPP_MRU+PPP_HDRLEN;

    if(new_style_driver){

       *buf++=PPP_ALLSTATIONS;

       *buf++=PPP_UI;

       len-=2;

    }

    nr=-1;

    if(ppp_fd>=0){

       nr=read(ppp_fd,buf,len); //讀/dev/ppp

       if(nr<0&& errno != EWOULDBLOCK&& errno!= EAGAIN

           && errno!= EIO&& errno!= EINTR)

           error("read: %m");

       if(nr<0&& errno == ENXIO)

           return0;

    }

    if(nr<0&&new_style_driver&&ppp_dev_fd>=0&&!bundle_eof){

       nr=read(ppp_dev_fd,buf,len);

       if(nr<0&& errno != EWOULDBLOCK&& errno!= EAGAIN

           && errno!= EIO&& errno!= EINTR)

           error("read /dev/ppp: %m");

       if(nr<0&& errno == ENXIO)

           nr=0;

       if(nr==0&&doing_multilink){

           remove_fd(ppp_dev_fd);

           bundle_eof=1;

       }

    }

    if(new_style_driver&&ppp_fd<0&&ppp_dev_fd<0)

       nr=0;

    return(new_style_driver&&nr>0)?nr+2:nr;

}

這個函數很簡單,實際上就是調用标準的檔案讀函數read()讀取/dev/ppp裝置檔案,其實就是調用到PPPK中的ppp_read:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> get_loop_output() -> read_packet() -> ppp_read():

staticssize_tppp_read(struct file*file,char__user*buf,

                     size_tcount, loff_t*ppos)

{

       struct ppp_file*pf=file->private_data;

       DECLARE_WAITQUEUE(wait, current);

       ssize_tret;

       struct sk_buff*skb=NULL;

       ret=count;

       if(pf==0)

              return-ENXIO;

       add_wait_queue(&pf->rwait,&wait); //注意:加入到等待隊列,會被ppp_send_frame()喚醒

       for(;;){

              set_current_state(TASK_INTERRUPTIBLE); //設定目前程序的狀态為可中斷睡眠

              skb=skb_dequeue(&pf->rq); //從接收隊列出隊列一個資料包

              if(skb) //如果有資料包,表示有資料可讀

                     break;

              ret=0;

              if(pf->dead) //unit或channel已經不存在了,這裡不讨論

                     break;

              if(pf->kind==INTERFACE){

                     struct ppp*ppp=PF_TO_PPP(pf);

                     if(ppp->n_channels==0

                         &&(ppp->flags&SC_LOOP_TRAFFIC)==0)

                            break;

              }

              ret=-EAGAIN;

              if(file->f_flags& O_NONBLOCK)//若fd為O_NONBLOCK,則不睡眠,直接跳出循環

                     break;

              ret=-ERESTARTSYS;

              if(signal_pending(current)) //收到signal也不睡眠,直接跳出循環

                     break;

              schedule(); //程序排程器,讓目前程序睡眠

       }

       set_current_state(TASK_RUNNING);

       remove_wait_queue(&pf->rwait,&wait);

       if(skb==0) //能到這裡表明fd為O_NONBLOCK或收到signal

              gotoout;

       ret=-EOVERFLOW;

       if(skb->len>count)

              gotooutf;

       ret=-EFAULT;

       if(copy_to_user(buf,skb->data,skb->len)) //将資料拷貝到使用者緩沖區

              gotooutf;

       ret=skb->len; //傳回值就是資料長度

 outf:

       kfree_skb(skb);

 out:

       returnret;

}

這個函數要把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會被調用:

pppd/demand.c

main() -> get_loop_output() -> loop_frame():

int

loop_frame(frame,len)

    unsignedchar*frame;

    intlen;

{

    struct packet*pkt;

    if(len<PPP_HDRLEN)

       return0;

    if((PPP_PROTOCOL(frame)&0x8000)!=0)

       return0;             

    if(!active_packet(frame,len)) //檢測發送的資料是否有效

       return0;

    pkt=(struct packet*)malloc(sizeof(struct packet)+len);

    if(pkt!= NULL){

       pkt->length=len;

       pkt->next= NULL;

       memcpy(pkt->data,frame,len);

       if(pend_q== NULL)

           pend_q=pkt;

       else

           pend_qtail->next=pkt;

       pend_qtail=pkt;

    }

    return1;

}

這裡實際上最後是調用IPCP協定塊的ip_active_pkt函數來檢查資料包有效性,這裡就不具體分析了。如果發送資料是合法的IP封包,後面會儲存這些資料包,并暫時放在pend_qtail隊列中,留待PPP鍊路建立後重新發送。

第三階段:

如果是demand撥号模式,并且假設有資料通過ppp0發送且是合法IP封包,第二階段中的嵌套for(;;)循環會被跳出,接下來的代碼和正常撥号模式就一樣了,真是殊途同歸啊。

再次回到主函數main() 中,我們要開始建立真正的PPP鍊路了:

pppd/main.c -> main():

……

    do_callback=0;

    for(;;){ 

……

       lcp_open(0);        //第一步:打開PPPK接口發送LCP幀

       while(phase!=PHASE_DEAD){ //第二步:PPPD狀态機循環進行事件處理

           handle_events(); //select事件處理

           get_input(); //對接收封包的處理

           if(kill_link)

              lcp_close(0,"User request");

           if(asked_to_quit){

              bundle_terminating=1;

              if(phase==PHASE_MASTER)

                  mp_bundle_terminated();

           }

……

       }

……

    }

第一步:調用lcp_open(0)建立LCP鍊路。

pppd/lcp.c

Main() -> lcp_open():

void

lcp_open(unit)

    intunit;

{

    fsm*f=&lcp_fsm[unit]; //LCP狀态機

    lcp_options*wo=&lcp_wantoptions[unit];

    f->flags&=~(OPT_PASSIVE|OPT_SILENT);

    if(wo->passive)

       f->flags|=OPT_PASSIVE;

    if(wo->silent)

       f->flags|=OPT_SILENT;

    fsm_open(f);

}

調用fsm_open打開LCP狀态機:

pppd/fsm.c

main() -> lcp_open() -> fsm_open():

void

fsm_open(f)

    fsm*f;

{

    switch(f->state){

    caseINITIAL:

       f->state=STARTING;

       if(f->callbacks->starting)

           (*f->callbacks->starting)(f); //初始化時開始建立鍊路

       break;

……

    }

}

初始化狀态,實際調用lcp_starting()-> link_required():

pppd/auth.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required():

void

link_required(unit)

    intunit;

{

    new_phase(PHASE_SERIALCONN); //PPPD狀态機為“序列槽連接配接”階段

    devfd=the_channel->connect(); //1. 調用connect_tty連接配接TTY驅動

    if(devfd<0)

       gotofail;

    fd_ppp=the_channel->establish_ppp(devfd); //2. 調用 tty_establish_ppp

    if(fd_ppp<0){

       status=EXIT_FATAL_ERROR;

       gotodisconnect;

    }

    if(!demand&&ifunit>=0) //如果是不是demand模式,需要設定 IFNAME環境變量

       set_ifunit(1);

    if(ifunit>=0)

       notice("Connect: %s <--> %s",ifname,ppp_devnam);

    else

       notice("Starting negotiation on %s",ppp_devnam);

    add_fd(fd_ppp); //把/dev/ppp檔案描述加入fds。如果是demand模式,由于在main()中已經remove了需要再次加入,對于非demand模式則是首次加入

    status=EXIT_NEGOTIATION_FAILED;

    new_phase(PHASE_ESTABLISH); //PPPD狀态機進入“鍊路建立”階段

    lcp_lowerup(0); //3. 發送LCP Configure Request封包,向對方請求建立LCP鍊路

    return;

 disconnect:

    new_phase(PHASE_DISCONNECT);

    if(the_channel->disconnect)

       the_channel->disconnect();

 fail:

    new_phase(PHASE_DEAD);

    if(the_channel->cleanup)

       (*the_channel->cleanup)();

}

這個函數的主要作用從函數命名上就能看出,就是将需要的實體鍊路都帶起來,現在PPPD狀态機進入PHASE_SERIALCONN階段。

1.     調用connect_tty打開序列槽TTY驅動并配置TTY參數,變量ppp_devnam是序列槽驅動的裝置檔案如/dev/ttyS0,/dev/ttyUSB0,/dev/ttyHDLC0等,具體可以參考相關的序列槽TTY驅動,這裡不作具體分析。

2.     然後調用tty_establish_ppp:

pppd/sys-linux.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp():

inttty_establish_ppp(inttty_fd)

{

    intret_fd;

……

#ifndef N_SYNC_PPP

#defineN_SYNC_PPP 14

#endif

    ppp_disc=(new_style_driver&&sync_serial)?N_SYNC_PPP: N_PPP;  //同步還是異步PPP

    if(ioctl(tty_fd, TIOCSETD,&ppp_disc)<0){ //2.1 設定PPP線路規程

       if(!ok_error(errno)){

           error("Couldn't set tty to PPP discipline: %m");

           return-1;

       }

    }

    ret_fd=generic_establish_ppp(tty_fd); //2.2 建立PPP接口

#defineSC_RCVB(SC_RCV_B7_0 | SC_RCV_B7_1 | SC_RCV_EVNP | SC_RCV_ODDP)

#defineSC_LOGB(SC_DEBUG | SC_LOG_INPKT | SC_LOG_OUTPKT | SC_LOG_RAWIN \

               | SC_LOG_FLUSH)

    if(ret_fd>=0){

       modify_flags(ppp_fd,SC_RCVB|SC_LOGB,

                   (kdebugflag* SC_DEBUG)&SC_LOGB);

    }else{

       if(ioctl(tty_fd, TIOCSETD,&tty_disc)<0&&!ok_error(errno))

           warn("Couldn't reset tty to normal line discipline: %m");

    }

    returnret_fd;

}

分成兩部分來具體深入分析:

2.1 首先調用ioctl(tty_fd, TIOCSETD, &ppp_disc)将TTY驅動綁定到PPP線路規程,這裡的ioctl是對TTY檔案描述符的操作,實際上是調用了核心中的tty_ioctl() -> tiocsetd():

linux-2.6.18/drivers/char/tty_io.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() -> tty_ioctl() -> tiocsetd():

staticinttiocsetd(struct tty_struct *tty,int__user*p)

{

       intldisc;

       if(get_user(ldisc,p))

              return-EFAULT;

       returntty_set_ldisc(tty,ldisc); //設定線路規程,本文設定為N_PPP即異步PPP

}

這個tiocsetd函數是個wrapper函數,隻是把使用者态傳入的int參數放在核心态的ldisc中,再調用tty_set_ldist設定線路規程:

linux-2.6.18/drivers/char/tty_io.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() -> tty_ioctl() -> tiocsetd() -> tty_set_ldisc():

staticinttty_set_ldisc(struct tty_struct *tty,intldisc)

{

       intretval=0;

       struct tty_ldisco_ldisc;

       charbuf[64];

       intwork;

       unsignedlongflags;

       struct tty_ldisc*ld;

       struct tty_struct*o_tty;

       if((ldisc< N_TTY)||(ldisc>= NR_LDISCS))

              return-EINVAL;

restart:

       ld=tty_ldisc_get(ldisc); //從tty_ldiscs全局數組找出注冊的N_PPP線路規程

       if(ld==NULL){

              request_module("tty-ldisc-%d",ldisc);

              ld=tty_ldisc_get(ldisc);

       }

       if(ld==NULL)

              return-EINVAL;

(如果目前TTY上已經設定了線路規程,這部分代碼會detach現有的線路規程,這裡忽略詳細分析)……

       if(tty->ldisc.close)  //調用目前線路規程的close()函數

              (tty->ldisc.close)(tty);

       tty_ldisc_assign(tty,ld); //将上面擷取的N_PPP線路規程attach到目前TTY

       tty_set_termios_ldisc(tty,ldisc);

       if(tty->ldisc.open)

              retval=(tty->ldisc.open)(tty); //調用N_PPP線路規程的open()

       if(retval<0){

              tty_ldisc_put(ldisc);

              tty_ldisc_assign(tty,tty_ldisc_get(o_ldisc.num));

              tty_set_termios_ldisc(tty,tty->ldisc.num);

              if(tty->ldisc.open&&(tty->ldisc.open(tty)<0)){

                     tty_ldisc_put(o_ldisc.num);

                     tty_ldisc_assign(tty,tty_ldisc_get(N_TTY));

                     tty_set_termios_ldisc(tty, N_TTY);

                     if(tty->ldisc.open){

                            intr=tty->ldisc.open(tty);

                            if(r<0)

                                   panic("Couldn't open N_TTY ldisc for "

                                         "%s --- error %d.",

                                         tty_name(tty,buf),r);

                     }

              }

       }

       if(tty->ldisc.num!=o_ldisc.num&&tty->driver->set_ldisc)

              tty->driver->set_ldisc(tty); //如果是新的線路規程則調用TTY驅動的set_ldisc函數

       tty_ldisc_put(o_ldisc.num); //釋放舊的線路規程參考計數

       tty_ldisc_enable(tty); //激活新的TTY線路規程,實際上是置位TTY_LDISC

       if(o_tty)

              tty_ldisc_enable(o_tty);

       if(work)

              schedule_delayed_work(&tty->buf.work,1);

       returnretval;

}

這個函數為TTY驅動綁定N_PPP線路規程,綁定後調用線路規程的open()函數,對于N_PPP實際上是調用ppp_asynctty_open:

linux-2.6.18/drivers/net/ppp_async.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() -> tty_ioctl() -> tiocsetd() -> tty_set_ldisc() -> ppp_asynctty_open():

staticint

ppp_asynctty_open(struct tty_struct*tty)

{

       struct asyncppp*ap;

       interr;

       err=-ENOMEM;

       ap=kmalloc(sizeof(*ap), GFP_KERNEL);

       if(ap==0)

              gotoout;

       memset(ap,0,sizeof(*ap));

       ap->tty=tty;

       ap->mru=PPP_MRU;

       spin_lock_init(&ap->xmit_lock);

       spin_lock_init(&ap->recv_lock);

       ap->xaccm[0]=~0U;

       ap->xaccm[3]=0x60000000U;

       ap->raccm=~0U;

       ap->optr=ap->obuf;

       ap->olim=ap->obuf;

       ap->lcp_fcs=-1;

       skb_queue_head_init(&ap->rqueue);

       tasklet_init(&ap->tsk,ppp_async_process,(unsignedlong)ap);//接收資料時使用的tasklet

       atomic_set(&ap->refcnt,1);

       init_MUTEX_LOCKED(&ap->dead_sem);

       ap->chan.private=ap; //channel反向指針指向struct asyncppp

       ap->chan.ops=&async_ops; //異步channel的操作函數集合

       ap->chan.mtu=PPP_MRU;

       err=ppp_register_channel(&ap->chan); //注冊異步channel

       if(err)

              gotoout_free;

       tty->disc_data=ap; //現在tty結構可以找到asyncppp了

       tty->receive_room=65536;

       return0;

 out_free:

       kfree(ap);

 out:

       returnerr;

}

此函數配置設定并初始化struct asyncppp結構來表示一個異步PPP,并将tty結構的disc_data指向該結構。另外調用ppp_register_channel注冊了一個異步PPP channel:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() -> tty_ioctl() -> tiocsetd() -> tty_set_ldisc() -> ppp_asynctty_open() -> ppp_register_channel():

int

ppp_register_channel(struct ppp_channel*chan)

{

       struct channel*pch;

       pch=kzalloc(sizeof(struct channel), GFP_KERNEL);

       if(pch==0)

              return-ENOMEM;

       pch->ppp=NULL;  //channel還不屬于任何PPP unit,初始化為NULL

       pch->chan=chan;  //channel中指向ppp_channel的指針

       chan->ppp=pch;   //ppp_channel中指向channel的指針

       init_ppp_file(&pch->file,CHANNEL); //初始化ppp_file,類型為CHANNEL

       pch->file.hdrlen=chan->hdrlen;

#ifdef CONFIG_PPP_MULTILINK

       pch->lastseq=-1;

#endif

       init_rwsem(&pch->chan_sem);

       spin_lock_init(&pch->downl);

       rwlock_init(&pch->upl);

       spin_lock_bh(&all_channels_lock);

       pch->file.index=++last_channel_index; //channel索引值,後面會用到

       list_add(&pch->list,&new_channels); //注冊到new_channels全局連結清單

       atomic_inc(&channel_count);

       spin_unlock_bh(&all_channels_lock);

       return0;

}

OK,到此ioctl(tty_fd, TIOCSETD, &ppp_disc)在核心中的實作就分析完了。

2.2 傳回tty_establish_ppp,繼續調用generic_establish_ppp建立PPP接口:

pppd/sys-linux.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() –> generic_establish_ppp():

intgeneric_establish_ppp(intfd)

{

    intx;

    if(new_style_driver){ //進入到這裡

       intflags;

       if(ioctl(fd, PPPIOCGCHAN,&chindex)==-1){ //1) 擷取channel number

           error("Couldn't get channel number: %m");

           gotoerr;

       }

       dbglog("using channel %d",chindex);

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

       if(fd<0){

           error("Couldn't reopen /dev/ppp: %m");

           gotoerr;

       }

       (void)fcntl(fd, F_SETFD, FD_CLOEXEC);

       if(ioctl(fd, PPPIOCATTCHAN,&chindex)<0){ //2) 将channel綁定到/dev/ppp

           error("Couldn't attach to channel %d: %m",chindex);

           gotoerr_close;

       }

       flags=fcntl(fd, F_GETFL);

       if(flags==-1||fcntl(fd, F_SETFL,flags| O_NONBLOCK)==-1) //設為非阻塞fd

           warn("Couldn't set /dev/ppp (channel) to nonblock: %m");

       set_ppp_fd(fd);  //将這個fd儲存到變量ppp_fd

       if(!looped)

           ifunit=-1;

       if(!looped&&!multilink){ //回想一下,在demand模式下open_ppp_loopback會将looped置為1

           if(make_ppp_unit()<0) //3) demand模式下已經調用過make_ppp_unit了,這裡用于正常撥号

              gotoerr_close;

       }

       if(looped)

           modify_flags(ppp_dev_fd, SC_LOOP_TRAFFIC,0); //對demand模式,清除

       if(!multilink){

           add_fd(ppp_dev_fd); //把ppp_dev_fd加入到select的fds中

           if(ioctl(fd, PPPIOCCONNECT,&ifunit)<0){ //4) 連接配接channel到unit

              error("Couldn't attach to PPP unit %d: %m",ifunit);

              gotoerr_close;

           }

       }

    }else{

(old driver忽略)……

    }

……

    looped=0;

    returnppp_fd;

……

}

這個函數可以分成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():

linux-2.6.18/drivers/char/tty_io.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() –> generic_establish_ppp() –> tty_ioctl():

inttty_ioctl(struct inode*inode,struct file*file,

             unsignedintcmd,unsignedlongarg)

{

       struct tty_struct*tty,*real_tty;

       void__user*p=(void__user*)arg;

       intretval;

       struct tty_ldisc*ld;

……

       ld=tty_ldisc_ref_wait(tty);//前面TTY已綁定了PPP線路規程,是以傳回的是異步PPP線路規程

       retval=-EINVAL;

       if(ld->ioctl){

              retval=ld->ioctl(tty,file,cmd,arg); //實際調用ppp_asynctty_ioctl

              if(retval==-ENOIOCTLCMD)

                     retval=-EINVAL;

       }

       tty_ldisc_deref(ld);

       returnretval;

}

異步PPP線路規程已經在核心檔案ppp_async.c中初始化了,并且在前面已經設定TTY的異步PPP線路規程,是以這裡的ld->ioctl實際指向的是ppp_asynctty_ioctl:

linux-2.6.18/drivers/net/ppp_async.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() –> generic_establish_ppp() –> tty_ioctl() –> ppp_asynctty_ioctl():

staticint

ppp_asynctty_ioctl(struct tty_struct*tty,struct file*file,

                 unsignedintcmd,unsignedlongarg)

{

       struct asyncppp*ap=ap_get(tty);

       interr,val;

       int__user*p=(int__user*)arg;

       if(ap==0)

              return-ENXIO;

       err=-EFAULT;

       switch(cmd){

       casePPPIOCGCHAN:

              err =-ENXIO;

              if(ap==0)

                     break;

              err=-EFAULT;

              if(put_user(ppp_channel_index(&ap->chan),p))//拷貝channel索引值到chindex

                     break;

              err=0;

              break;

……

}

ppp_channel_index實作:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() –> generic_establish_ppp() –> tty_ioctl() –> ppp_asynctty_ioctl() –> ppp_channel_index():

intppp_channel_index(struct ppp_channel*chan)

{

       struct channel*pch=chan->ppp;

       if(pch!=0)

              returnpch->file.index;//傳回ppp_register_channel()中初始化的channel索引值

       return-1;

}

第2部分:對/dev/ppp調用ioctl(fd, PPPIOCATTCHAN, &chindex),實際上調用ppp_ioctl -> ppp_unattached_ioctl:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() –> generic_establish_ppp() -> ppp_ioctl() -> ppp_unattached_ioctl():

staticintppp_unattached_ioctl(struct ppp_file *pf,struct file*file,

                            unsignedintcmd,unsignedlongarg)

{

       intunit,err=-EFAULT;

       struct ppp*ppp;

       struct channel*chan;

       int__user*p=(int__user*)arg;

……

       casePPPIOCATTCHAN:

              if(get_user(unit, p))

                     break;

              spin_lock_bh(&all_channels_lock);

              err=-ENXIO;

              chan=ppp_find_channel(unit);//通過參數chindex尋找注冊的channel

              if(chan!=0){

                     atomic_inc(&chan->file.refcnt);

                     file->private_data=&chan->file; //綁定到channel,指向channel的ppp_file

                     err=0;

              }

              spin_unlock_bh(&all_channels_lock);

              break;

       default:

              err=-ENOTTY;

       }

       returnerr;

}

這個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。

linux-2.6.18/drivers/net/ppp_generic.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() –> generic_establish_ppp() -> ppp_ioctl():

staticintppp_ioctl(struct inode *inode,struct file*file,

                   unsignedintcmd,unsignedlongarg)

{

       struct ppp_file*pf=file->private_data;

       struct ppp*ppp;

……

       if(pf->kind==CHANNEL){

              struct channel*pch=PF_TO_CHANNEL(pf);

              struct ppp_channel*chan;

              switch(cmd){

              casePPPIOCCONNECT:

                     if(get_user(unit, p))

                            break;

                     err=ppp_connect_channel(pch,unit); //連接配接channel到unit

                     break;

              casePPPIOCDISCONN:

                     err=ppp_disconnect_channel(pch);

                     break;

              default:

                     down_read(&pch->chan_sem);

                     chan=pch->chan;

                     err=-ENOTTY;

                     if(chan&&chan->ops->ioctl)

                            err=chan->ops->ioctl(chan,cmd,arg);

                     up_read(&pch->chan_sem);

              }

              returnerr;

       }

……

}

ppp_connect_channel實作:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> tty_establish_ppp() –> generic_establish_ppp() -> ppp_ioctl() -> ppp_connect_channel():

staticint

ppp_connect_channel(struct channel*pch,intunit)

{

       struct ppp*ppp;

       intret=-ENXIO;

       inthdrlen;

       mutex_lock(&all_ppp_mutex);

       ppp=ppp_find_unit(unit); //根據unit number找到struct ppp

       if(ppp==0)

              gotoout;

       write_lock_bh(&pch->upl);

       ret=-EINVAL;

       if(pch->ppp!=0)

              gotooutl;

       ppp_lock(ppp);

       if(pch->file.hdrlen>ppp->file.hdrlen)

              ppp->file.hdrlen=pch->file.hdrlen;

       hdrlen=pch->file.hdrlen+2;    

       if(ppp->dev&&hdrlen>ppp->dev->hard_header_len)

              ppp->dev->hard_header_len=hdrlen; //PPP協定幀頭長度

       list_add_tail(&pch->clist,&ppp->channels); //将channel加入到unit中的channel連結清單

       ++ppp->n_channels; //unit中的channel數目加1。注意:一個unit下可以有多個channel哦

       pch->ppp=ppp; //回憶一下,剛注冊channel時ppp指針為空。

       atomic_inc(&ppp->file.refcnt);

       ppp_unlock(ppp);

       ret=0;

 outl:

       write_unlock_bh(&pch->upl);

 out:

       mutex_unlock(&all_ppp_mutex);

       returnret;

}

3.     函數link_required中前兩步已經配置好了鍊路接口,接下來該做正事了:PPPD狀态機進入PHASE_ESTABLISH階段,然後用lcp_lowerup(0)發送LCP封包去建立連接配接。

實際調用lcp_lowerup() -> fsm_lowerup() -> fsm_sconfreq()-> fsm_sdata() -> output():

pppd/sys-linux.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> lcp_lowerup() -> fsm_lowerup() -> fsm_sconfreq() -> fsm_sdata() -> output():

voidoutput(intunit,unsignedchar*p,intlen)

{

    intfd=ppp_fd;

    intproto;

    dump_packet("sent",p,len);

    if(snoop_send_hook)snoop_send_hook(p,len);

    if(len<PPP_HDRLEN)

       return;

    if(new_style_driver){ //進入到這裡

       p+=2;

       len-=2;

       proto=(p[0]<<8)+p[1];

       if(ppp_dev_fd>=0&&!(proto>=0xc000||proto== PPP_CCPFRAG))

           fd=ppp_dev_fd; //注意:資料幀用ppp_dev_fd發送,LCP控制幀用ppp_fd發送

    }

    if(write(fd,p,len)<0){//調用核心函數ppp_write發送資料

       if(errno== EWOULDBLOCK|| errno == EAGAIN|| errno== ENOBUFS

           || errno== ENXIO|| errno == EIO|| errno== EINTR)

           warn("write: warning: %m (%d)", errno);

       else

           error("write: %m (%d)", errno);

    }

}

資料的發送要分兩種情況:

1.     LCP控制幀用ppp_fd發送。

2.     資料幀用ppp_dev_fd發送。

不管是ppp_fd還是ppp_dev_fd打開的裝置檔案都是/dev/ppp,是以調用的都是同一個函數ppp_write:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> lcp_lowerup() -> fsm_lowerup() -> fsm_sconfreq() -> fsm_sdata() -> output() -> ppp_write():

staticssize_tppp_write(struct file*file,constchar__user*buf,

                      size_tcount,loff_t*ppos)

{

       struct ppp_file*pf=file->private_data;

       struct sk_buff*skb;

       ssize_tret;

       if(pf==0)

              return-ENXIO;

       ret=-ENOMEM;

       skb=alloc_skb(count+pf->hdrlen, GFP_KERNEL); //Linux核心用sk_buff存放網絡資料包

       if(skb==0)

              gotoout;

       skb_reserve(skb,pf->hdrlen);

       ret=-EFAULT;

       if(copy_from_user(skb_put(skb,count),buf,count)){ //将發送資料拷貝到skb

              kfree_skb(skb);

              gotoout;

       }

       skb_queue_tail(&pf->xq,skb); //将skb放在發送隊列xq的隊尾

       switch(pf->kind){ //注意:通過ppp_file的kind字段判斷/dev/ppp綁定的是unit還是channel

       caseINTERFACE: //接口即unit

              ppp_xmit_process(PF_TO_PPP(pf));

              break;

       caseCHANNEL: //channel

              ppp_channel_push(PF_TO_CHANNEL(pf));

              break;

       }

       ret=count;

 out:

       returnret;

}

繼續對這個函數分析,現在要發送LCP幀去建立連接配接,是以調用ppp_channel_push來進行發送:

linux-2.6.18/drivers/net/ppp_generic.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> lcp_lowerup() -> fsm_lowerup() -> fsm_sconfreq() -> fsm_sdata() -> output() -> ppp_write() -> ppp_channel_push():

staticvoid

ppp_channel_push(struct channel*pch)

{

       struct sk_buff*skb;

       struct ppp*ppp;

       spin_lock_bh(&pch->downl);

       if(pch->chan!=0){ //已經在前面的ppp_register_channel初始化了,這裡不為空

              while(!skb_queue_empty(&pch->file.xq)){ //skb已放在發送隊列xq中

                     skb=skb_dequeue(&pch->file.xq);

                     if(!pch->chan->ops->start_xmit(pch->chan,skb)){

                            skb_queue_head(&pch->file.xq,skb);

                            break;

                     }

              }

       }else{

              skb_queue_purge(&pch->file.xq);

       }

       spin_unlock_bh(&pch->downl);

       if(skb_queue_empty(&pch->file.xq)){

              read_lock_bh(&pch->upl);

              ppp=pch->ppp;

              if(ppp!=0)//如果channel已經連接配接到unit了,則不為空

                     ppp_xmit_process(ppp); //這個函數用于發送資料幀

              read_unlock_bh(&pch->upl);

       }

}

實際調用ppp_async_send發送LCP幀:

linux-2.6.18/drivers/net/ppp_async.c

main() -> lcp_open() -> fsm_open() -> lcp_starting() -> link_required() -> lcp_lowerup() -> fsm_lowerup() -> fsm_sconfreq() -> fsm_sdata() -> output() -> ppp_write() -> ppp_channel_push() -> ppp_async_send():

staticint

ppp_async_send(struct ppp_channel*chan,struct sk_buff*skb)

{

       struct asyncppp*ap=chan->private;

       ppp_async_push(ap); //push到TTY驅動去發送

       if(test_and_set_bit(XMIT_FULL,&ap->xmit_flags))

              return0;      

       ap->tpkt=skb;

       ap->tpkt_pos=0;

       ppp_async_push(ap);

       return1;

}

實際調用ppp_async_push:

staticint

ppp_async_push(struct asyncppp*ap)

{

       intavail,sent,done=0;

       struct tty_struct*tty=ap->tty;

       inttty_stuffed=0;

       if(test_and_set_bit(XMIT_BUSY,&ap->xmit_flags))

              return0;

       spin_lock_bh(&ap->xmit_lock);

       for(;;){

              if(test_and_clear_bit(XMIT_WAKEUP,&ap->xmit_flags))

                     tty_stuffed=0;

              if(!tty_stuffed&&ap->optr<ap->olim){

                     avail=ap->olim-ap->optr;

                     set_bit(TTY_DO_WRITE_WAKEUP,&tty->flags);

                     sent=tty->driver->write(tty,ap->optr,avail); //最終調用TTY驅動的write發送幀

                     if(sent<0)

                            gotoflush;   

……

}

       到此,LCP Configure Request幀就發送出去了。

現在,再次傳回到PPPD中的主函數main()中:

第二步:PPPD狀态機循環進行事件處理

pppd/main.c -> main():

……

    do_callback=0;

    for(;;){ 

……

       lcp_open(0);        //第一步:打開PPPK接口發送LCP幀

       while(phase!=PHASE_DEAD){ //第二步:PPPD狀态機循環進行事件處理

           handle_events(); //select事件處理

           get_input(); //對接收封包的處理

           if(kill_link)

              lcp_close(0,"User request");

           if(asked_to_quit){

              bundle_terminating=1;

              if(phase==PHASE_MASTER)

                  mp_bundle_terminated();

           }

……

       }

……

    }

調用handle_events處理事件,見demand模式下對此函數的分析。注意:這裡等待事件處理的fds中包含有ppp_dev_fd。接下來調用get_input處理收到的封包:

pppd/main.c

main() -> get_input():

staticvoid

get_input()

{

    intlen,i;

    u_char *p;

    u_short protocol;

    struct protent*protp;

    p=inpacket_buf; 

    len=read_packet(inpacket_buf); //讀取接收封包到inpacket_buf緩沖區

(檢查接收資料的有效性,這裡不做分析)……

    p+=2;                         

    GETSHORT(protocol,p); //擷取封包中帶的協定号

    len-=PPP_HDRLEN; //有效資料長度

……

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

       if(protp->protocol==protocol&&protp->enabled_flag){

           (*protp->input)(0,p,len); //調用每個協定塊的input函數來處理接收封包

           return;

       }

        if(protocol==(protp->protocol&~0x8000)&&protp->enabled_flag

           &&protp->datainput!= NULL){

           (*protp->datainput)(0,p,len);

           return;

       }

    }

……

}

此函數調用read_packet讀取接收封包到inpacket_buf緩沖區,再提取出收到封包的協定号(LCP為0xC021),然後根據協定号比對調用對應協定塊的input和datainput函數。在第二階段分析demand模式時已經分析了read_packet函數,這裡就不啰嗦了。

至此,PPPD建立連接配接所需的資料收發基本流程就勾畫出來了,這裡我們看到的PPPD收發的資料包都是PPP控制幀如LCP,那像IP資料包這種資料流也都要經過PPPD嗎?如果連資料流都經過PPPD那效率豈不是很低?

首先來看資料流的接收:

當底層TTY驅動收到資料時會産生一個中斷,并在中斷處理函數(硬中斷或軟中斷BH)中調用TTY所綁定的線路規程的receive_buf函數指針。

linux-2.6.18/drivers/net/ppp_async.c

staticstruct tty_ldiscppp_ldisc={

       .owner =THIS_MODULE,

       .magic    = TTY_LDISC_MAGIC,

       .name    ="ppp",

       .open     =ppp_asynctty_open,

       .close     =ppp_asynctty_close,

       .hangup  =ppp_asynctty_hangup,

       .read      =ppp_asynctty_read,

       .write     =ppp_asynctty_write,

       .ioctl      =ppp_asynctty_ioctl,

       .poll=ppp_asynctty_poll,

       .receive_buf=ppp_asynctty_receive,

       .write_wakeup=ppp_asynctty_wakeup,

};

對于本文中綁定了N_PPP線路規程的TTY驅動來講,就是調用ppp_asynctty_receive:

linux-2.6.18/drivers/net/ppp_async.c

staticvoid

ppp_asynctty_receive(struct tty_struct*tty,constunsignedchar*buf,

                char*cflags,intcount)

{

       struct asyncppp*ap=ap_get(tty);

       unsignedlongflags;

       if(ap==0)

              return;

       spin_lock_irqsave(&ap->recv_lock,flags);

       ppp_async_input(ap,buf,cflags,count); //1. 配置設定skb并放在ap->rqueue隊列中

       spin_unlock_irqrestore(&ap->recv_lock,flags);

       if(!skb_queue_empty(&ap->rqueue)) //很顯然隊列不為空

              tasklet_schedule(&ap->tsk); //2. 排程tasklet來處理skb

       ap_put(ap);

       if(test_and_clear_bit(TTY_THROTTLED,&tty->flags)

           &&tty->driver->unthrottle)

              tty->driver->unthrottle(tty);

}

1.     這個函數首先調用ppp_async_input:

linux-2.6.18/drivers/net/ppp_async.c

staticvoid

ppp_async_input(struct asyncppp*ap,constunsignedchar*buf,

              char*flags,intcount)

{

       struct sk_buff*skb;

       intc,i,j,n,s,f;

       unsignedchar*sp;

(解析buf中的資料并配置設定一個skb并将ap->rpkt指針指向這個skb)……

       c=buf[n];

       if(flags!=NULL&&flags[n]!=0){

              ap->state|=SC_TOSS;

       }elseif(c==PPP_FLAG){ //看到PPP幀結束字元

              process_input_packet(ap); //把skb放在隊列中

……

}

函數ppp_asyc_input讀取buf中的資料放在sk_buff中,然後調用process_input_packet把收到的資料包放在接收隊列中:

linux-2.6.18/drivers/net/ppp_async.c

staticvoid

process_input_packet(struct asyncppp*ap)

{                

       struct sk_buff*skb;

       unsignedchar*p;

       unsignedintlen,fcs,proto;

       skb=ap->rpkt; //之前儲存接收資料的skb

(對skb中資料進行有效性檢查)……

       skb->cb[0]=ap->state;

       skb_queue_tail(&ap->rqueue,skb);   //把skb放在接收隊列中

       ap->rpkt=NULL;

       ap->state=0;

       return;

……

}

2.     再用tasklet_schedule(&ap->tsk)排程tasklet來處理,這個ap->tsk在哪個地方初始化的呢?前面分析過的ppp_asynctty_open已經初始化了tasklet。這時tasklet函數ppp_async_process會被執行:

linux-2.6.18/drivers/net/ppp_async.c

staticvoidppp_async_process(unsignedlongarg)

{

       struct asyncppp*ap=(struct asyncppp*)arg;

       struct sk_buff*skb;

       while((skb=skb_dequeue(&ap->rqueue))!=NULL){ //循環處理隊列中所有skb

              if(skb->cb[0])

                     ppp_input_error(&ap->chan,0);

              ppp_input(&ap->chan,skb); //調用ppp_input來處理skb

       }

       if(test_bit(XMIT_WAKEUP,&ap->xmit_flags)&&ppp_async_push(ap))

              ppp_output_wakeup(&ap->chan);

}

饒了一個圈,實際上是調用ppp_input來處理接收資料包:

linux-2.6.18/drivers/net/ppp_generic.c

void

ppp_input(struct ppp_channel*chan,struct sk_buff*skb)

{

       struct channel*pch=chan->ppp;

       intproto;

       if(pch==0||skb->len==0){

              kfree_skb(skb);

              return;

       }

       proto=PPP_PROTO(skb);

       read_lock_bh(&pch->upl);

       if(pch->ppp==0||proto>=0xc000||proto==PPP_CCPFRAG){

              skb_queue_tail(&pch->file.rq,skb); //放在channel的接收隊列中

              while(pch->file.rq.qlen>PPP_MAX_RQLEN

                     &&(skb=skb_dequeue(&pch->file.rq))!=0)

                     kfree_skb(skb);

              wake_up_interruptible(&pch->file.rwait); //喚醒PPPD程序進行read處理

       }else{ 

              ppp_do_recv(pch->ppp,skb,pch); //對非控制幀進行處理

       }

       read_unlock_bh(&pch->upl);

}

函數ppp_input分兩種情況分發封包:

1.     對控制流,放在channel的接收隊列中并喚醒PPPD程序讀取。

2.     對資料流,調用ppp_do_recv:

linux-2.6.18/drivers/net/ppp_generic.c

staticinlinevoid

ppp_do_recv(struct ppp*ppp,struct sk_buff*skb,struct channel*pch)

{

       ppp_recv_lock(ppp);

       if(ppp->dev!=0)

              ppp_receive_frame(ppp,skb,pch);

       else

              kfree_skb(skb);

       ppp_recv_unlock(ppp);

}

由于PPP unit上已建立了ppp0網絡接口,這裡會調用ppp_receive_frame:

linux-2.6.18/drivers/net/ppp_generic.c

staticvoid

ppp_receive_frame(struct ppp*ppp,struct sk_buff*skb,struct channel*pch)

{

       if(skb->len>=2){

#ifdef CONFIG_PPP_MULTILINK

              if(PPP_PROTO(skb)== PPP_MP)

                     ppp_receive_mp_frame(ppp, skb, pch);

              else

#endif

                     ppp_receive_nonmp_frame(ppp,skb); //非多鍊路PPP

              return;

       }

       if(skb->len>0)

              ++ppp->stats.rx_length_errors;

       kfree_skb(skb);

       ppp_receive_error(ppp);

}

對于非多鍊路PPP調用ppp_receive_nonmp_frame:

linux-2.6.18/drivers/net/ppp_generic.c

staticvoid

ppp_receive_nonmp_frame(struct ppp*ppp,struct sk_buff*skb)

{

       struct sk_buff*ns;

       intproto,len,npi;

……

              if((ppp->dev->flags&IFF_UP)==0

                  ||ppp->npmode[npi]!=NPMODE_PASS){

                     kfree_skb(skb);

              }else{

                     skb_pull_rcsum(skb,2);

                     skb->dev=ppp->dev;

                     skb->protocol=htons(npindex_to_ethertype[npi]);

                     skb->mac.raw=skb->data;

                     netif_rx(skb); //把skb放入Linux協定棧去處理

                     ppp->dev->last_rx=jiffies;

              }

       }

       return;

……

}

對于資料流,最終還是調用netif_rx(skb)将資料包放入Linux協定棧去處理。

結論:在PPP連接配接成功建立之前,為建立連接配接而傳輸的控制流都要通過PPPD進行封包解析并根據各控制協定的狀态機和使用者配置進行封包的收發、逾時及狀态遷移等處理。 當PPP連接配接經過三階段LCP->PAP/CHAP->IPCP成功建立之後, 經過ppp0接口的資料流就直接通過Linux核心進行處理而不必經過PPPD,實作了控制路徑與資料路徑,政策與機制的有效分離。

7樓  shaqianqing 2014-05-26 13:20發表 [回複] [引用] [舉報]

Linux PPP實作源碼分析-good
樓主有沒有關注過client對于DNS、網關這些資訊是如何處理的
Re:  osnetdev 2014-05-28 10:25發表 [回複] [引用] [舉報]
Linux PPP實作源碼分析-good
回複shaqianqing:當pppd撥号成功擷取到IP位址後,實際上就是PPP協定的IPCP階段已經完成,最終會調用ipcp_up()這個函數設定IP位址,子網路遮罩,網關,DNS,最後還會執行/etc/ppp/ip-up這個script。
Re:  shaqianqing 2014-05-28 13:33發表 [回複]
Linux PPP實作源碼分析-good
回複osnetdev:嗯,我看到在這個函數裡面有這些資訊的處理,但是沒看到有把路由資訊加到路由表中,或是把DNS寫到配置檔案中,但是有看到對預設網關的處理,這裡的預設網關與網關有什麼關系嗎
Re:  osnetdev 2014-05-28 16:52發表 [回複]
Linux PPP實作源碼分析-good
回複shaqianqing:這裡的網關指的就是預設網管或預設路由,對DNS的配置由函數create_resolv()處理,這個函數會把DNS伺服器資訊寫到/etc/ppp/resolve.conf,你可以按需要在ip-up腳本裡把系統的/etc/resolve.conf替換為這個檔案。
Re:  shaqianqing 2015-04-03 11:16發表 [回複]
Linux PPP實作源碼分析-good
回複osnetdev:樓主有關注過pppoe proxy的功能沒,此時pppoe是作為server存在的,來通過截獲下挂裝置的使用者名和密碼來向上一級撥号的,此時這個server功能是在哪裡實作的呢

繼續閱讀