[ Team LiB ] |
30.7 TCP Preforked Server, File Locking Around acceptThe implementation that we just described for 4.4BSD, which allows multiple processes to call accept on the same listening descriptor, works only with Berkeley-derived kernels that implement accept within the kernel. System V kernels, which implement accept as a library function, may not allow this. Indeed, if we run the server from the previous section on such a system, soon after the clients start connecting to the server, a call to accept in one of the children returns EPROTO, which means a protocol error.
The solution is for the application to place a lock of some form around the call to accept, so that only one process at a time is blocked in the call to accept. The remaining children will be blocked trying to obtain the lock. There are various ways to provide this locking around the call to accept, as we described in the second volume of this series. In this section, we will use POSIX file locking with the fcntl function. The only change to the main function (Figure 30.9) is adding a call to our my_lock_init function before the loop that creates the children. + my_lock_init("/tmp/lock.XXXXXX"); /* one lock file for all children */ for (i = 0; i < nchildren; i++) pids[i] = child_make(i, listenfd, addrlen); /* parent returns */ The child_make function remains the same as Figure 30.11. The only change to our child_main function (Figure 30.12) is to obtain a lock before calling accept and release the lock after accept returns. for ( ; ; ) { clilen = addrlen; + my_lock_wait(); connfd = Accept(listenfd, cliaddr, &clilen); + my_lock_release(); web_child(connfd); /* process request */ Close(connfd); Figure 30.16 shows our my_lock_init function, which uses POSIX file locking. Figure 30.16 my_lock_init function using POSIX file locking.server/lock_fcntl.c 1 #include "unp.h" 2 static struct flock lock_it, unlock_it; 3 static int lock_fd = -1; 4 /* fcntl() will fail if my_lock_init() not called */ 5 void 6 my_lock_init(char *pathname) 7 { 8 char lock_file[1024]; 9 /* must copy caller's string, in case it's a constant */ 10 strncpy(lock_file, pathname, sizeof(lock_file)); 11 lock_fd = Mkstemp(lock_file); 12 Unlink(lock_file); /* but lock_fd remains open */ 13 lock_it.l_type = F_WRLCK; 14 lock_it.l_whence = SEEK_SET; 15 lock_it.l_start = 0; 16 lock_it.l_len = 0; 17 unlock_it.l_type = F_UNLCK; 18 unlock_it.l_whence = SEEK_SET; 19 unlock_it.l_start = 0; 20 unlock_it.l_len = 0; 21 } 9–12 The caller specifies a pathname template as the argument to my_lock_init, and the mktemp function creates a unique pathname based on this template. A file is then created with this pathname and immediately unlinked. By removing the pathname from the directory, if the program crashes, the file completely disappears. But as long as one or more processes have the file open (i.e., the file's reference count is greater than 0), the file itself is not removed. (This is the fundamental difference between removing a pathname from a directory and closing an open file.) 13–20 Two flock structures are initialized: one to lock the file and one to unlock the file. The range of the file that is locked starts at byte offset 0 (a l_whence of SEEK_SET with l_start set to 0). Since l_len is set to 0, this specifies that the entire file is locked. We never write anything to the file (its length is always 0), but that is fine. The advisory lock is still handled correctly by the kernel.
Figure 30.17 shows the two functions that lock and unlock the file. These are just calls to fcntl, using the structures that were initialized in Figure 30.16. This new version of our preforked server now works on SVR4 systems by assuring that only one child process at a time is blocked in the call to accept. Comparing rows 2 and 3 in Figure 30.1 shows that this type of locking adds to the server's process control CPU time.
Effect of Too Many ChildrenWe can check this version to see if the same thundering herd problem exists, which we described in the previous section. We check by increasing the number of (unneeded) children and noticing that the timing results get worse proportionally. Figure 30.17 my_lock_wait and my_lock_release functions using fcntl.server/lock_fcntl.c 22 void 23 my_lock_wait() 24 { 25 int rc; 26 while ( (rc = fcntl(lock_fd, F_SETLKW, &lock_it)) < 0) { 27 if (errno == EINTR) 28 continue; 29 else 30 err_sys("fcntl error for my_lock_wait"); 31 } 32 } 33 void 34 my_lock_release() 35 { 36 if (fcntl(lock_fd, F_SETLKW, &unlock_it) < 0) 37 err_sys("fcntl error for my_lock_release"); 38 } Distribution of Connections to the ChildrenWe can examine the distribution of the clients to the pool of available children by using the function we described with Figure 30.14. Figure 30.2 shows the result. The OS distributes the file locks uniformly to the waiting processes (and this behavior was uniform across several operating systems we tested). |
[ Team LiB ] |