【问题标题】:Where would my data be getting lost at within this mutex/pthread_cond_wait structure?在这个 mutex/pthread_cond_wait 结构中,我的数据会在哪里丢失?
【发布时间】:2021-01-16 06:19:14
【问题描述】:

最终编辑:我选择的答案说明了问题的解决方案。代表性示例代码见diffhere

编辑:帖子底部的完整可编译代码。

我有这个简单的多线程服务器,它只接受一个连接,并且应该将文件描述符传递给一个线程,以允许该线程直接处理它,直到客户端断开连接。

出于某种原因,即使在服务器内部使用以下代码流,一些客户端也会“掉进裂缝”并陷入困境。 (他们永远不会被服务器处理,所以他们只是在接受连接后挂起)

以下块是我的服务器主运行循环:

    while(g_serv.b_running)
    {
        //printf("Awaiting connection.\n");
        client_fd = accept(g_serv.serv_listener_fd,
                           (struct sockaddr*)&cli_addr,
                           &clilen);
        if (0 > client_fd)
        {
            fprintf(stderr,
                    "Error accepting connection. [%s]\n",
                    strerror(errno));
            continue;
        }


        err = sem_trywait(&(g_serv.client_count_sem));
        if (0 > err)
        {
            fprintf(stderr,
                    "Max connections reached. [%s]\n",
                    strerror(errno));
            notify_client_max_connections(client_fd);
            close(client_fd);
            client_fd = 0;
            continue;
        }

        printf("A client has connected.\n");

        char byte[2] = "0";
        err = send(client_fd, byte, 1, 0);

        // Set up client FD in global position and wake up a thread to grab it
        //
        pthread_mutex_lock(&(g_serv.new_connection_fd_lock));
        g_serv.new_connection_fd = client_fd;
        if (0 != g_serv.new_connection_fd)
        {
            pthread_cond_signal(&(g_serv.new_connection));
        }
        pthread_mutex_unlock(&(g_serv.new_connection_fd_lock));
    }

这个块是线程处理函数:

    void* thread_handler(void* args)
    {
        serv_t* p_serv = (serv_t*)args;
        bool    thread_client_connected;
        int     thread_client_fd;
        while(p_serv->b_running)
        {
            pthread_mutex_lock(&(p_serv->new_connection_fd_lock));
            while (0 == p_serv->new_connection_fd && p_serv->b_running)
            {
                pthread_cond_wait(&(p_serv->new_connection),
                                  &(p_serv->new_connection_fd_lock));
            }
            thread_client_fd = p_serv->new_connection_fd;
            p_serv->new_connection_fd = 0;
            pthread_mutex_unlock(&(p_serv->new_connection_fd_lock));
    
            // In the case of a pthread cond broadcast for exiting the server.
            //
            if (0 == thread_client_fd)
            {
                continue;
            }
            
            thread_client_connected = true;
            while (thread_client_connected)
            {
                thread_client_connected = handle_client(thread_client_fd);
            }
            close(thread_client_fd);
            thread_client_fd = 0;
            sem_post(&(p_serv->client_count_sem));
        }
        return NULL;
    } /* thread_handler */

我的 serv_t 结构仅供参考:

    typedef struct serv_t {
        bool            b_running;
        int             max_connections;
        int             serv_listener_fd;
        sem_t           client_count_sem;
        pthread_mutex_t new_connection_fd_lock;
        pthread_cond_t  new_connection;
        int             new_connection_fd;
        pthread_t*      p_thread_ids;
    } serv_t;

基本上,如果我运行 netcat 或客户端程序,我通过 bash 命令使用多个实例来“后台”应用程序,其中一些实例会卡住。我让它将输出重定向到一个文件,但发生的事情是客户端/netcat 的特定实例在接受调用后卡住了。

更具体地说,如果我用两个线程运行我的程序,一个程序的一个实例会卡住,而后续副本不会卡住,即使在服务器上运行 6500 个实例。

如果我用 10 个线程运行它,多达 8 或 9 个实例会卡住,但线程在服务器中仍然正常运行。

编辑:

我指的客户端代码,从服务端开始,让客户端知道服务端已经准备好接收数据了:

    char buff[2] = { 0 };
    err = recv(client_socket_fd, buff, 1, 0);

    if ('0' != buff[0] && 1 != err)
    {
        fprintf(stderr,
                "Server handshake error. [%s]\n",
                strerror(errno));
        close(client_socket_fd);
        return EXIT_FAILURE;
    }

    if (NULL != p_infix_string)
    {
        if (MAX_BUFFER_SIZE < strlen(p_infix_string))
        {
            fprintf(stderr,
                    "Infix string is over 100 characters long.\n");
            return EXIT_FAILURE;
        }
        errno = 0;
        char* p_postfix = infix_to_postfix(p_infix_string);
        if (EINVAL == errno || NULL == p_postfix)
        {
            fprintf(stderr, "Error converting provided string.\n");
        }
        bool success = send_postfix(p_postfix, client_socket_fd);

        free(p_postfix);
        if (false == success)
        {
            fprintf(stderr,
                    "An error occured while sending the equation to the server.\n");
            close(client_socket_fd);
            return EXIT_FAILURE;
        }
    }

客户端卡在此处的接收呼叫中:

