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中再续。先说到这里把。