CIS 307: Unix II

[Pipes], [Protection], [Locking], [Signals], [Shared Memory]

Pipes

A Pipe can be used for communication, essentially as a file that can be written into at one end and read at the other end, within a family of processes that are descendants of the process that created the pipe.

    #include <unistd.h>
    int pipe(int filedescriptor[2]);
	It returns 0 for success and -1 for failure
	When successful it places two file descriptors in filedescriptor,
	one at 0 for reading, one at 1 for writing. These two
	files are the endpoints of a pipe. [In some systems both
	ends of a pipe are bidirectional.] 
	The capacity of a pipe in bytes is usually defined by the
	constant PIPE_BUF

Pipes are opened by a process which then forks. Then the parent process and the children processes can communicate through the pipe. For example:

Here is a complex way of writing our favorite program "Hello World":

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

    int main(void)
    {
	int	n, fd[2];
	pid_t	pid;
	char	line[MAXLINE];

	if (pipe(fd) < 0) {
		write(STDERR_FILENO, "pipe error\n", 11);
		exit(1);}
	if ( (pid = fork()) < 0) {
		fprintf(STDERR_FILENO, "fork error\n", 11);
		exit(1);}
	else if (pid == 0) {		/* child */
		close(fd[0]);
		write(fd[1], "hello world\n", 12);
	} else {			/* parent */
		close(fd[1]);
		n = read(fd[0], line, MAXLINE);
		write(STDOUT_FILENO, line, n);}
	exit(0);
    }

A useful implicit use of pipes is in the C standard library functions:

    #include <stdio.h>
    FILE *popen(const char *cmdstring, const char *type);
	It executes cmdstring and returns a file.
	We read from the file the output of the command, if type is "r"
	We write to the file the input for the command, if type is "w"
    int pclose(FILE *fp);
and here is a simple use of popen:
  #include <stdio.h>
  #define MAXSIZE 256

  main(argc, argv)
     int argc;
     char **argv;
  {
    char line[MAXSIZE];  
    FILE *fp;            

    if ((fp = popen("ls","r")) == NULL) {
      perror("popen error");
      exit(1);
    }
    while (fgets(line, MAXSIZE, fp) != NULL) {
      printf("%s",line);
    }
    pclose(fp);
    exit(0);
  }
Of course we could have achieved the same effect, in this case, with the simpler command system.

File Protection

Unix recognizes the following User Kinds: ogua [o: owner; g: group; u: user; a: all of the above] and the following Operation Rights: rwxst [r: Read access; w: Write access; x: Execute access; s: Set_user_id; s: Set_group_id; t: text;]
Beware that operation rights can have different meaning on directories and on regular files. Review the shell command chmod and the use of MASK. The set-user-id, set-group-id are particularly interesting (and protected by patent) because they allow a process to execute a file with the rights of the owner of the file and not of the owner of the process (why is this so nice?.)

File Locking

Locking can be file oriented or record oriented depending on the scope of the lock, the whole file or a portion thereof.
Locks can be read or write locks. Read locks are compatible with other read locks. Write locks conflict with both read and write locks.
Locking can be mandatory or advisory. Advisory locks have effect only for processes that lock a file before accessing it and unlock it after. Mandatory locks protect a file also against processes that do not use lock/unlock as required. Mandatory locks should be avoided whenever possible because of efficiency reasons.
Locks can be blocking or non blocking. They are blocking if when a lock cannot be immediately acquired the process executing the call waits; it is non-blocking if the call returns with a code that indicates success or failure in acquiring the lock.

A basic commands for locking files is fcntl which uses the data structure flock.

    #include <sys/types.h>
    #include <unistd.h>
    #include <fcntl.h>
    int fcntl(int filedes, int cmd, struct flock *flockptr); 
       cmd can be F_GETLK (get lock information), F_SETLK (non-blocking),
            F_SETLKW (blocking), ...
       fcntl return -1 in case of error, a non-negative number otherwise.

    struct flock{
      short l_type;   /* F_RDLCK, F_WRLCK, F_UNLCK */
      off_t l_start;  /* offset to beginning of record being locked */
                      /* starting from position specified by l_whence */
      short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
      off_t l_len;    /* length in bytes of record being protected */
      pid_t l_pid;}   /* pid of process owning the lock */
    The information in flock is relative to the current process. Other
    processes using this file will have their own flock structures.
