Site Administration Page 3 - Design and Architecture |
In the UNIX environment, the traditional way to handle multiple clients is to use the fork() system call. When an application callsfork(), an exact duplicate of the calling program is made, right down to the program counter (PC), and a new child process is started with that copy. Everything (except the parent’s process ID, or PID) is copied. This includes the parent’s heap, stack, data space, and all open descriptors. Then, the system call returns twice: once in the calling program and the next time in the child process. The return value in the calling program is the PID of the new child process, while in the child process it is 0. This can be a little confusing at first. How can a function that is called once return twice, you ask? If you think carefully about what thefork()call does, though, it is very logical. Callingfork()makes an exact copy of the program. This means that when the copy begins execution, it starts at the exact place the calling program was, which is thefork()call. Let’s see, in a little more detail, the consequences of copying descriptors. As mentioned previously, when the child process is created, everything is copied to the child, including all open descriptors. The Linux kernel keeps a reference count for each descriptor. So, when a child is created, the reference count is incremented for each copy. As a result, the client must close the descriptor of the listening socket used by the parent process, and the parent must close the descriptor of the client socket used by the child process. This will become evident on executing the programserver2.c. Ifclose()is not called on these sockets, the reference count in the kernel will be wrong, resulting in open or stale connections potentially abusing or exhausting system resources as time goes on. Using thefork()system call to handle multiple clients has several advantages. First, it’s simple. Creating a new process to handle each client is easy to implement. Second, using a process per client keeps any one client from monopolizing the server, because the Linux kernel will preemptively swap the processes in and out. Third, other child processes won’t be affected if one of the child processes crashes, because the kernel prevents one process from damaging memory in another process. Thefork()system call isn’t without its disadvantages, however. The most notable problem with the multiprocess approach is the lack of shared memory. Over the years, shared memory solutions (likeshmget()) have been made available for multiprocess applications, but it isn’t as elegant as with a threaded approach.shmget()is a system call that allows the allocation of a shared memory segment that can be accessed by multiple processes. The way it works is that the parent process creates a shared memory segment upon startup. Then, as each child is created, it inherits the attachment to the shared memory. Even with the shared memory, access to it must be synchronized with semaphores. Finally, with large programs, significant resources can be used because everything must be copied for each child, resulting in slow performance and potential exhaustion of resources.
The simplest architecture for a multiprocess server is to use one process per client. The server simply waits for a client to connect and then creates a process to handle it. From an application design standpoint, this is much less cumbersome than the multiplexing approach we examined earlier. Each client has a dedicated process, and the client logic flows linearly without worrying about stopping to service other connected clients, as compared to multiplexing, where a single process must deal with all clients simultaneously. A Forking Server In the following program (server2.c), the initial process waits for a client to connect. It then callsfork()to create a new child process to handle the client. Next, the child process reads the data from the client and echoes it back. Finally, the connection is closed, and the child exits. Meanwhile, the parent process loops back to listen for another connection. Figure 5-3 shows the basic architecture for a multiprocess server.
The initial section of the code is similar to the earlier program,server1.c: /* server2.c */ To trap the child exits and prevent zombies, we also need the following two header files: #include <sys/wait.h> Here’s our signal handler. It simply callswaitpid()for any exited children. The reason we call it in a loop is that there may not be a one-to-one correlation between exited children and calls to our signal handler. POSIX does not allow for the queuing of signal calls, so our handler may be called once when several children have exited and we need to callwaitpid()for each one. void sigchld_handler(int signo)
int main(int argc, char *argv[]) Then we create the socket that will accept the incoming connections. listensock = socket(AF_INET, SOCK_STREAM, Here we set ourSO_REUSEADDRoption. val = 1; We then bind it to a local port and all addresses associated with the machine. sAddr.sin_family = AF_INET; Afterward, we put the socket into listening mode to listen for incoming connections. result = listen(listensock, 5); Before we start looping, we install our signal handler. signal(SIGCHLD, sigchld_handler); We then callaccept()and allow it to block waiting for connection requests from clients. After accept returns, we callfork()to create a new process. If it returns 0, then we are in the child process; otherwise, the PID of the new child is returned. while (1) { Once in the child process, we close the listening socket. Remember that all descriptors are copied from the parent process to the child. The child process does not need the listening socket any longer, so we close the child’s reference on that socket. However, the socket will remain open in the parent process. Next, we read characters from the client and echo them to the screen. Finally, we send the characters back to the client, close the socket, and exit the child process: printf("child process %i created.\n", This line is only reached in the parent process. Since the child process has a copy of the client socket, the parent process closes its reference here to decrease the kernel reference count. The socket will remain open in the child process: close(newsock); The server can be compiled with a command similar to the example client. Figure 5-4 shows sample output obtained on executing the preceding program. The client was run with five child processes.
blog comments powered by Disqus |
|
|
|
|
|
|
|