50. 進程池


一般進程池和線程池都是併發編程中常見的, 如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]高效處理的方法.

  • 進程池實現的過程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章