1 进程池和线程池的设计思路
服务端框架
-
基于线程/进程的 apache
-
事件驱动模型(适合业务业务逻辑简单性能高的) nginx
-
事件驱动模型适用于多核机器:进程池
- 有多种设计方法·
- 我们采用 :
- 我们的设计: 当有多个连接,池子满时,进程池直接close,线程池排队
如何写一个大项目
1. 写文件目录
-
写好文件目录:编译,连接关系,写makefile
-
client : makfiel
SRCS:=$(wildcard *.c)
EXES:=$(patsubst %.c,%,$(SRCS))
CC:=gcc
all:$(EXES)
%:%.c
$(CC) $^ -o $@ -g -lpthread
clean:
$(RM) $(EXES)
rebuild:clean all
- server: makfie
SRCS:=$(wildcard *.c)
OBJS:=$(patsubst %.c,%.o,$(SRCS))
CC:=gcc
server:$(OBJS)
$(CC) $^ -o $@ -lpthread
%.o:%.c
$(CC) -c $^ -o $@ -g
clean:
$(RM) $(OBJS) server
rebuild:clean all
2. 设计数据结构
- .h文件
3. 设计相关的函数
1.1 设计的背景
1.2 进程池的整体结构
- 进程的个数
- 根据cpu的核心数,在1倍-2倍之间实测
2 进程池的实现
我们以一个文件下载的应用为例子来介绍进程池结构:客户端可以向服务端建立连接,随后将服务端中存储的文件通过网络传输发送到客户端,其中一个服务端可以同时处理多个客户端连接的,彼此之间互不干扰。
2.1 父子进程创建
2.2 父进程处理网络连接
2.3 本地套接字
2.4 父子进程共享文件描述符
- 父子进程之间只是单纯的传递文件描述符是没用的
#include <54func.h>
int main()
{
int fds[2];
pipe(fds);
if(fork())
{
close(fds[0]);
int fdfile = open("file1",O_RDWR);
printf("parent fdfile = %d\n",fdfile);
write(fdfile,"hello",5);
write(fds[1],&fdfile,sizeof(int));
wait(NULL);
}
else
{
close(fds[1]);
int fdfile;
read(fds[0],&fdfile,sizeof(int));
printf("Son fdile = %d\n",fdfile);
ssize_t sret = write(fdfile,"world",5);
printf("son sret = %ld\n",sret);
}
return 0;
}
- 发送一些内核的高级的数据结构,read和write不能满足要求,需要使用sendmsg和recvmsg。这两个是高级版本的read和write,除了可以发送数据,也可以发送内核的高级数据结构,比如文件对象
- 之前的send和recv都是要发送连续的数据
#include <54func.h>
int sendfd(int sockfd,int fdtosend)
{
struct msghdr hdr;
bzero(&hdr,sizeof(hdr));
// 1. 正文部分:无论正文部分是否有用,必须要有正文
char buf[] = "hello"; // 此外的数据
struct iovec vec[1];// 数组记录离散区域
vec[0].iov_base = buf; // 第一个离散碎片的首地址
vec[0].iov_len = 5;
hdr.msg_iov = vec; // 将离散区域的信息放入hdr
hdr.msg_iovlen = 1;
// 2. 控制部分的字段
struct cmsghdr * pcmsg = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int))); // data:fd(int)
pcmsg->cmsg_len = CMSG_LEN(sizeof(int));
pcmsg->cmsg_level = SOL_SOCKET;
pcmsg->cmsg_type = SCM_RIGHTS;
// 通过pcmsg得到data首地址,强转成int*再解引用;
*(int*)CMSG_DATA(pcmsg) = fdtosend;
hdr.msg_control = pcmsg; // 将控制字段的信息放入hdr中
hdr.msg_controllen = CMSG_LEN(sizeof(int));
int ret = sendmsg(sockfd,&hdr,0);
ERROR_CHECK(ret,-1,"sendmsg");
}
int recvfd(int sockfd,int * pfdtorecv)
{
// 接受和发送的区别,接收方不知道buf的内容和pcmsg->data的内容
struct msghdr hdr;
bzero(&hdr,sizeof(hdr));
// 1. 正文部分:无论正文部分是否有用,必须要有正文
char buf[6] = {
0}; // 此外的数据
struct iovec vec[1];// 数组记录离散区域
vec[0].iov_base = buf; // 第一个离散碎片的首地址
vec[0].iov_len = 5;
hdr.msg_iov = vec; // 将离散区域的信息放入hdr
hdr.msg_iovlen = 1;
// 2. 控制部分的字段
struct cmsghdr * pcmsg = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int))); // data:fd(int)
pcmsg->cmsg_len = CMSG_LEN(sizeof(int));
pcmsg->cmsg_level = SOL_SOCKET;
pcmsg->cmsg_type = SCM_RIGHTS;
// 通过pcmsg得到data首地址,强转成int*再解引用;
hdr.msg_control = pcmsg; // 将控制字段的信息放入hdr中
hdr.msg_controllen = CMSG_LEN(sizeof(int));
int ret = recvmsg(sockfd,&hdr,0);
ERROR_CHECK(ret,-1,"sendmsg");
printf("buf = %s,fd = %d\n",buf,*(int*)CMSG_DATA(pcmsg));
*pfdtorecv = *(int*)CMSG_DATA(pcmsg);
}
int main()
{
int fds[2];
//pipe(fds);
socketpair(AF_LOCAL,SOCK_STREAM,0,fds);
if(fork())
{
close(fds[0]);
int fdfile = open("file1",O_RDWR);
printf("parent fdfile = %d\n",fdfile);
write(fdfile,"hello",5);
sendfd(fds[1],fdfile);
wait(NULL);
}
else
{
// 子进程
close(fds[1]);
int fdfile;
recvfd(fds[0],&fdfile);
printf("Son fdile = %d\n",fdfile);
ssize_t sret = write(fdfile,"world",5);
printf("son sret = %ld\n",sret);
}
return 0;
}
- head.h
#ifndef __HEAD_H__
#define __HEAD_H__
#include<54func.h>
enum{
FREE,
BUSY
};
typedef struct workerdata_s
{
pid_t pid;
int status;
int pipesockfd;
}workerdata_t;
// 创造子进程 f:
int makeWorker(int workerNum,workerdata_t * workerArr);
int tcpInit(const char *ip,const char *port,int *psocket);
int epollAdd(int epfd,int fd);
int epollDell(int epfd,int fd);
int workLoop(int sockfd);
int sendfd(int sockfd,int fdtosend);
int recvfd(int sockfd,int * pfdtorecv);
#endif
- sendfd.c
#include "head.h"
int sendfd(int sockfd,int fdtosend)
{
struct msghdr hdr;
bzero(&hdr,sizeof(hdr));
// 1. 正文部分:无论正文部分是否有用,必须要有正文
char buf[] = "hello"; // 此外的数据
struct iovec vec[1];// 数组记录离散区域
vec[0].iov_base = buf; // 第一个离散碎片的首地址
vec[0].iov_len = 5;
hdr.msg_iov = vec; // 将离散区域的信息放入hdr
hdr.msg_iovlen = 1;
// 2. 控制部分的字段
struct cmsghdr * pcmsg = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int))); // data:fd(int)
pcmsg->cmsg_len = CMSG_LEN(sizeof(int));
pcmsg->cmsg_level = SOL_SOCKET;
pcmsg->cmsg_type = SCM_RIGHTS;
// 通过pcmsg得到data首地址,强转成int*再解引用;
*(int*)CMSG_DATA(pcmsg) = fdtosend;
hdr.msg_control = pcmsg; // 将控制字段的信息放入hdr中
hdr.msg_controllen = CMSG_LEN(sizeof(int));
int ret = sendmsg(sockfd,&hdr,0);
ERROR_CHECK(ret,-1,"sendmsg");
}
int recvfd(int sockfd,int * pfdtorecv)
{
// 接受和发送的区别,接收方不知道buf的内容和pcmsg->data的内容
struct msghdr hdr;
bzero(&hdr,sizeof(hdr));
// 1. 正文部分:无论正文部分是否有用,必须要有正文
char buf[6] = {
0}; // 此外的数据
struct iovec vec[1];// 数组记录离散区域
vec[0].iov_base = buf; // 第一个离散碎片的首地址
vec[0].iov_len = 5;
hdr.msg_iov = vec; // 将离散区域的信息放入hdr
hdr.msg_iovlen = 1;
// 2. 控制部分的字段
struct cmsghdr * pcmsg = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int))); // data:fd(int)
pcmsg->cmsg_len = CMSG_LEN(sizeof(int));
pcmsg->cmsg_level = SOL_SOCKET;
pcmsg->cmsg_type = SCM_RIGHTS;
// 通过pcmsg得到data首地址,强转成int*再解引用;
hdr.msg_control = pcmsg; // 将控制字段的信息放入hdr中
hdr.msg_controllen = CMSG_LEN(sizeof(int));
int ret = recvmsg(sockfd,&hdr,0);
ERROR_CHECK(ret,-1,"sendmsg");
printf("buf = %s,fd = %d\n",buf,*(int*)CMSG_DATA(pcmsg));
*pfdtorecv = *(int*)CMSG_DATA(pcmsg);
}
- tcpInit.c
#include"head.h"
int tcpInit(const char *ip,const char *port,int *psocket)
{
// socket setsockopt bind listen
*psocket = socket(AF_INET,SOCK_STREAM,0);
int reuse = 1;
int ret = setsockopt(*psocket,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
ERROR_CHECK(ret,-1,"setsocketopt");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(port));
addr.sin_addr.s_addr = inet_addr(ip);
ret = bind(*psocket,(struct sockaddr*)&addr,sizeof(addr));
ERROR_CHECK(ret,-1,"bind");
listen(*psocket,50);
}
- worker.c
#include "head.h"
int makeWorker(int workerNum,workerdata_t * workerArr)
{
for(int i = 0;i<workerNum;i++)
{
int pipefd[2];
socketpair(AF_LOCAL,SOCK_STREAM,0,pipefd);
pid_t pid = fork();
if(pid==0)
{
close(pipefd[1]);
workLoop(pipefd[0]);
}
close(pipefd[0]);
workerArr[i].status = FREE;
workerArr[i].pid = pid;
workerArr[i].pipesockfd = pipefd[1];
printf("makeWorker i = %d,pid=%d pipefd=%d\n",i,pid,pipefd[1]);
}
return 0;
}
int workLoop(int sockfd)
{
while(1)
{
int netfd;
recvfd(sockfd,&netfd); // 接受任务
printf("%d:开始干活\n",getpid());
sleep(5);
printf("%d:结束干活\n",getpid());
// 任务完成之后,向主人进程发送自己的pid 通过已经建立好的sockpair(全双工通信)
pid_t pid = getpid();
send(sockfd,&pid,sizeof(pid),0); // 即可读又可写 ,此时主进程可能阻塞再epoll_wait中
// 为了让子进程一发送,主进程立马醒来,可以将管道描述符放入监听集合中
}
}
- epoll.c
#include "head.h"
int epollAdd(int epfd,int fd)
{
struct epoll_event events;
events.events = EPOLLIN;
events.data.fd = fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&events);
return 0;
}
int epollDell(int epfd,int fd)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
return 0;
}
- main.c
#include "head.h"
int main(int argc,char **argv)
{
// ./server 127.0.0.1 1234 3
ARGS_CHECK(argc,4);
int workerNum = atoi(argv[3]);
workerdata_t *workerArr = (workerdata_t*)calloc(workerNum,sizeof(workerdata_t));
// 创建子进程,获取每个子进程的状态
makeWorker(workerNum,workerArr);
int sockfd;
// 初始化tcp服务器
tcpInit(argv[1],argv[2],&sockfd);
// 初始化epoll
int epfd = epoll_create(1);
epollAdd(epfd,sockfd);
// 为了让子进程一发送,主进程立马醒来,可以将管道描述符放入监听集合中
for(int i = 0;i<workerNum;i++)
{
epollAdd(epfd,workerArr[i].pipesockfd);
}
while(1)
{
struct epoll_event readySet[1024];
int readyNum = epoll_wait(epfd,readySet,1024,-1);
for(int i = 0;i<readyNum;i++)
{
if(readySet[i].data.fd == sockfd)
{
int netfd = accept(sockfd,NULL,NULL);
// 顺序查找一个空闲的进程
for(int j = 0;j<workerNum;j++)
{
if(workerArr[j].status == FREE)
{
sendfd(workerArr[j].pipesockfd,netfd); // 这里会导致 workLoop中的recvfd由阻塞变成就绪状态
workerArr[j].status = BUSY;
printf("新的客户端连接上了 %d\n",netfd);
break;
}
}
// 这个close很重要,有空闲的子进程时,就关闭父进程中的netfd,但是由于已经给子进程复制了一个,所以这个描述符不会关闭
// 当发现没有空闲的子进程时,关闭父进程的netfd,会断开客户端的连接,直接拒绝访问,不做等待队列
close(netfd);
}
else // 某个子进程完成任务了
{
int pipesockfd = readySet[i].data.fd;
// 使用顺序查找从fd找到是几号进程
for(int j = 0