PPP协议工作流程,结合ppp-2.4.9 源码分析

ppp-2.4.9 源码分析

PPP协议工作流程

当用户拨号接入ISP后,就建立了一条从用户个人电脑到ISP的物理连接。这时,用户个人电脑向ISP发送一系列的链路控制协议LCP分组(封装成多个PPP帧),以便建立LCP连接。这些分组及其响应选择了将要使用的一些PPP参数。接着还要进行网络层配置,网络控制协议NCP给新接入的用户个人电脑分配一个临时的IP地址。这样,用户个人电脑就成为互联网上的一个有IP地址的主机了。

当用户通信完毕时,NCP释放网络层连接,收回原来分配出去的IP地址。接着,LCP释放数据链路层连接。最后释放的是物理层的连接。

PPP链路的起始和终止状态永远是“链路静止”(Link Dead)状态,这时在用户个人电脑和ISP的路由器之间并不存在物理层的连接。

当用户个人电脑通过调制解调器呼叫路由器时(通常是在屏幕上用鼠标点击一个连接按钮),路由器就能够检测到调制解调器发出的载波信号。在双方建立了物理层连接后,PPP就进入“链路建立”(Link Establish)状态,其目的是建立链路层的LCP连接。

这时LCP开始协商一些配置选项,即发送LCP的配置请求帧(Configure-Request)。这是个PPP帧,其协议字段置为LCP对应的代码,而信息字段包含特定的配置请求。链路的另一端可以发送以下几种响应中的一种:

  1. 配置确认帧(Configure-Ack) 所有选项都接受。
  2. 配置否认帧(Configure-Nak) 所有选项都理解但不能接受。
  3. 配置拒绝帧(Configure-Reject) 选项有的无法识别或不能接受,需要协商。

LCP配置选项包括链路上的最大帧长、所使用的鉴别协议(authentication protocol)的规约(如果有的话),以及不使用PPP帧中的地址和控制字段(因为这两个字段的值是固定的,没有任何信息量,可以在PPP帧的首部中省略这两个字节)。

协商结束后双方就建立了LCP 链路,接着就进入“鉴别”(Authenticate)状态。在这一状态,只允许传送LCP协议的分组、鉴别协议的分组以及监测链路质量的分组。若使用口令鉴别协议PAP (Password Authentication Protocol), 则需要发起通信的一方发送身份标识符和口令。系统可允许用户重试若干次。如果需要有更好的安全性,则可使用更加复杂的口令握手鉴别协议CHAP (Challenge-Handshake Authentication Protocol)。 若鉴别身份失败,则转到“链路终止”(Link Terminate)状态。 若鉴别成功,则进入“网络层协议”(Network-Layer Protocol)状态。

在“网络层协议”状态,PPP链路的两端的网络控制协议NCP根据网络层的不同协议互相交换网络层特定的网络控制分组。这个步骤是很重要的,因为现在的路由器都能够同时支持多种网络层协议。总之,PPP协议两端的网络层可以运行不同的网络层协议,但仍然可使用同一个PPP协议进行通信。

如果在PP链路上运行的是IP协议,则对PPP链路的每一端配置IP协议模块(如分配IP地址)时就要使用NCP中支持IP的协议——IP 控制协议IPCP (IP Control Protocol)。IPCP分组也封装成PPP帧(其中的协议字段为0x8021)在PPP链路上传送。在低速链路_上运行时,双方还可以协商使用压缩的TCP和IP首部,以减少在链路上发送的比特数。

当网络层配置完毕后,链路就进入可进行数据通信的“链路打开”(Link Open)状态。链路的两个PPP端点可以彼此向对方发送分组。两个PPP端点还可发送回送请求LCP分组(Echo-Request)和回送回答LCP分组(Echo-Reply),以检查链路的状态。

数据传输结束后,可以由链路的一端 发出终止请求LCP分组(Terminate-Request)请求终止链路连接,在收到对方发来的终止确认LCP分组(Terminate-Ack)后,转到“链路终止”状态。如果链路出现故障,也会从“链路打开”状态转到“链路终止”状态。当调制解调器的载波停止后,则回到“链路静止”状态。

上述过程可用状态图来描述。

从设备之间无链路开始,到先建立物理链路,再建立链路控制协议LCP链路。经过鉴别后再建立网络控制协议NCP链路,然后才能交换数据。由此可见,PPP协议已不是纯粹的数据链路层的协议,它还包含了物理层和网络层的内容。

ppp-2.4.9 源码分析

全局变量和结构体说明

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

pppd/main.c

/*
 * PPP Data Link Layer "protocol" table.
 * One entry per supported protocol.
 * The last entry must be NULL.
 */
 // PPP数据链路层“协议”表。每个支持的协议一个条目。最后一个条目必须为空。
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)(int unit); // 初始化指针,在main()中被调用
    /* Process a received packet */
    void (*input)(int unit, u_char *pkt, int len); // 接收报文处理
    /* Process a received protocol-reject */
    void (*protrej)(int unit);  // 协议错误处理
    /* Lower layer has come up */
    void (*lowerup)(int unit); // 当下层协议UP起来后的处理
    /* Lower layer has gone down */
    void (*lowerdown)(int unit);  // 当下层协议DOWN后的处理
    /* Open the protocol */
    void (*open)(int unit); // 打开协议
    /* Close the protocol */
    void (*close)(int unit, char *reason);  // 关闭协议
    /* Print a packet in readable form */ //打印报文信息,调试用
    int  (*printpkt)(u_char *pkt, int len, printer_func printer, void *arg); 
    /* Process a received data packet */
    void (*datainput)(int unit, u_char *pkt, int len); //处理已收到的数据包
    bool enabled_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)(void);  // 检测和此协议有关的选项参数
    /* Configure interface for demand-dial */
    int  (*demand_conf)(int unit);  // 将接口配置为按需拨号需要做的 动作
    /* Say whether to bring up link for this pkt */
    int  (*active_pkt)(u_char *pkt, int len); // 判断报文类型并激活链路 
};

