Linux 环境下 PPP 拨号的嵌入式开发实现
一、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或ERRORif (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];// 设置APNsnprintf(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;}// 配置GPRSif (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;// 第一步forkpid = fork();if (pid < 0) {exit(EXIT_FAILURE);}if (pid > 0) {exit(EXIT_SUCCESS); // 父进程退出}// 创建新会话sid = setsid();if (sid < 0) {exit(EXIT_FAILURE);}// 第二步forkpid = 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 拨号管理程序提供了更完善的功能,包括配置文件读取、守护进程运行、网络状态监控和自动重连等特性,可作为实际项目的参考。