一、PPP 协议基础与嵌入式应用场景
PPP (Point-to-Point Protocol) 是一种在串行线路上传输多协议数据包的通信协议,广泛应用于拨号上网、VPN 和嵌入式系统的远程通信场景。在嵌入式开发中,PPP 常用于 GPRS/3G/4G 模块、工业路由器和物联网设备的网络连接。
PPP 协议提供了以下核心功能:
- 链路控制协议 (LCP):建立、配置和测试数据链路
- 网络层协议 (NCP):协商并配置不同的网络层协议
- 认证协议:支持 PAP、CHAP 等认证方式
二、Linux 下 PPP 拨号的系统架构
在 Linux 系统中,PPP 拨号主要涉及以下组件:
- PPPD 守护进程:用户空间程序,负责 PPP 链路的建立、维护和终止
- Chat 脚本:辅助工具,用于与调制解调器进行 AT 命令交互
- 内核 PPP 驱动:提供 PPP 协议的底层实现
- 网络配置工具:如 ifconfig、route 等,用于配置拨号后的网络参数
典型的嵌入式 PPP 拨号系统架构如下:
+---------------------+
| 应用程序/服务 |
+---------------------+
| PPPD守护进程 |
+---------------------+
| Chat脚本 |
+---------------------+
| 串口驱动/USB驱动 |
+---------------------+
| 调制解调器 |
+---------------------+
| 网络链路 |
+---------------------+
三、PPP 拨号程序实现方案
下面介绍在 Linux 嵌入式系统中实现 PPP 拨号的两种主要方案:
方案一:调用系统命令实现 PPP 拨号
这是最简单的实现方式,通过 system () 或 popen () 函数调用系统 pppd 命令和 chat 脚本:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
// PPP拨号函数
int ppp_dial(const char *device, const char *apn,
const char *username, const char *password) {
pid_t pid;
int status;
char cmd[256];
// 构建chat脚本内容
FILE *chat_file = fopen("/tmp/chatscript", "w");
if (chat_file == NULL) {
perror("Failed to create chat script");
return -1;
}
fprintf(chat_file, "#!/bin/sh\n");
fprintf(chat_file, "ABORT 'BUSY'\n");
fprintf(chat_file, "ABORT 'NO CARRIER'\n");
fprintf(chat_file, "ABORT 'NO DIALTONE'\n");
fprintf(chat_file, "ABORT 'ERROR'\n");
fprintf(chat_file, "TIMEOUT 30\n");
fprintf(chat_file, "SAY 'Starting PPP dial...\\n'\n");
fprintf(chat_file, "'' ATZ\\n");
fprintf(chat_file, "OK AT+CGDCONT=1,\"IP\",\"%s\"\\n", apn);
fprintf(chat_file, "OK ATD*99#\\n");
fprintf(chat_file, "CONNECT ''\n");
fclose(chat_file);
// 设置chat脚本可执行权限
system("chmod +x /tmp/chatscript");
// 构建pppd命令
snprintf(cmd, sizeof(cmd),
"pppd call /tmp/chatscript %s user %s password %s debug nodetach &",
device, username, password);
// 执行pppd命令
pid = fork();
if (pid < 0) {
perror("Fork failed");
return -1;
} else if (pid == 0) {
// 子进程执行pppd命令
execl("/bin/sh", "sh", "-c", cmd, NULL);
exit(EXIT_FAILURE);
} else {
// 父进程等待子进程结束
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
printf("PPP dial successful\n");
return 0;
} else {
printf("PPP dial failed, exit status: %d\n", WEXITSTATUS(status));
return -1;
}
}
}
// PPP断开连接函数
int ppp_hangup() {
return system("pkill -f pppd");
}
// 检查PPP连接状态
int ppp_check_status() {
FILE *fp;
char buffer[128];
int connected = 0;
// 检查pppd进程是否存在
fp = popen("ps aux | grep pppd | grep -v grep", "r");
if (fp != NULL) {
if (fgets(buffer, sizeof(buffer), fp) != NULL) {
connected = 1;
}
pclose(fp);
}
// 检查ppp0接口是否存在
if (connected) {
fp = popen("ifconfig ppp0", "r");
if (fp != NULL) {
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
connected = 0;
}
pclose(fp);
}
}
return connected;
}
int main() {
// PPP参数配置
const char *device = "/dev/ttyUSB0"; // 调制解调器设备
const char *apn = "internet"; // APN名称
const char *username = ""; // 用户名
const char *password = ""; // 密码
printf("Starting PPP dial...\n");
// 执行PPP拨号
if (ppp_dial(device, apn, username, password) == 0) {
printf("PPP connection established\n");
// 检查连接状态
if (ppp_check_status()) {
printf("PPP connection is active\n");
// 保持连接一段时间
sleep(300);
// 断开连接
printf("Hanging up PPP connection...\n");
ppp_hangup();
printf("PPP connection terminated\n");
} else {
printf("Failed to establish PPP connection\n");
}
} else {
printf("PPP dial failed\n");
}
return 0;
}
方案二:直接调用 PPP 库函数实现
更高级的实现方式是直接调用 PPP 相关的库函数,这种方式提供了更精细的控制:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
// PPP帧结构定义
#define PPP_FLAG 0x7E
#define PPP_ESC 0x7D
#define PPP_XOR 0x20
#define PPP_ADDR 0xFF
#define PPP_CTRL 0x03
#define PPP_LCP 0xC021
#define PPP_PAP 0xC023
#define PPP_IPCP 0x8021
#define PPP_IP 0x0021
// 串口初始化函数
int serial_init(const char *device, int baudrate) {
int fd;
struct termios options;
// 打开串口设备
fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
perror("Failed to open serial device");
return -1;
}
// 获取当前串口配置
if (tcgetattr(fd, &options) != 0) {
perror("Failed to get serial attributes");
close(fd);
return -1;
}
// 设置波特率
switch (baudrate) {
case 9600: cfsetispeed(&options, B9600); cfsetospeed(&options, B9600); break;
case 115200: cfsetispeed(&options, B115200); cfsetospeed(&options, B115200); break;
default: cfsetispeed(&options, B115200); cfsetospeed(&options, B115200); break;
}
// 设置串口参数:8数据位,1停止位,无校验
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag |= (CLOCAL | CREAD);
// 设置为原始模式
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag &= ~OPOST;
// 设置超时参数
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 10; // 1秒超时
// 应用新的配置
if (tcsetattr(fd, TCSANOW, &options) != 0) {
perror("Failed to set serial attributes");
close(fd);
return -1;
}
// 清空输入输出缓冲区
tcflush(fd, TCIOFLUSH);
return fd;
}
// 发送PPP帧
int ppp_send_frame(int fd, unsigned short protocol, const unsigned char *data, int len) {
unsigned char frame[4096];
int frame_len = 0;
int i;
// 添加帧头
frame[frame_len++] = PPP_FLAG;
// 添加地址和控制字段
frame[frame_len++] = PPP_ADDR;
frame[frame_len++] = PPP_CTRL;
// 添加协议字段(大端序)
frame[frame_len++] = (protocol >> 8) & 0xFF;
frame[frame_len++] = protocol & 0xFF;
// 添加数据并进行转义
for (i = 0; i < len; i++) {
if (data[i] == PPP_FLAG || data[i] == PPP_ESC) {
frame[frame_len++] = PPP_ESC;
frame[frame_len++] = data[i] ^ PPP_XOR;
} else {
frame[frame_len++] = data[i];
}
}
// 添加帧尾
frame[frame_len++] = PPP_FLAG;
// 发送帧
return write(fd, frame, frame_len);
}
// 接收PPP帧
int ppp_recv_frame(int fd, unsigned short *protocol, unsigned char *data, int max_len, int timeout_sec) {
unsigned char buffer[4096];
int buffer_len = 0;
int i, j;
int in_frame = 0;
int escaped = 0;
fd_set readfds;
struct timeval timeout;
int ret;
// 设置超时
timeout.tv_sec = timeout_sec;
timeout.tv_usec = 0;
while (1) {
// 初始化文件描述符集
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
// 等待数据或超时
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret < 0) {
perror("Select error");
return -1;
} else if (ret == 0) {
// 超时
return 0;
}
// 读取数据
unsigned char temp[256];
int n = read(fd, temp, sizeof(temp));
if (n <= 0) {
continue;
}
// 处理接收到的数据
for (i = 0; i < n; i++) {
unsigned char c = temp[i];
if (c == PPP_FLAG) {
if (in_frame && buffer_len >= 5) {
// 解析协议字段(大端序)
*protocol = (buffer[0] << 8) | buffer[1];
// 提取数据
int data_len = buffer_len - 4;
if (data_len > max_len) {
data_len = max_len;
}
memcpy(data, &buffer[2], data_len);
return data_len;
}
// 开始新帧
in_frame = 1;
buffer_len = 0;
escaped = 0;
continue;
}
if (!in_frame) {
continue;
}
if (c == PPP_ESC) {
escaped = 1;
continue;
}
if (escaped) {
c ^= PPP_XOR;
escaped = 0;
}
if (buffer_len < sizeof(buffer) - 1) {
buffer[buffer_len++] = c;
}
}
}
return 0;
}
// 发送AT命令并获取响应
int send_at_command(int fd, const char *command, char *response, int max_len, int timeout_sec) {
fd_set readfds;
struct timeval timeout;
int ret;
int response_len = 0;
// 发送AT命令
write(fd, command, strlen(command));
write(fd, "\r\n", 2);
// 设置超时
timeout.tv_sec = timeout_sec;
timeout.tv_usec = 0;
// 读取响应
while (1) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret < 0) {
perror("Select error");
return -1;
} else if (ret == 0) {
// 超时
break;
}
if (FD_ISSET(fd, &readfds)) {
char buffer[256];
int n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
// 追加到响应缓冲区
if (response_len + n < max_len) {
memcpy(response + response_len, buffer, n);
response_len += n;
response[response_len] = '\0';
}
// 检查是否收到OK或ERROR
if (strstr(response, "OK") != NULL || strstr(response, "ERROR") != NULL) {
break;
}
}
}
}
return response_len;
}
// 初始化调制解调器
int modem_init(int fd) {
char response[1024];
// 重置调制解调器
send_at_command(fd, "ATZ", response, sizeof(response), 5);
// 设置为命令模式
send_at_command(fd, "ATE0", response, sizeof(response), 5);
// 检查调制解调器是否就绪
if (send_at_command(fd, "AT", response, sizeof(response), 5) < 0) {
return -1;
}
if (strstr(response, "OK") == NULL) {
return -1;
}
return 0;
}
// 配置GPRS连接
int configure_gprs(int fd, const char *apn) {
char command[128];
char response[1024];
// 设置APN
snprintf(command, sizeof(command), "AT+CGDCONT=1,\"IP\",\"%s\"", apn);
if (send_at_command(fd, command, response, sizeof(response), 10) < 0) {
return -1;
}
if (strstr(response, "OK") == NULL) {
return -1;
}
return 0;
}
// 建立PPP连接
int establish_ppp_connection(int fd) {
char response[1024];
// 发起PPP连接
if (send_at_command(fd, "ATD*99#", response, sizeof(response), 30) < 0) {
return -1;
}
// 检查是否连接成功
if (strstr(response, "CONNECT") == NULL) {
return -1;
}
return 0;
}
int main() {
int fd;
const char *device = "/dev/ttyUSB0";
const char *apn = "internet";
// 初始化串口
fd = serial_init(device, 115200);
if (fd < 0) {
printf("Failed to initialize serial port\n");
return -1;
}
// 初始化调制解调器
if (modem_init(fd) < 0) {
printf("Failed to initialize modem\n");
close(fd);
return -1;
}
// 配置GPRS
if (configure_gprs(fd, apn) < 0) {
printf("Failed to configure GPRS\n");
close(fd);
return -1;
}
// 建立PPP连接
if (establish_ppp_connection(fd) < 0) {
printf("Failed to establish PPP connection\n");
close(fd);
return -1;
}
printf("PPP connection established successfully\n");
// PPP通信循环
unsigned short protocol;
unsigned char data[1024];
int len;
printf("Waiting for PPP frames...\n");
while (1) {
len = ppp_recv_frame(fd, &protocol, data, sizeof(data), 5);
if (len > 0) {
printf("Received PPP frame: protocol=0x%04X, len=%d\n", protocol, len);
// 处理不同类型的PPP帧
switch (protocol) {
case PPP_LCP:
printf(" LCP frame\n");
// 处理LCP帧
break;
case PPP_IPCP:
printf(" IPCP frame\n");
// 处理IPCP帧
break;
case PPP_IP:
printf(" IP frame\n");
// 处理IP数据报
break;
default:
printf(" Unknown protocol: 0x%04X\n", protocol);
break;
}
}
}
// 关闭连接
close(fd);
return 0;
}
四、PPP 拨号配置文件与参数说明
在 Linux 系统中,PPP 拨号通常需要配置以下文件:
- /etc/ppp/options:PPP 通用选项配置文件
# PPP通用选项
lock # 锁定串口设备
crtscts # 使用硬件流控制
asyncmap 0 # 禁用字符映射
defaultroute # 添加默认路由
usepeerdns # 使用DNS服务器提供的IP
- /etc/ppp/peers/provider:特定连接的配置文件
# 特定连接配置
/dev/ttyUSB0 # 串口设备
115200 # 波特率
connect '/usr/sbin/chat -v -f /etc/ppp/chatscripts/gprs' # chat脚本路径
noauth # 不使用认证
persist # 保持连接
maxfail 0 # 允许无限次连接尝试
holdoff 2 # 连接失败后等待2秒再尝试
lcp-echo-interval 30 # 每30秒发送一次LCP回显请求
lcp-echo-failure 4 # 连续4次LCP回显请求失败后断开连接
- /etc/ppp/chatscripts/gprs:Chat 脚本示例
ABORT "BUSY"
ABORT "NO CARRIER"
ABORT "NO DIALTONE"
ABORT "ERROR"
TIMEOUT 30
SAY "Connecting to GPRS network...\n"
'' ATZ
OK AT+CGDCONT=1,"IP","internet"
OK ATD*99#
CONNECT ""
五、错误处理与调试技巧
在 PPP 拨号过程中,可能会遇到各种问题,以下是一些常见问题及解决方法:
-
无法连接到调制解调器
- 检查串口设备路径是否正确
- 检查设备权限是否允许访问
- 使用 minicom 等工具测试串口通信
-
Chat 脚本执行失败
- 检查 AT 命令是否正确
- 增加 Chat 脚本中的调试信息
- 确认调制解调器支持的 AT 命令集
-
PPP 连接建立失败
- 检查 APN、用户名和密码是否正确
- 查看 /var/log/syslog 或 /var/log/messages 中的 PPP 日志
- 使用 pppd 的 debug 选项获取详细调试信息
-
IP 地址分配失败
- 检查网络服务提供商的 IP 分配策略
- 确认 IPCP 协商参数是否正确
- 尝试手动配置 IP 地址
调试 PPP 连接时,可以使用以下命令:
# 以调试模式运行pppd
pppd debug /dev/ttyUSB0 115200 nodetach
# 查看PPP连接状态
ifconfig ppp0
route -n
# 查看PPP日志
tail -f /var/log/syslog | grep pppd
六、PPP 拨号程序的优化与扩展
为了提高 PPP 拨号程序的稳定性和可靠性,可以考虑以下优化措施:
- 添加断线自动重连机制
- 实现网络连接状态检测
- 添加 PPP 进程监控和自动重启功能
- 支持多种网络连接方式的切换
- 实现 PPP 连接参数的动态配置
以下是一个增强版的 PPP 拨号管理程序框架:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
// 配置参数
#define CONFIG_FILE "/etc/ppp/config.ini"
#define LOG_FILE "/var/log/ppp_manager.log"
#define PID_FILE "/var/run/ppp_manager.pid"
#define CHECK_INTERVAL 30 // 连接检查间隔(秒)
#define RECONNECT_DELAY 10 // 重连延迟(秒)
#define MAX_RETRIES 5 // 最大重试次数
// 全局变量
volatile sig_atomic_t running = 1;
char device[64] = "/dev/ttyUSB0";
char apn[64] = "internet";
char username[64] = "";
char password[64] = "";
int debug_mode = 0;
// 日志函数
void log_message(const char *message) {
FILE *fp;
time_t t;
char time_str[26];
time(&t);
ctime_r(&t, time_str);
time_str[24] = '\0'; // 去掉换行符
if (debug_mode) {
printf("[%s] %s\n", time_str, message);
}
fp = fopen(LOG_FILE, "a");
if (fp) {
fprintf(fp, "[%s] %s\n", time_str, message);
fclose(fp);
}
}
// 读取配置文件
int read_config() {
FILE *fp;
char line[256];
fp = fopen(CONFIG_FILE, "r");
if (!fp) {
log_message("Failed to open config file");
return -1;
}
while (fgets(line, sizeof(line), fp)) {
// 去掉换行符
line[strcspn(line, "\n")] = 0;
// 跳过注释和空行
if (line[0] == '#' || line[0] == '\0') {
continue;
}
// 解析配置项
char *key = strtok(line, "=");
char *value = strtok(NULL, "=");
if (key && value) {
if (strcmp(key, "device") == 0) {
strncpy(device, value, sizeof(device) - 1);
} else if (strcmp(key, "apn") == 0) {
strncpy(apn, value, sizeof(apn) - 1);
} else if (strcmp(key, "username") == 0) {
strncpy(username, value, sizeof(username) - 1);
} else if (strcmp(key, "password") == 0) {
strncpy(password, value, sizeof(password) - 1);
} else if (strcmp(key, "debug") == 0) {
debug_mode = (strcmp(value, "1") == 0 || strcasecmp(value, "true") == 0);
}
}
}
fclose(fp);
return 0;
}
// 检查网络连接状态
int check_network_status() {
// 方法1: 检查ppp0接口是否存在
FILE *fp = popen("ifconfig ppp0 2>/dev/null", "r");
if (!fp) {
return 0;
}
char buffer[128];
int exists = (fgets(buffer, sizeof(buffer), fp) != NULL);
pclose(fp);
if (!exists) {
return 0;
}
// 方法2: 尝试ping外部服务器
fp = popen("ping -c 1 -W 2 8.8.8.8 2>/dev/null", "r");
if (!fp) {
return 1; // 接口存在但ping失败,仍认为连接存在
}
int connected = 0;
while (fgets(buffer, sizeof(buffer), fp)) {
if (strstr(buffer, "1 packets transmitted, 1 received") != NULL) {
connected = 1;
break;
}
}
pclose(fp);
return connected;
}
// PPP拨号函数
int ppp_dial() {
pid_t pid;
int status;
char cmd[256];
log_message("Starting PPP dial...");
// 构建pppd命令
snprintf(cmd, sizeof(cmd),
"pppd call provider %s user %s password %s debug nodetach",
device, username, password);
// 执行pppd命令
pid = fork();
if (pid < 0) {
log_message("Fork failed");
return -1;
} else if (pid == 0) {
// 子进程执行pppd命令
execl("/bin/sh", "sh", "-c", cmd, NULL);
exit(EXIT_FAILURE);
} else {
// 父进程等待子进程结束
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
log_message("PPP dial successful");
return 0;
} else {
log_message("PPP dial failed");
return -1;
}
}
}
// PPP断开连接函数
int ppp_hangup() {
log_message("Hanging up PPP connection...");
return system("pkill -f pppd");
}
// 信号处理函数
void signal_handler(int signum) {
switch (signum) {
case SIGINT:
case SIGTERM:
log_message("Received termination signal, exiting...");
running = 0;
break;
case SIGHUP:
log_message("Received reload signal, reloading configuration...");
read_config();
break;
}
}
// 守护进程化
void daemonize() {
pid_t pid, sid;
// 第一步fork
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出
}
// 创建新会话
sid = setsid();
if (sid < 0) {
exit(EXIT_FAILURE);
}
// 第二步fork
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出
}
// 改变工作目录
if (chdir("/") < 0) {
exit(EXIT_FAILURE);
}
// 关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 创建PID文件
FILE *fp = fopen(PID_FILE, "w");
if (fp) {
fprintf(fp, "%d\n", getpid());
fclose(fp);
}
}
int main(int argc, char *argv[]) {
int daemon = 1;
int retries = 0;
// 解析命令行参数
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--debug") == 0) {
daemon = 0;
debug_mode = 1;
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
printf("Usage: %s [-d|--debug] [-h|--help]\n", argv[0]);
return 0;
}
}
// 读取配置文件
if (read_config() < 0) {
return 1;
}
// 守护进程化
if (daemon) {
daemonize();
}
// 注册信号处理函数
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGHUP, signal_handler);
log_message("PPP Manager started");
// 主循环
while (running) {
if (!check_network_status()) {
log_message("Network connection lost, attempting to reconnect...");
// 断开当前连接
ppp_hangup();
sleep(2);
// 尝试重新连接
int success = 0;
retries = 0;
while (retries < MAX_RETRIES && running) {
retries++;
log_message("Reconnect attempt %d of %d", retries, MAX_RETRIES);
if (ppp_dial() == 0) {
// 等待连接稳定
sleep(5);
if (check_network_status()) {
log_message("Network reconnected successfully");
success = 1;
break;
} else {
log_message("Connection check failed after dial");
}
}
if (retries < MAX_RETRIES) {
log_message("Waiting %d seconds before next retry", RECONNECT_DELAY);
sleep(RECONNECT_DELAY);
}
}
if (!success) {
log_message("Failed to reconnect after %d attempts", MAX_RETRIES);
}
}
// 检查间隔
sleep(CHECK_INTERVAL);
}
// 清理工作
ppp_hangup();
unlink(PID_FILE);
log_message("PPP Manager stopped");
return 0;
}
七、总结
本文详细介绍了在 Linux 嵌入式系统中实现 PPP 拨号的方法,包括 PPP 协议基础、系统架构、编程实现方案、配置文件说明以及调试技巧等内容。通过调用系统命令或直接操作 PPP 库函数,可以实现可靠的 PPP 拨号程序。
在实际开发中,应根据具体需求选择合适的实现方案,并注意错误处理和断线重连等机制的实现,以提高系统的稳定性和可靠性。增强版的 PPP 拨号管理程序提供了更完善的功能,包括配置文件读取、守护进程运行、网络状态监控和自动重连等特性,可作为实际项目的参考。