对pppd状态机的定义

/*
 * Values for phase.
 */
#define PHASE_DEAD		0   // 静止
#define PHASE_INITIALIZE	1  // 初始化
#define PHASE_SERIALCONN	2  
#define PHASE_DORMANT		3 // 休眠
#define PHASE_ESTABLISH		4 // 建立
#define PHASE_AUTHENTICATE	5 // 身份认证
#define PHASE_CALLBACK		6 // 回调
#define PHASE_NETWORK		7 // 连接
#define PHASE_RUNNING		8 // 运行
#define PHASE_TERMINATE		9 // 终止
#define PHASE_DISCONNECT	10 // 断开连接
#define PHASE_HOLDOFF		11 // 延迟
#define PHASE_MASTER		12 // 控制

pppd的main函数程序开始分析其工作流程;

第一阶段 初始化

pppd/main.c

int
main(int argc, char *argv[])
{
    int i, t;
    char *p;
    struct passwd *pw;
    struct protent *protp;
    char numbuf[16];

    strlcpy(path_ipup, _PATH_IPUP, sizeof(path_ipup));
    strlcpy(path_ipdown, _PATH_IPDOWN, sizeof(path_ipdown));

    link_stats_valid = 0;
    new_phase(PHASE_INITIALIZE);  
	// PPPD中的状态机,初始化阶段

    script_env = NULL;

    /* Initialize syslog facilities */
    reopen_log();

    if (gethostname(hostname, MAXNAMELEN) < 0 ) {
	option_error("Couldn't get hostname: %m");
	exit(1);
    }
    hostname[MAXNAMELEN-1] = 0;

    /* make sure we don't create world or group writable files. */
    umask(umask(0777) | 022);

    uid = getuid();
    privileged = uid == 0;
    slprintf(numbuf, sizeof(numbuf), "%d", uid);
    script_setenv("ORIG_UID", numbuf, 0);

    ngroups = getgroups(NGROUPS_MAX, groups);

    /*
     * 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)
        (*protp->init)(0); 
    // protocols[]是全局变量的协议数组,初始化协议数组中所有协议

    /*
     * Initialize the default channel.
     */
    tty_init(); 
	//channel初始化,默认就是全局的tty_channel,里面包括很多TTY函数指针   

    progname = *argv;

    /*
     * Parse, in order, the system options file, the user's options file,
     * and the command line arguments.
     */  
	 //按顺序解析系统选项文件、用户选项文件和命令行参数。
    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.
     */
	 //计算出设备名(如果它还没有被指定的话),并解析tty的选项文件。
    if (the_channel->process_extra_options)
	(*the_channel->process_extra_options)();
	// 实际上是调用tty_process_extra_options解析TTY 参数
	
    if (debug)
	setlogmask(LOG_UPTO(LOG_DEBUG));

    /*
     * Check that we are running as root.
     */
	 // geteuid获取当前运行用户ruid,检查是否作为根用户运行。 
    // 本程序必须以root身份运行,否则会报错
    if (geteuid() != 0) {
	option_error("must be root to run %s, since it is not setuid-root",
		     argv[0]);
	exit(EXIT_NOT_ROOT);
    }
	
	// 检测/dev/ppp设备文件是否有效
    if (!ppp_available()) {
	option_error("%s", no_ppp_msg);
	exit(EXIT_NO_KERNEL_SUPPORT);
    }

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

函数ppp_available会尝试打开/dev/ppp设备文件来判断PPP驱动是否已加载在内核中。如果此设备文件不能打开则通过uname判断内核版本号来区分当前内核版本是否支持PPP驱动,要是内核版本很老(2.3.x以下),则打开PTY设备文件并设置PPP线路规程。

pppd/sys_linux.c

main() -> ppp_avaiable():

/********************************************************************
 *
 * ppp_available - check whether the system has any ppp interfaces
 * (in fact we check whether we can do an ioctl on ppp0).
 */
// ppp_available—检查系统是否有ppp接口(事实上我们检查是否可以在ppp0上执行ioctl)。
int ppp_available(void)
{
    int s, ok, fd;
    struct ifreq ifr;
    int    size;
    int    my_version, my_modification, my_patch;
    int osmaj, osmin, ospatch;

    /* get the kernel version now, since we are called before sys_init */
    // 现在获取内核版本,因为我们在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); // 打开 /dev/ppp
    if (fd >= 0) {
	new_style_driver = 1; // 支持PPPK
......
/*
 * Validate the version of the driver against the version that we used.
 */
// 根据我们使用的版本验证驱动程序的版本。
	    decode_version(VERSION,
			   &my_version,
			   &my_modification,
			   &my_patch);

	    /* The version numbers must match */
	    if (driver_version != my_version) // 版本号必须匹配
		ok = 0;

	    /* The modification levels must be legal */
	    if (driver_modification < 3) { // 修改级别必须合法
		if (driver_modification >= 2) {
		    /* we can cope with 2.2.0 and above */
		    driver_is_old = 1;
		} else {
		    ok = 0;
		}
	    }

	    if (!ok) {
		slprintf(route_buffer, sizeof(route_buffer),
			 "Sorry - PPP driver version %d.%d.%d is out of date\n",
			 driver_version, driver_modification, driver_patch);

		no_ppp_msg = route_buffer;
	    }
	}
    }
    close(s);
    return ok;
}

接下来会检查选项参数的合法性,这些参数包括系统参数,认证相关的参数等,还会检查每个控制协议的参数配置以及tty参数。后面是将pppd以daemon方式执行或保持在前台运行并设置一些环境变量和信号处理函数

