OpenBMC开发之obmc-ikvm服务介绍
1. 初始化步骤
- obmc-ikvm.cpp执行main()函数,创建ikvm::Manager实例并执行manager.run()
- ikvm_manager.hpp中创建Input input实例,并在Manager析构函数中初始化input(args.getKeyboardPath(), args.getPointerPath(), args.getUdcName())
- 创建Input实例时,析构函数中打开键鼠UDC设备文件:hidUdcPath = “/sys/kernel/config/usb_gadget/obmc_hid/UDC”
/* @brief Handle of the HID gadget UDC */
std::ofstream hidUdcStream;
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);
}
-
当没有任何KVM会话的时候,键鼠HID链路是断开的。
-
只有当第一个KVM客户端打开时,也就是server->numClients = 0时,才进行HID链路的建联,调用 server->input.connect()函数进行USBGadget配置,同时,server->frameCounter = 0。
enum rfbNewClientAction Server::newClient(rfbClientPtr cl)
{
Server* server = (Server*)cl->screen->screenData;
cl->clientData =new ClientData(server->video.getFrameRate(), &server->input);
cl->clientGoneHook = clientGone;
cl->clientFramebufferUpdateRequestHook = clientFramebufferUpdateRequest;
if (!server->numClients++)
{
server->input.connect();
server->pendingResize = false;
server->frameCounter = 0;
}
return RFB_CLIENT_ACCEPT;
}
- Input::connect()函数主要功能为获取USB端口号ID,写入hidUdcStream文件,以此配置键鼠的USBGadget
- 如果入参udcName指定,则直接写入: hidUdcStream << udcName << std::endl;
- 如果入参为空,则自动从路径/sys/bus/platform/devices/1e6a0000.usb-vhub中获取,获取规则为/sys/bus/platform/devices/1e6a0000.usb-vhub/1e6a0000.usb-vhub:pX/目录下包含gadget文件,且不存在suspended文件
void Input::connect()
for (const auto& port : fs::directory_iterator(usbVirtualHubPath))
{
// port=/sys/bus/platform/devices/1e6a0000.usb-vhub/1e6a0000.usb-vhub:pX
// 确认该路径为目录,而非链接文件
if (fs::is_directory(port) && !fs::is_symlink(port))
{
// 遍历/sys/bus/platform/devices/1e6a0000.usb-vhub/1e6a0000.usb-vhub:pX下的所有文件
// gadget=/sys/bus/platform/devices/1e6a0000.usb-vhub/1e6a0000.usb-vhub:pX/gadget.Y
for (const auto& gadget:fs::directory_iterator(port.path()))
{
// Kernel 6.0:
// /sys/.../1e6a0000.usb-vhub:pX/gadget.Y/suspended
// Kernel 5.15:
// /sys/.../1e6a0000.usb-vhub:pX/gadget/suspended
// 确认gadget路径为目录,而非链接文件,且路径中包含gadget,且不存在suspended文件
if (fs::is_directory(gadget) &&gadget.path().string().find("gadget") !=std::string::npos &&!fs::exists(gadget.path() / "suspended"))
{
const std::string portId = port.path().filename();
hidUdcStream << portId << std::endl;
found = true;
break;
}
}
}
}
- USBGadget配置后在设备终端的配置效果如下,UDC控制已成功赋值,相当于执行:echo “1e6a0000.usb-vhub:p6”>/sys/kernel/config/usb_gadget/obmc_hid/UDC
root@10020220507ABCD:/# cat /sys/kernel/config/usb_gadget/obmc_hid/UDC
1e6a0000.usb-vhub:p6
root@10020220507ABCD:/# ls -l /sys/kernel/config/usb_gadget/obmc_hid/
-rw-r--r-- 1 root root 4096 Nov 17 14:17 UDC
-rw-r--r-- 1 root root 4096 Nov 19 09:05 bDeviceClass
-rw-r--r-- 1 root root 4096 Nov 19 09:05 bDeviceProtocol
-rw-r--r-- 1 root root 4096 Nov 19 09:05 bDeviceSubClass
-rw-r--r-- 1 root root 4096 Nov 19 09:05 bMaxPacketSize0
-rw-r--r-- 1 root root 4096 Nov 17 10:32 bcdDevice
-rw-r--r-- 1 root root 4096 Nov 17 10:32 bcdUSB
drwxr-xr-x 3 root root 0 Nov 17 10:32 configs
drwxr-xr-x 4 root root 0 Nov 17 10:32 functions
-rw-r--r-- 1 root root 4096 Nov 17 10:32 idProduct
-rw-r--r-- 1 root root 4096 Nov 17 10:32 idVendor
-rw-r--r-- 1 root root 4096 Nov 19 09:05 max_speed
drwxr-xr-x 2 root root 0 Nov 17 10:32 os_desc
drwxr-xr-x 3 root root 0 Nov 17 10:32 strings
- void Input::connect()函数中USBGadget配置OK后,接着open键盘设备/dev/hidg0以及鼠标设备/dev/hidg1,获取对应描述符,用于键鼠的读写操作
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()));
}
}
- 键鼠写操作API函数如下
/* @brief File descriptor for the USB keyboard device */
int keyboardFd;
/* @brief File descriptor for the USB mouse device */
int pointerFd;
/* @brief Data for keyboard report */
uint8_t keyboardReport[KEY_REPORT_LENGTH];
/* @brief Data for pointer report */
uint8_t pointerReport[PTR_REPORT_LENGTH];
2. 卸载步骤
- 当关闭KVM会话时,会首先检测是不是最后一个会话,如果是,则执行server->input.disconnect()函数断开USB设备
void Server::clientGone(rfbClientPtr cl)
{
Server* server = (Server*)cl->screen->screenData;
delete (ClientData*)cl->clientData;
cl->clientData = nullptr;
//后置操作:numClients返回当前值判断是否==1,然后才递减,当最后一个会话时,执行断开操作
if (server->numClients-- == 1)
{
server->input.disconnect();
rfbMarkRectAsModified(server->server, 0, 0, server->video.getWidth(),
server->video.getHeight());
}
}
- 而Input::disconnect()函数中,通过将键鼠设备的UDC置空关闭USBGadget,相当于执行:echo “”>/sys/kernel/config/usb_gadget/obmc_hid/UDC
void Input::disconnect()
{
if (keyboardFd >= 0)
{
close(keyboardFd);
keyboardFd = -1;
}
if (pointerFd >= 0)
{
close(pointerFd);
pointerFd = -1;
}
try
{
hidUdcStream << "" << std::endl; //
}
catch (std::ofstream::failure& e)
{
log<level::ERR>("Failed to disconnect HID gadget",entry("ERROR=%s", e.what()));
}
}
3. 数据读写操作
- 键盘和鼠标数据长度分别为8Bytes/6Bytes,对应的写操作函数分别在ikvm_input.cpp中
- 两个写函数中分别引入了Retry机制,会尝试5次重写,间隔10ms
- 实测中发现10ms并不是一个合适的值,当连续发送失败时,可能会导入KVM视频帧率下降,从而引起KVM画面卡顿问题
static constexpr int KEY_REPORT_LENGTH = 8;
static constexpr int PTR_REPORT_LENGTH = 6;
static constexpr int HID_REPORT_RETRY_MAX = 5;
bool Input::writeKeyboard(const uint8_t* report)
void Input::writePointer(const uint8_t* report)
bool Input::writeKeyboard(const uint8_t* report)
{
std::unique_lock<std::mutex> lk(keyMutex);
uint retryCount = HID_REPORT_RETRY_MAX;
while (retryCount > 0)
{
if (write(keyboardFd, report, KEY_REPORT_LENGTH) == KEY_REPORT_LENGTH)
{
return true;
}
if (errno != EAGAIN)
{
if (errno != ESHUTDOWN)
{
log<level::ERR>("Failed to write keyboard report",entry("ERROR=%s", strerror(errno)));
}
break;
}
lk.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
lk.lock();
retryCount--;
}
return false;
}
4. 缺陷分析
- 当前OpenBMC架构键鼠Write()重发超时时间是10ms,这会造成Bios界面快速晃动鼠标情况下,KVM帧率降低,界面卡顿问题;
- 当前OpenBMC架构键鼠Write()操作比较暴力直接,尤其是定义Fd为O_NONBLOCK情况下,大量的数据冲击会造成Driver USB传输异常,从而引起键鼠无效;
- 比较好的解决方法是:调整重发间隔,并增加发送前的Poll机制(驱动提供该定义,但obmc-ikvm未调用)
static __poll_t f_hidg_poll(struct file *file, poll_table *wait)
{
struct f_hidg *hidg = file->private_data;
__poll_t ret = 0;
poll_wait(file, &hidg->read_queue, wait);
poll_wait(file, &hidg->write_queue, wait);
if (WRITE_COND)
ret |= EPOLLOUT | EPOLLWRNORM;
if (hidg->use_out_ep) {
if (READ_COND_INTOUT)
ret |= EPOLLIN | EPOLLRDNORM;
} else {
if (READ_COND_SSREPORT)
ret |= EPOLLIN | EPOLLRDNORM;
}
return ret;
}
bool Input::writeKeyboard()
{
struct pollfd fds[1];
fds[0] = keyboardFd;
fds[1].events = POLLOUT;
while(retryCount > 0)
{
ret = poll(fds, 1, 200)
if(ret == 0)
{
log<level::ERR>("Write keyboard report timeout!");
}
else if(ret < 0)
}
log<level::ERR>("Write keyboard report error!");
}
else if(fds[0].revents & POLLOUT)
{
if (write(keyboardFd, report, KEY_REPORT_LENGTH) == KEY_REPORT_LENGTH)
{
return true;
}
...............
}
}
}