Here is code, copied from Stevens, for using fcntl to set a read lock or a write lock (both blocking and non blocking versions) and for unlocking a file:

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

    int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
    {
    struct flock  lock;

    lock.l_type = type;       
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;
    return (fcntl(fd,cmd,&lock));
    }

    #define read_lock(fd, offset, whence, len) \
        lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)
    #define readw_lock(fd, offset, whence, len) \
        lock_reg(fd, F_SETLKW, F_RDLCK, offset, whence, len)
    #define write_lock(fd, offset, whence, len) \
        lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)
    #define writew_lock(fd, offset, whence, len) \
        lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, len)
    #define un_lock(fd, offset, whence, len) \
        lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, len)

Here is how we can use a file lock to implement a critical region (We assume we have the definitions for writew_lock and un_lock.)

    #define FILE_MODE (S_IRUSR | S_IRGRP | S_IROTH)
    /* default permissions for new files */

    int main(void)
    {
    int fd;

    if ((fd = open("lockfile", O_WRONLY | O_CREAT, FILE_MODE)) < 0) {
      perror ("open error");
      exit(1);}
    if (writew_lock(fd,0,SEEK_SET,1) < 0) {       /* prolog */
      perror ("lock error");
      exit(1);}

    sleep(30); /* this represents the critical region */

    un_lock(fd,0,SEEK_SET,1);                     /* epilog */
    exit(0);
    }

Stevens shows that advisory file record locking can be used in place of semaphores to implement critical regions essentially without any performance degradation.

There is also a function flock which is easier to use than fcntl. It works with advisory locks. However, while fcntl works even in a distributed file system, flock works only on processes sharing the same computer (at least it is so on OSF on my alpha).

Here is a simple use of flock to implement a critical region:

    #include <sys/file.h>

    int main(void){
    int fd;

    if ((fd = open("lockfile", O_WRONLY | O_CREAT)) < 0) {
      perror ("open error");
      exit(1);}
    flock(fd, LOCK_EX); /* prolog */
    sleep(30);          /* this represents the critical region */
    flock(fd, LOCK_UN); /* epilog */
    exit(0);
    }

You can another way of using file locks in the hints for a homework.

Signals

Signals represent a very limited form of interprocess communication. They are easy to use but they communicates very little information. In addition the sender (if it is a process) and receiver must belong to the same user id, or the sender must be the superuser. Signals are sent explicitly to a process from another process using the kill function. Alternatively signals are sent to a process from the hardware through mediation of the OS. There are only a few possible signals (32). A process can specify with a mask (1 bit per signal) what it wants to be done with a signal directed to it, whether to discard it or to deliver it. A process specifies with the signal function what it wants it to be done when signals are delivered to it. There are three things that can be done when a signal is delivered: it can be ignored; or the process can be terminated; or a handler function can be called. After the handler function is completed, execution resumes at the statement being executed when the signal was received.

The following diagram describes how a signal is raised, possibly blocked before delivery, and then handled.

Note that we may worry that signals may be lost while pending [if this happens, we talk of an unsafe signal mechanism; recent Unix system use only reliable mechanisms]. Multiple copies of the same signal may be pending to a process so the OS must have facilities to store this information. Multiple copies of an idempotent signal (i.e., two copies of the signal have the same effect as just one, like the termination request) are treated as single copies.

    #include <signal.h>
    void (*signal(int sign, void(*funcion)(int)))(int);
         The signal function takes two parameters, an integer
	 and the address of a function of one integer argument which
	 gives no return. Signal returns the address of a function of
	 one integer argument that returns nothing. 
         sign identifies a signal
         the second argument is either SIG_IGN (ignore the signal)
	 or SIG_DFL (do the default action for this signal), or
	 the address of the function that will handle the signal.
	 It returns the previous handler to the sign signal.

    #include <sys/types.h>
    #include <signal.h>
    int kill(pid_t process-id, int sign);
	Sends the signal sign to the process process-id.
	[kill may also be used to send signals to groups of
	processes.]
