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