EventHub與裝置、Input事件的互動

Cheelok發表於2017-10-25

關於EventHub的學習有一點前置知識很重要,就是epoll和inotify機制,可以參考這篇博文:www.cheelok.com/_inx/88。

在前面Input系列的博文中已經學習,來自底層的Input事件經過事件樞紐EventHub處理後,讓InputReader通過epoll和inotify從EventHub中讀取。那麼現在就來學習EventHub是如何讀取底層事件的吧。

EventHub註冊裝置節點的監聽

EventHub::EventHub(void) :
        mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD), mNextDeviceId(1), mControllerNumbers(),
        mOpeningDevices(0), mClosingDevices(0),
        mNeedToSendFinishedDeviceScan(false),
        mNeedToReopenDevices(false), mNeedToScanDevices(true),
        mPendingEventCount(0), mPendingEventIndex(0), mPendingINotify(false) {
    acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID);

    // 建立epoll物件用於監聽是否有可讀事件,指定最大監聽個數為EPOLL_SIZE_HINT
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);
    LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance.  errno=%d", errno);

    // 建立inotify物件用於監聽裝置節點DEVICE_PATH,即/dev/input,是否有變化(裝置增刪),裝置的增刪對應著裝置節點的檔案增刪
    mINotifyFd = inotify_init();
    int result = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
    LOG_ALWAYS_FATAL_IF(result < 0, "Could not register INotify for %s.  errno=%d",
            DEVICE_PATH, errno);

    struct epoll_event eventItem;
    memset(&eventItem, 0, sizeof(eventItem));
    eventItem.events = EPOLLIN;
    eventItem.data.u32 = EPOLL_ID_INOTIFY;
    // 將inotify物件註冊到epoll中監聽是否有新的可讀的裝置增刪事件
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance.  errno=%d", errno);

    // 建立管道,並將讀端交給epoll,喚醒端(寫端)交給InputReader,用於喚醒epoll,避免epoll阻塞在epoll_wait()中
    int wakeFds[2];
    result = pipe(wakeFds);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe.  errno=%d", errno);

    mWakeReadPipeFd = wakeFds[0];
    mWakeWritePipeFd = wakeFds[1];

    result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking.  errno=%d",
            errno);

    result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking.  errno=%d",
            errno);

    eventItem.data.u32 = EPOLL_ID_WAKE;
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance.  errno=%d",
            errno);

    int major, minor;
    getLinuxRelease(&major, &minor);
    // EPOLLWAKEUP was introduced in kernel 3.5
    mUsingEpollWakeup = major > 3 || (major == 3 && minor >= 5);
}複製程式碼

總結下來就是這張圖:

EventHub->getEvents

按照上面的學習,我們知道:EventHub在建立時,建立了兩個Fd,mEpollFd和mINotifyFd。其中mINotifyFd用於監聽裝置節點是否有裝置檔案的增刪,將mINotifyFd註冊到mEpollFd中,當發生新的裝置增刪,裝置節點下的裝置檔案也會隨之增刪,就會通知mEpollFd有新的可讀的裝置增刪事件,通知EventHub對裝置進行處理。

換言之,剛建立EventHub時,mEpollFd只監聽了mINotifyFd。

getEvents整個函式比較長,我就不貼所有程式碼了,這個函式主要做了以下事情:

  1. 建立一個大小為bufferSize的input_event的緩衝區,用於儲存讀取的Input事件
  2. 判斷是否需要重新開啟Input裝置
  3. 處理最後被新增/刪除的Input裝置,其中會為了處理新增的裝置而進行裝置掃描
  4. 判斷是否需要掃描裝置
  5. 獲取Input事件
  6. 開啟/關閉在步驟3中新增/刪除的Input裝置
  7. 如果裝置資訊發生變化,則通知
  8. 喚醒epoll_wait,通知InputReader讀取事件,需要注意的是,整個喚醒過程都是加鎖的

對於我們來說,學習重點自然是步驟4了,但是在看它做了什麼之前不妨先想想,建立EventHub之後,第一次getEvents時,mEpollFd只監聽了mINotifyFd,那mEpollFd要怎麼獲取裝置上的發生的Input事件呢?

