Ожидание дочерних процессов при использовании select() для мультиплексирования

Я столкнулся с некоторыми проблемами, связанными с зомби-процессами. Я написал простой сервер, который создает матчи в крестики-нолики между игроками. Я использую select() для мультиплексирования между несколькими подключенными клиентами. Всякий раз, когда есть два клиента, сервер будет разветвлять другой процесс, который выполняет программу арбитра соответствия.

Проблема в том, что select() блокирует. Итак, поэтому, скажем, если есть программа арбитра соответствия, работающая как дочерний процесс, и она завершается, родитель никогда не будет ждать дочернего процесса, если нет входящих соединений, потому что select() блокирует.

У меня есть мой код здесь, извиняюсь, так как он довольно беспорядочный.

while(1) {
    if (terminate)
        terminate_program();
    FD_ZERO(&rset);
    FD_SET(tcp_listenfd, &rset);
    FD_SET(udpfd, &rset);
    maxfd = max(tcp_listenfd, udpfd);

    /* add child connections to set */
    for (i = 0; i < MAXCLIENTS; i++) {
        sd = tcp_confd_lst[i];
        if (sd > 0)
            FD_SET(sd, &rset);
        if (sd > maxfd)
            maxfd = sd;
    }

    /* Here select blocks */
    if ((nready = select(maxfd + 1, &rset, NULL, NULL, NULL)) < 0) {
        if (errno == EINTR)
            continue;
        else
            perror("select error");
    }

    /* Handles incoming TCP connections */
    if (FD_ISSET(tcp_listenfd, &rset)) {
        len = sizeof(cliaddr);
        if ((new_confd = accept(tcp_listenfd, (struct sockaddr *) &cliaddr, &len)) < 0) {
            perror("accept");
            exit(1);
        }
        /* Send connection message asking for handle */
        writen(new_confd, handle_msg, strlen(handle_msg));
        /* adds new_confd to array of connected fd's */
        for (i = 0; i < MAXCLIENTS; i++) {
            if (tcp_confd_lst[i] == 0) {
                tcp_confd_lst[i] = new_confd;
                break;
            }
        }
    }

    /* Handles incoming UDP connections */
    if (FD_ISSET(udpfd, &rset)) {

    }

    /* Handles receiving client handles */
    /* If client disconnects without entering their handle, their values in the arrays will be set to 0 and can be reused. */
    for (i = 0; i < MAXCLIENTS; i++) {
        sd = tcp_confd_lst[i];
        if (FD_ISSET(sd, &rset)) {
            if ((valread = read(sd, confd_handle, MAXHANDLESZ)) == 0) {
                printf("Someone disconnected: %s\n", usr_handles[i]);
                close(sd);
                tcp_confd_lst[i] = 0;
                usr_in_game[i] = 0;
            } else {
                confd_handle[valread] = '\0';
                printf("%s\n", confd_handle); /* For testing */
                fflush(stdout);
                strncpy(usr_handles[i], confd_handle, sizeof(usr_handles[i]));
                for (j = i - 1; j >= 0; j--) {
                    if (tcp_confd_lst[j] != 0 && usr_in_game[j] == 0) { 
                        usr_in_game[i] = 1; usr_in_game[j] = 1;
                        if ((child_pid = fork()) == 0) {
                            close(tcp_listenfd);
                            snprintf(fd_args[0], sizeof(fd_args[0]), "%d", tcp_confd_lst[i]);
                            snprintf(fd_args[1], sizeof(fd_args[1]), "%d", tcp_confd_lst[j]);
                            execl("nim_match_server", "nim_match_server", usr_handles[i], fd_args[0], usr_handles[j], fd_args[1], (char *) 0);
                        }
                        close(tcp_confd_lst[i]); close(tcp_confd_lst[j]);
                        tcp_confd_lst[i] = 0; tcp_confd_lst[j] = 0;
                        usr_in_game[i] = 0; usr_in_game[j] = 0;
                    }
                }
            }
        }
    }
}

Есть ли метод, который позволяет ждать, даже когда select() блокируется? Желательно без обработки сигналов, так как они асинхронны.

РЕДАКТИРОВАТЬ: На самом деле, я обнаружил, что select имеет временную структуру данных, в которой мы можем указать тайм-аут. Было бы хорошей идеей использовать это?


person mrQWERTY    schedule 25.03.2015    source источник
comment
используйте параметр тайм-аута (последний параметр) в операторе select(). Тогда следующей инструкцией после выбора должна быть проверка тайм-аута. Один из методов проверки тайм-аута состоит в том, чтобы проверить, является ли ввод fd_set нулевым (или проверить, что целевые записи fd равны нулю).   -  person user3629249    schedule 25.03.2015
comment
Нет! Вы не должны просто использовать параметр тайм-аута и проверять, что тайм-аут произошел. Если тайм-аут равен 2 секундам и программа получает порцию данных каждую секунду, то тайм-аут никогда не наступает. Вы должны запускать синхронизированный код во всех случаях, когда вы возвращаетесь из выбора, а не только в случаях тайм-аута. Если вы хотите, чтобы код запускался каждую секунду, вы можете сохранить время последнего выполнения, а затем проверить, превышает ли cur_time() - last_exec_time одну секунду.   -  person juhist    schedule 25.03.2015


Ответы (2)


Если вы просто хотите предотвратить зомби-процессы, вы можете настроить обработчик сигналов SIGCHLD. Если вы действительно хотите дождаться статуса возврата, вы можете записать байты в конвейер (на всякий случай неблокирующий) из обработчика сигнала, а затем прочитать эти байты в цикле select.

Чтобы узнать, как обрабатывать SIGCHLD, см. http://www.microhowto.info/howto/reap_zombie_processes_using_a_sigchld_handler.html -- вы хотите сделать что-то вроде while (waitpid((pid_t)(-1), 0, WNOHANG) > 0) {}

Возможно, лучшим подходом является отправка одного байта из обработчика сигнала SIGCHLD в основной цикл select (на всякий случай неблокирующий) и выполнение цикла waitpid в цикле select, когда байты могут быть прочитаны из канала.

Вы также можете использовать файловый дескриптор signalfd для чтения сигнала SIGCHLD, хотя это работает только в Linux.

person juhist    schedule 25.03.2015

Я думаю, что ваши варианты:

  1. Сохраните все ваши дочерние дескрипторы в глобальном массиве и вызовите wait() из обработчика сигнала. Если вам не нужен статус выхода ваших детей в вашем основном цикле, я думаю, что это самый простой способ.

  2. Вместо выбора используйте pselect - он вернется после получения указанного (набора) сигнала (ов), в вашем случае SIGCHLD. Затем вызовите wait/WNOHANG для всех дочерних PID. Вам нужно будет заблокировать/разблокировать SIGCHLD в нужный момент до/после pselect(), см. здесь: http://pubs.opengroup.org/onlinepubs/9699919799/functions/pselect.html

  3. Подождите/очистите дочерние PID из вторичного потока. Я думаю, что это самое сложное решение (повторная синхронизация между потоками), но, поскольку вы спросили, технически это возможно.

person davlet    schedule 25.03.2015
comment
Можно ли использовать метод 2 в случае, когда родительский процесс передает сокет дочернему процессу и намеревается принять соединение от дочернего процесса, в то же время обрабатывая возможность выхода дочернего процесса из-за ошибки, предшествующей подключиться к розетке? Это позволило бы мне обрабатывать как сбой дочернего процесса, так и успех дочернего процесса с ожидающим подключением к сокету. - person CMCDragonkai; 21.01.2017