bool send_postfix(char* p_postfix, int client_socket_fd)
{
    if (NULL == p_postfix)
    {
        fprintf(stderr, "No postfix string provided to send to server.\n");
        return false;
    }

    printf("Sending postfix to server\n");
    int err = send(client_socket_fd,
                   p_postfix,
                   strnlen(p_postfix, MAX_BUFFER_SIZE),
                   0);
    if(strnlen(p_postfix, MAX_BUFFER_SIZE) > err)
    {
        fprintf(stderr,
                "Unable to send message to server. [%s]\n",
                strerror(errno));
        return false;
    }

    char response[MAX_BUFFER_SIZE] = { 0 };
    printf("Waiting for receive\n");
    err = recv(client_socket_fd, &response, MAX_BUFFER_SIZE, 0);
    if (0 == err)
    {
        fprintf(stderr,
                "Connection to server lost. [%s]\n",
                strerror(errno));
        return false;
    }
    else if (0 > err)
    {
        fprintf(stderr,
                "Unable to receive message on socket. [%s]\n",
                strerror(errno));
        return false;
    }

    printf("Server responded with: \n%s\n", response);
    return true;
} /* send_postfix */

编辑:https://github.com/TheStaplergun/Problem-Code 我将代码上传到这个 repo 并删除了对我使用的无关文件的需求,并用占位符填充它们。 您可以在服务器上使用命令./postfix_server -p 8888 -n 2 重新创建此问题,并在另一个终端中使用for i in {1..4}; do ./postfix_client -i 127.0.0.1 -p 8888 -e "3 + $i" &amp;&gt; $i.txt &amp; done 重新创建客户端问题

由于客户端顶部的setbuf,每个客户端的输出都会被强制刷新。运行它,看看是否有任何程序挂起,如果没有再次运行该命令。只需键入 PS 并查看其中一个是否挂起,然后查看生成的文本文件。你会看到它卡在接听电话上。

如果您对服务器 (CTRL + C) 进行签名,则被卡住的客户端将关闭并收到来自服务器的 Connection reset by peer 响应,因此服务器仍然将该文件描述符锁定在某个地方。

我相信某种竞争条件正在以某种方式发生,因为它只是随机发生的。

奇怪的是,每个服务器实例只发生一次。

如果我杀死那个挂起的实例并继续执行 10000 次,它永远不会再次挂起,直到服务器重置。

【问题讨论】:

  • 这可能是真的。如果p_serv-&gt;b_running 为假,while 循环将退出,线程将处理。我的关闭函数中有一个pthread_cond_broadcast 来唤醒所有等待的线程。
  • 是的。我拥有的客户端应该连接,然后等待"0",这意味着服务器实际上已经接受了连接,而不是连接卡在侦听器的积压中。客户端成功地处理了它,并到达了客户端将数据发送到服务器的地步。服务器应该用一条消息来回报这一点,以返回“你提供了错误的数据”或“这是你的答案”。客户端卡住了,我认为这与主线程和工作线程之间的竞争条件有关。
  • 如果我不清楚,我很抱歉。我正在试图弄清楚如何传达这一点。如果您需要更多信息,我可以发布它。如您所见,我对在 SO 上发帖还比较陌生。
  • 请创建一个minimal reproducible example,即允许我们编译和运行它以重现您的问题的最小程序。显示带有示例输入以及预期和实际输出的 netcat 命令。你调试过你的程序吗?在我看来,您似乎有一个用于将文件描述符传递给线程的单一数据结构。是否保证工作线程在主线程填充新FD之前拾取FD?请不要使用 cmets 来回答澄清请求。请edit您的问题并在那里添加所有信息。
  • 运行带有命令的代码以重新创建添加到帖子的问题。

标签: c unix pthreads


【解决方案1】:

出于某种原因,即使在 服务器,一些客户端“从裂缝中掉下来”并陷入困境。 (他们永远不会被服务器处理,所以他们只是挂在后面 接受连接)

可能还有其他问题,但我看到的第一个问题是主循环在尝试移交下一个连接之前并不能确保任何处理程序线程实际拾取新连接。即使在接受新连接时 CV 上已经有处理程序线程阻塞,主服务器线程也有可能向 CV 发出信号,循环返回,接受另一个连接,重新获取互斥体,并覆盖新连接 FD在任何处理程序线程拿起前一个之前。如果线程数多于内核数,这种可能性就会增加。

请注意,这也会干扰您对可用处理程序的基于信号量的计数 - 您为每个接受的信号量减少信号量,但仅对成功处理的信号量再次增加它。

有多种方法可以让主服务器线程等待处理程序获取新连接。一组将涉及服务器等待 CV 本身,并在获取连接后依靠处理程序发出信号。另一种可能更简单的方法将涉及使用信号量来达到类似的效果。但我建议改为等待,而是为可用连接创建一个线程安全队列,这样服务器就不必等待。如果这对您有用的话,这甚至允许排队比目前可用的处理程序更多的连接。

【讨论】:

  • 我的想法实际上遵循您所说的服务器在返回接受调用之前没有确保客户端得到处理。我做了一些调试输出,并注意到主线程将在线程实际打印它的唤醒值之前显示两次“客户端已连接”(唤醒值是全局位置中的文件描述符)。我目前正在编写一个反向条件信号/等待设置,以便工作线程让服务器主循环知道它已经接收到客户端,这样主线程就不会通过事先覆盖 FD 来压缩连接。
  • 所以,我这样做了,但它仍然会导致问题。在我开始看到挂断之前,我必须进行非常大量的客户端运行,现在不仅仅是一两个客户端卡住了。我可能需要重新访问客户端代码。
  • 经过一些核心挖掘后,似乎正确使用握手(服务器线程上的 cond_wait,抓取它的工作线程上的 cond_signal)已经解决了问题。 “挂断”此时操作系统只是以极其缓慢的速度通过我的客户端的最后 10-15 个实例(需要多达 5-10 分钟才能突然工作,这也显示在服务器输出上)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-21
  • 2021-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多