高性能服务器编程主要分为多进程和多线程、进程池和线程池,用来处理一个服务程序能够同时处理多个客户连接的问题。我们首先回顾下多进程和多线程的知识,因为进程池和线程池是在这个基础上进行改进的,也是服务器用的比较多的。
- 多进程
accept(); --》创建子进程,由子进程和客户端通讯。父进程继续接受客户连接
a.子进程继承父进程打开的文件描述符(accept返回的文件描述符)
b.父进程必须关闭文件描述符。
2.多线程
accept(); --》创建函数线程,由函数线程和客户端通讯,主线程继续接受客户连接
a.主线程接受客户连接的文件描述符如何传递给函数线程 –》值传递
b.主线程不能关闭文件描述符 ---》 进程的PCB中保存 同进程的多线程共享一个PCB
应用场景:
- 进程之间是相互独立的,不存在数据安全
- 进程相对线程而言,创建时,开辟的资源多,CPU调度时,比较慢
- 如果多进程要通讯,必须借助于特定的手段(信号,信号量,管道,共享内存,消息队列)
- Linux上进程中能创建的数量相比于系统进程数量小的多 (一个进程大概有300多线程)
一、进程池和线程池的概念
池:提前申请的大量资源存放的一个单位;
内存池: 使用内存之前,先申请大量的内存,当后续需要使用时,直接从内存池中分配,不需要通过系统分配。
多进程和多线程与线程池和进程思想对比:
- 多进程或者多线程:
- 有客户连接时,系统为这个客户创建出进程或者线程,当与客户端交互完成时,创建的进程或线程也就随之释放。
进程池或者线程池:
a.在服务器程序启动时,创建出多个线程或进程,将其维护在池中。 当有客户连接时,就在池中分配进程或者线程为客户端服务。
b.主线程或者父进程--》负责与客户端连接;函数线程或者子进程---》与特定客户端交互
我们看下我们linux下池中最多有几个进程:
由此可看有8个进程,通常池中分配大约的3---10个。线程池我们也看一下:
可看出也是有8个,其实在选择进程池还是线程池,也是根据我们的业务和环境的要求下进行选择。例如如果考虑安全性可以使用进程池,如果考虑切换速度,数据共享的话可以考虑线程池,各有各有好处和应用场景,更适合才是最重要的~
二、线程池的代码实现
我们通过代码再来看看线程池有些什么问题。在这之前,我们要明白线程池实现的几个难点:
- 主线程需要将文件描述符传递给函数线程 --》全局的数组
- 函数线程启动起来必须阻塞在获取文件描述符之前 --》信号量
- 信号量来控制主线程向函数线程通知获取文件描述符事件 --》信号量
- 主线程在数组中插入数据,以及函数线程获取数组中的数据都必须是互斥的,保持原子操作。 ---》互斥锁
图片是我们线程池的代码形式图,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
sem_t sem;
pthread_mutex_t mutex;
void* pthread_fun(void *arg);
int clilink[10];
void InitCliLink()
{
int i = 0;
for(; i < 10;++i)
{
clilink[i] = -1;
}
}
int Insert(int c)
{
pthread_mutex_lock(&mutex);
int i = 0;
for(; i < 10;++i)
{
if(clilink[i] == -1)
{
clilink[i] = c;
break; //成功与否 都要解锁
}
}
pthread_mutex_unlock(&mutex);
if(i >= 10)
return -1;
return 0;
}
int GetCli()
{
pthread_mutex_lock(&mutex);
int i = 0;
int c = -1;
for(; i < 10;++i)
{
if(clilink[i] != -1)
{
c = clilink[i];
clilink[i] = -1;
break;
}
}
pthread_mutex_unlock(&mutex);
if(i >= 10)
return -1;
return c;
//-1是无效的文件描述符
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6888);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
//创建线程池
int i = 0;
for(; i < 3; ++i)
{
pthread_t id;
res = pthread_create(&id,NULL,pthread_fun,NULL);
}
InitCliLink(); //初始化值
sem_init(&sem,0,0);
pthread_mutex_init(&mutex,NULL);
while(1)
{
int len = sizeof(cli);
int c = accept(sockfd,(struct sockaddr*)&cli,&len);
if(c < 0)
{
continue;
}
if(Insert(c) == -1)
{
close(c);
continue;
}
//对信号量的V操作
sem_post(&sem);
}
}
void *pthread_fun(void *arg)
{
while(1)
{
//阻塞 对信号量的P操作
sem_wait(&sem);
int c = GetCli();
if(c == -1)
{
continue;
}
while(1) //与特定的客户端通讯
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if(n <= 0)
{
close(c);
break;
}
printf("%d: %s\n",c,buff);
send(c,"ok",2,0);
}
}
}
结果如下:
因为我只维护了三个线程,因此当我开启第四个客户端的时候,它会在阻塞的状态,当我关闭前三个任意一个进程时,会发现它立刻可以执行。比如说关闭third客户端。
可以看出four立即就被打印了~~
对于以上代码我们会面临一个问题:后来的客户端有时候会被先服务,我们可以将代码改一下,每次接着遍历下一个,直到完成,再从头开始。保证先来先服务。
int GetCli()
{
pthread_mutex_lock(&mutex);
int i = 0;
int c = clilink[0];
for(; i < 9;++i)
{
clilink[i] = clilink[i + 1];
}
clilink[i] = -1;
pthread_mutex_unlock(&mutex);
if(i >= 10)
return -1;
return c;
//-1是无效的文件描述符
}
三、进程池
- 在程序启动时,创建多个进程,将子进程维护在进程池;
- 进程池中的进程必须阻塞在获取到客户端文件描述符之前;
- 主进程负责接收客户连接,并将获取到的客户连接文件描述符传递给进程池中的进程
必须借助于进程间通讯 不能仅仅传递c值,传递的是文件描述符--》指向相同的结构
如何在两个不相干的进程之间传递文件描述符呢? 我们可以利用socket在进程间传递特殊的辅助数据,以实现文件描述符的传递。另外我们还可以结合socketpair全双工管道,以及函数sendmsg 、recvmsg等相关知识学习下进程池的代码。
有兴趣的小伙伴可以看看《linux高性能服务编程》里的进程的代码实现~
两者的选择和多进程和多线程一样,需要因情况和场景选择。我们要明白一个线程或者线程开始为一个客户端服务时,只能等客户端退出才能服务下一个客户端,对于线程资源是一种浪费。阻塞在recv函数 。但是I/O复用可以解决同时处理与多个客户端交互的问题。