您正在查看: 服务器 分类下的文章

chapter 14


Chapter 14

14.1

The test program is shown in Figure C.14

Figure C.14. Determine record-locking behavior

#include "apue.h"
#include <fcntl.h>
#include <errno.h>

void
sigint(int signo)
{
}

int
main(void)
{
pid_t pid1, pid2, pid3;
int fd;

setbuf(stdout, NULL);
signal_intr(SIGINT, sigint);

/*

  • Create a file.
    /
    if ((fd = open("lockfile", O_RDWR|O_CREAT, 0666)) < 0)
    err_sys("can't open/create lockfile");
    /
  • Read-lock the file.
    /
    if ((pid1 = fork()) < 0) {
    err_sys("fork failed");
    } else if (pid1 == 0) { /
    child /
    if (lock_reg(fd, F_SETLK, F_RDLCK, 0, SEEK_SET, 0) < 0)
    err_sys("child 1: can't read-lock file");
    printf("child 1: obtained read lock on file\n");
    pause();
    printf("child 1: exit after pause\n");
    exit(0);
    } else { /
    parent */
    sleep(2);
    }

/*

  • Parent continues ... read-lock the file again.
    /
    if ((pid2 = fork()) < 0) {
    err_sys("fork failed");
    } else if (pid2 == 0) { /
    child /
    if (lock_reg(fd, F_SETLK, F_RDLCK, 0, SEEK_SET, 0) < 0)
    err_sys("child 2: can't read-lock file");
    printf("child 2: obtained read lock on file\n");
    pause();
    printf("child 2: exit after pause\n");
    exit(0);
    } else { /
    parent */
    sleep(2);
    }

/*

  • Parent continues ... block while trying to write-lock
  • the file.
    /
    if ((pid3 = fork()) < 0) {
    err_sys("fork failed");
    } else if (pid3 == 0) { /
    child /
    if (lock_reg(fd, F_SETLK, F_WRLCK, 0, SEEK_SET, 0) < 0)
    printf("child 3: can't set write lock: %s\n",
    strerror(errno));
    printf("child 3 about to block in write-lock...\n");
    if (lock_reg(fd, F_SETLKW, F_WRLCK, 0, SEEK_SET, 0) < 0)
    err_sys("child 3: can't write-lock file");
    printf("child 3 returned and got write lock????\n");
    pause();
    printf("child 3: exit after pause\n");
    exit(0);
    } else { /
    parent /
    sleep(2);
    }
    /
  • See if a pending write lock will block the next
  • read-lock attempt.
    */
    if (lock_reg(fd, F_SETLK, F_RDLCK, 0, SEEK_SET, 0) < 0)
    printf("parent: can't set read lock: %s\n",
    strerror(errno));
    else
    printf("parent: obtained additional read lock while"
    " write lock is pending\n");
    printf("killing child 1...\n");
    kill(pid1, SIGINT);
    printf("killing child 2...\n");
    kill(pid2, SIGINT);
    printf("killing child 3...\n");
    kill(pid3, SIGINT);
    exit(0);
    }

On all four systems described in this book, the behavior is the same: additional readers can starve pending writers. Running the program gives us

child 1: obtained read lock on file
child 2: obtained read lock on file
child 3: can't set write lock: Resource temporarily unavailable
child 3 about to block in write-lock...
parent: obtained additional read lock while write lock is pending
killing child 1...
child 1: exit after pause
killing child 2...
child 2: exit after pause
killing child 3...
child 3: can't write-lock file: Interrupted system call
14.2

Most systems define the fd_set data type to be a structure that contains a single member: an array of long integers. One bit in this array corresponds to each descriptor. The four FD_ macros then manipulate this array of longs, turning specific bits on and off and testing specific bits.

One reason that the data type is defined to be a structure containing an array and not simply an array is to allow variables of type fd_set to be assigned to one another with the C assignment statement.

14.3

Most systems allow us to define the constant FD_SETSIZE before including the header <sys/select.h>. For example, we can write

 #define FD_SETSIZE 2048
 #include <sys/select.h>