pppd/main.c

    /*
     * Check that the options given are valid and consistent.
     */
	 // 检查给出的选项是否有效和一致。
    check_options(); // 检查选项参数
    if (!sys_check_options()) // 检测系统参数
	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 (dump_options || dryrun) {
	init_pr_log(NULL, LOG_INFO);
	print_options(pr_log, NULL);
	end_pr_log();
    }

    if (dryrun)
	die(0);

    /* Make sure fds 0, 1, 2 are open to somewhere. */
    fd_devnull = open(_PATH_DEVNULL, O_RDWR);
    if (fd_devnull < 0)
	fatal("Couldn't open %s: %m", _PATH_DEVNULL);
    while (fd_devnull <= 2) {
	i = dup(fd_devnull);
	if (i < 0)
	    fatal("Critical shortage of file descriptors: dup failed: %m");
	fd_devnull = i;
    }

    /*
     * Initialize system-dependent stuff. 初始化系统的东西。
     */
    sys_init();

#ifdef USE_TDB
    pppdb = tdb_open(_PATH_PPPDB, 0, 0, O_RDWR|O_CREAT, 0644);
    if (pppdb != NULL) {
	slprintf(db_key, sizeof(db_key), "pppd%d", getpid());
	update_db_entry();
    } else {
	warn("Warning: couldn't open ppp database %s", _PATH_PPPDB);
	if (multilink) {
	    warn("Warning: disabling multilink");
	    multilink = 0;
	}
    }
#endif

    /*
     * Detach ourselves from the terminal, if required,
     * and identify who is running us.
     */
	 // 如果需要,将我们自己从终端分离出来,并确定谁在运行我们。
    if (!nodetach && !updetach)
	detach(); //默认放在后台以daemon执行,也可配置/etc/ppp/option中的nodetach参数放在前台执行
    p = getlogin();
    if (p == NULL) {
	pw = getpwuid(uid);
	if (pw != NULL && pw->pw_name != NULL)
	    p = pw->pw_name;
	else
	    p = "(unknown)";
    }
	 // 准备执行
    syslog(LOG_NOTICE, "pppd %s started by %s, uid %d", VERSION, p, uid);
	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.
	 */
	 // 打开环回通道并将其设置为ppp接口。
	fd_loop = open_ppp_loopback(); // 以按需拨号方式运行,可配置
	set_ifunit(1);  // 设置IFNAME环境变量为接口名称如ppp0
	/*
	 * Configure the interface and mark it up, etc.
	 */
	 // 配置接口并标记它,等等。
	demand_conf();
    }

当demand变量为1时,表示PPPD以按需拨号方式运行,打开环回通道并将其设置为ppp接口。

按需拨号的功能是:若没有到外部网络的数据流,PPP链路就不会建立,当检测到有流量访问外部网络时,PPP就开始拨号和ISP的拨号服务器建立连接,拨号成功后才产生计费。反之,如果在一定时间内没有访问外网的流量,PPP就会断开连接,为用户节省流量费用。

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;
    }
......
}

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

pppd/sys-linux.c

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

static int make_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");
       return x;
}

这里的unit可以理解为一个PPP接口,在Multilink PPP中,一个unit可以由多个channel组合而成,也就是说一个PPP接口下面可以有多个物理链路,这里的物理链路不一定是物理接口,也可以是一个物理接口上的多个频段(channel)比如HDLC channel。PPPK中channel用结构channel表示,unit用结构ppp表示。

在Linux中通过ifconfig看到的ppp0就是通过ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit)建立起来的,unit number是可以配置的,不过一般都不用配置,传入-1会自动分配一个未使用的unit number,默认从0开始。这个ioctl调用的是PPPK中注册的ppp_ioctl,之后ppp_ioctl调用 ppp_unattached_ioctl,这个函数又会调用ppp_create_interface创建一个ppp网络接口

linux-5.9/drivers/net/ppp/ppp_generic.c

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

/*
 * Stuff for handling the lists of ppp units and channels
 * and for initialization. 处理ppp单元和通道列表以及初始化的东西。
 */
/*
 * Create a new ppp interface unit.  Fails if it can't allocate memory
 * or if there is already a unit with the requested number.
 * unit == -1 means allocate a new number.
 */
// 创建一个新的ppp接口单元。如果它不能分配内存,或者已经有一个具有请求编号的单元,则失败。
// unit == -1表示分配一个新的号码。
static int ppp_create_interface(struct net *net, struct file *file, int *unit)
{
	struct ppp_config conf = {
		.file = file,
		.unit = *unit,
		.ifname_is_set = false,
	};
	struct net_device *dev;
	struct ppp *ppp;
	int err;
	dev = alloc_netdev(sizeof(struct ppp), "", NET_NAME_ENUM, ppp_setup); 
    //分配net_device,这个结构表示一个网络接口
	if (!dev) {
		err = -ENOMEM;
		goto err;
	}
	dev_net_set(dev, net);
	dev->rtnl_link_ops = &ppp_link_ops;
	rtnl_lock(); // 上锁
	err = ppp_dev_configure(net, dev, &conf); // 配置ppp; 初始化操作,
	if (err < 0)
		goto err_dev;
	ppp = netdev_priv(dev);
	*unit = ppp->file.index;
	rtnl_unlock(); // 互斥操作,解锁
	return 0;
err_dev:
	rtnl_unlock(); // 解锁
	free_netdev(dev); 
err:
	return err;
}

然后调用ppp_dev_configure配置接口:

linux-5.9/drivers/net/ppp/ppp_generic.c

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

