重新安装epoll线程的文件描述符是否安全?

时间:2015-07-21 06:29:12

标签: event-handling epoll epollet

来自this question我知道我可以调用epoll_ctl(2)而另一个帖子在epoll_wait(2)上阻止。我仍然有一个问题。

epollEPOLLONESHOT标记一起使用时,只会触发一个事件,并且必须使用epoll_ctl(2)重新设置fd。这是必要的,所以只有一个线程 将从fd读取并适当处理结果。

以下是一个时间线,可以看出我所谓的问题:

Thread1:                       Thread2:                  Kernel:
-----------------------------------------------------------------------
epoll_wait();
                                                         Receives chunk
dispatch chunk to thread 2
epoll_wait();                  Handle chunk
                               Still handle chunk        Receives chunk
                               Rearm fd for epoll
?

收到大块后重新启动fd时问号会发生什么? epoll会触发EPOLLIN事件,还是会无限期地阻塞,尽管套接字是可读的?我的建筑是否合情合理?

1 个答案:

答案 0 :(得分:3)

您的架构是合理的,它会起作用:epoll会将文件描述符标记为可读并触发EPOLLIN事件。

关于此的文件是稀缺和微妙的; man 7 epoll的Q / A部分简要提到了这一点:

  

Q8文件描述符上的操作是否会影响已收集的操作   但尚未报道事件?

     

A8你可以在一个上做两个操作   现有文件描述符。对于这种情况,删除将毫无意义。   修改将重新读取可用的I / O.

您可以对现有文件描述符执行的两个操作(现有文件描述符是过去已添加到epoll集的文件描述符 - 包括等待重新安装的文件描述符)将被删除,修改。如联机帮助页所述,删除在这里没有意义,修改将重新评估文件描述符中的条件。

但是,没有什么比现实世界的实验更好了。以下程序测试了这个边缘情况:

#include <stdio.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <assert.h>
#include <semaphore.h>
#include <sys/epoll.h>
#include <unistd.h>

static pthread_t tids[2];
static int epoll_fd;
static char input_buff[512];
static sem_t chunks_sem;

void *dispatcher(void *arg) {
    struct epoll_event epevent;

    while (1) {
        printf("Dispatcher waiting for more chunks\n");
        if (epoll_wait(epoll_fd, &epevent, 1, -1) < 0) {
            perror("epoll_wait(2) error");
            exit(EXIT_FAILURE);
        }

        ssize_t n;
        if ((n = read(STDIN_FILENO, input_buff, sizeof(input_buff)-1)) <= 0) {
            if (n < 0)
                perror("read(2) error");
            else
                fprintf(stderr, "stdin closed prematurely\n");
            exit(EXIT_FAILURE);
        }

        input_buff[n] = '\0';
        sem_post(&chunks_sem);
    }

    return NULL;
}

void *consumer(void *arg) {
    sigset_t smask;
    sigemptyset(&smask);
    sigaddset(&smask, SIGUSR1);

    while (1) {
        sem_wait(&chunks_sem);
        printf("Consumer received chunk: %s", input_buff);
        /* Simulate some processing... */
        sleep(2);
        printf("Consumer finished processing chunk.\n");
        printf("Please send SIGUSR1 after sending more data to stdin\n");

        int signo;
        if (sigwait(&smask, &signo) < 0) {
            perror("sigwait(3) error");
            exit(EXIT_FAILURE);
        }

        assert(signo == SIGUSR1);

        struct epoll_event epevent;
        epevent.events = EPOLLIN | EPOLLONESHOT;
        epevent.data.fd = STDIN_FILENO;

        if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, STDIN_FILENO, &epevent) < 0) {
            perror("epoll_ctl(2) error when attempting to readd stdin");
            exit(EXIT_FAILURE);
        }

        printf("Readded stdin to epoll fd\n");
    }
}

int main(void) {

    sigset_t sigmask;
    sigfillset(&sigmask);
    if (pthread_sigmask(SIG_SETMASK, &sigmask, NULL) < 0) {
        perror("pthread_sigmask(3) error");
        exit(EXIT_FAILURE);
    }

    if ((epoll_fd = epoll_create(1)) < 0) {
        perror("epoll_create(2) error");
        exit(EXIT_FAILURE);
    }

    struct epoll_event epevent;
    epevent.events = EPOLLIN | EPOLLONESHOT;
    epevent.data.fd = STDIN_FILENO;

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &epevent) < 0) {
        perror("epoll_ctl(2) error");
        exit(EXIT_FAILURE);
    }

    if (sem_init(&chunks_sem, 0, 0) < 0) {
        perror("sem_init(3) error");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tids[0], NULL, dispatcher, NULL) < 0) {
        perror("pthread_create(3) error on dispatcher");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tids[1], NULL, consumer, NULL) < 0) {
        perror("pthread_create(3) error on consumer");
        exit(EXIT_FAILURE);
    }

    size_t i;
    for (i = 0; i < sizeof(tids)/sizeof(tids[0]); i++) {
        if (pthread_join(tids[i], NULL) < 0) {
            perror("pthread_join(3) error");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}

它的工作方式如下:调度程序线程将stdin添加到epoll集,然后使用epoll_wait(2)stdin获取输入,只要它变得可读。当输入到达时,调度员唤醒工作线程,工作线程打印输入并通过睡眠2秒模拟一些处理时间。与此同时,调度程序返回主循环并再次阻塞epoll_wait(2)

工作线程不会重新stdin,直到您通过发送SIGUSR1告诉它。因此,我们只需将更多内容写入stdin,然后将SIGUSR1发送到流程。工作线程接收到信号,然后它重新启动stdin - 这时已经可读,并且调度员已经在等待epoll_wait(2)

您可以从输出中看到调度程序已正确唤醒,一切都像魅力一样:

Dispatcher waiting for more chunks
testing 1 2 3 // Input
Dispatcher waiting for more chunks // Dispatcher notified worker and is waiting again
Consumer received chunk: testing 1 2 3
Consumer finished processing chunk.
Please send SIGUSR1 after sending more data to stdin
hello world // Input
Readded stdin to epoll fd // Rearm stdin; dispatcher is already waiting
Dispatcher waiting for more chunks // Dispatcher saw new input and is now waiting again
Consumer received chunk: hello world
Consumer finished processing chunk.
Please send SIGUSR1 after sending more data to stdin