Here are some of the possible signals, with the number associated to them, and their default handling.

    SIGNAL      ID   DEFAULT  DESCRIPTION
    ======================================================================
    SIGHUP      1    Termin.  Hang up
    SIGINT      2    Termin.  Interrupt
    SIGQUIT     3    Core     Generated when at terminal we enter CNRTL-\
    SIGILL      4    Core     Generated when we executed an illegal instruction
    SIGKILL     9    Termin.  Termination (can't catch, block, ignore)
    SIGBUS     10    Core     Generated in case of hardware fault
    SIGSEGV    11    Core     Generated in case of illegal address
    SIGPIPE    13    Termin.  Generated when writing to a pipe or a socket
                              while no process is reading at other end
    SIGALRM    14    Termin.  Generated by clock when alarm expires
    SIGTERM    15    Termin.  Software termination signal
    SIGCHLD    20    Ignore   The status of a child process has changed
    SIGTTIN    21    Stop     Generated when a backgorund process reads
                              from terminal
    SIGTTOUT   22    Stop     Generated when a background process writes
                              to terminal
    SIGXCPU    24    Discard  CPU time has expired
    SIGUSR1    30    Termin.  User defiled signal 1
    SIGUSR2    31    Termin.  User defined signal 2

One can see the effect of these signal by executing in the shell the kill command. For example
    kill -SIGTERM pidid
Two other useful functions are:
    #include <unistd.h>
    unsigned int alarm(unsigned int n);
        It requests the delivery in n seconds of a SIGALRM signal.
        If n is 0 it cancels a requested alarm.
        It returns the number of seconds left for the previous call to
        alarm (0 if none is pending).

    #include <unistd.h>
    int pause(void);
        It requests to be put to sleep until the process receives a signal.
        It returns -1.
More in depth uses of signals involve the function sigaction and the notion of signal sets.

Here is a very simple example of use of signals (modified from Stevens):

    #include	<sys/types.h>
    #include	<signal.h>
    #include	<stdio.h>

    static void	sig_cld();

    int main()
    {
	pid_t	pid;

	if ((int)signal(SIGCLD, sig_cld) == -1) {
		perror("signal error");
                exit(1);}
	if ( (pid = fork()) < 0) {
                perror("fork error");
		exit(1);}
	else if (pid == 0) {		/* child */
		sleep(2);
		exit(0);
	}
	pause();	/* parent */
	exit(0);
    }

    static void sig_cld()
    {
	pid_t	pid;
	int	status;

	printf("SIGCLD received\n");
	if ((int)signal(SIGCLD, sig_cld) == -1) {/* reestablish handler */
		perror("signal error");
	        exit(1);}
	if ( (pid = wait(&status)) < 0){	/* fetch child status */
		perror("wait error");
                exit(1);}
	printf("pid = %d\n", pid);
	return;		                        /* interrupts pause() */
    }

Shared Memory

As we have mentioned in the past, Unix processes have distinct address spaces. Here we will see that it is possible to have two processes share memory. The processes share real memory, not virtual memory, i.e. the same physical location may have different (virtual) addresses in the processes's spaces.

Shared memory segments are an example of IPC resources (other examples in Unix are semaphores and named queues). In Unix these resources are associated to identifiers, i.e. to unique names (unique at an instant since identifiers may be reused). We need to make sure that communicating processes all know the identity of the shared IPC. This can be done by agreeing on a key (a string) to be converted to an identifier by the system (but nobody guaranties uniqueness of keys) or by passing the identifiers at run time in some fashion among the communicating processes.

When using IPC resources in Unix it is important to make sure that these resources are deleted after use and do not remain in the system. You can make sure that things are ok by using the shell command ipcs to determine which resources are in use and the command ipcrm to remove those you own and want removed.

Here are the basic functions and constants for using shared memory:


    SHMLBA means 'low boundary address multiple'. It is a power of 2.
    It represents the required alignment for the shared segment.
    SHM_R and SHM_W are flag for read and write permission.
    SHM_LOCK and SHM_UNLOCK specify 'Lock segment in core' and
    'Unlock segment from core'.

     int shmget (key_t key, int size, int flag);
     get shared memory segment identifier.
     Returns the shared memory identifier associated with KEY,
     or -1 in case of error.
     A shared memory identifier and associated data structure and
     shared memory segment of at least size are created for KEY if one 
     of the following are true:
	key  is equal to IPC_PRIVATE, or
	key  does not already have a shared memory identifier
          associated with it and IPC_CREAT is specified in flag.
        We will only use the IPC_PRIVATE form. 
     Upon creation, the data structure associated with the new
     shared memory identifier is initialized.
     The shmget command should be executed by exactly one
     of the processes sharing the memory segment.

     int shmctl (int shmid, int cmd, struct shmid_ds *buf);
	It is used to examine or update the characteristics of an 
	existing segment.
	Returns 0 if ok, -1 on error
	cmd parameter is:
	IPC_STAT  Fetch shmid_ds for this segment and store its value at buf
	IPC_SET   Set shm_perm.uid, shm_perm.gid, shm_perm.mode
		  from buf. Can be executed only by shm_perm.cuid, or 
		  shm_perm.uid, or superuser.
	IPC_RMID  Remove shared memory segment from system. 
		  [It reduces count of users. When 0, it is deleted.]
		  It can be executed only by shm_perm.cuid, 
		  or shm_perm.uid, or superuser.
	SHM_LOCK  Lock segment in memory. Only the superuser.
	SHM_UNLOCK Unlock segment from memory. Only the superuser.


    void *shmat(int shmid, void *addr, int flag);
	It is used to bind an address in the process space to the origin
	of a shared segment.
	Returns pointer to shared memory segment if ok, -1 on error.
	If addr is 0, the segment is attached at the first available address
	(in the user space) as selected by the kernel. Best way.
	If addr<>0 and SHM_RND is not specified, the segment is attached
	at the address given by addr.
	If addr<>0 and SHM_RND is specified, the segment is attached at
	the address given by (addr - (addr mod SHMLBA)). 

    int shmdt(void *addr);
	Detaches this segment from the process's address space.
	The segment is not removed from system until the segment
	is actually removed by using of shmctl.

Here is an example of use of shared memory. A process forks. The child will ask interactively an integer from the user, store it in shared memory, and then terminate. The parent will wait for the child to terminate, read the integer from shared memory and print it out. It is all fairly easy since the identity of the shared segment as created by the parent is inherited by the child.
    #include    <stdio.h>
    #include	<sys/types.h>
    #include	<sys/ipc.h>
    #include	<sys/shm.h>

    #define	SHM_MODE	(SHM_R | SHM_W)	/* user read/write */

    int main(void){
	int	shmid;
	char	*shmptr;
	int     *p, *q;
        pid_t   pid;

	/* allocate ipc key in parent for a small shared segment*/
	if ( (shmid = shmget(IPC_PRIVATE, sizeof(int), SHM_MODE)) < 0) {
		perror("shmget error");
		exit(1);}
	/* create child; it inherits the ipc key from parent */
        if ((pid = fork()) < 0) {
	       perror("fork error");
	       exit(1);}
	else if (pid == 0) {  /* child */
	    /* attach to shared area and read an integer */
	    if ( (shmptr = (char *)shmat(shmid, 0, 0)) == (void *) -1) {
		perror("shmat error in child");
	        exit(1);}
	    p = (int *)shmptr;
	    printf("Enter an integer: ");
	    scanf("%d", p);
            /* release the shared memory */
	    if (shmctl(shmid, IPC_RMID, 0) < 0) {
	      perror("shmctl error");
	      exit(1);}
	    exit(0);}
	else { /* parent */
	  /* in the parent attach the shared area and write the integer */
	  /* that was read by the child into the shared area. */
	  if ( (shmptr = (char *)shmat(shmid, 0, 0)) == (void *) -1) {
		perror("shmat error in parent");
	        exit(1);}
	  q = (int *)shmptr;
	  /* we wait for child to terminate and then print */
	  wait();
	  printf("The number was %d\n", *q);
	  /* release the shared memory */
	  if (shmctl(shmid, IPC_RMID, 0) < 0) {
	    perror("shmctl");
	    exit(1);}
	  exit(0);}
      }

It is now fairly easy to implement monitors within Unix, if one so desires.

ingargiola.cis.temple.edu