如何在套接字关闭时唤醒select()?

时间:2009-08-25 16:37:11

标签: linux multithreading sockets pthreads posix

我目前正在使用select循环来管理代理中的套接字。此代理的一个要求是,如果代理向外部服务器发送消息并且在特定时间内未收到响应,则代理应关闭该套接字并尝试连接到辅助服务器。结束发生在一个单独的线程中,而select线程阻塞等待活动。

我无法确定如何检测此套接字是否专门关闭,以便我可以处理故障。如果我在另一个线程中调用close(),我得到一个EBADF,但是我无法判断哪个套接字已关闭。我试图通过异常fdset检测套接字,认为它将包含已关闭的套接字,但我没有得到任何返回。我也听说调用shutdown()会将FIN发送到服务器并收回FIN,这样我就可以关闭它;但重点是我试图通过在超时期限内没有得到回应来关闭它,所以我也不能这样做。

如果我的假设是错误的,请告诉我。任何想法都将不胜感激。

编辑: 响应有关使用select time out的建议:我需要异步关闭,因为连接到代理的客户端将超时,我不能等待选择被轮询。这只有在我把选择时间缩小到非常小的情况下才有效,这会不断地进行轮询和浪费我不想要的资源。

7 个答案:

答案 0 :(得分:9)

通常我只是将套接字标记为在另一个线程中关闭,然后当select()从activity或timeout返回时,我运行一个清理传递并关闭所有死连接并更新fd_set。以任何其他方式执行此操作会打开您放弃连接的竞争条件,就像select()最终识别它的某些数据一样,然后关闭它,但另一个线程尝试处理检测到的数据并获取心烦找到连接关闭。

哦,并且poll()通常比select()更好,无需复制尽可能多的数据。

答案 1 :(得分:3)

当另一个线程正在或可能正在使用它时,您无法在一个线程中释放资源。在另一个线程中可能正在使用的套接字上调用close将永远无法正常工作。总会有潜在的灾难性的竞争条件。

您的问题有两个很好的解决方案:

  1. 让调用select的线程总是使用不超过您愿意等待处理超时的最长时间。发生超时时,请指示调用select的线程从select返回时会注意到的某个位置。让该线程在close之间调用套接字的实际select

  2. 让线程检测套接字上的超时调用shutdown。这将导致select返回,然后让该线程执行close

答案 2 :(得分:1)

如何在select()上处理EBADF:

int fopts = 0;
for (int i = 0; i < num_clients; ++i) {
    if (fcntl(client[i].fd, F_GETFL, &fopts) < 0) {
        // call close(), FD_CLR(), and remove i'th element from client list
    }
}

此代码假定您有一组客户端结构,其中包含套接字描述符的“fd”成员。 fcntl()调用检查套接字是否仍然“活动”,如果没有,我们就会采取措施删除死套接字及其相关的客户端信息。

答案 3 :(得分:0)

当只看到大象的一小部分时,很难评论,但也许你已经结束了复杂的事情?

据推测,您有一些结构可以跟踪每个套接字及其信息(比如接收回复的时间)。您可以更改select()循环以使用超时。在其中检查是否是时候关闭套接字。做你需要做的关闭,不要在下次将它添加到fd集。

答案 4 :(得分:0)

如果你按照其他答案中的建议使用poll(2),你可以使用POLLNVAL状态,它基本上是EBADF,但是基于每个文件描述符,而不是整个系统调用,因为它是select( 2)。

答案 5 :(得分:-1)

对select使用超时,如果read-ready / write-ready / had-error序列都为空(w.r.t那个socket),请检查它是否已关闭。

答案 6 :(得分:-1)

在每个可能以零超时关闭的套接字上运行“test select”并检查选择结果和errno,直到找到已关闭的那个。

以下示例代码在不同的线程上启动两个服务器套接字,并创建两个客户端套接字以连接到任一服务器套接字。然后它启动另一个线程,它将在10秒后随机杀死一个客户端套接字(它将关闭它)。关闭任一客户端套接字会导致select失败,主线程中出现错误,下面的代码现在将测试两个套接字中的哪一个实际关闭。

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <pthread.h>
#include <stdbool.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/socket.h>


static void * serverThread ( void * threadArg )
{
    int res;
    int connSo;
    int servSo;
    socklen_t addrLen;
    struct sockaddr_in soAddr;
    uint16_t * port = threadArg;

    servSo = socket(PF_INET, SOCK_STREAM, 0);
    assert(servSo >= 0);

    memset(&soAddr, 0, sizeof(soAddr));
    soAddr.sin_family = AF_INET;
    soAddr.sin_port = htons(*port);

    // Uncommend line below if your system offers this field in the struct
    // and also needs this field to be initialized correctly.
//  soAddr.sin_len = sizeof(soAddr);

    res = bind(servSo, (struct sockaddr *)&soAddr, sizeof(soAddr));
    assert(res == 0);

    res = listen(servSo, 10);
    assert(res == 0);

    addrLen = 0;
    connSo = accept(servSo, NULL, &addrLen);
    assert(connSo >= 0);

    for (;;) {
        char buffer[2048];
        ssize_t bytesRead;

        bytesRead = recv(connSo, buffer, sizeof(buffer), 0);
        if (bytesRead <= 0) break;

        printf("Received %zu bytes on port %d.\n", bytesRead, (int)*port);
    }
    free(port);
    close(connSo);
    close(servSo);
    return NULL;
}

