openBmc KVM基本框架和原理

openBmc KVM基本框架和原理
一、总体框架

​ openBmc KVM功能实现的总体框架如图1所示,由obmc_ikvm、bmcweb、webui三大部分构成。其中,obmc_ikvm和bmcweb组成VNC Server,webui为VNC Client。VNC Server与VNC Client间通过Websocket协议通信,而obmc_ikvm与bmcweb间通过TCP套接字机制通信。
在这里插入图片描述

二、obmc_ikvm的工作流程

​ 作为KVM框架中的主要部分,obmc_ikvm是直接与服务器进行交互的角色。一方面,它读取服务器的视频画面并通过bmcweb发送到VNC Client,另一方面它以虚拟键鼠设备的身份,向服务器映射VNC Client进行的键鼠操作。看到这里应该能够知道,视频和键鼠数据在传输的过程中的方向是相反的。

​ obmc_ikvm程序源代码实现了三个类:input类,video类和server类,分析这几个类的命名我们可能会不由自主的去猜想它们对应的功能。确实就像它们的名字所表示的那样,input类实现虚拟键鼠功能、video类实现视频的传输功能而server类负责这些功能之间的调度。另外还有两个重要的类:args和manager。同样,顾名思义,args这个类是负责读取和解析命令行参数的(从执行程序的命令中读取和解析用户输入的参数供程序使用)而manager类是obmc-ikvm程序的管理函数,它调用input类和video类的成员函数进行视频流和键鼠的传输。相信阅读到这里你会对obmc_ikvm的框架有了基本了解。下面我们看看这个程序的主函数:

#include "ikvm_args.hpp"
#include "ikvm_manager.hpp"

int main(int argc, char* argv[])
{
    ikvm::Args args(argc, argv);
    ikvm::Manager manager(args);

    manager.run();

    return 0;
}

​ 可以看到,程序首先读取命令函参数对Manager进行初始化,随后运行了manager.run()函数。那么我们继续分析manager类和其成员函数manager.run()。

//manager类的初始化
Manager::Manager(const Args& args) :
    continueExecuting(true), serverDone(false), videoDone(true),
    input(args.getKeyboardPath(), args.getPointerPath(), args.getUdcName()),
    video(args.getVideoPath(), input, args.getFrameRate(),
          args.getSubsampling()),
    server(args, input, video)
{}

·····

//run函数
void Manager::run()
{
    //创建新线程run,独立线程中执行rfb更新操作
    std::thread run(serverThread, this);

    while (continueExecuting)
    {
        if (server.wantsFrame())
        {
            video.start();
            video.getFrame();
            server.sendFrame();
        }
        else
        {
            video.stop();
        }

        if (video.needsResize())
        {
            videoDone = false;
            waitServer();
            video.resize();
            server.resize();
            setVideoDone();
        }
        else
        {
            setVideoDone();
            waitServer();
        }
    }

    run.join();
}

/*

serverThread函数是run函数中创建的新线程的执行函数,在此函数中,
不断运行server类的成员函数run,执行rfb更新操作
*/
void Manager::serverThread(Manager* manager)
{
    while (manager->continueExecuting)
    {
        manager->server.run();
        manager->setServerDone();
        manager->waitVideo();
    }
}

//调用到的其他的一些函数是用于两个线程之间的同步的辅助函数

​ 可以看到,在manager类的初始化过程中,程序依据args提供的参数依次初始化了input、video和server三个类的实例。接着manager.run()函数调用这些类提供的函数进行视频和键鼠的传输。

2.1 视频流传输

​ openBmc原生的的视频传输是基于V4L2框架的,其本质是在用户空间用户空间打开/dev/video0设备后进行图像数据的循环捕获和流式传输。obmc_ikvm接收一个V4L2_BUF_TYPE_VIDEO_CAPTURE类型的缓冲区作为视频源,通过一系列的操作实现向客户端发送视频。主要代码如下:

while (continueExecuting)
    {
        if (server.wantsFrame())
        {
            video.start();
            video.getFrame();
            server.sendFrame();
        }
        else
        {
            video.stop();
        }

        if (video.needsResize())
        {
            videoDone = false;
            waitServer();
            video.resize();
            server.resize();
            setVideoDone();
        }
        else
        {
            setVideoDone();
            waitServer();
        }
    }

