framework 学习笔记17. input输入事件番外1(Linux知识)

1. inotify 和 epoll:
在日常使用电脑的时候,我们通常会遇到更换外设的情况,比如在使用笔记本时外接键盘等等;那么会有如下两个问题:

input输入事件1.1.png

1.1 inotify:用来监测目录/文件的变化;(安卓中就采用此方案来监测输入事件)
(1)使用:

// 第一步:初始化
fd = inotify_init();

// 第二步:监测
inotify_add_watch(fd, "目录/文件 的名称", "事件: 创建/删除");

// 第三步:通过 read() 函数读取 目录/文件 的变化
// 没有变化时 read() 函数就会进入休眠,有变化时read() 函数就会返回
char event_buf[512];
res = read(fd, event_buf, sizeof(event_buf));

// 这里的返回值 res 是以下的结构体的个数(一个或者多个):
strucrt notify_event {
  __s32 wd;
  __u32 mask;  // 表示文件发生了什么变化,是创建还是删除
  __u32 cookie;
  __u32 len;  // name 的长度
  // 字符数组,大小为0,
  char name[0];  // 发生变化的文件
};

这里的数组 char name[0] 是一个字符数组,大小为0,其真实地址紧随结构体buffer之后,而这个地址就是结构体后面数据的地址(如果给这个结构体分配的内容大于这个结构体实际大小,后面多余的部分就是这个data的内容);使用长度为0的数组有以下好处:
(a)指针本身需要占用内存,而长度为0的数组不需要;
(b)长度为0的数组定义出的缓冲区可以和结构体处在同一片连续地址中,只要一次malloc操作和free操作。如果用指针,需要分别申请和释放结构体内存和指针指向的内存块,至少需要两次以上的内存操作;

(2)安卓源码中使用 inotify(简化代码):

// EventHub.cpp 中:
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) {

    // 初始化
    mINotifyFd = inotify_init();  
    // 监测文件创建/删除
    int result = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);  
    // ...
}


status_t EventHub::readNotifyLocked() {
    int res;
    char devname[PATH_MAX];
    char *filename;
    char event_buf[512];
    int event_size;
    int event_pos = 0;
    struct inotify_event *event;

    ALOGV("EventHub::readNotify nfd: %d\n", mINotifyFd);
    res = read(mINotifyFd, event_buf, sizeof(event_buf));  // read() 函数
    if(res < (int)sizeof(*event)) {
        if(errno == EINTR)
            return 0;
        ALOGW("could not get event, %s\n", strerror(errno));
        return -1;
    }

    strcpy(devname, DEVICE_PATH);
    filename = devname + strlen(devname);
    *filename++ = '/';

    while(res >= (int)sizeof(*event)) {
        event = (struct inotify_event *)(event_buf + event_pos);
        if(event->len) {
            strcpy(filename, event->name);
            if(event->mask & IN_CREATE) {  // 如果是创建文件
                openDeviceLocked(devname);
            } else {    // 否则是删除文件
                ALOGI("Removing device '%s' due to inotify event\n", devname);
                closeDeviceByPathLocked(devname);
            }
        }
        event_size = sizeof(*event) + event->len;
        res -= event_size;
        event_pos += event_size;
    }
    return 0;
}

1.2 epoll:用来监测多个文件(有无数据供读出或者有无空将供写入);
(1)使用

// 1:创建文件句柄
int mEpollFd = epoll_create(EPOLL_SIZE_HINT);

// 2:对每个文件使用 epoll_ctl(),表示要监测它(的行为);
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);

// 3:执行 epoll_wait() 等待某个文件可用;某个文件有数据可读/有空间可使用,此时epoll_wait() 就会返回
int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);

// 4:不需要再监测某个文件时,同样使用 epoll_ctl()
epoll_ctl(mEpollFd, EPOLL_CTL_DEL, device->fd, NULL);

(2)安卓源码中使用epoll:

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);
    // 创建文件句柄
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);  
    LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance.  errno=%d", errno);

    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;  // EPOLLIN 表示文件有数据时就可以监测到
    eventItem.data.u32 = EPOLL_ID_INOTIFY;
    // 监测 mINotifyFd
    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);

    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;
    // 监测 mWakeReadPipeFd
    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);
}

