zoukankan      html  css  js  c++  java
  • linux下服务器程序的几种基本模型【一】单/多进程模式/多进程池(prefork)模型

    服务器的基本模型,不知道这么扯对不对,其实就是linux下服务器和客户端的通信模式。也就是面对客户端如狼似渴的访问需求,服务器该如何快速的响应。

    我总结下来有这么几种:

    • 单进程提供服务
    • 多进程提供服务
    • 多进程池服务(prefork)
    • io复用提供服务(select,poll)
    • epoll(其实也是一种IO复用)
    • 多线程提供服务
    • 多线程池提供服务
    • 信号驱动提供服务

    一一按照自己的想法写出来,还想和大家后续一起探讨下非阻塞io,异步io,共享内存,进程间通信等服务器常用技术。废话不说,直接开始。

    单进程提供服务

    这种模式只存在于我们的学习中,一个客户端请求由服务器响应后,这个客户完全占有了服务器,这回如何再来一个新客户,他必须等待服务器伺候完现有的这个客户。伺候不完,服务器是不会为新客户提供服务的,这个就是完全占有。

    服务器和客户端的行为:

    server : bind -> listen -> accept one request -> do request, send response -> close accept fd  -> accept next request ....

    client  :  connect -> send request  -> wait response -> recieve response -> close connect

    服务器的行为,一般是创建一个socket,然后把相关的端口,ip使用bind捆绑到socket上,通过listen监听该端口,当有客户请求到达时,处理请求,给出回应。处理完一个客户请求后,关闭该请求,在处理下一个。

    客户端的行为,通过connect连接到server上去,发送请求,等待回应,收到答复,关闭连接。

    这种方式的弊端,显而易见,在一个客户请求未处理完毕时,另一个客户必须等待,直到被accept。在web服务这种高并发请求中,这种服务器模型显然不行。

    有一个参数这里我一直觉得很诡异,就是listen的第二个参数 backlog ,按照说明 这个参数是在建立三次握手中的连接数和完成网络连接但尚未被accept的连接数的和的最大值,但貌似在各个内核中实现又有所差异。在自己的本中实现了下,貌似超过了还会connect ok 。 

    贴上我实验用的server代码,为了节约代码量,所有的错误处理均被忽略

    /*
     * auther : wully_happy@163.com
     */
    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/socket.h>
    #include<sys/types.h>
    #include<netinet/in.h>
    #include<sys/wait.h>
    #include<string.h>
    #include<time.h>
    #define MAX_CONNECTION 2
    
    int main(int argc, char** argv){
        int fd;
        time_t ticks;
        int port = 99999;
        fd = socket(AF_INET,SOCK_STREAM,0);
        struct sockaddr_in addr;
        memset(&addr,0,sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
        int size = sizeof(struct sockaddr);
        bind(fd,(struct sockaddr*)&addr,size);
        listen(fd,MAX_CONNECTION);
        struct sockaddr_in client_addr;
        while (1){
            memset(&client_addr, 0, sizeof (client_addr));
            char buf[1024];
            memset(&buf,0,sizeof(buf));
            int client_fd = accept(fd, (struct sockaddr *) &client_addr, &size);
            ticks = time (0);
            snprintf(buf, sizeof(buf), "%s", ctime(&ticks));
            write(client_fd, buf, sizeof(buf));
            sleep(3600);
            close(client_fd);
        }
        close(fd);
        return 0;
    }
    

      可以用  telnet 127.0.0.1 99999 测试,可以看到但一个客户端使用telnet请求时,下一个必须等待。

    多进程提供服务

     既然一个进程提供服务已经应付不过来,不如多生几个儿子来处理请求,作为老子只管对儿子进行监控就好。多进程应该就是这么个道理。每个请求都fork一个子进程来处理。

    这个模式相对于第一种的优点就是 可以对多个请求进行处理,响应及时。

    缺点就是每次请求都要生成一个新进程,处理完毕,还要销毁。成本有些高,在并发请求较高的时候,会把cpu耗尽。毕竟进程这个东西还是稍微有些重的东西。

    修改程序很简单 ,在 accept后面插入代码即可,插入的代码为

            int client_fd = accept(fd, (struct sockaddr *) &client_addr, &size);
            
            pid = fork();
            if(pid > 0){
                close(client_fd);
                continue;
            }
            close(fd);
            ticks = time (0);
    

     还缺少一步,就是防止子进程成为僵尸进程,要对信号SIGCHLD进行处理,使其在接到该信号后调用waitpid函数 回收子进程。

    多进程池的服务 

     每次请求都生成新进程其实必要性并不大,大部分并发服务器处理的每秒并发量一般最多就在几百左右,因此一般几个或者十几个进程循环提供服务就可以hold住,为了减少每次请求建立新进程的成本,我们的前辈又发明了多进程池(prefork)的模式,预先生成若干进程来处理请求。

    见过两种多进程池的实现,一种是父进程只管listen,子进程对每个请求accept。另一种是父进程负责accept,然后把accept后得到的confd句柄传递给子进程。

    这里我先说下第一种实现,关于第二种的实现我会在select模式中来说,原因就是第二种模式的实现配合select的效果更佳。

    第一种的测试代码为:(这里没有添加错误的处理程序)

    /*
     * auther : wully_happy@163.com
     */
    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/socket.h>
    #include<sys/types.h>
    #include<netinet/in.h>
    #include<sys/wait.h>
    #include<string.h>
    #include<time.h>
    #define MAX_CONNECTION 2
    
    int main(int argc, char** argv){
        int fd = -1;
        time_t ticks;
        pid_t pid; 
        pid_t pids[10]; 
        int port = 99999;
        fd = socket(AF_INET,SOCK_STREAM,0);
        struct sockaddr_in addr;
        memset(&addr,0,sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
        int size = sizeof(struct sockaddr);
        bind(fd,(struct sockaddr*)&addr,size);
        listen(fd,MAX_CONNECTION);
    
        int i;
        for(i = 0; i< 10;i++){
            pid = fork();
            if(pid > 0){
                continue;
            }
            pids[i] = pid;
            struct sockaddr_in client_addr;
            while (1){
                memset(&client_addr, 0, sizeof (client_addr));
                char buf[1024];
                memset(&buf,0,sizeof(buf));
                printf("wait\n");
                int client_fd = accept(fd, (struct sockaddr *) &client_addr, &size);
    
                close(fd);
                ticks = time (0);
                snprintf(buf, sizeof(buf), "%s", ctime(&ticks));
                write(client_fd, buf, sizeof(buf));
                sleep(3600);
                close(client_fd);
            }
        }
        close(fd);
        for(i = 0; i< 10;i++){
            int status;
            if(pids[i] < 0){
                continue;
            }
            waitpid(pids[i],&status,0);
        }
        return 0;
    }
    

      

    这里在原来有一个比较纠结的地方,就是accept,原来的linux版本会有惊群现象,也就是当一个请求到来时,多个子进程同时在accept阻塞中被唤醒,导致资源消耗过大,这个比较纠结的问题在现在的较新的linux内核中已经解决,另外一个纠结的问题时select的冲突,这个咱们在select中再续。先说到这里把。

  • 相关阅读:
    Xshell初步设置
    【R shiny】一些应用记录
    R shiny 小工具Windows本地打包部署
    生信工程师如何写一个小工具?
    Android 照片墙应用实现,再多的图片也不怕崩溃
    Android 高效加载大图、多图解决方案,有效避免程序OOM
    SparseArray 详解
    ActivityThread
    Activity 启动模式详解 (activity 加载模式)
    Activity 生命周期
  • 原文地址:https://www.cnblogs.com/wully/p/forksserver.html
Copyright © 2011-2022 走看看