​ 你一定会发现,这段代码是manager.run()函数的部分。其工作逻辑很清晰,这里就不多说啦。

2.2 虚拟键鼠模拟

​ 实际上,在manager.run()中,还创建了一个独立的新的线程:

std::thread run(serverThread, this);
void Manager::serverThread(Manager* manager)
{
    while (manager->continueExecuting)
    {
        manager->server.run();
        manager->setServerDone();
        manager->waitVideo();
    }
}

​ 在这个线程中循环运行server类的成员函数run:

void Server::run()
{
    rfbProcessEvents(server, processTime);

    if (server->clientHead)
    {
        frameCounter++;
        if (pendingResize && frameCounter > video.getFrameRate())
        {
            doResize();
            pendingResize = false;
        }
    }
}

​ server.run()进行rfb事件的处理,具体来说它调用了rfbProcessEvents()函数进行rfb事件的更新。rfbProcessEvents 函数是属于LibVNCServer库的一部分,这是一个开源的库,用于实现VNC服务器功能。在VNC架构中,服务器发送屏幕的图像更新到客户端,客户端发送回键盘和鼠标事件。rfbProcessEvents 函数在这个过程中起到了核心作用,它负责处理从客户端接收的各种事件和服务器端的响应,这其中就包括了键鼠事件。

​ 到了这里,其实可以对前面提到的“视频和键鼠是两个方向的传输”做出进一步的解释:manager.run()的两个线程就对应了视频和键鼠两个功能,两个线程中的数据传输的方向应该是相反的。

​ 好的,对于鼠标和键鼠的模拟,我们已经搞定了”输入“的部分,也就是说我们已经通过 rfbProcessEvents()函数获得了VNC Client的键鼠操作。自然而然,我们现在需要做的事情是在服务器上模拟出这些键鼠的操作,这部分的工作obmc_ikvm程序通过USB Gadget实现。鼠标和键盘都属于USB HID设备类型,USB Gadget是一种在嵌入式系统中实现USB设备功能的技术,因此采用USB Gadget将开发板模拟成USB HID设备,客户端通过rfb协议发送keyevent和pointerevent数据包到服务器端,服务器端收到数据包后模拟相应的键盘按键和鼠标操作。关于Gadget设备的知识,感兴趣的同学可以看看我相关的推文。

  • 创建和配置USB Gadget
#create USBhid.sh

echo 0x0100 > bcdDevice     #配置固件版本号
echo 0x0200 > bcdUSB        #配置USB规范版本号
echo 0x0104 > idProduct		#配置USB设备产品ID
echo 0x1d6b > idVendor		#配置USB设备的厂商ID

mkdir  strings/0x409        #创建用于存放于存放USB设备的描述符字符串的目录,0x409表示英文的语言代码
echo "OpenBMC" > strings/0x409/manufacturer  #表示设备的制造厂商
echo "virtual_input" > strings/0x409/product #标识设备的产品名称
echo "OBMC0001" > strings/0x409/serialnumber #标识USB设备的序列号

#键盘设备 
mkdir  functions/hid.0  #创建用于存放USB Gadget设备的功能模块的目录
echo 1 > functions/hid.0/protocol	#指定HID设备协议为键盘协议
echo 8 > functions/hid.0/report_length  #指定HID设备的输入报告长度,这里设置为8字节
echo 1 > functions/hid.0/subclass  #指定HID设备的子类别为键盘协议的子类别
#写入HID设备的报告描述符,定义HID设备的按键输入格式
echo -ne '\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x03\x95\x05\x75\x01\x05\x08\x19\x01\x29\x05\x91\x02\x95\x01\x75\x03\x91\x03\x95\x06\x75\x08\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\xc0' > functions/hid.0/report_desc