2. 输入系统简介:socketpair()
用法:
(1) 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往fd[0]中写,从fd[1]中读;或者从fd[1]中写,从fd[0]中读;
(2) 如果往一个套接字(如fd[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(fd[1])上读成功;
(3)读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述符fd[0]和fd[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。

#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
const char* str = "SOCKET PAIR TEST.";
int main(int argc, char* argv[]){
    char buf[128] = {0};
    int socket_pair[2]; 
    pid_t pid; 
 
    if(socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1 ) { 
        printf("Error, socketpair create failed, errno(%d): %s\n", errno, strerror(errno));
        return EXIT_FAILURE; 
    } 
 
    pid = fork();
    if(pid < 0) {
        printf("Error, fork failed, errno(%d): %s\n", errno, strerror(errno));
        return EXIT_FAILURE;
    } else if(pid > 0) {
        //关闭另外一个套接字
        close(socket_pair[1]);
        int size = write(socket_pair[0], str, strlen(str));
        printf("Write success, pid: %d\n", getpid());
 
    } else if(pid == 0) {
        //关闭另外一个套接字
        close(socket_pair[0]);
        read(socket_pair[1], buf, sizeof(buf));        
        printf("Read result: %s, pid: %d\n",buf, getpid());
    }
 
    for(;;) {
        sleep(1);
    }
 
    return EXIT_SUCCESS;    
}

输入系统设计:一个进程用于读取分发输入事件,一个应用程序用于处理输入事件;


input输入事件1.2.png

3. socketpair + Binder 任意进程间的双向通信
Linux 知识简单介绍:Linux 内核每个进程都有一个 task_struct 结构体,在 task_struct 中使用 files_struct 来管理打开的文件;
当使用 open 函数打开一个文件时,其过程为:操作系统生成一个新的 file 结构体来保存这个文件的信息,并在 files_struct 的文件数组 fdtable 中生成一个指向这个 file 结构体的指针,然后向进程返回这个指针的下标值,而这个下标值正是文件描述符 fd,在进程中也正是通过这个 fd 来访问目的文件。如下代码所示:

struct task_struct {
  /* open file information */
  struct files_struct *files;
  // ...
}

struct files_struct {
  struct fdtable *fdt;
  // ...
}

struct fdtable {
  struct file *fd;
  // ...
}

3.1 使用 binder 传输文件句柄:
(1)App1 打开文件得到 fd1;
(2)通过 Binder 驱动,根据 fd1 得到一个 file 结构体:App1 -> files -> fdtable -> fd[fd1](这就是 file 结构体);
(3)从 App2 的 files -> fdtable -> 中取出一个空项(假设为fd2),让其指向 file 这个结构体;
相当于 App2 中的 files -> fdtable -> fd[fd2] = file;
(4)这样 App1 通过 fd1 + App2 通过 fd2 就可以访问同一份文件了;

3.2 安卓源码中的应用:
(1)建立 socket 通信:
InputChannel.openInputChannelPair(name) 是一个native方法,在 frameworks/base/core/jni/
android_view_InputChannel.cpp中:

// 该方法由 WMS 的 addWindow() 调用
static jobjectArray android_view_InputChannel_nativeOpenInputChannelPair(JNIEnv* env,
        jclass clazz, jstring nameObj) {
    const char* nameChars = env->GetStringUTFChars(nameObj, NULL);
    String8 name(nameChars);
    env->ReleaseStringUTFChars(nameObj, nameChars);

    sp serverChannel;
    sp clientChannel;
    // 创建一对 socket 通信
    status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);

    // 封装成java对象    
    jobjectArray channelPair = env->NewObjectArray(2, gInputChannelClassInfo.clazz, NULL);
    if (env->ExceptionCheck()) {
        return NULL;
    }

    jobject serverChannelObj = android_view_InputChannel_createInputChannel(env,
            new NativeInputChannel(serverChannel));
    if (env->ExceptionCheck()) {
        return NULL;
    }

    jobject clientChannelObj = android_view_InputChannel_createInputChannel(env,
            new NativeInputChannel(clientChannel));
    if (env->ExceptionCheck()) {
        return NULL;
    }

    env->SetObjectArrayElement(channelPair, 0, serverChannelObj);
    env->SetObjectArrayElement(channelPair, 1, clientChannelObj);
    return channelPair;
}
// inputTransport.cpp 中:
status_t InputChannel::openInputChannelPair(const String8& name,
        sp& outServerChannel, sp& outClientChannel) {
    int sockets[2];
    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {  // 调用 socketpair()
        status_t result = -errno;
        ALOGE("channel '%s' ~ Could not create socket pair.  errno=%d",
                name.string(), errno);
        outServerChannel.clear();
        outClientChannel.clear();
        return result;
    }

    int bufferSize = SOCKET_BUFFER_SIZE;
    setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));

    String8 serverChannelName = name;
    serverChannelName.append(" (server)");
    outServerChannel = new InputChannel(serverChannelName, sockets[0]);

    String8 clientChannelName = name;
    clientChannelName.append(" (client)");
    outClientChannel = new InputChannel(clientChannelName, sockets[1]);
    return OK;
}

(2)binder 通信:现在只需要有一个概念,后续将会详细分析;
WindowMangerService.addView() ->
ViewRootImpl.setView() ->
WindowSession.addToDisplay() ->

//mWindowSession.addToDisplay() 就是 调用了Session的 addToDisplay() 方法:
// Session 的 addToDisplay() 方法:跨进程通信
    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets,
            InputChannel outInputChannel) {
        // outInputChannel 是 2.2 中 mInputChannel = new InputChannel(),此时的指针还未赋值
        // mService 就是 new Session 时传入的 WMS
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outInputChannel); 
    }

本节内容介绍了 inotify,epoll,socketpair 三个知识点,到这里就结束了;下一节我们将继续分析 input 事件分发的框架设计。

你可能感兴趣的:(framework 学习笔记17. input输入事件番外1(Linux知识))