to define the fd_set data type to accommodate 2,048 descriptors. This works on FreeBSD 5.2.1, Mac OS X 10.3, and Solaris 9. linux 2.4.22 implements things differently.

14.4

The following table lists the functions that do similar things.

FD_ZERO

sigemptyset

FD_SET

sigaddset

FD_CLR

sigdelset

FD_ISSET

sigismember


There is not an FD_xxx function that corresponds to sigfillset. With signal sets, the pointer to the set is always the first argument, and the signal number is the second argument. With descriptor sets, the descriptor number is the first argument, and the pointer to the set is the next argument.

14.5

Up to five types of information are returned by getmsg: the data itself, the length of the data, the control information, the length of the control information, and the flags.

14.6

Figure C.15 shows an implementation using select.

Figure C.15. Implementation of sleep_us using select

#include "apue.h"
#include <sys/select.h>

void
sleep_us(unsigned int nusecs)
{
struct timeval tval;

tval.tv_sec = nusecs / 1000000;
tval.tv_usec = nusecs % 1000000;
select(0, NULL, NULL, NULL, &tval);
}


Figure C.16 shows an implementation using poll.

Example C.16. Implementation of sleep_us using poll

#include <poll.h>

void
sleep_us(unsigned int nusecs)
{
struct pollfddummy;
int timeout;

if ((timeout = nusecs / 1000) <= 0)
timeout = 1;
poll(&dummy, 0, timeout);
}


As the BSD usleep(3) manual page states, usleep uses the setitimer interval timer and performs eight system calls each time it's called. It correctly interacts with other timers set by the calling process, and it is not interrupted if a signal is caught.

14.7

No. What we would like to do is have TELL_WAIT create a temporary file and use 1 byte for the parent's lock and 1 byte for the child's lock. WAIT_CHILD would have the parent wait to obtain a lock on the child's byte, and TELL_PARENT would have the child release the lock on the child's byte. The problem, however, is that calling fork releases all the locks in the child, so the child can't start off with any locks of its own.

14.8

A solution is shown in Figure C.17.

Figure C.17. Calculation of pipe capacity using nonlocking writes

#include "apue.h"
#include <fcntl.h>

int
main(void)
{
int i, n;
int fd[2];

if (pipe(fd) < 0)
err_sys("pipe error");
set_fl(fd[1], O_NONBLOCK);

/*

  • Write 1 byte at a time until pipe is full.
    */
    for (n = 0; ; n++) {
    if ((i = write(fd[1], "a", 1)) != 1) {
    printf("write ret %d, ", i);
    break;
    }
    }
    printf("pipe capacity = %d\n", n);
    exit(0);
    }

The following table shows the values calculated for our four platforms.

Platform

Pipe Capacity (bytes)

FreeBSD 5.2.1

16,384

linux 2.4.22

4,096

Mac OS X 10.3

8,192

Solaris 9

9,216


These values can differ from the corresponding PIPE_BUF values, because PIPE_BUF is defined to be the maximum amount of data that can be written to a pipe atomically. Here, we calculate the amount of data that a pipe can hold independent of any atomicity constraints.

14.10

Whether the program in Figure 14.32 updates the last-access time for the input file depends on the operating system and the type of file system on which the file resides.

chapter 15


Chapter 15

15.1

If the write end of the pipe is never closed, the reader never sees an end of file. The pager program blocks forever reading from its standard input.

15.2

The parent terminates right after writing the last line to the pipe. The read end of the pipe is automatically closed when the parent terminates. But the parent is probably running ahead of the child by one pipe buffer, since the child (the pager program) is waiting for us to look at a page of output. If we're running a shell, such as the Korn shell, with interactive command-line editing enabled, the shell probably changes the terminal mode when our parent terminates and the shell prints a prompt. This undoubtedly interferes with the pager program, which has also modified the terminal mode. (Most pager programs set the terminal to noncanonical mode when awaiting input to proceed to the next page.)

15.3

