50. 进程池

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Function_Dou/article/details/90064885

一般进程池和线程池都是并发编程中常见的, 如nginx采用进程池, 也有实现协程降低上下文切换的代价等等, 使用和实现这些方法都是为了提高我们服务端的并发能力.

进程池和线程池都是避免服务端频繁的创建进程(线程), 毕竟创建进程(线程)的代价很大. 所以可先在程序运行时便分配出一定的进程(线程)数, 如果有事件就绪便可以直接调用分配好的进程, 让进程池中的进程区处理事件, 事件处理结束后进程不会被释放而是继续回到池等待事件, 这样就可以让池循环的利用.

本节先介绍进程池的编写, 下一节分析线程池.


进程池

因为进程池的代码很多, 粘贴看也很不方便, 所以我还是只粘贴出部分重要的代码进行分析.

源码文件位置 : /network_code/seriver_client/Fork/processpoll


1. 所要用到的结构体

enum {
	MAX_PROCESSPOOL = 10,	/* 线程池个数 */ 
	MAX_USER_PROCESS_NUM = 65535,	/* 子进程处理事件的个数 */ 
	MAX_EPOLL_PROCESS = 10000,	/* epoll 一次性能够处理的事件数 */ 
};

struct process{
	pid_t pid;	/* 当前进程ID */
	int pipe[2];	/* 管道, 用来统一事件源 */ 
};

struct processpool{
	int pool_id;	/* 进程 ID */ 
	int epoll_fd;	/* epoll 文件描述符 */ 
	int listen_fd;	/* 监听文件描述符 */ 
	int stop;	/* 进程状态 */ 
	struct process sub_process[MAX_PROCESSPOOL];	
};

2. 初始化进程池

因为使用 半同步/半异步 中图二的模式, 所以父进程需要与每个子进程之间建立管道, 以便之后有连接到来后通知指定子进程调用 accept 保持连接. 父进程关闭管道的读端, 子进程关闭管道的写端.

需要注意 : 用于本地内部进程通讯的套接字需要将协议族设置为 XX_UNIX 或者 XX_LOCAL[1] .

// 初始化 processpool 结构体
void init(struct processpool * init){
	init->stop = 0;
	init->pool_id = -1;

	for(int i = 0; i < MAX_PROCESSPOOL; ++i){
		// 因为是主机进程间的通信, 所有协议族应该使用 XX_UNIX 
		socketpair(PF_UNIX, SOCK_STREAM, 0, init->sub_process[i].pipe);
		pid_t pid = init->sub_process[i].pid = fork();
		if(pid > 0){
			close(init->sub_process[i].pipe[1]);	// 父进程关闭读端
			printf("id = %d, pid = %d\n", i, pid);
			continue;
		}
		else if(pid == 0){
			close(init->sub_process[i].pipe[0]);	// 子进程关闭写端
			init->pool_id = i;	// 子进程保存所在进程数组中的 id 
			break;
		}
	}
}

3. 执行与初始化

// 执行监听和处理, 其实就是执行父进程
void run(struct processpool * pool){
	init(pool);
	if(pool->pool_id != -1){
		run_client(pool);
		return;
	}
	run_paren(pool);
}

4. 父进程负责监听TCP连接并通知子进程

有TCP连接就绪, 父进程就通过向管道写入数据通知指定的子进程.

void run_paren(struct processpool * pool){
    ....
    
    add_event(epollfd, pool->listen_fd);	// 注册监听事件
	while(!pool->stop){
		n = epoll_wait(epollfd, evs, MAX_EPOLL_PROCESS, -1);
        
		for(int i = 0; i < n; ++i){
			int fd = evs[i].data.fd;
			// 有连接就绪
			if(fd == pool->listen_fd && (evs[i].events & EPOLLIN)){
				// 从进程中寻找一个进程
				int id = next_id;
				do{
					if(pool->sub_process[id].pid != -1)
						break;
					id = (id + 1) % MAX_PROCESSPOOL;
				}while(id != next_id);
				if(pool->sub_process[id].pid == -1){
					pool->stop = 1;
					break;
				}

				write(pool->sub_process[id].pipe[0], (char *)&informClient, 
							sizeof(informClient));
				next_id = (id + 1) % MAX_PROCESSPOOL;
				printf("send request to child %d\n", id);
			}
            ....
		}
	}
	close(epollfd);
}

5. 子进程负责进程管道和其他就绪描述符

子进程监听管道, 如果管道就绪, 则有就绪的TCP连接. 所以子进程调用 accept 获取连接并将其注册到epoll的监听事件中, 如果事件就绪就调用 processing 函数进行处理.

void run_client(struct processpool * pool){
	...
        
    // 保存子进程与父进程的管道描述符, 以便后面直接使用并直接注册管道监听
	int pipefd = pool->sub_process[pool->pool_id].pipe[1];
	add_event(epollfd, pipefd);

	while(!pool->stop){
		n = epoll_wait(epollfd, evs, sizeof(evs), -1);
		for(int i = 0; i < n; ++i){
			int fd = evs[i].data.fd;
			// 如果是父进程通过管道发的消息, 则表示有连接就绪
			if(fd == pipefd && (evs[i].events & EPOLLIN)){
				int retinform;
				int ret;
				ret = read(pipefd, (char *)&retinform, sizeof(retinform));
				if(ret < 0) break;
				clientfd = accept(pool->listen_fd, NULL, NULL);
				if(clientfd  < 0){
					fprintf(stderr, "accept error\n");
					break;
				}
				add_event(epollfd, clientfd);
				printf("accept success, clientfd = %d\n", clientfd);
				// 将连接TCP描述符保存, 以便之后可以直接使用
				fdsinit(&userfds[clientfd], epollfd, clientfd);
			}
			// 是就绪文件描述符, 就直接调用处理函数即可
			else if(evs[i].events & EPOLLIN){
				if(processing(&userfds[fd]) != 0){
					del_event(epollfd, fd);
				}
			}
            ...
		}
	}
	close(epollfd);
	close(pipefd);
}

在 main 函数目录下执行 make. 可通过 lsof -i:端口 查看进程的监听和连接状态.

在这里插入图片描述
在这里插入图片描述


小结

在代码中会看到将信号也注册到 epoll 监听事件中, 这种做法其实是统一事件源, 这中统一事件源是 libevent[2]高效处理的方法.

  • 进程池实现的过程

猜你喜欢

转载自blog.csdn.net/Function_Dou/article/details/90064885