华为昇腾310B1芯片DVPP模块VENC视频编码接口调用流程、代码流程梳理、epoll机制整理

目录

1 接口调用流程

2 代码流程梳理

3 Linux epoll机制

3.1 基本概念

3.2 epoll API

3.2.1 epoll_create(int size)

3.2.2 epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

3.2.3 epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

3.3 触发机制

4 华为VENC代码中的epoll分析

5 VENC解码代码细节

5.1 几个类之间的关系

5.2 std::mutex m_lock这个锁用在哪里了

参考文献:


1 接口调用流程

在CANN 8.0.RC1 AscendCL应用软件开发指南 (C&C++, 推理)  01.pdf 文档中有接口调用流程

2 代码流程梳理

 代码在samples: CANN Samples - Gitee.com

然后我把这个代码完整的看了一遍,然后梳理了详细的代码流程,如下图所示,可以右键-图片另存为,然后就可以放大查看了。

3 Linux epoll机制

3.1 基本概念

epoll:是一种 I/O 时间通信机制,是 Linux 内核实现 IO 多路复用的一种方式。

IO 多路复用:在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候 返回,然后对其进行读写操作。

输入输出源:可以是文件(file)、网络(socket)、进程间的管道(pipe),因在 Linux 中 “一切皆文件”,所以都是用文件描述符(fd)来表示

可读事件:当 fd 关联的内核缓冲区非空有数据可读时,则触发可读事件; 可写事件:当 fd 关联的内核缓冲区不满有空闲空间可写时,则触发可写事件;

通知机制:当事件发生时,主动通知; 轮询机制:循环检查是否有事件发生,是通知机制的反面;

再来解读 epoll 机制:当 fd 关联的内核缓冲区非空时,发出可读信号;当缓冲区不满时,发出 可写信号。

3.2 epoll API

epoll 的核心是 3 个 API,其核心数据结构(eventpoll)在内核源码中位于 fs/eventpoll.c 文件中,如下:

/* 存储在文件结构的“ private_data”成员内部的数据结构,用于表示eventpoll接口的主要数据结构。*/
struct eventpoll {
    /* 调用 epoll_create 时会在内核中创建一个特殊的 file 节点 */
    struct file *file;
    /* 红黑树,用于存储监听的 fd,即 epoll_ctl 传过来的 fd*/
    struct rb_root_cached rbr;
    /* 双向链表,用于存储将要通过 epoll_wait 返回给用户的满足条件的事件 */
    struct list_head rdllist;
    /* 就绪链表,用于存储将从内核空间转移到用户空间的已就绪的 epitem */
    struct epitem *ovflist;
};

3.2.1 epoll_create(int size)

参数 size 表示最多可以监听多少个 fd,但是通过 Linux 开发手册来看,新版本中已经弃用了 该参数,但是注意不要传 0;返回值 >= 0 表示成功,< 0 表示失败

调用该 API 后操作系统内核会产生一个 eventpoll 实例的数据结构并返回一个ep fd,这个 epfd 就是 epoll 实例的句柄,下面的两个 API 都以它为中心。

3.2.2 epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

管理注册事件,向 epoll 添加、删除或修改要监听的 fd 这里的控制指令有: EPOLL_CTL_ADD/EPOLL_CTL_DEL/EPOLL_CTL_MOD epfd 表示 epoll_create() 的返回值, op 表示控制指令, fd 表示要监听的 fd event 表示 epoll_event 结构体

typedef union epoll_data {
   void        *ptr;
   int          fd;
   uint32_t     u32;
   uint64_t     u64;
} epoll_data_t;

struct epoll_event {
   uint32_t     events;  // 表示监听的事件类型(EPOLLIN/EPOLLHUP/EPOLLOUT...)
   epoll_data_t data; // 用户自定义数据,当事件发生时将会原样返回给用户
};

该结构体声明在 /usr/include/linux/eventpoll.h 中,如下:

/* Valid opcodes to issue to sys_epoll_ctl() */
/* epoll_ctl() op 参数可用的值*/
#define EPOLL_CTL_ADD 1 // 添加
#define EPOLL_CTL_DEL 2 // 删除
#define EPOLL_CTL_MOD 3 // 修改

/* Epoll event masks */
/* epoll_event->events 可用的值,表示感兴趣的 epoll 事件*/
#define EPOLLIN     0x00000001
#define EPOLLOUT    0x00000004
#define EPOLLERR    0x00000008
#define EPOLLHUP    0x00000010

struct epoll_event {
    __u32 events;
    __u64 data;
} EPOLL_PACKED;

3.2.3 epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

等待监听的事件到来,返回值表示到来事件的个数,返回的事件存储在 events 数组中

当此方法调用时,观察 eventpoll->rdllist 链表中是否有数据即可,有数据就返回无数据则陷 入等待状态,等 timeout 事件到后即使链表没数据也返回,因此 epoll_wait 是高效的

3.3 触发机制

1、水平触发:level trigger(LT)

读:只要缓冲区不为空就返回读就绪 写:只要缓冲区不满就返回写就绪