The popen function returns a file pointer because the shell is executed. But the shell can't execute the nonexistent command, so it prints

sh: line 1: ./a.out: No such file or directory

on the standard error and terminates with an exit status of 127. pclose returns the termination status of the command as it is returned by waitpid.

15.4

When the parent terminates, look at its termination status with the shell. For the Bourne shell, Bourne-again shell, and Korn shell, the command is echo $?. The number printed is 128 plus the signal number.

15.5

First add the declaration

FILE*fpin, *fpout;

Then use fdopen to associate the pipe descriptors with a standard I/O stream, and set the streams to be line buffered. Do this before the while loop that reads from standard input:

 if ((fpin = fdopen(fd2[0], "r")) == NULL)
  err_sys("fdopen error");
 if ((fpout = fdopen(fd1[1], "w")) == NULL)
  err_sys("fdopen error");
 if (setvbuf(fpin, NULL, _IOLBF, 0) < 0)
  err_sys("setvbuf error");
 if (setvbuf(fpout, NULL, _IOLBF, 0) < 0)
  err_sys("setvbuf error");

The write and read in the while loop are replaced with

  if (fputs(line, fpout) == EOF)
err_sys("fputs error to pipe");
  if (fgets(line, MAXLINE, fpin) == NULL) {
err_msg("child closed pipe");
break;
  }
15.6

The system function calls wait, and the first child to terminate is the child generated by popen. Since that's not the child that system created, it calls wait again and blocks until the sleep is done. Then system returns. When pclose calls wait, an error is returned, since there are no more children to wait for. Then pclose returns an error.

15.7

The select function indicates that the descriptor is readable. When we call read after all the data has been read, it returns 0 to indicate the end of file. But with poll (assuming a STREAMS-based pipe), the POLLHUP event is returned, and this event may be returned while there is still data to be read. Once we have read all the data, however, read returns 0 to indicate the end of file. After all the data has been read, the POLLIN event is not returned, even though we need to issue a read to receive the end-of-file notification (the return of 0).

With an output descriptor that refers to a pipe that has been closed by the reader, select indicates that the descriptor is writable. But when we call write, the SIGPIPE signal is generated. If we either ignore this signal or return from its signal handler, write returns an error of EPIPE. With poll, however, if the pipe is STREAMS based, poll returns with a POLLHUP event for the descriptor.

15.8

Anything written by the child to standard error appears wherever the parent's standard error would appear. To send standard error back to the parent, include the shell redirection 2>&1 in the cmdstring.

15.9

The popen function forks a child, and the child executes the shell. The shell in turn calls fork, and the child of the shell executes the command string. When cmdstring terminates, the shell is waiting for this to happen. The shell then exits, which is what the waitpid in pclose is waiting for.

15.10

The trick is to open the FIFO twice: once for reading and once for writing. We never use the descriptor that is opened for writing, but leaving that descriptor open prevents an end of file from being generated when the number of clients goes from 1 to 0. Opening the FIFO twice requires some care, as a nonblocking open is required. We have to do a nonblocking, read-only open first, followed by a blocking open for write-only. (If we tried a nonblocking open for write-only first, it would return an error.) We then turn off nonblocking for the read descriptor. Figure C.18 shows the code for this.

Figure C.18. Opening a FIFO for reading and writing, without blocking

#include "apue.h"
#include <fcntl.h>

#define FIFO "temp.fifo"

int
main(void)
{
int fdread, fdwrite;

unlink(FIFO);
if (mkfifo(FIFO, FILE_MODE) < 0)
err_sys("mkfifo error");
if ((fdread = open(FIFO, O_RDONLY | O_NONBLOCK)) < 0)
err_sys("open error for reading");
if ((fdwrite = open(FIFO, O_WRONLY)) < 0)
err_sys("open error for writing");
clr_fl(fdread, O_NONBLOCK);
exit(0);
}

15.11

Randomly reading a message from an active queue would interfere with the clientserver protocol, as either a client request or a server's response would be lost. To read the queue, all that is needed is for the process to know the identifier for the queue and for the queue to allow world-read access.

