利用fork实现并发服务器

Posted 不想取名字所以就随便写了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用fork实现并发服务器相关的知识,希望对你有一定的参考价值。

(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系统调用, 实现了一个简单的并发服务器

 

以上是关于利用fork实现并发服务器的主要内容,如果未能解决你的问题,请参考以下文章

Swift新async/await并发中利用Task防止指定代码片段执行的数据竞争(Data Race)问题

Java技术指南「并发编程专题」Fork/Join框架基本使用和原理探究(原理及源码篇)

Java技术指南「并发编程专题」Fork/Join框架基本使用和原理探究(基础篇)

JAVA并行框架Fork/Join:简介和代码示例

Python并发编程—fork的使用

使用fork并发处理多个client的请求和对等通信p2p