static int ppp_dev_configure(struct net *src_net, struct net_device *dev,
			     const struct ppp_config *conf)
{
	struct ppp *ppp = netdev_priv(dev);
	int indx;
	int err;
	int cpu;
 
	ppp->dev = dev;  // 指向分配的net_device结构
	ppp->ppp_net = src_net;
	ppp->mru = PPP_MRU; // 初始化MRU(最大接收单元)
	ppp->owner = conf->file;
 
	init_ppp_file(&ppp->file, INTERFACE); //初始化ppp_file结构,类型为INTERFACE
	ppp->file.hdrlen = PPP_HDRLEN - 2; /* don't count proto bytes */
 
	for (indx = 0; indx < NUM_NP; ++indx)
		ppp->npmode[indx] = NPMODE_PASS;
	INIT_LIST_HEAD(&ppp->channels); //PPP接口中的channel链表
	spin_lock_init(&ppp->rlock); //接收队列的锁
	spin_lock_init(&ppp->wlock); //发送队列的锁
 ......
	err = ppp_unit_register(ppp, conf->unit, conf->ifname_is_set);
    // 注册ppp网络接口,这时ifconfig才能看到这个接口
	if (err < 0)
		goto err2; 
	conf->file->private_data = &ppp->file;
	return 0;
err2:
	free_percpu(ppp->xmit_recursion);
err1:
	return err;
}

现在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协议栈最终会调用到此函数。

linux-5.9/drivers/net/ppp_generic.c

ppp_start_xmit() -> ppp_xmit_process()-> ppp_send_frame():

/*
 * Compress and send a frame.
 * The caller should have locked the xmit path,
 * and xmit_pending should be 0.
 */
