进程池和线程池
并发的给多个客户端提供服务。
一、内核控制信息的理解
进程池最难的地方:当客户端连接服务器时,服务器产生一个new_fd,主进程如何将new_fd发送给子进程。使用下列两种方法传递内核控制信息,是不可以的:
1.通过fork一个子进程,父进程打开一个文件 fd,通过管道把fd给子进程。这种方法是错误的。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当进程打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
2.为什么不先创建fd,然后再fork,这样ford出来的子进程就同主进程的一样了,这样就相当于已经把fd传过来了。这种方法也是不可取的,因为实在应用的时候,一个服务器启动多少个进程或线程都已经定好了。需要让子进程处于待命状态,这样效率才最佳。所以要先创建子进程。
测试用例:通过管道传递描述符,是读不了的。
int main(){
int fds[2]; //创建无名管道,对应文件描述符为3,4
pipe(fds);
if(!fork()){ //子进程
close(fds[1]); //客户端关闭写端,使用读端 。关闭4,保留3
int fd;
read(fds[0],&fd,sizeof(fd)); //读4个字节,放到fd中,此时fd=3。
printf("child fd=%d\n",fd);
char buf[10]={0};
read(fd,buf,sizeof(buf)); //后面对fd操作,其实就是对fds[0]进行操作。本来我想对file进行操作,其实没有达目的。
printf("buf=%s\n",buf);
exit(0);
}else{ //父进程
close(fds[0]); //关闭3,保留4
int fd=open("file",O_RDWR); //打开一个文件,此时fd=3
printf("fd=%d\n",fd);
write(fds[1],&fd,sizeof(fd)); //向管道里面写fd。也就是把数字3写进管道。等价于write(4,&fd,4);
wait(NULL);
return 0;
}
}
二、如何把一个进程描述符的控制信息,传递给另一个进程。
第一步,初始化socketpair类型描述符
int fds[2];
socketpair(AF_LOCAL,SOCK_STREAM,0,fds); //必须要用这个管道。这个管道是可以传递控制信息。这个管道只能用于父子进程之 间。这个管道是全双工的。
第二步:sendmsg接口发送描述符
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); //告诉内核我们要传递描述信息,并不是我们把秒数信息放在这个函数里 传递,内核并没有把描述信息告诉我们。sockfd即sockpair初始化的描述符
fds[1]。Sendmsg关键是初始化msghdr结构体。
struct msghdr {
void *msg_name; //没用,memset为0即可
socklen_t msg_namelen; //没用,memset为0即可
struct iovec *msg_iov; //结构体指针。这个机构体跟一次可以写多个buf有关。虽然不传内容,但是因为有激活的问题,所以 必须要写点东西。
size_t msg_iovlen; //结构体个数。因为前面是struct *类型。
void *msg_control; //关键,即下面的cmsghdr结构体地址。告诉它我们要传递哪一个的信息。
size_t msg_controllen; //cmsghdr结构体的长度。这个长度只能是字节,因为前面是void * 类型,没有类型,所以不可能是个数.
int msg_flags; //没用
};
struct cmsghdr{ //cmsghdr是控制信息结构体。可以通过man cmsg查看。这是一个变长结构体。
socklen_t cmsg_len; //变长结构体的长度。
//因为前三个成员变量的长度是一定的,所以可以通过偏移找到最后一个成员的首地址, 也可以通过接口SMSG_DATA()来得到最后一个成员的首地址。
//变长结构体长度的计算就是前三个成员变量的长度(3*4)加上最后一个成员变量的长度。 在本应用中,我们在最后一个成员变量中传递fd,所以最后一个成员变量的长度为4。所以 cmsg_len等于16,除了自己计算的方法,还可以通过接口:CMSG_LEN()来得到长度。 CMSG_LEN的参数为最后一个成员变量的长度。
int cmsg_level; //填SOL_SOCKET即可。
int cmsg_type; //填SCM_RIGHTS即可。
//followed by unsigned char cmsg_data[]; //最后一个位置自己放什么都行。在本应用中,我们想存放fd.
};
首先定义 struct cmsghdr *cmsg 指针
cmsg_len 中存取cmsghdr结构体的长度,通过CMSG_LEN进行计算,我们传递的fd的大小为整型四个字节,所以
Int len = CMSG_LEN(sizeof(int)); //先得到边长结构体的大小 然后为结构体申请空间:
cmsg = (struct cmsghdr *)calloc(1,len); //calloc函数可以动态分配空间,分配的空间大小为1*len。与malloc的区别是:calloc函数 不光能申请空间,而且会把空间初始化为0。
cmsg->cmsg_len = len;
cmsg->cmsg_level =SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
int *fdptr;
fdptr= (int *) CMSG_DATA(cmsg); //得到最后一个元素的首地址,因为要放int,所以强转一下。解引用就是一个整形数 。
*fdptr = fd;
第三步: Recvmsg接收文件描述符,接收的msghdr结构体初始化和sendmsg几乎完全一致,区别如下: *fd = *fdptr;
例1:writev的使用:一次可以写多个buf
ssize_t writev(int fd,const struct iovec*iov,int iovcnt); //iov是结构体指针。iovcnt是结构体的个数。表示向fd中写几个结构体。
struct iovec{ //通过man writev 可以看到iovec结构体
void * iov_base; //起始地址
size_t iov_len; //长度
}
int main(){
int fd=open("file1",O_RDWR);
char buf1[10]="hello ";
char buf2[10]="world";
struct iovec iov[2]; //定义两个结构体
iov[0].iov_base=buf1; //第一个结构体的起始地址
iov[0].iov_len=6; //第一个结构体的长度
iov[1].iov_base=buf2; //第二个结构体的起始地址
iov[1].iov_len=5; //第二个结构体的长度
int ret=writev(fd,iov,2); //向fd中写2个结构体,起始地址是iov。执行后可以看到文件的内容是:hello world.
if(-1==ret){
perror("writev");
return -1;
}
return 0;
}
例2:在进程间传递内核控制信息(这段代码以后就这么写,不会的时候抄就行了)
void send_fd(int fds,int fd){ struct msghdr msg; memset(&msg,0,sizeof(msg)); struct iovec iov[2]; char buf1[10]="hello"; char buf2[10]="world"; iov[0].iov_base=buf1; iov[0].iov_len=5; iov[1].iov_base=buf2; iov[1].iov_len=5; msg.msg_iov=iov; msg.msg_iovlen=2; struct cmsghdr* cmsg; int len=CMSG_LEN(sizeof(int)); cmsg=(struct cmsghdr*)calloc(1,len); cmsg->cmsg_len=len; cmsg->cmsg_level=SOL_SOCKET; cmsg->cmsg_type=SCM_RIGHTS; *(int*)CMSG_DATA(cmsg)=fd; msg.msg_control=cmsg; msg.msg_controllen=len; int ret=sendmsg(fds,&msg,0); //上面的都是为了这个函数做准备 if(-1==ret){ perror("sendmsg"); return; } } void recv_fd(int fds,int* pfd){ struct msghdr msg; memset(&msg,0,sizeof(msg)); struct iovec iov[2]; char buf1[10]={0}; char buf2[10]={0}; iov[0].iov_base=buf1; iov[0].iov_len=5; iov[1].iov_base=buf2; iov[1].iov_len=5; msg.msg_iov=iov; msg.msg_iovlen=2; struct cmsghdr* cmsg; int len=CMSG_LEN(sizeof(int)); cmsg=(struct cmsghdr*)calloc(1,len); cmsg->cmsg_len=len; cmsg->cmsg_level=SOL_SOCKET; cmsg->cmsg_type=SCM_RIGHTS; msg.msg_control=cmsg; msg.msg_controllen=len; int ret=recvmsg(fds,&msg,0); //上面的都是为了这个函数做准备 if(-1==ret){ perror("recvmsg"); return; } *pfd=*(int*)CMSG_DATA(cmsg); } int main(){ int fds[2]; int ret; ret=socketpair(AF_LOCAL,SOCK_STREAM,0,fds); //第一步。 if(-1==ret){ //成功返回0,失败返回-1 perror("socketpair"); return -1; } if(!fork()){ //子进程 close(fds[1]); //关闭一端 int fd; recv_fd(fds[0],&fd); //通过fds[0],接收放到fd中 printf("child fd=%d\n",fd); char buf[10]={0}; read(fd,buf,sizeof(buf)); printf("buf=%s\n",buf); exit(0); }else{ //父进程 close(fds[0]); //关闭一端 int fd=open("file",O_RDWR); printf("parent fd=%d\n",fd); send_fd(fds[1],fd); //发送fd。调用函数,在函数中可以使用这个描述符。所以值传递即可。 wait(NULL); return 0; } } |
三、进程池的实现:使用多进程,能够实现多个客户端同时下载文件
子进程要告诉父进程我不忙了。
子进程不忙的时候应该睡觉,父进程给子进程发送了描述符,子进程才醒来。
向fds[0]写入1,当父进程知道对应的fds[1]可读,父进程把对应的子进程标示为非忙碌。
recvmsg在没有接收到父进程给的描述符,子进程在睡觉。
程序分析:
父进程流程:
1.sfd=socket();
bind();
listen()监听客户端的请求;
创建10个子进程(),等待我把new_fd发送给它,让子进程通过new_fd与客户端通话。创建的子进程要接收父进程传过来的new_fd,需要 用到socketpair();
epfd=epoll_creat();
epoll_ctl(ADD,sfd);
epoll_ctl(ADD,十个进程的读端);
epoll_wait();
if(sfd){ //有客户端请求
accept();
分配一个空闲的进程
把new_fd传递给这个进程
把这个进程标记为忙碌
}
if(子进程的各个端口){ //说明数据已经发送完了
把这个进程标记为空闲
}
子进程流程:
1.等待父进程把new_fd发送给自己
2.收到new_fd之后开始发送数据
3.发送完数据之后告诉父进程已经发送完了
客户端流程:
1.sfd=socket();
2.connect();
3.接收文件
细节二:如何处理客户端请求的数目和处理的数目
方式一,链接数控制:listen(个数),让业务进程数目和listen的数目相等。方式二,队列模式。本设计中使用方式一。
方式一,链接数控制:listen(个数),让业务进程数目和listen的数目相等。方式二,队列模式。本设计中使用方式一。
细节三:内存管理:ARC(引用计数) GC(自动回收)
引用计数:一个结构体或对象,假如有一个进程使用它,使用他一次引用技术就加1。只有当为0时,才回收。
C、C++使用引用计数,java使用GC。
当把new_fd发送给了子进程之后,new_fd的引用计数为2。
可以当把new_fd发给了子进程之后,就关闭new_fd。这样引用计数就变回了1。这种的劣势是
可以当子进程给客户端发送完数据,再关闭对应的new_fd。
细节四:如果想要定义一个数组,但是数组的个数通过参数传进来的。
创建结构体数组,不知道多少时,用堆内存来做。
struct student *s=(struct student *)calloc(num,sizeof(struct student)); //calloc一块空间,大小为num个结构体的大小。这样就可以用 p[0],p[1]...p[num-1]来访问数组了。
细节五:如何创建多个进程。
a=fork();
if(a==0){
//子进程1
}else if(a!=0){
b=fork();
if(b==0){
//子进程2
}else{
//主进程
}
}
等价于:
if(!fork()){
//子进程1
}
if(!fork()){
//子进程2
}
.. //主进程
细节六:如何传递一个文件,双方协商好什么时候开始发送,什么时候发送完
首先发送文件名,再发送数据。当发送完之后,发送一个只有火车头没有数据的火车。对方收到以后得知数据已经发送完毕。
typedef struct{ //小火车,只有车长才知道后面有多少个字节。这是一个简单的协议。这就是上层协议。
int len; //小火车:接下来我要发多少。
char buf[1000]; //存放数据。
}data,*pdata;
细节七:当发送大文件时,可能会出错。socket的缓冲区只有64K。极有可能在网络中传输的速度不匹配。
通过循环一直写,直到把自己想写的内容完全写进去为止。
通过循环一直读,直到读出自己想要的数目为止。
细节八:如何看两个文件完全相同
查看两个文件是否完全相同:md5查看内容是否一样。闪电算法:做一致性校验。
md5sum file
例:使用多进程,实现多个客户端同时下载文件
//服务器端 /****头文件****/ #define FILENAME "hello.avi" typedef struct{ pid_t pid; int fds; int busy; }child,*pchild; typedef struct{ //传输数据使用的结构体。该结构体约定双方传递数据使用的协议。 int len; //len为后面要发送的数据长度 char buf[1000]; }data,*pdata; void child_handle(int); /****进程间传递描述符*****/ void send_fd(int fds,int fd){ //把fd发送给进程fds struct msghdr msg; memset(&msg,0,sizeof(msg)); struct iovec iov[2]; char buf1[10]="hello"; char buf2[10]="world"; iov[0].iov_base=buf1; iov[0].iov_len=5; iov[1].iov_base=buf2; iov[1].iov_len=5; msg.msg_iov=iov; msg.msg_iovlen=2; struct cmsghdr* cmsg; int len=CMSG_LEN(sizeof(int)); cmsg=(struct cmsghdr*)calloc(1,len); cmsg->cmsg_len=len; cmsg->cmsg_level=SOL_SOCKET; cmsg->cmsg_type=SCM_RIGHTS; *(int*)CMSG_DATA(cmsg)=fd; msg.msg_control=cmsg; msg.msg_controllen=len; int ret=sendmsg(fds,&msg,0); if(-1==ret){ perror("sendmsg"); return; } free(cmsg); //释放calloc出来的内容 } void recv_fd(int fds,int* pfd){ struct msghdr msg; memset(&msg,0,sizeof(msg)); struct iovec iov[2]; char buf1[10]={0}; char buf2[10]={0}; iov[0].iov_base=buf1; iov[0].iov_len=5; iov[1].iov_base=buf2; iov[1].iov_len=5; msg.msg_iov=iov; msg.msg_iovlen=2; struct cmsghdr* cmsg; int len=CMSG_LEN(sizeof(int)); cmsg=(struct cmsghdr*)calloc(1,len); cmsg->cmsg_len=len; cmsg->cmsg_level=SOL_SOCKET; cmsg->cmsg_type=SCM_RIGHTS; msg.msg_control=cmsg; msg.msg_controllen=len; int ret=recvmsg(fds,&msg,0); if(-1==ret){ perror("recvmsg"); return; } *pfd=*(int*)CMSG_DATA(cmsg); free(cmsg); } /****发送文件****/ #include "func.h" void send_n(int new_fd,char* buf,int len){ //向new_fd发送数据,数据首地址为buf,长度为len。可以解决对方读的慢,自己写 的快,导致不能一次全写进去的问题。 int ret; int total=0; //total表示已经发送了多少 while(total<len){ ret=send(new_fd,buf+total,len-total,0); //已经发送了total个,接下来还要发送len-total个,首地址为buf+total total=total+ret; } } void recv_n(int new_fd,char* buf,int len){ //从new_fd接收数据,放到buf中,要接收的长度为len.可以解决对方写的慢,自己读 的快,导致一次不能读len个字节的问题。 int ret; int total=0; //total表示已经接收了多少 while(total<len){ ret=recv(new_fd,buf+total,len-total,0); //已经接收了total个,接下来还要接收len-total个,放到buf+total中 total=total+ret; } } void send_file(int fdw){ //子进程向客户端发送文件。子进程通过fdw与客户端通信。 data d; //定义小火车结构体 memset(&d,0,sizeof(d)); d.len=strlen(FILENAME); //文件名的长度 strcpy(d.buf,FILENAME); //文件名放到buf中 int ret; ret=send(fdw,&d,4+d.len,0); //首先发送文件名 if(-1==ret){ perror("send"); exit(-1); } int fd=open(FILENAME,O_RDONLY); //打开文件,要把文件的内容读出来发送给客户端 if(-1==fd){ perror("open"); exit(-1); } while(memset(&d,0,sizeof(d)),(d.len=read(fd,d.buf,sizeof(d.buf)))>0){ //首先清空小火车,把fd中的内容读到小火车的buf中.只要能读 出内容就循环.用d.len记录读出的数量,以便记录在火车头上,让对方知道. send_n(fdw,&d,4+d.len); //把小火车发送给客户端 } ret=0; send_n(fdw,&ret,sizeof(int)); //当发送的小火车,只有车头时,表示数据已经发送完毕。 close(fdw); } /****创建多个子进程*****/ #include "func.h" void child_handle(int fdr){ int new_fd; int flag=1; while(1){ recv_fd(fdr,&new_fd); //接收父进程发送的连接描述符new_fd,如果没有就睡觉。接收到的fd放到new_fd中 send_file(new_fd); //子进程向客户端发送文件。子进程通过new_fd与客户端通信。 write(fdr,&flag,sizeof(flag)); //子进程向父进程发送通知,完成任务 } } void make_child(pchild p,int n){ //创建n个子进程 int i; int fds[2]; pid_t pid; int ret; for(i=0;i<n;i++){ //循环创建n个子进程,注意写法 ret=socketpair(AF_LOCAL,SOCK_STREAM,0,fds); //创建管道 if(-1==ret){ perror("socketpair"); exit(-1); } pid=fork(); //创建子进程 if(pid==0){ close(fds[1]); //子进程关闭fds[1]端,使用fds[0]端 child_handle(fds[0]); //子进程的业务流程函数 } close(fds[0]); //父进程关闭fds[0]端,使用fds[1]端 p[i].fds=fds[1]; //父进程使用的管道fds[1]与子进程进行通信 p[i].pid=pid; //在p中记录下子进程ID p[i].busy=0; //在p中初始化子进程的忙碌状态,0代表子进程非忙碌 } } /*****main.c****/ #include "func.h" int main(int argc,char* argv[]){ //传入参数:IP地址、端口、要创建的进程数目 if(argc!=4){ printf("error args\n"); return -1; } int num=atoi(argv[3]); //要创建的进程数目 pchild p=(pchild)calloc(num,sizeof(child)); //calloc出一块num*sizeof(child)大小的内存空间,存放child结构体。并把首地址给p. make_child(p,num); //创建n个子进程 int sfd=socket(AF_INET,SOCK_STREAM,0); //生成套接字描述符 if(-1==sfd){ perror("socket"); return -1; } struct sockaddr_in ser; //socket信息数据结构 memset(&ser,0,sizeof(ser)); ser.sin_family=AF_INET; //IPV4 ser.sin_port=htons(atoi(argv[2])); //端口 ser.sin_addr.s_addr=inet_addr(argv[1]); //地址 int ret; ret=bind(sfd,(struct sockaddr*)&ser,sizeof(struct sockaddr)); //给sfd绑定IP地址和端口号 if(-1==ret){ perror("bind"); return -1; } int epfd=epoll_create(1); //创建epoll句柄 struct epoll_event event; struct epoll_event* evs=(struct epoll_event*)calloc(num+1,sizeof(event)); //本来是要写evs[num+1],但是数目是传进来的,不是确定 的数值,所以动态分配。 event.events=EPOLLIN; event.data.fd=sfd; ret=epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&event); //注册sfd。想要监听有没有客户请求。 if(-1==ret){ perror("epoll_ctl"); return -1; } int i; for(i=0;i<num;i++){ event.data.fd=p[i].fds; ret=epoll_ctl(epfd,EPOLL_CTL_ADD,p[i].fds,&event); //注册p[0].fds,p[1].fds,p[2].fds等,想要监听进程传送完之后的通知。 if(-1==ret){ perror("epoll_ctl"); return -1; } } ret=listen(sfd,num); //监听。要使得子进程数目与监听的数目保持一致 if(-1==ret){ perror("listen"); return -1; } int new_fd; int j; int flag; while(1){ memset(evs,0,(num+1)*sizeof(event)); //把evs清空 int ret=epoll_wait(epfd,evs,num+1,-1); //监听epfd,结果放到evs中 if(ret >0){ for(i=0;i<ret;i++){ if(evs[i].data.fd == sfd){ //如果有客户端请求。 new_fd=accept(sfd,NULL,NULL); //接收请求,产生new_fd for(j=0;j<num;j++){ //找到一个不忙的进程 if(p[j].busy ==0) break; } send_fd(p[j].fds,new_fd); //把new_fd发送给进程p[j].fds p[j].busy=1; //把该进程的状态该为忙碌 printf("give child is ok\n"); close(new_fd); //发过去之后,子进程就能使用new_fd了,这里就可以关闭,使引用计数为1 } for(j=0;j<num;j++){ if(evs[i].data.fd == p[j].fds){ //如果有子进程请求,表示子进程已经干完活了。 read(p[j].fds,&flag,sizeof(flag)); //把子进程发过来的数据读出来,虽然不使用这个数据,但是要读出来 p[j].busy=0; //把该进程状态改为不忙碌 printf("child is not busy\n"); } } } } } } |
//客户端 #include "func.h" void send_n(int new_fd,char* buf,int len){ int ret; int total=0; while(total<len){ ret=send(new_fd,buf+total,len-total,0); total=total+ret; } } void recv_n(int new_fd,char* buf,int len){ int ret; int total=0; while(total<len){ ret=recv(new_fd,buf+total,len-total,0); total=total+ret; } } int main(int argc,char** argv){ //传入参数:IP地址、端口 if(argc !=3){ printf("error args\n"); return -1; } int sfd=socket(AF_INET,SOCK_STREAM,0); if(-1==sfd){ perror("socket"); return -1; } struct sockaddr_in ser; memset(&ser,0,sizeof(ser)); ser.sin_family=AF_INET; ser.sin_port=htons(atoi(argv[2])); ser.sin_addr.s_addr=inet_addr(argv[1]); int ret; ret=connect(sfd,(struct sockaddr*)&ser,sizeof(struct sockaddr)); if(-1==ret){ perror("connect"); return -1; } data d; //定义小火车结构体 memset(&d,0,sizeof(d)); recv_n(sfd,&d.len,sizeof(int)); //读取要接收文件名字的长度 recv_n(sfd,d.buf,d.len); //读取文件名 int fd; fd=open(d.buf,O_RDWR|O_CREAT,0666); //在本地创建文件 if(-1==fd){ perror("open"); return -1; }sdd while(1){ memset(&d,0,sizeof(d)); recv_n(sfd,&d.len,sizeof(int)); //接收小火车头 if(d.len >0){ //如果后面有数据 recv_n(sfd,d.buf,d.len); //读出buf的长度。一定要保证读出len的长度,而不能读少了。 write(fd,d.buf,d.len); //读完之后写到文件中。 }else{ //如果只有火车头,后面没有数据,说明文件已经下载完了 break; } } close(sfd); close(fd); return 0; } |