[ Team LiB ] Previous Section Next Section

30.5 TCP Concurrent Server, One Child per Client

Traditionally, a concurrent TCP server calls fork to spawn a child to handle each client. This allows the server to handle numerous clients at the same time, one client per process. The only limit on the number of clients is the OS limit on the number of child processes for the user ID under which the server is running. Figure 5.12 is an example of a concurrent server and most TCP servers are written in this fashion.

The problem with these concurrent servers is the amount of CPU time it takes to fork a child for each client. Years ago (the late 1980s), when a busy server handled hundreds or perhaps even a few thousand clients per day, this was acceptable. But the explosion of the Web has changed this attitude. Busy Web servers measure the number of TCP connections per day in the millions. This is for an individual host, and the busiest sites run multiple hosts, distributing the load among the hosts. (Section 14.2 of TCPv3 talks about a common way to distribute this load using what is called "DNS round robin.") Later sections will describe various techniques that avoid the per-client fork incurred by a concurrent server, but concurrent servers are still common.

Figure 30.4 shows the main function for our concurrent TCP server.

Figure 30.4 main function for TCP concurrent server.

server/serv01.c

 1 #include     "unp.h"

 2 int
 3 main(int argc, char **argv)
 4 {
 5     int     listenfd, connfd;
 6     pid_t   childpid;
 7     void    sig_chld(int), sig_int(int), web_child(int);
 8     socklen_t clilen, addrlen;
 9     struct sockaddr *cliaddr;

10     if (argc == 2)
11         listenfd = Tcp_listen(NULL, argv[1], &addrlen);
12     else if (argc == 3)
13         listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
14     else
15         err_quit("usage: serv01 [ <host> ] <port#>");
16     cliaddr = Malloc(addrlen);

17     Signal(SIGCHLD, sig_chld);
18     Signal(SIGINT, sig_int);

19     for ( ; ; ) {
20         clilen = addrlen;
21         if  ( (connfd = accept(listenfd, cliaddr, &clilen)) < 0) {
22              if (errno == EINTR)
23                  continue;      /* back to for() */
24              else
25                  err_sys("accept error");
26         }

27         if ( (childpid = Fork()) == 0) { /* child process */
28             Close(listenfd);     /* close listening socket */
29             web_child(connfd);   /* process request */
30             exit(0);
31         }
32         Close(connfd);            /* parent closes connected socket */
33     }
34 }

This function is similar to Figure 5.12: It calls fork for each client connection and handles the SIGCHLD signals from the terminating children. This function, however, we have made protocol-independent by calling our tcp_listen function. We do not show the sig_chld signal handler: It is the same as Figure 5.11, with the printf removed.

We also catch the SIGINT signal, generated when we type our terminal interrupt key. We type this key after the client completes, to print the CPU time required for the program. Figure 30.5 shows the signal handler. This is an example of a signal handler that does not return.

Figure 30.5 Signal handler for SIGINT.

server/serv01.c

35 void
36 sig_int(int signo)
37 {
38     void    pr_cpu_time(void);

39     pr_cpu_time();
40     exit(0);
41 }

Figure 30.6 shows the pr_cpu_time function that is called by the signal handler.

Figure 30.6 pr_cpu_time function: prints total CPU time.

server/pr_cpu_time.c

 1 #include    "unp.h"
 2 #include    <sys/resource.h>

 3 #ifndef HAVE_GETRUSAGE_PROTO
 4 int     getrusage(int, struct rusage *);
 5 #endif

 6 void
 7 pr_cpu_time(void)
 8 {
 9     double user, sys;
10     struct rusage myusage, childusage;

11     if (getrusage(RUSAGE_SELF, &myusage) < 0)
12         err_sys("getrusage error");
13     if (getrusage(RUSAGE_CHILDREN, &childusage) < 0)
14         err_sys("getrusage error");

15     user = (double) myusage.ru_utime.tv_sec +
16         myusage.ru_utime.tv_usec / 1000000.0;
17     user += (double) childusage.ru_utime.tv_sec +
18         childusage.ru_utime.tv_usec / 1000000.0;
19     sys = (double) myusage.ru_stime.tv_sec +
20         myusage.ru_stime.tv_usec / 1000000.0;
21     sys += (double) childusage.ru_stime.tv_sec +
22         childusage.ru_stime.tv_usec / 1000000.0;

23     printf("\nuser time = %g, sys time = %g\n", user, sys);
24 }

The getrusage function is called twice to return the resource utilization of both the calling process (RUSAGE_SELF) and all the terminated children of the calling process (RUSAGE_CHILDREN). The values printed are the total user time (CPU time spent in the user process) and total system time (CPU time spent within the kernel, executing on behalf of the calling process).

The main function in Figure 30.4 calls the function web_child to handle each client request. Figure 30.7 shows this function.

Figure 30.7 web_child function to handle each client's request.

server/web_child.c

 1 #include    "unp.h"

 2 #define MAXN    16384     /* max # bytes client can request */

 3 void
 4 web_child(int sockfd)
 5 {
 6     int     ntowrite;
 7     ssize_t nread;
 8     char    line[MAXLINE], result[MAXN];

 9     for ( ; ; ) {
10         if ( (nread = Readline(sockfd, line, MAXLINE)) == 0)
11             return;             /* connection closed by other end */

12             /* line from client specifies #bytes to write back */
13         ntowrite = atol(line);
14         if ((ntowrite <= 0) || (ntowrite > MAXN))
15             err_quit("client request for %d bytes", ntowrite);

16         Writen(sockfd, result, ntowrite);
17     }
18 }

After the client establishes the connection with the server, the client writes a single line specifying the number of bytes the server must return to the client. This is some-what similar to HTTP: The client sends a small request and the server responds with the desired information (often an HTML file or a GIF image, for example). In the case of HTTP, the server normally closes the connection after sending back the requested data, although newer versions are using persistent connections, holding the TCP connection open for additional client requests. In our web_child function, the server allows additional requests from the client, but we saw in Figure 30.3 that our client sends only one request per connection and the client then closes the connection.

Row 1 of Figure 30.1 shows the timing result for this concurrent server. When compared to the subsequent lines in this figure, we see that the concurrent server requires the most CPU time, which is what we expect with one fork per client.

One server design that we do not measure in this chapter is one invoked by inetd, which we covered in Section 13.5. From a process control perspective, a server invoked by inetd involves a fork and an exec, so the CPU time will be even greater than the times shown in row 1 of Figure 30.1.

    [ Team LiB ] Previous Section Next Section