#鼠标设备
mkdir  functions/hid.1  #创建用于存放USB Gadget设备的功能模块的目录
echo 2 > functions/hid.1/protocol	#指定HID设备协议为鼠标协议
echo 5 > functions/hid.1/report_length  #指定HID设备的输入报告长度,这里设置为5字节
echo 1 > functions/hid.1/subclass  #指定HID设备的子类别为鼠标协议的子类别
#写入HID设备的报告描述符,定义HID设备鼠标的输入格式,包括鼠标的移动和点击等事件
echo -ne '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00\x05\x09\x19\x01\x29\x03\x15\x00\x25\x01\x95\x03\x75\x01\x81\x02\x95\x01\x75\x05\x81\x03\x05\x01\x09\x30\x09\x31\x35\x00\x46\xff\x7f\x15\x00\x26\xff\x7f\x65\x11\x55\x00\x75\x10\x95\x02\x81\x02\xc0\xc0' > functions/hid.1/report_desc

#创建配置文件夹
mkdir  configs/c.1
mkdir  configs/c.1/strings/0x409

echo 0x80 > configs/c.1/bmAttributes  #配置USB Gadget设备通过外部电源供电
echo 200 > configs/c.1/MaxPower  #配置USB Gadget设备最大负载时需要200毫安的电流
echo "" > configs/c.1/strings/0x409/configuration  #作为配置项的标识符

#将HID功能模块链接到USB Gadget设备的配置项中,这样,USB Gadget设备就具备了HID设备的功能,可以模拟键盘和鼠标等输入设备
ln -s functions/hid.0 configs/c.1
ln -s functions/hid.1 configs/c.1
  • 连接至主机
cd /sys/kernel/config/usb_gadget/obmc_hid
echo fc400000.usb > UDC
  • 实现虚拟键鼠操作

​ 在系统中运行上面的create USBhid.sh脚本后,会在系统中创建/dev/hidg0/dev/hidg1两个设备节点,分别对应键盘和鼠标设备。键鼠操作的实现基于通过设备节点获取文件描述符,再通过读写文件描述符实现虚拟键鼠的操作。create USBhid.sh属于configfs的应用,它是一种Gadget设备的创建方法。接下来涉及到的代码,均在ikvm_input.cpp文件中。

//input类初始化
Input::Input(const std::string& kbdPath, const std::string& ptrPath,
             const std::string& udc) :
    keyboardFd(-1),
    pointerFd(-1), keyboardReport{0}, pointerReport{0}, keyboardPath(kbdPath),
    pointerPath(ptrPath), udcName(udc)
{
    hidUdcStream.exceptions(std::ofstream::failbit | std::ofstream::badbit);
    hidUdcStream.open(hidUdcPath, std::ios::out | std::ios::app);
}

​ 在input类的初始化过程中,根据命令行参数,程序初始化了KeyboardPathpointerPathkeyboardFdpointerFd是键盘和鼠标的文件描述符,keyboardReportpointerReport是键鼠的hid报文。udcNamehidUdcStream用于使能Gadget,即将Gadget绑定到UDC

//获取文件描述符
if (!keyboardPath.empty())
    {
        keyboardFd =
            open(keyboardPath.c_str(), O_RDWR | O_CLOEXEC | O_NONBLOCK);
        if (keyboardFd < 0)
        {
            log<level::ERR>("Failed to open input device",
                            entry("PATH=%s", keyboardPath.c_str()),
                            entry("ERROR=%s", strerror(errno)));
            elog<Open>(xyz::openbmc_project::Common::File::Open::ERRNO(errno),
                       xyz::openbmc_project::Common::File::Open::PATH(
                           keyboardPath.c_str()));
        }
    }

if (!pointerPath.empty())
    {
        pointerFd = open(pointerPath.c_str(), O_RDWR | O_CLOEXEC | O_NONBLOCK);
        if (pointerFd < 0)
        {
            log<level::ERR>("Failed to open input device",
                            entry("PATH=%s", pointerPath.c_str()),
                            entry("ERROR=%s", strerror(errno)));
            elog<Open>(xyz::openbmc_project::Common::File::Open::ERRNO(errno),
                       xyz::openbmc_project::Common::File::Open::PATH(
                           pointerPath.c_str()));
        }
    }

​ 获取到文件描述符后,向其中写入hid报文就能够模拟键鼠操作,这部分操作由keyevent()和pointerevent()实现。但是我们从VNC Client获取的数据还不是能够直接向文件描述符写入的形式,因此在这两个函数中还进行了相关的解析和转换。我对关键部分进行了简单注释,这里不再进行更多解释。

