本文展示四種進程模型伺服器的演進路徑,並完整實作前兩種:每連接一進程 與 預先 Fork 靜態進程池,附帶驚群效應解決方案(檔案鎖)與訊號處理。
accept 後 fork 子進程處理,處理完退出。fork 固定數量子進程,共享 listen socket,競爭 accept。fork/exit 開銷大,無法應對高併發。void process_mode(int sock_fd, int connect_fd) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); exit(errno); }
if (pid == 0) { // 子進程
close(sock_fd); // 關閉監聽 socket
// 處理連接...
close(connect_fd);
exit(0);
}
// 父進程
close(connect_fd); // 關閉連接 socket
}
注意:fork 後父子進程必須分別關閉不需要的 socket 描述符,否則會導致檔案描述符洩漏或連接無法正確關閉。
多個子進程同時阻塞在 accept,當新連接到來時,所有進程被喚醒但只有一個成功,其餘白白消耗 CPU。解決方案:檔案鎖序列化 accept。
struct flock lock_it = {F_WRLCK, SEEK_SET, 0, 0};
struct flock unlock_it = {F_UNLCK, SEEK_SET, 0, 0};
int lock_fd = -1;
void my_lock_init(char *pathname) {
char lock_file[1024];
strncpy(lock_file, pathname, sizeof(lock_file));
lock_fd = mkstemp(lock_file);
unlink(lock_file); // 建立匿名臨時檔案
}
void my_lock_wait() {
while (fcntl(lock_fd, F_SETLKW, &lock_it) < 0) {
if (errno == EINTR) continue;
perror("fcntl"); exit(errno);
}
}
void my_lock_release() {
if (fcntl(lock_fd, F_SETLKW, &unlock_it) < 0) {
perror("fcntl"); exit(errno);
}
}
void child_loop(int sock_fd) {
int connect_fd;
struct sockaddr_in client;
socklen_t client_len;
while (1) {
my_lock_wait();
client_len = sizeof(client);
connect_fd = accept(sock_fd, (struct sockaddr*)&client, &client_len);
printf("process %d deal with connector\n", getpid());
process(connect_fd); // 業務邏輯
close(connect_fd);
my_lock_release();
}
}
void preprocess(int sock_fd, int n) {
pool.nchild = n;
pool.pids = malloc(n * sizeof(pid_t));
my_lock_init("/tmp/lock.XXXXXX");
for (int i = 0; i < n; i++)
pool.pids[i] = make_child(sock_fd); // fork 並進入 child_loop
signal(SIGINT, sig_call_back); // 捕捉 Ctrl+C 清理子進程
for (;;) pause(); // 父進程等待訊號
}
// 連線指定次數發送 "Hello World"
for (i = 0; i < atoi(argv[3]); i++)
connect_server(argv[1], atoi(argv[2]));
SIGINT 並 kill 所有子進程,避免孤兒進程。