static void * killSocketIn10Seconds ( void * threadArg )
{
    int * so = threadArg;

    sleep(10);
    printf("Killing socket %d.\n", *so);
    close(*so);
    free(so);
    return NULL;
}


int main ( int argc, const char * const * argv )
{
    int res;
    int clientSo1;
    int clientSo2;
    int * socketArg;
    uint16_t * portArg;
    pthread_t killThread;
    pthread_t serverThread1;
    pthread_t serverThread2;
    struct sockaddr_in soAddr;

    // Create a server socket at port 19500
    portArg = malloc(sizeof(*portArg));
    assert(portArg != NULL);
    *portArg = 19500;
    res = pthread_create(&serverThread1, NULL, &serverThread, portArg);
    assert(res == 0);

    // Create another server socket at port 19501
    portArg = malloc(sizeof(*portArg));
    assert(portArg != NULL);

    *portArg = 19501;
    res = pthread_create(&serverThread1, NULL, &serverThread, portArg);
    assert(res == 0);

    // Create two client sockets, one for 19500 and one for 19501
    // and connect both to the server sockets we created above.

    clientSo1 = socket(PF_INET, SOCK_STREAM, 0);
    assert(clientSo1 >= 0);

    clientSo2 = socket(PF_INET, SOCK_STREAM, 0);
    assert(clientSo2 >= 0);

    memset(&soAddr, 0, sizeof(soAddr));
    soAddr.sin_family = AF_INET;
    soAddr.sin_port = htons(19500);
    res = inet_pton(AF_INET, "127.0.0.1", &soAddr.sin_addr);
    assert(res == 1);

    // Uncommend line below if your system offers this field in the struct
    // and also needs this field to be initialized correctly.
//  soAddr.sin_len = sizeof(soAddr);

    res = connect(clientSo1, (struct sockaddr *)&soAddr, sizeof(soAddr));
    assert(res == 0);

    soAddr.sin_port = htons(19501);
    res = connect(clientSo2, (struct sockaddr *)&soAddr, sizeof(soAddr));
    assert(res == 0);

    // We want either client socket to be closed locally after 10 seconds.
    // Which one is random, so try running test app multiple times.
    socketArg = malloc(sizeof(*socketArg));
    srandomdev();
    *socketArg = (random() % 2 == 0 ? clientSo1 : clientSo2);
    res = pthread_create(&killThread, NULL, &killSocketIn10Seconds, socketArg);
    assert(res == 0);

    for (;;) {
        int ndfs;
        int count;
        fd_set readSet;

        // ndfs must be the highest socket number + 1
        ndfs = (clientSo2 > clientSo1 ? clientSo2 : clientSo1);
        ndfs++;

        FD_ZERO(&readSet);
        FD_SET(clientSo1, &readSet);
        FD_SET(clientSo2, &readSet);

        // No timeout, that means select may block forever here.
        count = select(ndfs, &readSet, NULL, NULL, NULL);

        // Without a timeout count should never be zero.
        // Zero is only returned if select ran into the timeout.
        assert(count != 0);

        if (count < 0) {
            int error = errno;

            printf("Select terminated with error: %s\n", strerror(error));

            if (error == EBADF) {
                fd_set closeSet;
                struct timeval atonce;

                FD_ZERO(&closeSet);
                FD_SET(clientSo1, &closeSet);
                memset(&atonce, 0, sizeof(atonce));
                count = select(clientSo1 + 1, &closeSet, NULL, NULL, &atonce);
                if (count == -1 && errno == EBADF) {
                    printf("Socket 1 (%d) closed.\n", clientSo1);
                    break; // Terminate test app
                }

                FD_ZERO(&closeSet);
                FD_SET(clientSo2, &closeSet);
                // Note: Standard requires you to re-init timeout for every
                // select call, you must never rely that select has not changed
                // its value in any way, not even if its all zero.
                memset(&atonce, 0, sizeof(atonce));
                count = select(clientSo2 + 1, &closeSet, NULL, NULL, &atonce);
                if (count == -1 && errno == EBADF) {
                    printf("Socket 2 (%d) closed.\n", clientSo2);
                    break; // Terminate test app
                }
            }
        }
    }
    // Be a good citizen, close all sockets, join all threads
    close(clientSo1);
    close(clientSo2);
    pthread_join(killThread, NULL);
    pthread_join(serverThread1, NULL);
    pthread_join(serverThread2, NULL);

    return EXIT_SUCCESS;
}

两次运行此测试代码的示例输出:

$ ./sockclose 
Killing socket 3.
Select terminated with error: Bad file descriptor
Socket 1 (3) closed.

$  ./sockclose 
Killing socket 4.
Select terminated with error: Bad file descriptor
Socket 1 (4) closed.

但是,如果您的系统支持poll(),我强烈建议您考虑使用此API代替select()。 Select是过去相当丑陋的遗留API,只留下来与现有代码向后兼容。 Poll有一个更好的接口来完成这个任务,它有一个额外的标志来直接向你发出套接字已在本地关闭的信号:如果这个套接字已经关闭,POLLNVAL将在revents上设置,无论哪个标志您请求事件,因为POLLNVAL是仅输出标志,这意味着在events上设置时会忽略它。如果套接字未在本地关闭但远程服务器刚刚关闭连接,则标记POLLHUP将在revents中设置(也是仅输出标志)。 poll的另一个优点是超时只是一个int值(毫秒,对于真实的网络套接字来说足够精细),并且对可以监视的套接字数量或它们的数值范围没有限制。