(1) fork 浅析
linux 中, 一个进程可以通过fork()系统调用来创建一个与自己相同的子进程, 这个子进程是父进程的克隆, 他继承了父进程的整个地址空间, 包括进程上下文, 堆栈地址, 内存信息, 进程控制块等。值得注意的是, 调用fork一次, 他却返回两次, 一次是在父进程中返回子进程的进程id, 一次是在子进程中返回0, 这看起来有点难理解, 我们先看下面这段程序:
#include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <string.h> int main() { 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系统调用, 实现了一个简单的并发服务器
原文地址:https://www.cnblogs.com/bzaq/p/9908568.html