目录
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)
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这个锁。