即:只要有数据就会触发,缓冲区剩余未读尽的数据会导致 epoll_wait() 返回

2、边缘触发:edge trigger(ET)

读:缓冲区由不可读变为可读(空->非空)、有新数据到达时、缓冲区有数据可读且使用 EPOLL_CTL_MODE 修改 EPOLLIN 事件时 写:缓冲区由不可写变为可写(满->非满)、旧数据被送走时、缓冲区有空间可写且使用 EPOLL_CTL_MODE 修改 EPOLLOUT 事件时

即:只有数据到来才触发,不管缓冲区中是否有数据,缓冲区剩余未读尽的数据不会导致 epoll_wait() 返回

通过上面的对比,ET 模式的效率是高于 LT 模式的,但不知为何 epoll 默认的工作模式是 LT 模式

4 华为VENC代码中的epoll分析

编码代码中先是

int32_t HmevLooper::create_epoll(int32_t size)
{
    int32_t ret;

    std::lock_guard<std::mutex> guardLock(m_lock);

    hi_mpi_sys_create_epoll(10, &m_epoll_fd);
    m_polling = true;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    ret = pthread_create(&m_thread, &attr, trampoline, this);
    if (ret != HI_SUCCESS) {
        HMEV_HISDK_PRT(ERROR, "pthread_create failed");
        m_thread = 0;
        return HMEV_FAILURE;
    }

    return HMEV_SUCCESS;
}

这里用hi_mpi_sys_create_epoll(10, &m_epoll_fd);创建了epoll实例。

然后trampoline线程中有等待函数

void HmevLooper::loop()
{
    aclError aclRet = aclrtSetCurrentContext(g_context);
    if (aclRet != ACL_SUCCESS) {
        HMEV_HISDK_PRT(ERROR, "set current context failed:%d", aclRet);
        return;
    }

    prctl(PR_SET_NAME, m_name.c_str(), 0, 0, 0);
    HMEV_HISDK_PRT(DEBUG, "m_epoll_fd:%d looper timeout %d", m_epoll_fd, m_timeout);
    hi_dvpp_epoll_event events[m_epoll_size];
    int32_t eventCount = 0;
    int32_t ret = 0;
    while (m_polling) {
        memset(events, 0, sizeof(events));

        ret = hi_mpi_sys_wait_epoll(m_epoll_fd, events, m_epoll_size, m_timeout, &eventCount);
        for (int i = 0; i < eventCount; i++) {
            hi_dvpp_epoll_event* ev = &events[i];
            int32_t fd = (int32_t)(unsigned long)ev->data;
            uint32_t epollEvents = ev->events;
            if ((epollEvents & HI_DVPP_EPOLL_IN) == 0 || fd < 0) {
                HMEV_HISDK_PRT(WARN, "epoll events is not epollin, or fd is invalid");
                continue;
            }
            std::shared_ptr <MsgHandler> msg = NULL;
            {
                std::lock_guard<std::mutex> guardLock(m_lock);
                if (m_msg_map.find(fd) != m_msg_map.end()) {
                    msg = m_msg_map[fd];
                }
            }
            if (msg != NULL) {
                msg->handle_message();
                msg = NULL;
            }
        }
    }
}

这里的hi_mpi_sys_wait_epoll(m_epoll_fd, events, m_epoll_size, m_timeout, &eventCount);就是等待编码完成的函数,这里面

  • m_epoll_fd:是epoll实例的文件描述符。
  • events:这里面保存的是满足条件的事件。
  • m_epoll_size:用来指定调用者一次愿意处理的最大事件数。这意味着 events 数组应该有足够的空间来存储最多 m_epoll_size 个事件,以便 hi_mpi_sys_wait_epoll 函数可以返回所有当前满足条件的事件,但不超过这个数量。
  • m_timeout:超时事件
  • eventCount:满足条件的事件个数。

然后在m_encLooper->add_fd(hi_mpi_venc_get_fd(m_encParam.channelId), handler);里面先是hi_mpi_venc_get_fd(m_encParam.channelId)将通道ID转换为一个文件句柄,然后将文件句柄添加到epoll实例中。

int32_t HmevLooper::add_fd(int32_t fd, MsgHandler* msg)
{
    int32_t ret;
    HMEV_HISDK_CHECK_RET_EXPRESS(fd < 0, "fd is invalid");

    std::lock_guard<std::mutex> guardLock(m_lock);
    HMEV_HISDK_CHECK_RET_EXPRESS(m_epoll_fd < 0, "mEpollFd is invalid");
    auto iter = m_msg_map.find(fd);
    HMEV_HISDK_CHECK_RET_EXPRESS(iter != m_msg_map.end(), "fd already added");
    std::shared_ptr <MsgHandler> msgHandler(msg);
    m_msg_map[fd] = msgHandler;

    hi_dvpp_epoll_event event;
    event.events = HI_DVPP_EPOLL_IN;
    event.data = (void*)(unsigned long)(fd);
    ret = hi_mpi_sys_ctl_epoll(m_epoll_fd, HI_DVPP_EPOLL_CTL_ADD, fd, &event);
    HMEV_HISDK_PRT(DEBUG, "mEpollFd:%d fd:%d", m_epoll_fd, fd);
    HMEV_HISDK_CHECK_RET_EXPRESS(ret == -1, "Could not add fd to epoll instance");
    return HMEV_SUCCESS;
}

