1. 程式人生 > >多程序偵聽同一埠

多程序偵聽同一埠

一、埠偵聽
我們知道,系統中的網際網路埠地址是系統級唯一的,在預設情況下,IPV4和IPV6的同一個協議的套介面也不能再同一個埠偵聽,而套介面程式設計的五元組就是<IP,port,peerip,peerport,inet proto>,其中沒有程序區分,所以一個系統的套介面對於同一個網路地址來說是唯一的。但是有時候為了實現負載平衡,可能希望有多個程序來偵聽同一個套介面,從而併發執行某個任務,此時就希望多個相同的程序(相同的可執行檔案)來對同一個套介面進行偵聽,從而完成負載分流和平衡。
當然,多執行緒也是一種實現方法,但是缺點就是需要實現使用者態編碼,不對可執行程式透明,使用者態的程式碼需要自己呼叫pthread_create來建立多個執行緒,這樣屬於一種硬編碼的方式,有其資源共享的優點,但是會增加維護的複雜度。而一個程式同時執行多份的話,由於程式碼段共享的原因,系統同樣不會有太大的記憶體開銷,並且可以方便的由使用者態決定啟動多少個任務而不依賴程式碼實現。
二、fastcgi starter實現
通產來說,如果讓同一個程序依次派生執行,那麼這個多程序偵聽同一個套介面是一定無法實現的,因為在bind系統呼叫會返回埠被佔用錯誤,所以此時就需要由一個父程序來完成這個同一個的bind+listen動作,這時候把一個套介面已經培養到可以執行accept系統呼叫來獲得連線請求的時候,這個fd相當於已經被培育成熟,所以此時根據需要個數派生服務程序,這樣子程序就可以在照約定的檔案描述符上進行accept接收外部連線請求。或者任務fastcgi派生的都是“官二代”,當這些子程序啟動起來之後,它就可以直接從一個檔案描述符上進行accept來接見各種連線請求,並且每個子程序都有這種接收機會。
這個流程無論從實現和原理上來講都不是很複雜,但是比較有創意。大家經常說“檔案是unix的精髓”,但是能夠把它用到這種地步還真是不容易,同樣的套介面,同樣的檔案描述符,就是可以做到多程序偵聽同一個埠的實現。這一點和busybox的可執行檔案“多路複用”一樣,是一種化腐朽為神奇,或者至少是“化平凡為神奇”的實現方法。而兩者也的確是依靠這兩個比較有創意的思路,實現了兩種非常有用的機制,busybox在嵌入式中幾乎是根檔案系統的基礎,而fastcgi則是網路伺服器中的快速響應流行模型。
httpd-2.4.2\support\fcgistarter.c中相關程式碼:

  rv = apr_sockaddr_info_get(&skaddr, interface, APR_UNSPEC, port, 0, pool);
    if (rv) {
        exit_error(rv, "apr_sockaddr_info_get");
    }

    rv = apr_socket_create(&skt, skaddr->family, SOCK_STREAM, APR_PROTO_TCP, pool);
    if (rv) {
        exit_error(rv, "apr_socket_create");
    }

    rv = apr_socket_bind(skt, skaddr);
    if (rv) {
        exit_error(rv, "apr_socket_bind");
    }

    rv = apr_socket_listen(skt, 1024);
    if (rv) {
        exit_error(rv, "apr_socket_listen");
    }
 while (--num_to_start >= 0) {完成套介面偵聽之後迴圈建立子程序。
        rv = apr_proc_fork(&proc, pool);
……            apr_os_file_t oft = 0;注意這個檔案描述符,被fcgi派生的子程序就是通過在這個檔案描述符上直接執行accept系統呼叫來完成服務請求的,這個檔案描述符在fastcgi.h中定義為#define FCGI_LISTENSOCK_FILENO 0,數值同樣為零。
            apr_os_sock_t oskt;

}

三、多程序競爭連線請求
核心實現部分其實並不重要,也沒什麼好說的,只是比較好奇,就大致看一下相關實現。
1、等待佇列頭建立
最原始的等待佇列在sock_alloc--->>>
static struct inode *sock_alloc_inode(struct super_block *sb)
{
    init_waitqueue_head(&ei->socket.wait);
}
中實現,這裡其實沒有什麼初始化,就是初始化了一個自旋鎖,並且初始化為可獲取狀態,它並沒有初始化方法成員。
然後在__sock_create--->>inet_create--->>>sock_init_data
        sk->sk_sleep    =    &sock->wait;
這裡將sk結構中的等待佇列頭指向socket中的wait成員,而這個sk_sleep將會是accept的等待佇列頭地址。
2、accept阻塞
sys_accept---->>>inet_accept--->>inet_csk_accept---->>>inet_csk_wait_for_connect--->>prepare_to_wait_exclusive(sk->sk_sleep, &wait,TASK_INTERRUPTIBLE)
wait->flags |= WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&q->lock, flags);
    if (list_empty(&wait->task_list))
        __add_wait_queue_tail(q, wait);
    /*
在加入等待佇列之後,通過timeo = schedule_timeout(timeo);讓出排程權。
這裡比較特殊的是這裡的喚醒是互斥的,也就是那個 WQ_FLAG_EXCLUSIVE標誌,這個標誌會在喚醒函式中使用,當遇到這個標誌並且喚醒互斥程序個數為1(預設情況)時只喚醒一個程序,其中的prepare_to_wait_exclusiv的wait是通過下面巨集建立的
DEFINE_WAIT(wait);
3、連線到來時喚醒
tcp_v4_do_rcv--->>>tcp_child_process
        /* Wakeup parent, send SIGIO */
        if (state == TCP_SYN_RECV && child->sk_state != state)
            parent->sk_data_ready(parent, 0);
inet_create--->>>sock_init_data

    sk->sk_state_change    =    sock_def_wakeup;
    sk->sk_data_ready    =    sock_def_readable;
    sk->sk_write_space    =    sock_def_write_space;
    sk->sk_error_report    =    sock_def_error_report;
    sk->sk_destruct        =    sock_def_destruct;
也就是執行的sk_data_ready即為sock_def_readable函式,在該函式中,其執行操作為
static void sock_def_readable(struct sock *sk, int len)
{
    read_lock(&sk->sk_callback_lock);
    if (sk->sk_sleep && waitqueue_active(sk->sk_sleep))
        wake_up_interruptible(sk->sk_sleep);
    sk_wake_async(sk,1,POLL_IN);
    read_unlock(&sk->sk_callback_lock);
}
#define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
可以看到,通過sk->sk_sleep喚醒了正在accept的接收套介面,並且其中__wake_up的喚醒互斥任務個數為1,所以只會喚醒一個程序,這次連線的到來對其它任務透明。