使用多进程或多线程与客户进行交互的时候(子进程/子线程实现并发服务器),每一个客户端链接就会给其分配一个为其服务的进程/线程,有什么弊端?
进程池和线程池可以帮助我们解决如上问题
进程池与线程池相似,我们这里以进程池为例。与内存池相似,进程池就是提前创建一组子进程,这些子进程的数目在3~10个之间(与CPU的处理能力有关,不能创建太多,要不然时间也都浪费在cpu切换上了)。可以参考httpd守护进程就是使用包含7个子进程的进程池实现并发的。线程池中的线程数量和CPU数量差不多就行(多了还是得切换,没必要)。进程池中的所有子进程运行着相同的代码,具有相同的属性(优先级,PGID等),进程池在服务器启动之初就创建好了,每个子进程没有从父进程继承一些没有必要的文件描述符,也不会从父进程复制大块的堆内存。当有客户端与服务器交互,主进程就通过某种方式选择进程池中的一个进程为其服务,相比于选择一个已存在的子进程代价显然比动态创建一个进程小的多。
我们看看httpd守护进程的进程池(要使用root用户开启httpd服务)ps -efl |grep httpd可以查看线程号
root为主进程,其他都是进程池中的
1:主线程需要将文件描述符传递给函数线程 使用全局数组即可进行线程间通讯
2:函数线程启动起来需要阻塞在获取文件描述符之前(没有客户连接就获取不到,没必要轮询做无用功,也即有客户连接函数线程获取这个链接描述符,需要同步控制),信号量控制主线程向函数线程通知获取文件描述符,函数线程即可阻塞。
3:各个函数线程之间从全局数组获取文件描述符需要互斥,另外函数线程获取文件描述符与主线程往全局数组插入文件描述符要互斥,需要加锁进行同步控制。
示例代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
sem_t sem; //该信号量是为了阻塞函数线程获取链接描述符
pthread_mutex_t mutex; //加锁的目的为了插入和获取同时只有一个函数对数组操作
int clink[10]; //定义为一个变量不好,有可能函数线程还没执行操作就被主线程获取新的连接描述符覆盖,成为新的一个客户端链接
void InitClink() //初始化存放链接描述符的数组
{
memset(clink,-1,sizeof(clink));
/* int i=0;
for(;i<10;i++)
clink[i]=-1;*/
}
int Insert(int c) //找到第一个可以插入的位置将c插入
{
pthread_mutex_lock(&mutex); //避免插入与获取同时执行,线程不安全
int i=0;
for(;i<10;i++)
{
if(clink[i]==-1)
{
clink[i]=c;
break;
}
}
pthread_mutex_unlock(&mutex);
if(i>=10) //插入失败
return -1;
return 0;
}
int GetCli() //获取链接描述符
{
pthread_mutex_lock(&mutex); //也避免了多个线程同时获取c
int i=0;
int c=clink[0];
for(;i<9&&clink[i+1]!=-1;i++)
{
clink[i]=clink[i+1]; //按照队列顺序获取链接描述符,将后面的元素前移
}
clink[i]=-1;
pthread_mutex_unlock(&mutex);
return c;
}
void* pthread_fun(void *arg)
{
while(1) //保证函数线程不退出,处理多个客户端
{
//连接描述符数组中没有新链接,函数线程就阻塞在这里等待获取新链接
sem_wait(&sem);
int c=GetCli();
while(1) //与特定的客户端交互
{
char buff[128]={0};
int n=recv(c,buff,127,0);
if(n<=0)
{
printf("recv over");
close(c); //与多线程一样,直接就关闭了链接
break;
}
printf("%d:%s\n",c,buff);
send(c,"ok",2,0);
}
}
}
int main()
{
int listenfd=socket(AF_INET,SOCK_STREAM,0);
assert(listenfd!=-1);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(8000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(listenfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res!=-1);
listen(listenfd,5);
sem_init(&sem,0,0); //确保有链接插入到数组中,才能在函数线程中获取
pthread_mutex_init(&mutex,NULL);
//创建线程池
int i=0;
for(;i<3;i++)
{
pthread_t id;
pthread_create(&id,NULL,pthread_fun,NULL);
}
InitClink();
while(1) //主线程依旧是为了接受客户端连接
{
int len=sizeof(cli);
int c=accept(listenfd,(struct sockaddr*)&cli,&len);
if(c<0)
continue;
if(Insert(c)==-1) //只有三个线程处理,所以即便插不进去也没必要扩容
{
close(c); //直接关闭获取到的链接描述符
continue; //避免v操作
}
sem_post(&sem);//v操作代表插入成功,使得函数线程可以获取到c进行操作
}
}
实际上我们定义的链接描述符的数组大小是大于等于子线程个数即可的,但是也没必要开的很大,因为我只定义了三个线程,最多同时也就处理三个线程,你将链接数组开的很大,其他的客户端还是在等待这,没有什么用。另外,线程也没必要开的特别多,这个是取决于你的CPU的核数,否则切换效率还是不高。
对于线程池实现,我只实现了一种子线程只要有空就可以获取链接进行交互;我们对于主线程如何分配描述符给子线程也是有相关的分配算法,实现分布式的负载均衡的一种分配策略。待实现。。。。
1:子进程获取文件描述符是从管道中获取的,管道中没有描述符的话获取就会阻塞,不用进行同步控制。
2:因为进程池间通讯是很麻烦的,所以也就不存在需要同步控制,防止进程间数据混乱。但是主进程对于链接描述符如何传递给子进程,有人就会说就像线程池那样直接传递那个文件描述符的值不就行了,但线程池是同一个进程的,文件描述符是针对的进程,所以各个线程之间是共享的,所以我们传递值过去是没有什么毛病的,但是进程不一样啊。又有人说哪不是fork之间的文件描述符共享的吗,对呀,但是我们子进程的创建是在服务器之初就创建了,正如我上面说的,这几个子进程都是“白板”,啥都没有,这个时候你就不能只给她传递一个值,因为这个值对于子进程来说就是一个下标,但是它的这个下标的内存中存放的是NULL,而不是像父进程那样指向了一个strut file的结构,所以我们需要通过管道方式在进程间传递文件描述符(网络通讯的管道,不是进程间通讯的管道,网络的管道socketpair是全双工滴。。。)
代码实现:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static const int CONTROL_LEN=CMSG_LEN(sizeof(int));
void send_fd(int fd,int fd_to_send) //将父进程打开的链接描述符发给子进程
{
struct iovec mem;
struct msghdr msg;
char buff[0];
mem.iov_base=buff; //设置内存的起始地址
mem.iov_len=1; //设置内存的长度
msg.msg_name=NULL;
msg.msg_namelen=0;
msg.msg_iov=&mem;
msg.msg_iovlen=1; //内存块个数
struct cmsghdr cm;
cm.cmsg_len=CONTROL_LEN;
cm.cmsg_level=SOL_SOCKET;
cm.cmsg_type=SCM_RIGHTS;
*(int *)CMSG_DATA(&cm)=fd_to_send; //通过辅助数据发送的链接描述符
msg.msg_control=&cm;
msg.msg_controllen=CONTROL_LEN;
sendmsg(fd,&msg,0);
}
int recv_fd(int fd)
{
struct iovec mem;
struct msghdr msg;
char buff[0];
mem.iov_base=buff;
mem.iov_len=1;
msg.msg_name=NULL;
msg.msg_namelen=0;
msg.msg_iov=&mem;
msg.msg_iovlen=1;
struct cmsghdr cm;
msg.msg_control=&cm;
msg.msg_controllen=CONTROL_LEN;
recvmsg(fd,&msg,0);
int fd_to_read=*(int *)CMSG_DATA(&cm);
return fd_to_read;
}
int main()
{
int pipefd[2]; //创建管道描述符
int fd_to_pass=0;
int ret=socketpair(PF_UNIX,SOCK_DGRAM,0,pipefd);
assert(ret!=-1); //创建父子进程间的网络通讯管道
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(7000);
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,status;
for(i=0;i<3;i++)
{
status=fork();
if(status==0)break; //避免子进程生成孙子进程
}
if(0==status) //子进程执行模块
{
while(1)
{
close(pipefd[1]); //关闭写端
fd_to_pass=recv_fd(pipefd[0]);
printf("hello\n");
while(1)
{
char buff[128]={0};
int n=recv(fd_to_pass,buff,127,0);
if(n<=0)
{
printf("recv over\n");
close(fd_to_pass);
break;
}
printf("%d:%s\n",fd_to_pass,buff);
send(fd_to_pass,"ok",2,0);
}
}
}
else //父进程执行模块
{
close(pipefd[0]); //在父进程中将读通道关闭,虽然是全双工,但是父进程写的只能子进程读
while(1)
{
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
if(c<0)
continue;
send_fd(pipefd[1],c);
}
}
}
这里我并没有开辟一个数组用来存储链接描述符,而是直接往管道中写就行了,等到子进程有空闲的,直接从管道中获取就行。
线程切换比较快,并且线程通讯比较方便,但是这也是问题,因为通讯比较方便有可能会发生线程不安全,需要进行同步控制。这倒不是比较进程与线程的区别,主要选择那个需要看我们的程序需要怎样的功能及需求。