Linux串口网卡(二)——用户态转发程序的实现

在上一篇《Linux串口网卡(一)——通用虚拟网卡的实现》中我已经实现了一个内核模块,也就是一张虚拟网卡。现在,所有需要通过该网卡(eth_uart)发送出去的帧都可以从/proc/eth_uart/uio这个虚拟文件中读到,一次read()就是一个帧。也可以向/proc/eth_uart/uio写入一个接收到的帧。所以这次的重点就是收发串口与转发。

本来我打算开两个线程A和B,线程A从/proc/eth_uart/uio阻塞式读取帧,每次读到一个,就通过串口阻塞式发送出去;线程B从串口阻塞式读取帧,读到一个帧就交给/proc/eth_uart/uio。但是经过实验,发现这里面有一个大坑——x86上的串口驱动程序有bug,对同一个串口进行并发读写时会卡死。其中的心酸泪就不说了,记住这个结论就好。

既然不能使用多线程,那么也就是说所有的IO操作都要在一个线程中完成咯,于是自然而然地想到最佳的办法就是select/poll机制。所以我在虚拟网卡中特意增加了对select/poll机制的支持。并不是什么先见之明,而是马后炮罢了,呵呵呵。

对于/proc/eth_uart/uio而言,read()和write()的交互单位都是一整个帧,所以没有什么难点。但是对于串口,读写的单位都是字节。串口的收发速度是有限的,select/poll机制下,需同时监控可读和可写事件。当可读时,就从串口中读取尽量多的字节。当可写时,就向串口写出尽量多的字节。因此,需要监控最多三个事件:

  1. /proc/eth_uart/uio可读事件
  2. 串口可读事件
  3. 串口可写事件

之所以不需要监控/proc/eth_uart/uio可写事件,是因为对/proc/eth_uart_uio的write()操作几乎是立即返回的,而且随时可写。这三个事件中,“串口可写事件”并不是一直需要监控的。只有当有数据需要通过串口发出时,才有监控“串口可写事件“的必要。由于串口可读可写时,可能只能读取或写入少许几个字节,所以必须得维持一个”待发送缓冲区“和”接收缓冲区“。

另一个问题是,当帧变成字节流传输在串口线上时,如何标记帧的开始与结束?这种问题的通用解就是定一个开头和结尾标记,并且真正的数据需要使用转义。具体方案很多种,我的方案如下:

  1. 两个连续字节如果是255, 0,那么是START命令,即一个帧的开头
  2. 两个连续字节如果是255, 1,那么是END命令,即一个帧的结尾
  3. 两个连续字节如果是255, x(x > 1),那么就是单字节x

在该方案中,把255当作一个转义符号,表示其后面的一个字节有特殊含义。既然255作为转义符号,那么如果数据中本身就有255,那么怎么办?所以就变成两个字节255, 255。

好了,那么就直接看代码吧!

eth_uart.c

#include <fcntl.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>
#include <sys/select.h>

// 打开串口,配置为115200, 8N1, 无流控
int open_uart(const char* dev)
{
    struct termios options;
    //清空所有属性
    memset(&options, 0, sizeof(options));
    //设置输入输出速率为115200
    cfsetispeed(&options, B115200);
    cfsetospeed(&options, B115200);
    //8个数据位
    options.c_cflag |= CS8;
    //无校验位(其实这句可以不写,因为已经全部清0,只需要设置为1的位)
    options.c_cflag &= ~PARENB;
    //1个停止位(其实这句可以不写,因为已经全部清0,只需要设置为1的位)
    options.c_cflag &= ~CSTOPB;
    //超时时间为0
    options.c_cc[VTIME] = 0;
    //每次接收长度为1个字节
    options.c_cc[VMIN] = 1;
    // 可能的错误信息
    char error[1024];
    // 打开设备
    int fd = open(dev, O_RDWR | O_NOCTTY | O_NDELAY);
    if(fd < 0)
    {
        sprintf(error, "<eth_uart> open('%s') failed!\n", dev);
        goto err;
    }
    // 设置属性
    if(tcsetattr(fd, TCSANOW, &options) != 0)
    {
        strcpy(error, "<eth_uart> tcsetattr() failed!\n");
        goto err;
    }
    // 清空缓存
    if(tcflush(fd, TCIOFLUSH) != 0)
    {
        strcpy(error, "<eth_uart> tcflush() failed!\n");
        goto err;
    }
    return fd;
    // 错误处理
    err:
        if(fd > 0)
            close(fd);
        printf("%s", error);
        return -1;
}

