Linux系統多進程多工喚醒衝突如何解決

來源:互聯網
上載者:User

Linux 對於 accept(2) 的驚群(thundering herd)問題, 早已解決。目前許多人也把這種現象稱為新的驚群:用多工模型時, 不同的進程監控的檔案描述符集合的交集不為空白, 等這個交集的某個檔案IO事件觸發後, 核心將的多個監控了這個io且阻塞在 select(2), poll(2) 或 epoll_wait(2) 的進程喚醒。但嚴格來說, 這種現象不叫驚群(thundering herd), 而是衝突(collision). 對於核心來說, 喚醒所有監控這一IO事件的進程是合理的。這是因為: select/poll/epoll 不同與 accept, 它們監控的檔案描述符是可以被多個進程同時處理的, 比如一個進程唯讀取這個檔案控制代碼一小部分資料, 另一進程讀剩餘部分。而 accept 處理的通訊端是互斥的,一個通訊端不能被兩個進程 accept.

我注意到,對這種 select/poll/epoll 衝突的理解存在許多誤區,比如有人都用如下類似的代碼類比select衝突(網上搜 select 驚群或 epoll 驚群有真相):

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <strings.h>
#include <arpa/inet.h>

void worker_hander(int listenfd)
{
    fd_set rset;
    int connfd, ret;

    printf("worker pid#%d is waiting for connection...n", getpid());
    for (;;) {
        FD_ZERO(&rset);
        FD_SET(listenfd,&rset);
        ret = select(listenfd+1,&rset,NULL,NULL,NULL);
        if(ret < 0)
            perror("select");
        else if(ret > 0 && FD_ISSET(listenfd, &rset)) {
            printf("worker pid#%d 's listenfd is readablen",
                    getpid());
            connfd = accept(listenfd, NULL, 0);
            if(connfd < 0) {
                perror("accept error");
                continue;
            }
            printf("worker pid#%d create a new connection...n",
                    getpid());
            sleep(1);
            close(connfd);
        }
    }
}

static int fd_set_noblock(int fd)
{
    int flags;

    flags = fcntl(fd, F_GETFL);
    if (flags == -1)
        return -1;
    flags |= O_NONBLOCK;
    flags = fcntl(fd, F_SETFL, flags);
    return flags;
}

int main(int argc,char*argv[])
{
    int listenfd;
    struct sockaddr_in servaddr;
    int sock_opt = 1;

    listenfd = socket(AF_INET,SOCK_STREAM,0);
    if (listenfd < 0) {
        perror("socket");
        exit(1);
    }
    fd_set_noblock(listenfd);
    if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void *)&sock_opt,
            sizeof(sock_opt))) < 0) {
        perror("setsockopt");
        exit(1);
    }
    bzero(&servaddr, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(1234);
    bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
    listen(listenfd, 10);

    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    } else if (pid == 0)
        worker_hander(listenfd);
    worker_hander(listenfd);
    return 0;
}

編譯後用先運行以上的服務端, 用戶端可以用 netcat 類比串連:

nc 127.0.0.1 1234

以上代碼是兩個進程同時監控同一個檔案描述符,返回的結果基本是只有一個select返回。於是實驗人認為"並不是將所有背景工作處理序全部喚醒,而只是喚醒了一部分".

這個錯誤的認識在於沒有理解喚醒的含義. 並不是要從 select(2) 返回才叫喚醒。

一個進程在等待的io事件發生之前,核心會為這個進程描述符的state欄位設定 TASK_INTERRUPTIBLE 狀態, 此時進程描述符位於等待隊列中。一旦等待的事件發生後,進程就會被喚醒,進程描述符就會被移到運行隊列中, 發生進程切換時,核心進程調度器會根據調度策略從運行隊列選擇一個進程執行。

因此,上述程式實際上喚醒了所有的兩個進程, 只不過先被調度的那個進程 select(2) 返回後, 如果執行到accept(2) 也沒有發生進程切換,把IO事件處理掉了。而等到後調度的那個進程執行時,select(2) 裡面已經沒有這個IO事件了, 核心檢測這個進程沒有監控的事件發生,會把這個進程繼續放到等待隊列裡面去, select(2) 並沒有返回。 這種情況的機率是非常大的。另一種機率很小的情況是:先被調度的進程執行到 accept(2) 就發生了進程切換,而在下一次運行前,調度器啟動了後一個進程,這樣的話,後一個進程也將會從select(2)返回。

後一種情況很不容易發生, 在 accetp(2) 之前插入 usleep(3) 或 sleep(3) 就可以提高發生的機率了。

核心喚醒進程又不能讓這個進程執行, 再次把它移動到等待隊列, 造成了一定的開銷浪費。nginx 是這樣處理的: 用一個管理進程管理多個背景工作處理序的多工。背景工作處理序在epoll_wait(2)前向管理進程申請鎖, 確保同一時刻, 多個進程在epoll監聽的檔案描述符集合的交集為空白。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.