利用fork實現並發服務器
(1) fork 淺析
linux 中, 一個進程可以通過fork()系統調用來創建一個與自己相同的子進程, 這個子進程是父進程的克隆, 他繼承了父進程的整個地址空間, 包括進程上下文, 堆棧地址, 內存信息, 進程控制塊等。值得註意的是, 調用fork一次, 他卻返回兩次, 一次是在父進程中返回子進程的進程id, 一次是在子進程中返回0, 這看起來有點難理解, 我們先看下面這段程序:
#include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <string.h> intmain() { pid_t pid = fork(); if(pid == -1) { perror("fork error"); return -1; } if(pid > 0) { printf("parent: child pid is %d\n", pid); } else if(pid == 0) { printf("child: parent pid is %d \t, self pid id %d \n", getppid(), getpid()); } printf("after fork\n"); return 0; }
運行結果如下:
這個結果很容易理解, 當父進程調用fork後, 系統創建了一個與父進程同樣的子進程, 他們擁有一樣的上下文, 在父進程中, fork()返回了子進程的id, 在子進程中返回了0, 然後他們分別往下運行, 父進程走入了if(pid > 0) 程序段中, 而子進程走入了else if(pid == 0) 程序段中, 然後他們又分別繼續往下運行, 都打印了after fork\n。這裏需要註意的是, fork()出來的子進程並不是從頭開始運行, 因為他跟父進程有一樣的上下文, 這也是為什麽他會返回兩次(父子進程中各返回一次), 同時父子進程的運行順序是不確定的, 多核機器上可能交替執行也可能同時運行, 所以打印順序沒有太大意義。
(2) 利用fork實現服務器的並發
簡單了解了一下fork, 我們知道了調用fork以後, 會創建一個與父進程一樣的子進程, 他們擁有一樣的資源, 於是我們就可以利用這個特性來實現一個簡單的並發服務器。
首先來看一下下面這段代碼:
1 #include <sys/socket.h> 2 #include <sys/types.h> 3 #include <arpa/inet.h> 4 #include <netinet/in.h> 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <unistd.h> 9 10 #define SERV_PORT 4333 11 #define MAXLINE 1024 12 13 /*讀入客戶端的輸入, 然後帶上處理進程id加以返回*/ 14 void dosomething(int sockfd) 15 { 16 pid_t pid = getpid(); 17 int n; 18 char buff[MAXLINE], sendbuff[MAXLINE]; 19 while(true) 20 { 21 n = read(sockfd, buff, MAXLINE); 22 if(n > 0) 23 { 24 snprintf(sendbuff, MAXLINE, "pid: %d\t %s\n", getpid(), buff); 25 write(sockfd, sendbuff, strlen(sendbuff)); 26 } 27 else if(n < 0) 28 { 29 perror("read error"); 30 exit(-1); 31 } 32 else 33 break; 34 } 35 } 36 37 int main() 38 { 39 int servfd, connfd; 40 sockaddr_in servaddr; 41 pid_t pid; 42 43 if((servfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 44 { 45 perror("create socket error"); 46 exit(-1); 47 } 48 49 // 初始化監聽地址 50 bzero(&servaddr, sizeof(servaddr)); 51 servaddr.sin_family = AF_INET; 52 servaddr.sin_port = htons(SERV_PORT); 53 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 54 55 // 綁定監聽地址 56 if(bind(servfd, (sockaddr*)&servaddr, sizeof(servaddr)) < 0) 57 { 58 perror("bind error"); 59 exit(-1); 60 } 61 62 if(listen(servfd, 5) < 0) 63 { 64 perror("listen error"); 65 exit(-1); 66 } 67 68 for(;;) 69 { 70 connfd = accept(servfd, (sockaddr*)nullptr, nullptr); 71 if((pid = fork()) == 0) 72 { 73 close(servfd); //減少一次引用 74 dosomething(connfd); 75 close(connfd); //在子進程中真正關閉套接字 76 exit(0); 77 } 78 close(connfd); //減少一次引用 79 } 80 81 82 return 0; 83 }
這是一個並發服務器的簡單例子,它的功能是把客戶端發來的字符串帶上處理的進程id然後發回給客戶端, 在代碼71行, 我們調用fork創建了一個子進程, 這樣對客戶端的響應就交給了子進程。第73行我們關閉了一次servfd, 他並沒有真正關閉套接字, 而僅僅減少了一次套接字的引用, 只有套接字的引用減為零才會執行四次揮手來真正關閉連接。創建子進程後, servfd的引用加了1, 如果不在這裏對其關閉一次, 那麽當子進程退出後, servfd的引用將無法減至0, 這將導致套接字servfd永遠無法真正關閉。第78行和75行意義與此相同。
接下來給出客戶端的代碼:
#include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <netinet/in.h> #define MAXLINE 1024 #define SERV_PORT 4333 void dosomething(FILE* fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while(fgets(sendline, MAXLINE, fp) != NULL) { write(sockfd, sendline, strlen(sendline)); bzero(recvline, MAXLINE); if(read(sockfd, recvline, MAXLINE) <= 0) { perror("read error"); exit(-1); } fputs(recvline, stdout); } } int main(int argc, char** argv) { int connfd; sockaddr_in servaddr; if((connfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("create socket error"); exit(-1); } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &servaddr.sin_addr.s_addr); if((connect(connfd, (sockaddr*)&servaddr, sizeof(servaddr))) < 0) { perror("connect error"); exit(-1); } dosomething(stdin, connfd); return 0; }
編譯後, 我們首先運行服務端程序, 之後我們運行兩次客戶端程序來看效果
至此, 我們利用fork系統調用, 實現了一個簡單的並發服務器
利用fork實現並發服務器