15.13

We never store actual addresses in a shared memory segment, since it's possible for the server and all the clients to attach the segment at different addresses. Instead, when a linked list is built in a shared memory segment, the list pointers should be stored as offsets to other objects in the shared memory segment. These offsets are formed by subtracting the start of the shared memory segment from the actual address of the object.

15.14

Figure C.19 shows the relevant events.

figure C.19. Alternation between parent and child in Figure 15.33

Parent i set to

Child i set to

Shared value set to

update returns

Comment

0

initialized by mmap

1

child runs first, then is blocked 0 parent runs

0

parent runs

1

0

then parent is blocked

2

child resumes

1

3

then child is blocked

2

parent resumes

3

2

then parent is blocked

4

3

5

then child is blocked

4

parent resumes

chapter 16


Chapter 16

16.1

Figure C.20 shows a program that prints the system's byte order.

Figure C.20. Determine byte order on system

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

int
main(void)
{
uint32_t i;
unsigned char *cp;

i = 0x04030201;
cp = (unsigned char )&i;
if (
cp == 1)
printf("little-endian\n");
else if (*cp == 4)
printf("big-endian\n");
else
printf("who knows?\n");
exit(0);
}

16.3

For each endpoint we will be listening on, we need to bind the proper address and record an entry in an fd_set structure corresponding to each file descriptor.

We will use select to wait for connect requests to arrive on multiple endpoints. Recall from Section 16.4 that a passive endpoint will appear to be readable when a connect request arrives on it. When a connect request does arrive, we will accept the request and process it as before.

16.5

In the main procedure, we need to arrange to catch SIGCHLD by calling our signal function (Figure 10.18), which will use sigaction to install the handler specifying the restartable system call option. Next, we need to remove the call to waitpid from our serve function. After forking the child to service the request, the parent closes the new file descriptor and resumes listening for additional connect requests. Finally, we need a signal handler for SIGCHLD, as follows:

  void
  sigchld(int signo)
  {
while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
 ;
  }
16.6

To enable asynchronous socket I/O, we need to establish socket ownership using the F_SETOWN fcntl command, and then enable asynchronous signaling using the FIOASYNC ioctl command. To disable asynchronous socket I/O, we simply need to disable asynchronous signaling. The reason we mix fcntl and ioctl commands is to find the methods that are most portable. The code is shown in Figure C.21.

Figure C.21. Enable and disable asynchronous socket I/O

#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#if defined(BSD) || defined(MACOS) || defined(SOLARIS)
#include <sys/filio.h>
#endif

int
setasync(int sockfd)
{
int n;

if (fcntl(sockfd, F_SETOWN, getpid()) < 0)
return(-1);
n = 1;
if (ioctl(sockfd, FIOASYNC, &n) < 0)
return(-1);
return(0);
}

int
clrasync(int sockfd)
{
int n;

n = 0;
if (ioctl(sockfd, FIOASYNC, &n) < 0)
return(-1);
return(0);
}

chapter 10


Chapter 10

10.1

The program terminates the first time we send it a signal. The reason is that the pause function returns whenever a signal is caught.

10.3

Figure C.10 shows the stack frames.

Figure C.10. Stack frames before and after longjmp

[View full size image]


The longjmp from sig_alrm back to main effectively aborts the call to sig_int.

10.4

We again have a race condition, this time between the first call to alarm and the call to setjmp. If the process is blocked by the kernel between these two function calls, the alarm goes off, the signal handler is called, and longjmp is called. But since setjmp was never called, the buffer env_alrm is not set. The operation of longjmp is undefined if its jump buffer has not been initialized by setjmp.

10.5

See "Implementing Software Timers" by Don Libes (C Users Journal, vol. 8, no. 11, Nov. 1990) for an example.

10.7

If we simply called _exit, the termination status of the process would not show that it was terminated by the SIGABRT signal.

10.8

If the signal was sent by a process owned by some other user, the process has to be set-user-ID to either root or to the owner of the receiving process, or the kill won't work. Therefore, the real user ID provides more information to the receiver of the signal.