// 初始化网卡IO口和串口
int init(const char* eth_uio, const char* uart_dev, int* fds)
{
    // 可能的错误信息
    char error[1024];
    // 打开网卡输入输出口
    int eth_fd = open(eth_uio, O_RDWR);
    if(eth_fd < 0)
    {
        sprintf(error, "<eth_uart> open('%s') failed!\n", eth_uio);
        goto err_1;
    }
    // 设置为非阻塞
    if(fcntl(eth_fd, F_SETFL, fcntl(eth_fd, F_GETFL, 0) | O_NONBLOCK) < 0)
    {
        strcpy(error, "<eth_uart> fcntl(eth, O_NONBLOCK) failed!\n");
        goto err_2;
    }
    // 打开串口
    int uart_fd = open_uart(uart_dev);
    if(uart_fd < 0)
    {
        // 错误信息在open_uart()中已经输出
        strcpy(error, "");
        goto err_2;
    }
    // 两个文件描述符,传出
    fds[0] = eth_fd;
    fds[1] = uart_fd;
    return 0;
    // 错误处理
    err_2:
        close(eth_fd);
    err_1:
        ;
        printf("%s", error);
        return -1;
}

int main_routine(int eth_fd, int uart_fd, int max_buf)
{
    // 可能的错误信息
    char error[1024];
    // 要通过串口发出的字节流
    uint8_t to_send[max_buf];
    // 要通过串口发出的字节流的长度
    int to_send_len = 0;
    // 已经通过串口发出的长度
    int sent_len = 0;
    // 正在通过串口接收的帧
    uint8_t recving[max_buf];
    // 已经接收的帧长度
    int recving_len = 0;
    // 下一个接收到的字节是否需要转义
    int is_escape = 0;
    while(1)
    {
        // 标记“网卡可读”,“串口可读”,“串口可写”
        int can_eth_read = 0, can_uart_read = 0, can_uart_write = 0;
        // 没有需要通过串口发送的字节流
        if(to_send_len == 0)
        {
            // 等待可读的文件描述符集合
            fd_set rds;
            FD_ZERO(&rds);
            FD_SET(eth_fd, &rds);
            FD_SET(uart_fd, &rds);
            // select等待
            if(select((eth_fd > uart_fd ? eth_fd : uart_fd) + 1, &rds, 0, 0, 0) < 0)
            {
                strcpy(error, "<eth_uart> select(eth + uart, READ) error!\n");
                goto err;
            }
            can_eth_read = FD_ISSET(eth_fd, &rds);
            can_uart_read = FD_ISSET(uart_fd, &rds);
        }
        // 有需要通过串口发送的字节流
        else
        {
            // 等待可读的文件描述符集合
            fd_set rds;
            FD_ZERO(&rds);
            FD_SET(uart_fd, &rds);
            // 等待可写的文件描述符集合
            fd_set wrs;
            FD_ZERO(&wrs);
            FD_SET(uart_fd, &wrs);
            // select等待
            if(select(uart_fd + 1, &rds, &wrs, 0, 0) < 0)
            {
                strcpy(error, "<eth_uart> select(uart, READ + WRITE) error!\n");
                goto err;
            }
            can_uart_read = FD_ISSET(uart_fd, &rds);
            can_uart_write = FD_ISSET(uart_fd, &wrs);
        }
        // 如果网卡可读
        if(can_eth_read)
        {
            uint8_t frame[max_buf];
            // 从网卡读一个帧
            int len = read(eth_fd, frame, max_buf);
            // 避免poll机制的误判
            if(len == 0)
                continue;
            if(len < 0)
            {
                sprintf(error, "<eth_uart> read(eth) == %d!\n", len);
                goto err;
            }
            else if(len > max_buf / 2)
            {
                sprintf(error, "<eth_uart> read(eth) == %d, too long!\n", len);
                goto err;
            }
            // 编码成串口上发送的字节流
            // 开头有START指令,表达为 255,0
            to_send[to_send_len++] = 255;
            to_send[to_send_len++] = 0;
            // 原始帧中的255编码为 255,255,其他不变
            for(int i = 0; i < len; i++)
            {
                to_send[to_send_len++] = frame[i];
                if(frame[i] == 255)
                    to_send[to_send_len++] = 255;
            }
            // 结尾有END指令,表达为 255,1
            to_send[to_send_len++] = 255;
            to_send[to_send_len++] = 1;
        }
        // 如果串口可读
        else if(can_uart_read)
        {
            // 经过编码的字节流
            uint8_t encoded[max_buf];
            // 从串口读
            int len = read(uart_fd, encoded, max_buf);
            if(len < 0)
            {
                sprintf(error, "<eth_uart> read(uart) == %d!\n", len);
                goto err;
            }
            // 依次处理每个字节
            for(int i = 0; i < len; i++)
            {
                uint8_t abyte = encoded[i];
                // 如果当前是转义状态
                if(is_escape)
                {
                    // 遇到了 255,0,表示START指令,则清空之前的数据
                    if(abyte == 0)
                        recving_len = 0;
                    // 遇到了 255,1,表示END指令,则说明接收完了一个数据包
                    else if(abyte == 1)
                    {
                        // 解码后的帧传给网卡
                        if(write(eth_fd, recving, recving_len) != recving_len)
                        {
                            sprintf(error, "<eth_uart> write(eth, %d) != %d!\n", recving_len, recving_len);
                            goto err;
                        }
                    }
                    else
                        recving[recving_len++] = abyte;
                    // 退出转义状态
                    is_escape = 0;
                }
                // 不是转义状态
                else
                {
                    // 遇到了 255,说明接下来的字节是转义状态
                    if(encoded[i] == 255)
                        is_escape = 1;
                    else
                        recving[recving_len++] = abyte;
                }
                if(recving_len == max_buf)
                {
                    strcpy(error, "<eth_uart> too big frame!\n");
                    goto err;
                }
            }
        }
        // 如果串口可写
        else if(can_uart_write)
        {
            //尝试向串口写出
            int sz = write(uart_fd, to_send + sent_len, to_send_len - sent_len);
            if(sz <= 0)
            {
                sprintf(error, "<eth_uart> write(uart) == %d!\n", sz);
                goto err;
            }
            sent_len += sz;
            // 全部发出
            if(sent_len == to_send_len)
            {
                to_send_len = 0;
                sent_len = 0;
            }
        }
    }
    err:
        printf("%s", error);
        return -1;
}