5 VENC解码代码细节

5.1 几个类之间的关系

class MsgHandler {
public:
    MsgHandler() {}

    virtual ~MsgHandler() {}

    virtual int32_t handle_message()
    {
        return HMEV_SUCCESS;
    }
    int32_t m_what = 0;
    void* m_obj;
    bool m_quit = false;
};

这个MsgHandler类啥也没有,他就是个基类,用来被别人继承的,然后OnEncStreamHandler 继承了MsgHandler 。

class OnEncStreamHandler : public MsgHandler {
public:
    OnEncStreamHandler(HmevEncoder* encoder) : MsgHandler()
    {
        penc = encoder;
    }

    ~OnEncStreamHandler()
    {
    }

    virtual int32_t handle_message()
    {
        CHECK_NULL_PTR(penc);
        return penc->on_encode_stream();
    }

private:
    HmevEncoder* penc;
};

但是OnEncStreamHandler 类里面也没什么东西,它里面主要是有一个HmevEncoder。HmevEncoder类如下


class HmevEncoder {
    friend OnEncStreamHandler;
public:
    HmevEncoder(VencParam* encParam);
    ~HmevEncoder();
    int32_t codec_init();
    int32_t start_receive_frame();
    int32_t stop_receive_frame();
    int32_t process_frame(hi_video_frame_info* buffer);
    int32_t cancel_frame(hi_video_frame_info* buffer);
    hi_venc_stream* find_match_stream_cache(uint32_t packCount);
    void return_stream_cache(hi_venc_stream* vencStream);
    int32_t dequeue_input_buffer(hi_u32 width, hi_u32 height, hi_pixel_format pixelFormat,
        hi_data_bit_width bitWidth, hi_compress_mode cmpMode, hi_u32 align, hi_video_frame_info** inputFrame);
    uint32_t get_channel_id()
    {
        std::lock_guard<std::recursive_mutex> guardLock(m_lock);
        return m_encParam.channelId;
    }

private:
    int32_t on_encode_stream();
    int32_t do_creat_channel();
    int32_t alloc_input_buffer(hi_u32 width, hi_u32 height, hi_pixel_format pixelFormat,
        hi_data_bit_width bitWidth, hi_compress_mode cmpMode, hi_u32 align, hi_video_frame_info* inputFrame);

    mutable std::recursive_mutex m_lock;
    mutable std::condition_variable_any m_cv;
    VencParam m_encParam;
    uint32_t m_sendFrame = 0;
    uint32_t m_getFrame = 0;
    hi_venc_chn_attr m_stChnAttr;
    hi_venc_gop_attr m_stGopAttr;
    CodecState m_codecState = GHOST;
    std::map<hi_video_frame_info*, uint32_t> m_inBufferSizeMap;
    std::map<uint64_t, hi_video_frame_info*> m_ptsMap;
    std::list<hi_video_frame_info*> m_freeInputBufQueue;
    std::list<hi_venc_stream*> m_freeStreamBuffer;
    const uint32_t FREE_FRAME_QUEUE_LEN = 10;
    HmevLooper* m_encLooper{nullptr};
};

主要的操作其实都在HmevEncoder类里面了。

然后还有一个类HmevLooper,在上面HmevEncoder的HmevEncoder::codec_init()函数里面有一行

m_encLooper = new HmevLooper(name.c_str(), 1, 1000); // timeout 1000 ms

看一下HmevLooper这个类。

class HmevLooper {
public:
    HmevLooper(int32_t size, int32_t timeout);

    HmevLooper(int32_t size);

    HmevLooper(const char* looperName, int32_t size, int32_t timeout);

    HmevLooper(const char* looperName, int32_t size);

    virtual ~HmevLooper();

    int32_t add_fd(int32_t fd, MsgHandler* msg);

    int32_t remove_fd(int32_t fd);

    int32_t quit();

    int32_t do_quit();

    pthread_t m_thread;

private:
    int32_t create_epoll(int32_t size);

    static void* trampoline(void* p);

    void loop();

    std::string m_name;
    int32_t m_epoll_fd; // guarded by mLock but only modified on the looper thread
    int32_t m_epoll_size;
    int32_t m_timeout;
    bool m_polling;
    std::map <int32_t, std::shared_ptr<MsgHandler>> m_msg_map;
    mutable std::mutex m_lock;
};

5.2 std::mutex m_lock这个锁用在哪里了

HmevLooper类里面的几个函数都用到了m_lock这个锁。

参考文献:

samples: CANN Samples - Gitee.com

linux epoll 机制 | sleticalboy

彻底搞懂epoll高效运行的原理 - 简书

一文读懂 Linux epoll 实现原理_linux epoll原理-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈 洪 伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值