//keyEvent
void Input::keyEvent(rfbBool down, rfbKeySym key, rfbClientPtr cl)
{
    Server::ClientData* cd = (Server::ClientData*)cl->clientData;
    Input* input = cd->input;
    bool sendKeyboard = false;

    if (input->keyboardFd < 0)
    {
        return;
    }

    if (down)
    {
        uint8_t sc = keyToScancode(key);//转换为hid报文

        if (sc)
        {
            if (input->keysDown.find(key) == input->keysDown.end())
            {
                for (unsigned int i = 2; i < KEY_REPORT_LENGTH; ++i)
                {
                    if (!input->keyboardReport[i])
                    {
                        input->keyboardReport[i] = sc;
                        input->keysDown.insert(std::make_pair(key, i));
                        sendKeyboard = true;
                        break;
                    }
                }
            }
        }
        else
        {
            uint8_t mod = keyToMod(key);//转换为hid报文

            if (mod)
            {
                input->keyboardReport[0] |= mod;
                sendKeyboard = true;
            }
        }
    }
    else
    {
        auto it = input->keysDown.find(key);

        if (it != input->keysDown.end())
        {
            input->keyboardReport[it->second] = 0;
            input->keysDown.erase(it);
            sendKeyboard = true;
        }
        else
        {
            uint8_t mod = keyToMod(key);

            if (mod)
            {
                input->keyboardReport[0] &= ~mod;
                sendKeyboard = true;
            }
        }
    }

    if (sendKeyboard)
    {
        input->writeKeyboard(input->keyboardReport);//将hid报文写入描述符
    }
}
//pointerevet
void Input::pointerEvent(int buttonMask, int x, int y, rfbClientPtr cl)
{
    Server::ClientData* cd = (Server::ClientData*)cl->clientData;
    Input* input = cd->input;
    Server* server = (Server*)cl->screen->screenData;
    const Video& video = server->getVideo();

    if (input->pointerFd < 0)
    {
        return;
    }

    if (buttonMask > 4) //转换为hid报文
    {
        input->pointerReport[0] = 0;
        if (buttonMask == 8)
        {
            input->pointerReport[5] = 1;
        }
        else if (buttonMask == 16)
        {
            input->pointerReport[5] = 0xff;
        }
    }
    else
    {
        input->pointerReport[0] = ((buttonMask & 0x4) >> 1) |
                                  ((buttonMask & 0x2) << 1) |
                                  (buttonMask & 0x1);
        input->pointerReport[5] = 0;
    }

    if (x >= 0 && (unsigned int)x < video.getWidth())
    {
        uint16_t xx = (uint16_t)(x * (SHRT_MAX + 1) / video.getWidth());

        memcpy(&input->pointerReport[1], &xx, 2);
    }

    if (y >= 0 && (unsigned int)y < video.getHeight())
    {
        uint16_t yy = (uint16_t)(y * (SHRT_MAX + 1) / video.getHeight());

        memcpy(&input->pointerReport[3], &yy, 2);
    }

    rfbDefaultPtrAddEvent(buttonMask, x, y, cl);
    input->writePointer(input->pointerReport); //将hid报文写入描述符
}
2.3 与bmcweb的通信

​ bmcweb是obmc_ikvm的另一个直接交互的对象(另一个是服务器),上面的我们提到的获取键鼠事件和发送视频,其直接对象都是bmcweb,而最终的对象是VNC Client,也就是webui。obmc_ikvm与bmcweb的通信是通过TCP套接字来实现的,obmc_ikvm程序与bmcweb程序直接通过TCP套接字机制进行“本地通信”或称为“进程间通信”。

​ 具体来说,两个程序可以通过绑定到本地主机的IP地址(如 127.0.0.1,也称为回环地址)上的端口来建立TCP连接。在这里,obmc_ikvm作为服务器,监听端口5900;bmcweb作为客户端,连接到服务器的5900端口。在bmcweb中,这个实例是hostsocket。

三、bmcweb的工作流程

在这里插入图片描述

四、webui的工作流程
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值