CIS 307: Unix III

Select

At times a process needs to wait for messages from more than one source, say two sources. If the process blocks on one source, it is in no position to know if there is a message from the other source. If it checks each source without blocking, it performs a busy loop. Unix gives us a mechanism, select, for blocking on more than one source, waking up when there is information from any of them. In fact select allows us to wait at the same time for messages, events, or write operations.

Select is a complex service:

    #include <sys/types.h>
    #include <sys/time.h>
    int select(
          int nfds,          /* Number of objects monitored*/
          fd_set *readfds,   /* Set of file descriptors open for reading*/
          fd_set *writefds,  /* Set of file descript. open for writing*/
          fd_set *exceptfds, /* Set of pending exception conditions. Always 0 for us. */
          struct timeval *timout); /* Structure specifying timeout */
        If timout is null, then we block until one of the fd objects
        becomes ready. If timout points to a structure with timeout 
        equal to 0, then we do not block at all. Otherwise we will block
        up to the time specified by timout.
    FD_ZERO(&fdset)   /* fdset becomes the empty set */
    FD_SET(fd,&fdset) /* fd is added to fdset */
    FD_CLR(fd,&fdset) /* fd is removed from fdset */
    FD_ISSET(fd,&fdset) /* it determines if fd is in fdset */
    The structure timeval, defined in sys/time.h is
	struct timeval { int tv_sec;    /* second */
			 int tv_usec;}  /* microseconds */
    Upon return from select readfds, writefds, exceptfds have set only the 
    bits that correspond to ready files. The select function returns the
    number of bits that are set in readfds, writefds, exceptfds.
    [fd_set is just an array of integers, where each integer is interpreted
     as a bit vector. More information about fd_set and the various operations
     FD_XERO, FD_SET, etc. are in sys/select.h ]

Here are four examples of use of select.

The first is a simple program from Stevens for determining the maximum size for a pipe. [When run on my alpha the program prints that the size is 65536.]

    #include	<sys/types.h>
    #include	<sys/time.h>

    int main(void)
    {
	int		i, n, fd[2];
	fd_set		writeset;
	struct timeval	tv;

	if (pipe(fd) < 0){
		perror("pipe");
		exit(1);}
	FD_ZERO(&writeset);  /* set to zero the writeset */

	for (n = 0; ; n++) { /* write 1 byte at a time until pipe is full */
		FD_SET(fd[1], &writeset); /*add write-end of pipe to writeset*/
		tv.tv_sec = tv.tv_usec = 0; /* don't wait at all */
		/* select returns the number of objects (in readset, */
		/* writeset, or exceptionset) that are ready. */
		/* In our case there will be at most one ready object.*/
		if ( (i = select(fd[1]+1, NULL, &writeset, NULL, &tv)) < 0) {
			perror("select");
			exit(1);}
		else if (i == 0) /*We cannot write to pipe, i.e. it is full*/
			break;
		if (write(fd[1], "a", 1) != 1){
			perror("write error");
			exit(1);}
	}
	printf("pipe capacity = %d\n", n);
	exit(0);
    }
The second is a program also from Stevens for waiting on a timer.
    #include	<sys/types.h>
    #include	<sys/time.h>

    main(int argc, char *argv[])
    {
	long			atol();
	static struct timeval	timeout;

	if (argc != 3) {
		printf("usage: timer <#seconds> <#microseconds>");
		exit(1);}
	timeout.tv_sec  = atol(argv[1]);
	timeout.tv_usec = atol(argv[2]);

	/* select blocks waiting for the timeout to expire. */
	if (select(0, (fd_set *) 0, (fd_set *) 0, (fd_set *) 0, &timeout) < 0){
		perror("select error");
		exit(1);}
	exit(0);
    }

The third is a simple program where a process reads fixed size messages from two pipes. You should terminate the program from the terminal with a CONTROL-C.

    #include <sys/types.h>
    #include <sys/time.h>
    #include <unistd.h>
    #include <stdio.h>
    #define MAXLINE 12

    int main(void)
    {
        int     i, m, n; 
	int     fd1[2];   /* pipe for communications from child1 to parent */
	int     fd2[2];   /* pipe for communications from child2 to parent */
        pid_t   pid;
        char    line[MAXLINE];
	fd_set   readset;
	static struct timeval timeout;

        if (pipe(fd1) < 0) {
                perror("pipe1");
                exit(1);}
        if (pipe(fd2) < 0) {
                perror("pipe2");
                exit(1);}
	/* child1 */
        if ( (pid = fork()) < 0) {
                perror("fork1");
                exit(1);}
        else if (pid == 0) {            
                close(fd1[0]);
		while (1) {
                    write(fd1[1], "from child1\n", MAXLINE);
		    sleep(1);}
		exit(0);}
	/* child2 */
        if ( (pid = fork()) < 0) {
                perror("fork2");
                exit(1);}
        else if (pid == 0) {            
                close(fd2[0]);
		while (1) {
                    write(fd2[1], "from child2\n", MAXLINE);
		    sleep(1.3);}
		exit(0);}
        /* parent */
        close(fd1[1]);
        close(fd2[1]);
	m = 1 + ((fd1[0]<fd2[0])?fd2[0]:fd1[0]); /*Max # of objs to wait for*/
	FD_ZERO(&readset);
	while (1) {
	    FD_SET(fd1[0], &readset);
	    FD_SET(fd2[0], &readset);
	    if (select(m,&readset, NULL,NULL,NULL) < 0) {
	       perror("select");
	       exit(1);}
            if (FD_ISSET(fd1[0], &readset)) {
               n = read(fd1[0], line, MAXLINE);
               write(STDOUT_FILENO, line, n);}
            if (FD_ISSET(fd2[0], &readset)) {
               n = read(fd2[0], line, MAXLINE);
               write(STDOUT_FILENO, line, n);}
	  }
        exit(0);
    }