//压缩并发送一个帧。调用者应该锁定了xmit路径,并且xmit_pending应该为0。
static void
ppp_send_frame(struct ppp *ppp, struct sk_buff *skb)
{
 ......
	/*
	 * If we are waiting for traffic (demand dialling),
	 * queue it up for pppd to receive.
	 */
	if (ppp->flags & SC_LOOP_TRAFFIC) {
		if (ppp->file.rq.qlen > PPP_MAX_RQLEN)
			goto drop;
		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():

/*
 * demand_conf - configure the interface for doing dial-on-demand.
 */
// 配置按需拨号的接口。
void
demand_conf(void)
{
 ......
    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");

#ifdef PPP_FILTER
    set_filters(&pass_filter, &active_filter);
#endif

    /*
     * Call the demand_conf procedure for each protocol that's got one.
     */
    // 对每个有一个协议的协议调用demand_conf过程。
    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():

/*
 * ip_demand_conf - configure the interface as though
 * IPCP were up, for use with dial-on-demand.
 */
// ip_demand_conf—将接口配置为IPCP启动状态,以便使用按需拨号。
static int
ip_demand_conf(int u)
{
    ipcp_options *wo = &ipcp_wantoptions[u];

    if (wo->hisaddr == 0 && !noremoteip) {
	/* make up an arbitrary address for the peer */
	wo->hisaddr = htonl(0x0a707070 + ifunit); //对端地址
	wo->accept_remote = 1;
    }
    if (wo->ouraddr == 0) {
	/* make up an arbitrary address for us */
	wo->ouraddr = htonl(0x0a404040 + ifunit);  //本端地址
	wo->accept_local = 1;
	ask_for_local = 0;	/* don't tell the peer this address */
    }
    if (!sifaddr(u, wo->ouraddr, wo->hisaddr, GetMask(wo->ouraddr)))
	return 0;
    ipcp_script(_PATH_IPPREUP, 1);
    if (!sifup(u))
	return 0;
    if (!sifnpmode(u, PPP_IP, NPMODE_QUEUE))
        // 在ppp0接口上配置本端地址和对端地址及子网掩码
	return 0;
    if (wo->default_route)
	if (sifdefaultroute(u, wo->ouraddr, wo->hisaddr, wo->replace_default_route))
	    // 设置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);
    if (wo->hisaddr)
	notice("remote IP address %I", wo->hisaddr);

    return 1;
}

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

第二阶段 开始链接

回到主函数main()中

pppd/main.c

/* 第一阶段, 链路初始化 */
......
    do_callback = 0;
    for (;;) {

	bundle_eof = 0;
	bundle_terminating = 0;
	listen_time = 0;
	need_holdoff = 1;
	devfd = -1;
	status = EXIT_OK;
	++unsuccess;
	doing_callback = do_callback;
	do_callback = 0;

	if (demand && !doing_callback) {  // 按需拨号
	    /*
	     * Don't do anything until we see some activity.
	     */
	    new_phase(PHASE_DORMANT); // PPPD状态机,延迟状态
	    demand_unblock();
	    add_fd(fd_loop); // 1. 将fd_loop即/dev/ppp的文件描述符加入select的fds中
	    for (;;) {
		handle_events();  // 2. select事件处理
		if (asked_to_quit)
		    break;
		if (get_loop_output())  // 发送数据有效就跳出循环
		    break;
	    }
	    remove_fd(fd_loop); 
        //注意:要把/dev/ppp文件描述符从fds中remove掉,后面还会再加入
	    if (asked_to_quit)
		break;

	    /*
	     * Now we want to bring up the link.
	     */
	    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():

/*
 * add_fd - add an fd to the set that wait_input waits for.
 */
// add_fd -在wait_input等待的集合中添加一个fd。
void add_fd(int fd)
{
    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;
}
  1. 在嵌套的for(;;)死循环里调用handle_events函数进行事件处理。

pppd/main.c:

main() -> handle_events():

/*
 * handle_events - wait for something to happen and respond to it.
 */
// 等待事情发生,然后做出反应。
static void
handle_events(void)
{
    struct timeval timo;
    unsigned char buf[16];

    kill_link = open_ccp_flag = 0;

    /* alert via signal pipe */
    waiting = 1;
    /* flush signal pipe */
    for (; read(sigpipe[0], buf, sizeof(buf)) > 0; );
    add_fd(sigpipe[0]);
    /* wait if necessary */
    if (!(got_sighup || got_sigterm || got_sigusr2 || got_sigchld))
	wait_input(timeleft(&timo)); // 调用select进行I/O多路复用
    waiting = 0;
    remove_fd(sigpipe[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;
    }
......
}

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

pppd/sys-linux.c:

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

/********************************************************************
 *
 * wait_input - wait until there is data available,
 * for the length of time specified by *timo (indefinite
 * if timo is NULL).
 */
//wait_input—等待数据,直到有可用的数据,时间长度由*timo指定(如果timo为空,则为无限期)。
void wait_input(struct timeval *timo)
{
    fd_set ready, exc;
    int n;

    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():

/********************************************************************
 *
 * get_loop_output - get outgoing packets from the ppp device,
 * and detect when we want to bring the real link up.
 * Return value is 1 if we need to bring up the link, 0 otherwise.
 */
// get_loop_output—从ppp设备获取传出的数据包,并检测我们什么时候想要启动真正的链路。
// 如果需要打开链接,返回值为1,否则返回值为0。
int
get_loop_output(void)
{
    int rv = 0;
    int n;

    if (new_style_driver) {
	while ((n = read_packet(inpacket_buf)) > 0)  // 有数据通过ppp0发送
	    if (loop_frame(inpacket_buf, n)) // 发送数据合法时为真
		rv = 1; // 返回1,导致嵌套的for(;;)循环退出
	return rv;
    }
......
}

首先调用read_packet读取数据到inpacket_buf中:read_packet -从串行设备获取一个PPP数据包。这个函数很简单,实际上就是调用标准的文件读函数read()读取/dev/ppp设备文件,其实就是调用到PPPK中的ppp_read:

linux-5.9/drivers/net/ppp_generic.c

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

static ssize_t ppp_read(struct file *file, char __user *buf,
			size_t count, loff_t *ppos)
{
	struct ppp_file *pf = file->private_data;
	DECLARE_WAITQUEUE(wait, current);
	ssize_t ret;
	struct sk_buff *skb = NULL;
	struct iovec iov;
	struct iov_iter to;
 
	ret = count;
 
	if (!pf)
		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) {
			/*
			 * Return 0 (EOF) on an interface that has no
			 * channels connected, unless it is looping
			 * network traffic (demand mode).
			 */
			struct ppp *ppp = PF_TO_PPP(pf);
 
			ppp_recv_lock(ppp);
			if (ppp->n_channels == 0 &&
			    (ppp->flags & SC_LOOP_TRAFFIC) == 0) {
				ppp_recv_unlock(ppp);
				break;
			}
			ppp_recv_unlock(ppp);
		}
		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) // 能到这里表明fd为O_NONBLOCK或收到signal
		goto out;
  
	ret = -EOVERFLOW;
	if (skb->len > count)
		goto outf;
	ret = -EFAULT;
	iov.iov_base = buf;
	iov.iov_len = count;
	iov_iter_init(&to, READ, &iov, 1, count);
	if (skb_copy_datagram_iter(skb, 0, &to, skb->len)) // 将数据拷贝到用户缓冲区
		goto outf;
	ret = skb->len; // 返回值就是数据长度
 
 outf:
	kfree_skb(skb);
 out:
	return ret;
}

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

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

第三阶段 建立PPP链路

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

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

pppd/main.c

/* 第二阶段 */
......
	get_time(&start_time);
	script_unsetenv("CONNECT_TIME");
	script_unsetenv("BYTES_SENT");
	script_unsetenv("BYTES_RCVD");

	lcp_open(0);		/* Start protocol */
	// 第一步:打开PPPK接口发送LCP帧
	start_link(0);
	while (phase != PHASE_DEAD) { // 第二步:PPPD状态机循环进行事件处理; pppd非静止状态
	    handle_events();  //select事件处理
	    get_input(); // 对接收报文的处理
	    if (kill_link) // Kill_link是全局变量,初始化为0,一旦被置为1,表示用户请求断开连接
		lcp_close(0, "User request"); // 中断连接
	    if (asked_to_quit) {
		bundle_terminating = 1;
		if (phase == PHASE_MASTER) // pppd为受控状态
		    mp_bundle_terminated();
	    }
	    if (open_ccp_flag) {
		if (phase == PHASE_NETWORK || phase == PHASE_RUNNING) { // pppd为连接或静止状态
		    ccp_fsm[0].flags = OPT_RESTART; /* clears OPT_SILENT 清除OPT_SILENT */ 
		    (*ccp_protent.open)(0);// 协议处理函数指针,链路打开
		}
	    }
	}

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

pppd/lcp.c

Main() -> lcp_open():

/*
 * lcp_open - LCP is allowed to come up.
 */
void
lcp_open(int unit)
{
    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():

/*
 * fsm_open - Link is allowed to come up.
 */
void
fsm_open(fsm *f)
{
    switch( f->state ){
    case INITIAL:
	f->state = STARTING;
	if( f->callbacks->starting )
	    (*f->callbacks->starting)(f); // 初始化时开始建立链路
	break;

    case CLOSED: // 如果关闭
	if( f->flags & OPT_SILENT )
	    f->state = STOPPED;
	else {
	    /* Send an initial configure-request 发送一个初始配置请求 */
	    fsm_sconfreq(f, 0);
	    f->state = REQSENT;
	}
	break;

    case CLOSING:
	f->state = STOPPING;
	/* fall through */
    case STOPPED:
    case OPENED:
	if( f->flags & OPT_RESTART ){
	    fsm_lowerdown(f);
	    fsm_lowerup(f);
	}
	break;
    }
}

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

pppd/auth.c

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

/*
 * An Open on LCP has requested a change from Dead to Establish phase.
 */
// LCP的一个开放要求从静止阶段转变为建立阶段。
void
link_required(int unit)
{
}

/*
 * Bring the link up to the point of being able to do ppp.
 */
void start_link(int unit)
{
    status = EXIT_CONNECT_FAILED;
    new_phase(PHASE_SERIALCONN); // PPPD状态机为“串口连接”阶段

    hungup = 0;
    devfd = the_channel->connect();  //1. 调用connect_tty连接TTY驱动
    if (devfd < 0)
	goto fail;

    /* set up the serial device as a ppp interface */
    /*
     * N.B. we used to do tdb_writelock/tdb_writeunlock around this
     * (from establish_ppp to set_ifunit).  However, we won't be
     * doing the set_ifunit in multilink mode, which is the only time
     * we need the atomicity that the tdb_writelock/tdb_writeunlock
     * gives us.  Thus we don't need the tdb_writelock/tdb_writeunlock.
     */
    fd_ppp = the_channel->establish_ppp(devfd); //2. 调用 tty_establish_ppp
    if (fd_ppp < 0) {
	status = EXIT_FATAL_ERROR;
	goto disconnect;
    }

    if (!demand && ifunit >= 0) //如果是不是demand模式,需要设置 IFNAME环境变量
	set_ifunit(1);

    /*
     * Start opening the connection and wait for
     * incoming events (reply, timeout, etc.).
     */
    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():

/********************************************************************
 *
 * tty_establish_ppp - Turn the serial port into a ppp interface.
 */
// tty_establish_ppp—将串口变为ppp接口。
int tty_establish_ppp (int tty_fd)
{
    int ret_fd;

/*
 * Ensure that the tty device is in exclusive mode.确保tty设备处于独占模式。
 */
    if (ioctl(tty_fd, TIOCEXCL, 0) < 0) {
	if ( ! ok_error ( errno ))
	    warn("Couldn't make tty exclusive: %m");
    }
/*
 * Demand mode - prime the old ppp device to relinquish the unit.
 需求模式-启动旧的ppp设备放弃单元。
 */
    if (!new_style_driver && looped
	&& ioctl(slave_fd, PPPIOCXFERUNIT, 0) < 0) {
	error("ioctl(transfer ppp unit): %m, line %d", __LINE__);
	return -1;
    }
/*
 * Set the current tty to the PPP discpline 将当前tty设置为PPP规则
 */

#ifndef N_SYNC_PPP
#define N_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接口
......
}

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

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

linux-5.9/drivers/tty/tty_io.c

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

/**
 *	tiocsetd	-	set line discipline
 *	@tty: tty device
 *	@p: pointer to user data
 *
 *	Set the line discipline according to user request.
 *
 *	Locking: see tty_set_ldisc, this function is just a helper
 */
static int tiocsetd(struct tty_struct *tty, int __user *p)
{
	int disc;
	int ret;
	if (get_user(disc, p))
		return -EFAULT;
	ret = tty_set_ldisc(tty, disc);//设定线路规程,即异步PPP或同步ppp
	return ret;
}

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

linux-5.9/drivers/tty/tty_ldisc.c

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

/**
 *	tty_set_ldisc		-	set line discipline
 *	@tty: the terminal to set
 *	@ldisc: the line discipline
 *
 *	Set the discipline of a tty line. Must be called from a process
 *	context. The ldisc change logic has to protect itself against any
 *	overlapping ldisc change (including on the other end of pty pairs),
 *	the close of one side of a tty/pty pair, and eventually hangup.
 */
// 设置tty line的纪律。必须从流程上下文中调用。ldisc更改逻辑必须保护自己不受任何重叠的ldisc更改(包括在pty对的另一端)、tty/pty对一侧的关闭以及最终挂机的影响。
int tty_set_ldisc(struct tty_struct *tty, int disc)
{
......
	new_ldisc = tty_ldisc_get(tty, disc);  //从tty_ldiscs全局数组找出注册的PPP线路规程
	if (IS_ERR(new_ldisc))
		return PTR_ERR(new_ldisc);
......
}

这个函数为TTY驱动绑定N_PPP线路规程,绑定后调用线路规程的open()函数,对于N_PPP实际上是调用ppp_asynctty_open,此函数分配并初始化struct asyncppp结构来表示一个异步PPP,并将tty结构的disc_data指向该结构。另外调用ppp_register_net_channel注册了一个异步PPP channel:

linux-5.9/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() -> ppp_register_net_channel():

int ppp_register_channel(struct ppp_channel *chan)
{
	return ppp_register_net_channel(current->nsproxy->net_ns, chan);
}
/* Create a new, unattached ppp channel for specified net. */
int ppp_register_net_channel(struct net *net, struct ppp_channel *chan)
{
	struct channel *pch;
	struct ppp_net *pn;
	pch = kzalloc(sizeof(struct channel), GFP_KERNEL);
	if (!pch)
		return -ENOMEM;
	pn = ppp_pernet(net);
	pch->ppp = NULL;  // channel还不属于任何PPP unit,初始化为NULL
	pch->chan = chan; // channel中指向ppp_channel的指针
	pch->chan_net = get_net(net);
	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 /* CONFIG_PPP_MULTILINK */
	init_rwsem(&pch->chan_sem);
	spin_lock_init(&pch->downl);
	rwlock_init(&pch->upl);
	spin_lock_bh(&pn->all_channels_lock);
	pch->file.index = ++pn->last_channel_index; // channel索引值,后面会用到
	list_add(&pch->list, &pn->new_channels); // 注册到new_channels全局链表
	atomic_inc(&channel_count);
	spin_unlock_bh(&pn->all_channels_lock);
	return 0;
}

到此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():

/********************************************************************
 *
 * generic_establish_ppp - Turn the fd into a ppp interface.
 */
int generic_establish_ppp (int fd)
{
......
	/* Open an instance of /dev/ppp and connect the channel to it */
	if (ioctl(fd, PPPIOCGCHAN, &chindex) == -1) {  // 1) 获取channel number
	    error("Couldn't get channel number: %m");
	    goto err;
	}
	dbglog("using channel %d", chindex);
	fd = open("/dev/ppp", O_RDWR);  // 打开/dev/ppp
	if (fd < 0) {
	    error("Couldn't reopen /dev/ppp: %m");
	    goto err;
	}
	(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);
	    goto err_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
	    /*
	     * Create a new PPP unit.
	     */
	    if (make_ppp_unit() < 0)  // 3) demand模式下已经调用过make_ppp_unit了,这里用于正常拨号
		goto err_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);
		goto err_close;
	    }
	}
......
}

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

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

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

  3. 对于正常拨号,调用make_ppp_unit创建ppp0网络接口并将此接口绑定,绑定后的/dev/ppp文件描述符保存在ppp_dev_fd。(ppp_dev_fd文件描述符代表的是一个unit,ppp_fd文件描述符代表的是一个channel)

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

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

pppd/sys-linux.c

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

/********************************************************************
 *
 * output - Output PPP packet.
 */
void output (int unit, unsigned char *p, int len)
{
    int fd = ppp_fd;
    int proto;

    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-5.9/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() :

static ssize_t ppp_write(struct file *file, const char __user *buf,
			 size_t count, loff_t *ppos)
{
	struct ppp_file *pf = file->private_data;
	struct sk_buff *skb;
	ssize_t ret;
	if (!pf)
		return -ENXIO;
	ret = -ENOMEM;
	skb = alloc_skb(count + pf->hdrlen, GFP_KERNEL);  // Linux内核用sk_buff存放网络数据包
	if (!skb)
		goto out;
	skb_reserve(skb, pf->hdrlen);
	ret = -EFAULT;
	if (copy_from_user(skb_put(skb, count), buf, count)) { // 将发送数据拷贝到skb
		kfree_skb(skb);
		goto out;
	}
	switch (pf->kind) {  //注意:通过ppp_file的kind字段判断/dev/ppp绑定的是unit还是channel
	case INTERFACE:  // 接口即unit
		ppp_xmit_process(PF_TO_PPP(pf), skb);
		break;
	case CHANNEL: // channel
		skb_queue_tail(&pf->xq, skb);
		ppp_channel_push(PF_TO_CHANNEL(pf)); // 调用 ppp_channel_push
		break;
	}
	ret = count;
 out:
	return ret;
}

继续对这个函数分析,现在要发送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() -> _ppp_channel_push:

static void ppp_channel_push(struct channel *pch)
{
	read_lock_bh(&pch->upl);
	if (pch->ppp) {
		(*this_cpu_ptr(pch->ppp->xmit_recursion))++;
		__ppp_channel_push(pch);
		(*this_cpu_ptr(pch->ppp->xmit_recursion))--;
	} else {
		__ppp_channel_push(pch);
	}
	read_unlock_bh(&pch->upl);
}

/* Try to send data out on a channel */
static void __ppp_channel_push(struct channel *pch)
{
	struct sk_buff *skb;
	struct ppp *ppp;
	spin_lock(&pch->downl);
	if (pch->chan) { // 已经在前面的ppp_register_channel初始化了,这里不为空
		while (!skb_queue_empty(&pch->file.xq)) { // skb已放在发送队列xq中
			skb = skb_dequeue(&pch->file.xq);
        // 前面的ppp_asynctty_open已经初始化了ops为async_ops,所以这里实际调用ppp_async_send
			if (!pch->chan->ops->start_xmit(pch->chan, skb)) {
				/* put the packet back and try again later */
				skb_queue_head(&pch->file.xq, skb);
				break;
			}
		}
	} else {
		/* channel got deregistered */
		skb_queue_purge(&pch->file.xq);
	}
	spin_unlock(&pch->downl);
	/* see if there is anything from the attached unit to be sent */
	if (skb_queue_empty(&pch->file.xq)) {
		ppp = pch->ppp;
		if (ppp) // 如果channel已经连接到unit了,则不为空
			__ppp_xmit_process(ppp, NULL); // 这个函数用于发送数据帧
	}
}

实际调用ppp_async_send发送LCP帧。 到此,LCP Configure Request帧就发送出去了。

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

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

pppd/main.c -> main():

	lcp_open(0);		/* Start protocol */
	// 第一步:打开PPPK接口发送LCP帧
	start_link(0);
	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():

/*
 * get_input - called when incoming data is available.
 */
static void
get_input(void)
{
......
    p = inpacket_buf;	/* point to beginning of packet buffer */

    len = read_packet(inpacket_buf); //读取接收报文到inpacket_buf缓冲区
    if (len < 0)
	return;
......
    p += 2;				/* Skip address and control */
    GETSHORT(protocol, p); // 获取报文中带的协议号
    len -= PPP_HDRLEN; // 有效数据长度
......
    /*
     * Upcall the proper protocol input routine.
     */
    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函数。

至此,PPPD建立连接所需的数据收发基本流程就勾画出来了。

第四阶段:链路终止

pppd/main.c

main()

/* 第三阶段 */
......
	/* restore FSMs to original state */
	lcp_close(0, ""); // 关闭协议

	if (!persist || asked_to_quit || (maxfail > 0 && unsuccess >= maxfail))
	    break; // 链路故障或请求关闭

	if (demand)
	    demand_discard(); // 设置每个网络协议丢弃有错误的数据包。
	t = need_holdoff? holdoff: 0;
	if (holdoff_hook)
	    t = (*holdoff_hook)();
	if (t > 0) {
	    new_phase(PHASE_HOLDOFF); // pppd状态机。pppd为延迟状态
	    TIMEOUT(holdoff_end, NULL, t);
	    do {
		handle_events();
		if (kill_link)  // 用户请求关闭
		    new_phase(PHASE_DORMANT); /* allow signal to end holdoff */
             // pppd状态机。pppd进入休眠状态
	    } while (phase == PHASE_HOLDOFF);
	    if (!persist)
		break;
	}
    }

直接调用lcp_close关闭LCP协议。如果发生链路故障或用户请求关闭,直接跳出循环,回收子进程。否则正常退出,调用 demand_discard函数:

pppd/demand.c

main() -> demand_discard()

/*
 * demand_discard - set each network protocol to discard packets
 * with an error. 设置每个网络协议丢弃有错误的数据包。
 */
void
demand_discard(void)
{
    struct packet *pkt, *nextpkt;
    int i;
    struct protent *protp;

    for (i = 0; (protp = protocols[i]) != NULL; ++i) // 遍历所有协议
	if (protp->enabled_flag && protp->demand_conf != NULL)
	    sifnpmode(0, protp->protocol & ~0x8000, NPMODE_ERROR);
    get_loop_output(); // 又是这个函数,从ppp设备获取传出的数据包

    /* discard all saved packets 丢弃所有保存的包 */
    for (pkt = pend_q; pkt != NULL; pkt = nextpkt) {
	nextpkt = pkt->next;
	free(pkt); // 释放
    }
    pend_q = NULL;
    framelen = 0;
    flush_flag = 0;
    escape_flag = 0;
    fcs = PPP_INITFCS;
}

这个函数遍历所有当前以及配置好的协议,对美国协议调用get_loop_output函数获取传出的数据包,依次释放。

pppd/main.c

main()

......
    /* Wait for scripts to finish 等待脚本完成 */
    reap_kids(); // 从任何死亡的子进程中获取状态,并记录异常终止的消息。
    if (n_children > 0) {
	if (child_wait > 0)
	    TIMEOUT(childwait_end, NULL, child_wait); // 超时,回收子进程
	if (debug) {
	    struct subprocess *chp;
	    dbglog("Waiting for %d child processes...", n_children);
	    for (chp = children; chp != NULL; chp = chp->next)
		dbglog("  script %s, pid %d", chp->prog, chp->pid);
	}
	while (n_children > 0 && !childwait_done) {
	    handle_events(); // 处理函数
	    if (kill_link && !childwait_done)
		childwait_end(NULL);
	}
    }

    die(status); // 结束
    return 0;
}

之后main函数将pppd的状态置为延迟状态,当接到用户的关闭请求后,进入休眠状态,链路关闭。最后回收子进程,main函数结束。

参考资料

ppp协议工作流程部分来自 《计算机网络》第7版 谢希仁

对ppp协议的源码分析参考了博客【Linux PPP实现源码分析】https://blog.csdn.net/osnetdev/article/details/8958058 对 pppd android 2.3源码包 的分析

其中查阅的 linux 源码来自 https://code.woboq.org/linux/linux/

  • 8
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 点大商城v2-2.4.9独立版源码是一种电子商务平台的源代码,可以用于搭建一个独立的在线商城。这个源码具有以下特点和功能: 1. 功能丰富:点大商城v2-2.4.9独立版源码提供了完整的电子商务功能,包括商品展示、购物车、订单管理、支付、物流跟踪等。用户可以方便地在商城中浏览商品、下单购买,并付款完成交易。 2. 界面简洁:该源码拥有简洁美观的界面设计,符合现代用户体验的要求。用户可以轻松地浏览商品,添加到购物车,下单付款等操作。 3. 可定制性强:点大商城v2-2.4.9独立版源码支持自定义开发和定制,可以根据不同的商城需求进行修改和扩展。开发人员可以根据具体需求进行二次开发,满足商城的个性化需求。 4. 安全性保证:该源码经过了严格的安全测试和加密处理,保证了用户信息的安全。支付和订单处理等关键操作都经过严格的验证和加密,确保用户的交易过程安全可靠。 5. 支持多平台:点大商城v2-2.4.9独立版源码支持多终端访问,包括PC端、移动端等。用户可以通过电脑、手机或平板等设备访问商城,并进行购物和交易。 总的来说,点大商城v2-2.4.9独立版源码是一个功能强大、界面美观、安全可靠的电子商务平台源代码,能够帮助用户快速搭建一个独立的在线商城,满足用户的购物需求。 ### 回答2: 点大商城v2-2.4.9独立版源码是一个开源的电子商务平台系统源码,使用该源码可以快速搭建一个具备商品管理、订单管理、会员管理、支付管理等功能的电子商城。 该独立版源码是基于点大商城的v2版本进行开发和优化的,通过源码搭建的电商平台可以适应不同规模的商家需求,提供全面的电子商务解决方案。 在该版本的源码中,开发者可以自由定制商城的界面风格和功能模块。通过灵活的配置选项和插件机制,可以满足不同商家的特定需求。同时,源码也提供了丰富的API接口,方便与第三方系统进行集成。 除了基本的管理功能,该源码还提供了一些高级功能,如多店铺管理、优惠券管理、秒杀活动等,可以帮助商家提升销售额和用户体验。 同时,该独立版源码也考虑到了安全和稳定性的需求,在代码编写和系统设计上进行了大量优化,提供了多层次的权限管理和数据加密机制,以保障商家和用户的信息安全。 总之,通过点大商城v2-2.4.9独立版源码,开发者可以快速搭建一个稳定、安全、功能丰富的电子商务平台,为商家提供全面的解决方案,助力其实现线上业务的发展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值