zoukankan      html  css  js  c++  java
  • 多进程侦听同一端口

    一、端口侦听
    我们知道,系统中的互联网端口地址是系统级唯一的,在默认情况下,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.2supportfcgistarter.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,所以只会唤醒一个进程,这次连接的到来对其它任务透明。

  • 相关阅读:
    informix 外部表 pipe
    关于XML的一些解析操作
    oracle 导出导入数据库
    判断请求访问的浏览器类型设备
    git与SVN的区别
    Java获取文件路径
    <DIV>内容显示隐藏功能实现
    文件下载
    文件上传
    记录启动Nginx启动失败
  • 原文地址:https://www.cnblogs.com/tsecer/p/10487388.html
Copyright © 2011-2022 走看看