目标:
多进程服务器的性能提升。
前面介绍过一种server模式为每个客户端连接都创建一个子进程,这种方式对server的压力较大,首先创建进程会消耗时间,其次,进程没有办法得到重复利用也会浪费了进程的创建,最后,进程间切换会带来性能上的影响。
本程序使用prefork思想,预先为server派生多个子进程,方便在需要时可以马上使用,而不需要等待进程创建,同时,进程还可以重复使用。
思路:
程序预先fork多个子进程,用来处理每个客户端socket的信息交互或者请求(决定预先fork多少个子进程是个问题!)一开始每个子进程都阻塞在和父进程的通信通道上,等待父进程的消息通知(处理某个客户请求),然后才执行与客户端socket的交互。
所有accept操作由“父进程”完成接收到客户端连接socket后,将该socket的文件描述符传送给子进程(怎么传送是一个问题!这里使用域socket来作为父子进程之间的通信介质—Unix域可以做到让同一台主机上的任何一个进程向其他进程传递打开的文件描述符,进程间用sendmsg在域套接口上发送一个特殊的消息,然后由内核做特殊处理,最后完成将打开的描述字从发送方传递到接收方,同时Unix域套接口也可以作为同一台主机上IPC通信的另一种方式,而且性能速度通常是TCP套接口的两倍【两个好处:速度快,可传递套接字】)。
同时,父进程还会使用select来监听所有孩子的通信介质,当子进程完成了客户端socket的信息交互请求后,就会通过该通信介质通知父进程,父进程在select捕获到消息后,则将发出该消息的子进程标识为空闲状态。
实现:
(1)父进程为了获知或者控制子进程,所以需要定义一个结构来维护子进程的信息
typedef struct
{
pid_t child_pid; //进程ID
int child_pipefd; //父子间的stream pipe(连接父子进程的字节流管道描述字)
int child_status; //状态
long child_count; //已处理的客户数
} Child;
(2)prefork
创建一定数量的子进程,同时每个子进程需要开启一条通道以便于和父进程做消息传递。
//initial "server 0.5" element
nchildren=10; //假设接收10个
navail=nchildren;
cptr=(Child*)calloc(nchildren,sizeof(Child));
for(i=0;i<nchildren;i++)
{
child_make(i,listenfd,addrlen);
//注意 这时候父进程还没有所谓的进程描述表项存在,没有所谓的文件打开描述结构
FD_SET(cptr[i].child_pipefd,&allset);
maxfd=std::max(maxfd,cptr[i].child_pipefd);
}
//initial "server 0.5" element
//child_make实现
pid_t child_make(int i, int listenfd, int addrlen)
{
int sockfd[2];
pid_t pid;
void child_main(int,int,int);
socketpair(AF_LOCAL,SOCK_STREAM,0,sockfd);
if ( (pid=fork())>0)
{
close(sockfd[1]);
cptr[i].child_pid=pid;
cptr[i].child_pipefd=sockfd[0];
cptr[i].child_status=0;
return pid;
}
dup2(sockfd[1],STDERR_FILENO);
close(sockfd[0]);
close(sockfd[1]);
close(listenfd);
child_main(i,listenfd,addrlen);
}
注意到,子进程和父进程之间的通信通道是Unix域socket,这也是一种IPC的方式,而且它很适合进程在同一台机器上的情况,注意上述代码中对每个接口都很小心的做了处理(关闭等)。完成创建后,子进程执行child_main阻塞在Unix域socket的信息recv上。(父进程这时候只需要发送新接收的客户端socket fd即可以让子进程马上进行相应的处理)。
(3)父进程监听和消息通知
所有子进程创建后,父进程还需要监听和各个子进程的通信fd,因为每个子进程处理完父进程分配给他们的任务后,要告诉父进程,这样父进程就可以标识子进程为可用,从而做到复用子进程。
同时父进程当然也会监听listen socket的fd,一旦有新连接来,它就会选取一个空闲子进程,然后通过unix域套接字将新连接socket fd作为消息发送给子进程。
这里的Unix域通信实现参考自网络
1 size_t recv_fd(int fd, void*data, size_t bytes, int*recvfd)
2 {
3 struct msghdr msghdr_recv; /*接收消息接收*/
4 struct iovec iov[1]; /*接收数据的向量*/
5 size_t n;
6 int newfd;
7 union{
8 struct cmsghdr cm;
9 char control[CMSG_SPACE(sizeof(int))];
10 }control_un;
11 struct cmsghdr*pcmsghdr; /*消息头部*/
12 msghdr_recv.msg_control = control_un.control; /*控制消息*/
13 msghdr_recv.msg_controllen = sizeof(control_un.control); /*控制消息的长度*/
14 msghdr_recv.msg_name = NULL; /*消息的名称为空*/
15 msghdr_recv.msg_namelen = 0; /*消息的长度为空*/
16
17 iov[0].iov_base = data; /*向量的数据为传入的数据*/
18 iov[0].iov_len = bytes; /*向量的长度为传入数据的长度*/
19 msghdr_recv.msg_iov = iov; /*消息向量指针*/
20 msghdr_recv.msg_iovlen = 1; /*消息向量的个数为1个*/
21 if((n = recvmsg(fd, &msghdr_recv, 0))<=0) /*接收消息*/
22 return n;
23 if((pcmsghdr = CMSG_FIRSTHDR(&msghdr_recv))!= NULL && /*获得消息的头部*/
24 pcmsghdr->cmsg_len == CMSG_LEN(sizeof(int))) { /*获得消息的长度为int*/
25 if(pcmsghdr->cmsg_level != SOL_SOCKET) /*消息的level应该为SOL_SOCKET*/ printf("control level != SOL_SOCKET\n");
26 if(pcmsghdr->cmsg_type != SCM_RIGHTS) /*消息的类型判断*/
27 printf("control type != SCM_RIGHTS\n");
28 *recvfd =*((int*)CMSG_DATA(pcmsghdr)); /*获得打开文件的描述符*/
29 } else
30 *recvfd = -1;
31 return n; /*返回接收消息的长度*/
32 }
1 ssize_t send_fd(int fd, void*data, size_t bytes, int sendfd)
2 {
3 struct msghdr msghdr_send; /*发送消息*/
4 struct iovec iov[1]; /*向量*/
5 size_t n; /*大小*/
6 int newfd; /*文件描述符*/ /*方便操作msg的结构*/
7 union{
8 struct cmsghdr cm; /*control msg结构*/
9 char control[CMSG_SPACE(sizeof(int))]; /*字符指针,方便控制*/
10 }control_un;
11 struct cmsghdr*pcmsghdr=NULL; /*控制头部的指针*/
12 msghdr_send.msg_control = control_un.control; /*控制消息*/
13 msghdr_send.msg_controllen = sizeof(control_un.control); /*长度*/
14 pcmsghdr = CMSG_FIRSTHDR(&msghdr_send); /*取得第一个消息头*/
15 pcmsghdr->cmsg_len = CMSG_LEN(sizeof(int)); /*获得长度*/
16 pcmsghdr->cmsg_level = SOL_SOCKET; /*用于控制消息*/
17 pcmsghdr->cmsg_type = SCM_RIGHTS;
18 *((int*)CMSG_DATA(pcmsghdr))= sendfd; /*socket值*/
19 msghdr_send.msg_name = NULL; /*名称*/
20 msghdr_send.msg_namelen = 0; /*名称长度*/
21 iov[0].iov_base = data; /*向量指针*/
22 iov[0].iov_len = bytes; /*数据长度*/
23 msghdr_send.msg_iov = iov; /*填充消息*/
24 msghdr_send.msg_iovlen = 1;
25 return (sendmsg(fd, &msghdr_send, 0)); /*发送消息*/
26 }
代码:
#include<sys/types.h> #include<sys/socket.h> #include<strings.h> #include<arpa/inet.h> #include<unistd.h> #include<stdlib.h> #include<stdio.h> #include<string.h> #include<errno.h> #include<signal.h> #include<sys/wait.h> #include<sys/time.h> #include<pthread.h> #include<algorithm> #include<sys/stat.h> #define LISTEN_PORT 84 /////////////////// size_t recv_fd(int fd, void*data, size_t bytes, int*recvfd) { struct msghdr msghdr_recv; /*接收消息接收*/ struct iovec iov[1]; /*接收数据的向量*/ size_t n; int newfd; union{ struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; }control_un; struct cmsghdr*pcmsghdr; /*消息头部*/ msghdr_recv.msg_control = control_un.control; /*控制消息*/ msghdr_recv.msg_controllen = sizeof(control_un.control); /*控制消息的长度*/ msghdr_recv.msg_name = NULL; /*消息的名称为空*/ msghdr_recv.msg_namelen = 0; /*消息的长度为空*/ iov[0].iov_base = data; /*向量的数据为传入的数据*/ iov[0].iov_len = bytes; /*向量的长度为传入数据的长度*/ msghdr_recv.msg_iov = iov; /*消息向量指针*/ msghdr_recv.msg_iovlen = 1; /*消息向量的个数为1个*/ if((n = recvmsg(fd, &msghdr_recv, 0))<=0) /*接收消息*/ return n; if((pcmsghdr = CMSG_FIRSTHDR(&msghdr_recv))!= NULL && /*获得消息的头部*/ pcmsghdr->cmsg_len == CMSG_LEN(sizeof(int))) { /*获得消息的长度为int*/ if(pcmsghdr->cmsg_level != SOL_SOCKET) /*消息的level应该为SOL_SOCKET*/ printf("control level != SOL_SOCKET\n"); if(pcmsghdr->cmsg_type != SCM_RIGHTS) /*消息的类型判断*/ printf("control type != SCM_RIGHTS\n"); *recvfd =*((int*)CMSG_DATA(pcmsghdr)); /*获得打开文件的描述符*/ } else *recvfd = -1; return n; /*返回接收消息的长度*/ } ssize_t send_fd(int fd, void*data, size_t bytes, int sendfd) { struct msghdr msghdr_send; /*发送消息*/ struct iovec iov[1]; /*向量*/ size_t n; /*大小*/ int newfd; /*文件描述符*/ /*方便操作msg的结构*/ union{ struct cmsghdr cm; /*control msg结构*/ char control[CMSG_SPACE(sizeof(int))]; /*字符指针,方便控制*/ }control_un; struct cmsghdr*pcmsghdr=NULL; /*控制头部的指针*/ msghdr_send.msg_control = control_un.control; /*控制消息*/ msghdr_send.msg_controllen = sizeof(control_un.control); /*长度*/ pcmsghdr = CMSG_FIRSTHDR(&msghdr_send); /*取得第一个消息头*/ pcmsghdr->cmsg_len = CMSG_LEN(sizeof(int)); /*获得长度*/ pcmsghdr->cmsg_level = SOL_SOCKET; /*用于控制消息*/ pcmsghdr->cmsg_type = SCM_RIGHTS; *((int*)CMSG_DATA(pcmsghdr))= sendfd; /*socket值*/ msghdr_send.msg_name = NULL; /*名称*/ msghdr_send.msg_namelen = 0; /*名称长度*/ iov[0].iov_base = data; /*向量指针*/ iov[0].iov_len = bytes; /*数据长度*/ msghdr_send.msg_iov = iov; /*填充消息*/ msghdr_send.msg_iovlen = 1; return (sendmsg(fd, &msghdr_send, 0)); /*发送消息*/ } /////////////////// //服务器处理 void str_echo(int sockfd) // 服务器收到客户端的消息后的响应 { ssize_t n; char line[512]; printf("ready to read\n"); while( (n=read(sockfd,line,512))>0 ) //阻塞IO版本 { line[n]='\0'; printf("Client : %s\n",line); char msgBack[512]; snprintf(msgBack,sizeof(msgBack),"recv: %s\n",line); write(sockfd,msgBack,strlen(msgBack)); bzero(&line,sizeof(line)); } printf("end read\n"); } //子进程结束的信号处理 float timeuse; void sig_child(int signo) //父进程对子进程结束的信号处理 { pid_t pid; int stat; struct timeval tpstart,tpend; //float timeuse; gettimeofday(&tpstart,NULL); while( (pid=waitpid(-1,&stat,WNOHANG))>0) printf("child %d terminated\n",pid); gettimeofday(&tpend,NULL); timeuse+=1000000*(tpend.tv_sec-tpstart.tv_sec)+tpend.tv_usec-tpstart.tv_usec; //timeuse/=1000000; printf("Use Time:%f\n",timeuse/1000000); return; } /* prefork + select */ typedef struct { pid_t child_pid; int child_pipefd; int child_status; long child_count; }Child; Child *cptr; //我们必须为每个进程维护一个信息结构来管理各子进程 //子进程ID,父子字节流管道描述字,子进程状态,子进程已处理客户数 pid_t child_make(int i, int listenfd, int addrlen) { int sockfd[2]; pid_t pid; void child_main(int,int,int); socketpair(AF_LOCAL,SOCK_STREAM,0,sockfd); if ( (pid=fork())>0) { close(sockfd[1]); cptr[i].child_pid=pid; cptr[i].child_pipefd=sockfd[0]; cptr[i].child_status=0; return pid; } dup2(sockfd[1],STDERR_FILENO); close(sockfd[0]); close(sockfd[1]); close(listenfd); child_main(i,listenfd,addrlen); } //在使用fork创建子进程之前,先创建一个字节流管道(Unix域),随后利用管道通信 void child_main(int i, int listenfd ,int addrlen) { char c; int connfd; ssize_t n; printf("child %ld starting\n",(long)getpid()); for(;;) { if ((n=recv_fd(STDERR_FILENO,&c,1,&connfd))==0) //FILE* newFile=new FILE(); //传递整个FILE结构也可以 //if((n=read(STDERR_FILENO,newFile,sizeof(FILE)))==0) //不能直接接收新连接的fd,因为这时候子进程fork出来时并没有所谓的进程fd表 //如果直接访问某个fd那会失败!!所以需要Unix域通信 { printf("read_fd returned 0\n");return ;} //else printf("connfd is %d\n",connfd); else printf("read success\n"); //connfd=fileno(newFile); if(connfd<0) { printf("no descriptor form read_fd\n");return ;} else printf("fd is %d\n",connfd); str_echo(connfd); close(connfd); write(STDERR_FILENO,"\n",1); } //子进程初始化后,阻塞等待父进程接收到连接的socket后发过来 //发过来后,就开始在这个socket上等待,处理 //处理完成后,再写到管道,这时候父进程select的是各个子进程的管道 //一旦select到了,就表示子进程处理好了,就可以设置子进程为空闲 //下一个connect又可以给这个子进程来处理了。 //父子进程间,主要是通过read,write来写消息来完成控制! } //处理完客户请求后,通过字节流管道向父进程发回单个字节,通知父亲标识它为可用。 //如果意外终止,字节流管道所在端会关闭,read返回0,父进程捕获。 /* prefork + select */ static int nchildren; int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t chilen; struct sockaddr_in chiaddr,servaddr; //for select int i,maxi,maxfd,sockfd; int nready,client[FD_SETSIZE]; ssize_t n; fd_set rset,allset; //for select listenfd=socket(AF_INET,SOCK_STREAM,0); if(listenfd==-1) { printf("socket established error: %s\n",(char*)strerror(errno)); } bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_addr.s_addr=htonl(INADDR_ANY); servaddr.sin_port=htons(LISTEN_PORT); int bindc=bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); if(bindc==-1) { printf("bind error: %s\n",strerror(errno)); } listen(listenfd,SOMAXCONN); //limit是SOMAXCONN //for server 0.5 int navail,nset,rc; socklen_t addrlen; //cliaddr //for server 0.5 //initial "select" elements maxfd=listenfd; //新增listenfd,所以更新当前的最大fd maxi=-1; for(i=0;i<FD_SETSIZE;i++) client[i] = -1; FD_ZERO(&allset); FD_SET(listenfd,&allset); //end initial //initial "server 0.5" element nchildren=10; //假设接收10个 navail=nchildren; cptr=(Child*)calloc(nchildren,sizeof(Child)); for(i=0;i<nchildren;i++) { child_make(i,listenfd,addrlen); //注意 这时候父进程还没有所谓的进程描述表项存在,没有所谓的文件打开描述结构 FD_SET(cptr[i].child_pipefd,&allset); maxfd=std::max(maxfd,cptr[i].child_pipefd); } //initial "server 0.5" element int cliconn=0; signal(SIGCHLD,sig_child); for(;;) { //for select rset=allset; if( navail<=0) FD_CLR( listenfd,&rset); //如果可用进程数为0,则将监听套接口从select描述字集中删除 //没有可用子进程前不accept它们。 nready=select(maxfd+1,&rset,NULL,NULL,NULL); //一开始select监听的是监听口 //如果有timeout设置,那么每次select之前都要再重新设置一下timeout的值 //因为select会修改timeout的值。 if(FD_ISSET(listenfd,&rset)) { chilen=sizeof(chiaddr); connfd=accept(listenfd,(struct sockaddr*)&chiaddr,&chilen); //阻塞在accept,直到三次握手成功了才返回 if(connfd==-1) printf("accept client error: %s\n",strerror(errno)); else printf("client connected:%d\n",++cliconn); // server 0.5 for(i=0;i<nchildren;i++) if(cptr[i].child_status==0) //找出第一个可用子进程 break; if(i==nchildren) { printf("no available children"); return 0; } cptr[i].child_status=1; cptr[i].child_count++; navail--; n=send_fd( cptr[i].child_pipefd,(void*)"",1,connfd); //n=write(cptr[i].child_pipefd,connFile,sizeof(FILE)); //printf("%d--%d\n",n,sizeof(FILE)); //n=write(cptr[i].child_pipefd,&connfd,sizeof(connfd)); //这时候不是说写个fd过去给孩子就可以,孩子根本就没有从父进程继承到这个记录 //因为是之前fork出来的子进程 printf("n is %d,connfd is %d , child id is %d\n",n, connfd,i); //把已连接描述字通过父子Unix域管道传递给子进程 //close(connfd); if(--nset==0) continue; // server 0.5 //for select } // server 0.5 char rcc; for ( i=0;i<nchildren;i++) { if(FD_ISSET(cptr[i].child_pipefd,&rset)) { if ( ( n=read(cptr[i].child_pipefd,&rcc,1))==0) { printf("child %d terminated unexpectedly",i);return 0;} else printf("recycle client process: %d\n",i); //write过来的没有结束符,所以在write加个\n来表示行结束 cptr[i].child_status=0; navail++; if(--nset==0) break; } } } }