The fourth example combines the third example with the use of a timeout. It uses clock_gettime to determine the time when we execute the select statement and the time after we have read from the pipes. It uses clock_getres to determine the resolution of the real time clock being used by your program [on my machine it is 1 millisecond]. [When using these functions you have to compile your programs using the real time library, i.e. the flag -lrt]

   /* timedselect.c - It reads from two pipes with timeout. It uses the
                   select function
    */


   #include <sys/types.h>
   #include <sys/time.h>
   #include <unistd.h>
   #include <stdio.h>
   #define MAXLINE 20

   void child(int fd[2], int seed, int who);

   int main(void)
   {
        int     m, n, rc; 
	int fd1[2];     /* pipe for communications from child1 to parent */
	int fd2[2];     /* pipe for communications from child2 to parent */
        pid_t   pid;
        char    line[MAXLINE];
	fd_set   readset;
	static struct timeval timeout;
	static struct timespec tspecb, tspeca, tspec;
        char line1[60];

	/* child1 */
        if (pipe(fd1) < 0) {
	  perror("pipe1");
	  exit(1);}
        if ( (pid = fork()) < 0) {
	  perror("fork1");
	  exit(1);}
        else if (pid == 0) {  /*We are in child 1*/
	  child(fd1, 17, 1);}

	/* child2 */
        if (pipe(fd2) < 0) {
	  perror("pipe2");
	  exit(1);}
        if ( (pid = fork()) < 0) {
	  perror("fork2");
	  exit(1);}
        else if (pid == 0) {  /*We are in child 2*/
	  child(fd2, 19, 2);}

	/* parent */
	
	close(fd1[1]);
        close(fd2[1]);
	if (clock_getres(CLOCK_REALTIME, &tspec) < 0) {
	  perror("clock_getres");
	  exit(1);
	}
	printf("Resolution: %d second, %d nanoseconds\n", tspec.tv_sec,
	       tspec.tv_nsec);
	m = 1 + ((fd1[0] < fd2[0])?fd2[0]:fd1[0]); /*Max # of objs to wait for*/
        /*Setting a timeout for 1.1 seconds*/
        timeout.tv_sec = 1;
        timeout.tv_usec = 100000;
	while (1) {
	  if (clock_gettime(CLOCK_REALTIME, &tspecb) < 0) {
	    perror("clock_gettime");
	    exit(1);
	  }
	  sprintf(line1, "Time before select: sec: %d  sec: %d nsec\n",
		  tspecb.tv_sec, tspecb.tv_nsec);
	  write(STDOUT_FILENO, line1, strlen(line1));
	  FD_ZERO(&readset);
	  FD_SET(fd1[0], &readset);
	  FD_SET(fd2[0], &readset);
	  if ((rc = select(m,&readset, NULL, NULL, &timeout)) < 0) {
	    perror("select");
	    exit(1);}
          if (rc == 0) { /*No read operation has completed*/
            write(STDOUT_FILENO, "\nTimeout: \n", 11);
          } else { /*Some read operation has completed*/
	    if (FD_ISSET(fd1[0], &readset)) {
	      bzero(line1,60);
	      n = read(fd1[0], line, MAXLINE);
	      if (clock_gettime(CLOCK_REALTIME, &tspeca) < 0) {
		perror("clock_gettime");
		exit(1);
	      }
              sprintf(line1, "No Timeout        : sec: %1d nsec: %6d %20s\n",
		      tspeca.tv_sec, tspeca.tv_nsec, line);
              write(STDOUT_FILENO, line1, strlen(line1));
	    }
	    if (FD_ISSET(fd2[0], &readset)) {
	      bzero(line1,60);
	      n = read(fd2[0], line, MAXLINE);
	      if (clock_gettime(CLOCK_REALTIME, &tspeca) < 0) {
		perror("clock_gettime");
		exit(1);
	      }
              sprintf(line1, "No Timeout        : sec: %1d nsec: %6d %20s\n",
		      tspeca.tv_sec, tspeca.tv_nsec, line);
              write(STDOUT_FILENO, line1, strlen(line1));
	    }
	  }
	}
	exit(0);
   }

   void child (int fd[2], int seed, int who) {
        char line[MAXLINE];

        close(fd[0]);/*Close end of pipe we are not using*/
	srand(seed); /*Initializing the seed for the standard 
		       random number generator*/
        sprintf(line, "From child %d\n", who); /*String sent on pipe*/
	while (1) {
	  write(fd[1], line, 13);
	  sleep(rand() % 3 + 1);} /*we sleep 1 to 3 seconds*/
	exit(0);
   }

Note that in a more realistic program we will deal with messages that are of variable sizes and more than two pipes (thinking of how you would deal with many file descriptors without having to increase the size of your program? can you also think of ways to be fair to the message sources?)

There will be other occasions to see select in action. Also, much of the need to use select calls to wait for a number of possible concurrent events can be obviated by the use of concurrent threads, each waiting for a specific event.
Two significant examples of uses of the select function are in the notes dealing with sockets: http://www.cis.temple.edu/~ingargio/cis307/readings/tst.c and http://www.cis.temple.edu/~ingargio/cis307/readings/multiserver1.c

ingargio@joda.cis.temple.edu