10.10

On one system used by the author, the value for the number of seconds increased by 1 about every 6090 minutes. This skew occurs because each call to sleep schedules an event for a time in the future, but is not awakened exactly when that event occurs (because of CPU scheduling). In addition, a finite amount of time is required for our process to start running and call sleep again.

A program such as the cron daemon has to fetch the current time every minute, as well as to set its first sleep period so that it wakes up at the beginning of the next minute. (Convert the current time to the local time and look at the tm_sec value.) Every minute, it sets the next sleep period so that it'll wake up at the next minute. Most of the calls will probably be sleep(60), with an occasional sleep(59) to resynchronize with the next minute. But if at some point the process takes a long time executing commands or if the system gets heavily loaded and scheduling delays hold up the process, the sleep value can be much less than 60.

10.11

Under linux 2.4.22 and Solaris 9, the signal handler for SIGXFSZ is never called. But write returns a count of 24 as soon as the file's size reaches 1,024 bytes.

When the file's size has reached 1,000 bytes under FreeBSD 5.2.1 and Mac OS X 10.3, the signal handler is called on the next attempt to write 100 bytes, and the write call returns 1 with errno set to EFBIG ("File too big").

10.12

The results depend on the implementation of the standard I/O library: how the fwrite function handles an interrupted write.

chapter 11


Chapter 11

11.1

A version of the program that allocates the memory dynamically instead of using an automatic variable is shown in Figure C.11.

Figure C.11. Correct use of thread return value

#include "apue.h"
#include <pthread.h>

struct foo {
int a, b, c, d;
};

void
printfoo(const char *s, const struct foo *fp)
{
printf(s);
printf(" structure at 0x%x\n", (unsigned)fp);
printf(" foo.a = %d\n", fp->a);
printf(" foo.b = %d\n", fp->b);
printf(" foo.c = %d\n", fp->c);
printf(" foo.d = %d\n", fp->d);
}

void * thr_fn1(void *arg)
{
struct foo *fp;

if ((fp = malloc(sizeof(struct foo))) == NULL)
err_sys("can't allocate memory");
fp->a = 1;
fp->b = 2;
fp->c = 3;
fp->d = 4;
printfoo("thread:\n", fp);
return((void *)fp);
}

int
main(void)
{
int err;
pthread_t tid1;
struct foo *fp;

err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
err_exit(err, "can't join with thread 1");
printfoo("parent:\n", fp);
exit(0);
}

11.2

To change the thread ID of a pending job, the readerwriter lock must be held in write mode to prevent anyone from searching the list while the ID is being changed. The problem with the way the interfaces are currently defined is that the ID of a job can change between the time that the job is found with job_find and the job is removed from the list by calling job_remove. This problem can be solved by embedding a reference count and a mutex inside the job structure and having job_find increment the reference count. The code that changes the ID can then avoid any job in the list that has a nonzero reference count.

11.3

First of all, the list is protected by a readerwriter lock, but the condition variable needs a mutex to protect the condition. Second, the condition each thread should wait to be satisfied is that there is a job for it to process, so we need to create a per thread data structure to represent this condition. Alternatively, we can embed the mutex and condition variable in the queue structure, but this means that all worker threads will wait on the same condition. If there are many worker threads, we can run into a thundering herd problem, whereby many threads are awakened without work to do, resulting in a waste of CPU resources and increased lock contention.

11.4

It depends on the circumstances. In general, both can be correct, but each alternative has drawbacks. In the first sequence, the waiting threads will be scheduled to run after we call pthread_cond_broadcast. If the program is running on a multiprocessor, some threads will run and immediately block because we are still holding the mutex (recall that pthread_cond_wait returns with the mutex held). In the second sequence, a running thread can acquire the mutex between steps 3 and 4, invalidate the condition, and release the mutex. Then, when we call pthread_cond_broadcast, the condition will no longer be true, and the threads will run needlessly. This is why the awakened threads must recheck the condition and not assume that it is true merely because pthread_cond_wait returned.