int main()
{
    // 两个文件描述符,分别是网卡IO口和串口
    int fds[2];
    if(init("/proc/eth_uart/uio", "/dev/ttyUSB0", fds) != 0)
        return -1;
    if(main_routine(fds[0], fds[1], 4096) == -1)
        return -1;
    // 关闭文件
    close(fds[0]);
    close(fds[1]);
    return 0;
}

其实代码还是很简单的,只需要注意select/poll机制的合理使用即可。

接下来就是见证奇迹的时刻了!首先需要有两台Linux机器,命名为A和B,具体硬件平台无关。两台机器各自插上一个USB-TTL,两个USB-TTL之间使用三条线连接,即

  • A.GND----B.GND
  • A.RX----B.TX
  • A.TX----B.RX

并确保在两台机器上,各自的USB-TTL都被识别为/dev/ttyUSB0。

按照《Linux串口网卡(一)——通用虚拟网卡的实现》最后说的,编译加载模块,使得能通过ifconfig -a看到网卡。然后使能网卡,并配置IP地址。A机器上:

ifconfig eth_uart up
ifconfig eth_uart 192.168.4.1

B机器上:

ifconfig eth_uart up
ifconfig eth_uart 192.168.4.2

注意在很多Ubuntu机器上,可能存在一些自动管理网络接口的工具,记得关闭,或者让工具不要管理eth_uart这个网口。

接着编译、运行上面的代码(两台机器上都需要执行):

gcc -std=gnu99 eth_uart.c -o eth_uart
./eth_uart

此时,在A机器上:

ping 192.168.4.2

可以看到能ping通,而且两个USB-TTL都在闪烁:

接着试试ssh。从A机器上发起登陆(先确保B机器开了ssh服务):

成功登陆!而且可以正常的操作!操作的时候,两个串口都闪得很厉害,说明在进行通信~~~