掃描裝置

我們在getEvents裡面可以看到,在獲取Input事件之前,會判斷是否需要掃描裝置:

if (mNeedToScanDevices) {
    mNeedToScanDevices = false;
    scanDevicesLocked();
    mNeedToSendFinishedDeviceScan = true;
}複製程式碼

mNeedToScanDevices在建立EventHub時是預設賦值為true的,那麼第一次走getEvents肯定會走進來,scanDevicesLocked最終會呼叫scanDirLocked掃描/dev/input。在這個函式裡面就是迴圈掃描/dev/input下的裝置檔案了:

status_t EventHub::scanDirLocked(const char *dirname)
{
    ……
    dir = opendir(dirname);
    ……
    while((de = readdir(dir))) {
        ……
        openDeviceLocked(devname);
    }
    ……
}複製程式碼

看起來掃描到裝置檔案後就會openDeviceLocked,在這個函式裡其實沒做什麼特別的事情,就是新增裝置以及各種處理,有一個地方需要關注下:

// Register with epoll.
struct epoll_event eventItem;
memset(&eventItem, 0, sizeof(eventItem));
eventItem.events = EPOLLIN;
if (mUsingEpollWakeup) {
    eventItem.events |= EPOLLWAKEUP;
}
eventItem.data.u32 = deviceId;
if (epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, &eventItem)) {
    ALOGE("Could not add device fd to epoll instance.  errno=%d", errno);
    delete device;
    return -1;
}

String8 wakeMechanism("EPOLLWAKEUP");
if (!mUsingEpollWakeup) {
#ifndef EVIOCSSUSPENDBLOCK
    // uapi headers don't include EVIOCSSUSPENDBLOCK, and future kernels
    // will use an epoll flag instead, so as long as we want to support
    // this feature, we need to be prepared to define the ioctl ourselves.
#define EVIOCSSUSPENDBLOCK _IOW('E', 0x91, int)
#endif
    if (ioctl(fd, EVIOCSSUSPENDBLOCK, 1)) {
        wakeMechanism = "<none>";
    } else {
        wakeMechanism = "EVIOCSSUSPENDBLOCK";
    }
}複製程式碼

這裡我們將新裝置的fd註冊到mEpollFd中進行監聽,並且寫入E喚醒epoll。於是前面的問題就解決了,由於每次獲取Input事件前都會更新裝置資訊,因此mEpollFd能監聽到最新的裝置fd。

獲取Input事件

獲取Input事件的整個流程程式碼很多,我就不貼了,主要做了以下事情:

  1. 迴圈讀取mPendingEventItems中的eventItem
  2. 判斷eventItem是否為合法的inotify型別(eventItem.data.u32為EPOLL_ID_INOTIFY,且eventItem.events & EPOLLIN為true),如果是,說明有新的裝置增刪事件,則需要更新裝置列表資訊
  3. 判斷eventItem是否為合法的喚醒管道讀端的事件(eventItem.data.u32為EPOLL_ID_WAKE),若合法,則喚醒管道的讀端,也就是InputReader
  4. 判斷eventItem的接收裝置是否合法
  5. 如果eventItem不屬於EPOLL_ID_INOTIFY、EPOLL_ID_WAKE型別,且是epoll的輸入事件(EPOLLIN),則開始事件讀取的邏輯

事件讀取的邏輯裡做了以下事情:

  1. 首先通過裝置的readSize判斷裝置是否被移除、裝置讀取的事件總大小是否為input_event倍數(是否符合讀取格式)、readSize是否合法,通過以上檢查則獲取裝置id
  2. 迴圈讀取readBuffer中的input_event
  3. 對input_event的發生時間進行處理,儲存到RawEvent的when中
  4. 將deviceId、type、code、value封裝到RawEvent中

於是EventHub就成功地得到了來自裝置的事件,併成功將它們轉化為RawEvent,交給InputReader。

至此,本文結束。

題外話

如果你覺得我的分享有幫助到你的話,請我吃個零食/喝杯咖啡唄~

相關文章